mirror of
https://github.com/modrinth/code.git
synced 2026-08-31 03:55:59 +00:00
feat: clean up link detection in summary and description
This commit is contained in:
@@ -1,7 +1,14 @@
|
||||
import { defineMessages } from '@modrinth/ui/i18n'
|
||||
import LinkifyIt from 'linkify-it'
|
||||
import tlds from 'tlds' with { type: 'json' }
|
||||
|
||||
import type { Nag, ProjectValidationContext } from '../../types/nags.ts'
|
||||
import { findBlockedProjectContentLink } from '../../validators/links/detection.ts'
|
||||
import { URL_SHORTENERS } from '../../validators/links/block-list.ts'
|
||||
import {
|
||||
getLinkHostname,
|
||||
hostnameMatchesDomain,
|
||||
isIpAddress,
|
||||
} from '../../validators/links/syntax-checks.ts'
|
||||
import { evaluateRules } from '../evaluate-rules.ts'
|
||||
import {
|
||||
evaluateNonStandardText,
|
||||
@@ -85,11 +92,46 @@ const messages = defineMessages({
|
||||
},
|
||||
})
|
||||
|
||||
export const DESCRIPTION_MAX_PROFANITY_COUNT = 1
|
||||
export const DESCRIPTION_MAX_PROFANITY_COUNT = 2
|
||||
export const DESCRIPTION_NON_STANDARD_TEXT_FAILURE_THRESHOLD = 0.05
|
||||
export const MIN_DESCRIPTION_CHARS = 200
|
||||
export const MAX_HEADER_LENGTH = 80
|
||||
export const MIN_CHARS_PER_IMAGE = 60
|
||||
export const BANNED_DESCRIPTION_LINK_DOMAINS = [...URL_SHORTENERS] as const
|
||||
|
||||
const descriptionLinkify = new LinkifyIt({
|
||||
fuzzyEmail: false,
|
||||
fuzzyIP: true,
|
||||
fuzzyLink: true,
|
||||
}).tlds(tlds)
|
||||
|
||||
export function extractDescriptionLinks(description: string): string[] {
|
||||
const matches = descriptionLinkify.match(description) ?? []
|
||||
return [
|
||||
...new Set(
|
||||
matches
|
||||
.map((match) => match.url)
|
||||
.filter((url) => {
|
||||
const hostname = getLinkHostname(url)
|
||||
return hostname === null || !isIpAddress(hostname)
|
||||
}),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
export function findBannedDescriptionLink(description: string): string | null {
|
||||
for (const url of extractDescriptionLinks(description)) {
|
||||
const hostname = getLinkHostname(url)
|
||||
if (
|
||||
hostname &&
|
||||
BANNED_DESCRIPTION_LINK_DOMAINS.some((domain) => hostnameMatchesDomain(hostname, domain))
|
||||
) {
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function analyzeHeaderLength(markdown: string): {
|
||||
hasLongHeaders: boolean
|
||||
@@ -199,8 +241,12 @@ export const projectDescriptionValidationRules = {
|
||||
'project-description-banned-link': {
|
||||
severity: 'error',
|
||||
evaluate: (description) => {
|
||||
const blockedLink = findBlockedProjectContentLink(description ?? '')
|
||||
return blockedLink ? { valid: false, values: { fullUrl: blockedLink.url } } : { valid: true }
|
||||
const bannedLink = findBannedDescriptionLink(description ?? '')
|
||||
if (bannedLink) {
|
||||
return { valid: false, values: { fullUrl: bannedLink } }
|
||||
} else {
|
||||
return { valid: true }
|
||||
}
|
||||
},
|
||||
presentation: {
|
||||
message: messages.bannedLink,
|
||||
@@ -213,9 +259,11 @@ export const projectDescriptionValidationRules = {
|
||||
const normalized = normalizeProjectFieldText(description ?? '')
|
||||
if (!normalized) return { valid: true }
|
||||
const length = countText(normalized)
|
||||
return length < MIN_DESCRIPTION_CHARS
|
||||
? { valid: false, values: { length, minChars: MIN_DESCRIPTION_CHARS } }
|
||||
: { valid: true }
|
||||
if (length < MIN_DESCRIPTION_CHARS) {
|
||||
return { valid: false, values: { length, minChars: MIN_DESCRIPTION_CHARS } }
|
||||
} else {
|
||||
return { valid: true }
|
||||
}
|
||||
},
|
||||
presentation: {
|
||||
message: messages.tooShort,
|
||||
@@ -226,9 +274,11 @@ export const projectDescriptionValidationRules = {
|
||||
severity: 'warning',
|
||||
evaluate: (description) => {
|
||||
const { longHeaders } = analyzeHeaderLength(description ?? '')
|
||||
return longHeaders.length > 0
|
||||
? { valid: false, values: { count: longHeaders.length } }
|
||||
: { valid: true }
|
||||
if (longHeaders.length > 0) {
|
||||
return { valid: false, values: { count: longHeaders.length } }
|
||||
} else {
|
||||
return { valid: true }
|
||||
}
|
||||
},
|
||||
presentation: {
|
||||
message: messages.longHeaders,
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { defineMessages } from '@modrinth/ui/i18n'
|
||||
import LinkifyIt from 'linkify-it'
|
||||
import tlds from 'tlds' with { type: 'json' }
|
||||
|
||||
import type { Nag, ProjectValidationContext } from '../../types/nags.ts'
|
||||
import {
|
||||
containsExplicitHttpProjectLink,
|
||||
findBlockedProjectContentLink,
|
||||
} from '../../validators/links/detection.ts'
|
||||
import { evaluateRules } from '../evaluate-rules.ts'
|
||||
import {
|
||||
evaluateNonStandardText,
|
||||
@@ -49,10 +47,6 @@ const messages = defineMessages({
|
||||
id: 'nags.project-summary-non-standard-text.description',
|
||||
defaultMessage: 'Non-standard text characters, such as “₮ɆӾ₮”, are not allowed.',
|
||||
},
|
||||
bannedLink: {
|
||||
id: 'nags.project-summary-banned-link.description',
|
||||
defaultMessage: '“{fullUrl}” is not allowed in project summaries.',
|
||||
},
|
||||
matchesName: {
|
||||
id: 'project.text-validation.summary-matches-title',
|
||||
defaultMessage: "A project summary cannot be the same as it's title.",
|
||||
@@ -76,6 +70,16 @@ export interface ProjectSummaryValidationInput {
|
||||
name: string | null | undefined
|
||||
}
|
||||
|
||||
const summaryLinkify = new LinkifyIt({
|
||||
fuzzyEmail: false,
|
||||
fuzzyIP: true,
|
||||
fuzzyLink: true,
|
||||
}).tlds(tlds)
|
||||
|
||||
function containsProjectSummaryLinkOrIp(summary: string): boolean {
|
||||
return summaryLinkify.test(summary)
|
||||
}
|
||||
|
||||
export function projectSummaryMatchesName(summary: string, name: string) {
|
||||
const normalizedSummary = normalizeProjectFieldText(summary).replace(/\s+/g, '')
|
||||
const normalizedName = normalizeProjectFieldText(name).replace(/\s+/g, '')
|
||||
@@ -127,23 +131,12 @@ export const projectSummaryValidationRules = {
|
||||
nag: { title: messages.fixSummary, ...commonNagPresentation },
|
||||
},
|
||||
},
|
||||
'project-summary-banned-link': {
|
||||
severity: 'error',
|
||||
evaluate: ({ summary }) => {
|
||||
const blockedLink = findBlockedProjectContentLink(summary ?? '')
|
||||
return blockedLink ? { valid: false, values: { fullUrl: blockedLink.url } } : { valid: true }
|
||||
},
|
||||
presentation: {
|
||||
message: messages.bannedLink,
|
||||
nag: { title: messages.fixSummary, ...commonNagPresentation },
|
||||
},
|
||||
},
|
||||
'project-summary-matches-title': {
|
||||
severity: 'error',
|
||||
evaluate: ({ summary, name }) => ({
|
||||
valid:
|
||||
!summary ||
|
||||
containsExplicitHttpProjectLink(summary) ||
|
||||
containsProjectSummaryLinkOrIp(summary) ||
|
||||
!name ||
|
||||
!projectSummaryMatchesName(summary, name),
|
||||
}),
|
||||
@@ -155,7 +148,7 @@ export const projectSummaryValidationRules = {
|
||||
'summary-too-short': {
|
||||
severity: 'warning',
|
||||
evaluate: ({ summary }) => {
|
||||
if (!summary || containsExplicitHttpProjectLink(summary)) return { valid: true }
|
||||
if (!summary || containsProjectSummaryLinkOrIp(summary)) return { valid: true }
|
||||
const length = normalizeProjectFieldText(summary).length
|
||||
return length < MIN_SUMMARY_CHARS
|
||||
? { valid: false, values: { length, minChars: MIN_SUMMARY_CHARS } }
|
||||
@@ -171,7 +164,7 @@ export const projectSummaryValidationRules = {
|
||||
evaluate: ({ summary }) => ({
|
||||
valid:
|
||||
!summary ||
|
||||
(!hasProjectSummaryFormatting(summary) && !containsExplicitHttpProjectLink(summary)),
|
||||
(!hasProjectSummaryFormatting(summary) && !containsProjectSummaryLinkOrIp(summary)),
|
||||
}),
|
||||
presentation: {
|
||||
message: messages.specialFormatting,
|
||||
|
||||
Reference in New Issue
Block a user