mirror of
https://github.com/modrinth/code.git
synced 2026-08-30 19:46:33 +00:00
feat: check summary formatting by move link detection into its own validator
This commit is contained in:
@@ -266,6 +266,12 @@
|
||||
"nags.project-summary-content.title": {
|
||||
"defaultMessage": "Review the project summary"
|
||||
},
|
||||
"nags.project-summary-links.description": {
|
||||
"defaultMessage": "Any links, URLs, or IPs are not allowed in the project summary."
|
||||
},
|
||||
"nags.project-summary-links.title": {
|
||||
"defaultMessage": "Remove links from the summary"
|
||||
},
|
||||
"nags.project-summary-non-standard-text.description": {
|
||||
"defaultMessage": "Non-standard text characters, such as “₮ɆӾ₮”, are not allowed."
|
||||
},
|
||||
@@ -345,7 +351,7 @@
|
||||
"defaultMessage": "Visit versions settings"
|
||||
},
|
||||
"nags.summary-special-formatting.description": {
|
||||
"defaultMessage": "Your summary should not contain formatting, line breaks, special characters, or links. The summary only displays plain text."
|
||||
"defaultMessage": "Your summary cannot not contain Markdown or HTML, as it displays in plain text."
|
||||
},
|
||||
"nags.summary-special-formatting.title": {
|
||||
"defaultMessage": "Clean up the summary"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defineMessages } from '@modrinth/ui/i18n'
|
||||
import { md } from '@modrinth/utils/parse.ts'
|
||||
import LinkifyIt from 'linkify-it'
|
||||
import tlds from 'tlds' with { type: 'json' }
|
||||
|
||||
@@ -34,7 +35,11 @@ const messages = defineMessages({
|
||||
},
|
||||
cleanUpSummary: {
|
||||
id: 'nags.summary-special-formatting.title',
|
||||
defaultMessage: 'Clean up the summary',
|
||||
defaultMessage: 'Remove Markdown and HTML from the summary',
|
||||
},
|
||||
removeSummaryLinks: {
|
||||
id: 'nags.project-summary-links.title',
|
||||
defaultMessage: 'Remove links from the summary',
|
||||
},
|
||||
editSummary: {
|
||||
id: 'nags.edit-summary.title',
|
||||
@@ -68,8 +73,11 @@ const messages = defineMessages({
|
||||
},
|
||||
specialFormatting: {
|
||||
id: 'nags.summary-special-formatting.description',
|
||||
defaultMessage:
|
||||
'Your summary should not contain formatting, line breaks, special characters, or links. The summary only displays plain text.',
|
||||
defaultMessage: 'Your summary cannot contain Markdown or HTML, as it displays in plain text.',
|
||||
},
|
||||
links: {
|
||||
id: 'nags.project-summary-links.description',
|
||||
defaultMessage: 'Links, URLs, or IPs are not allowed in the project summary.',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -86,6 +94,10 @@ const summaryLinkify = new LinkifyIt({
|
||||
fuzzyLink: true,
|
||||
}).tlds(tlds)
|
||||
|
||||
const summaryMarkdown = md({ linkify: false })
|
||||
const allowedSummaryBlockTokenTypes = new Set(['paragraph_open', 'inline', 'paragraph_close'])
|
||||
const allowedSummaryInlineTokenTypes = new Set(['text', 'softbreak', 'hardbreak'])
|
||||
|
||||
function containsProjectSummaryLinkOrIp(summary: string): boolean {
|
||||
return summaryLinkify.test(summary)
|
||||
}
|
||||
@@ -98,17 +110,11 @@ export function projectSummaryMatchesName(summary: string, name: string) {
|
||||
}
|
||||
|
||||
export function hasProjectSummaryFormatting(summary: string) {
|
||||
return Boolean(
|
||||
summary.match(/# .*/g) ||
|
||||
summary.match(/---/g) ||
|
||||
summary.match(/\n/g) ||
|
||||
summary.match(/`.*`/g) ||
|
||||
summary.match(/\*.*\*/g) ||
|
||||
summary.match(/_.*_/g) ||
|
||||
summary.match(/~~.*~~/g) ||
|
||||
summary.match(/```/g) ||
|
||||
summary.match(/> /g),
|
||||
)
|
||||
return summaryMarkdown.parse(summary, {}).some((token) => {
|
||||
if (!allowedSummaryBlockTokenTypes.has(token.type)) return true
|
||||
|
||||
return token.children?.some((child) => !allowedSummaryInlineTokenTypes.has(child.type)) ?? false
|
||||
})
|
||||
}
|
||||
|
||||
const commonNagPresentation = {
|
||||
@@ -182,15 +188,23 @@ export const projectSummaryValidationRules = {
|
||||
'summary-special-formatting': {
|
||||
severity: 'error',
|
||||
evaluate: ({ summary }) => ({
|
||||
valid:
|
||||
!summary ||
|
||||
(!hasProjectSummaryFormatting(summary) && !containsProjectSummaryLinkOrIp(summary)),
|
||||
valid: !summary || !hasProjectSummaryFormatting(summary),
|
||||
}),
|
||||
presentation: {
|
||||
message: messages.specialFormatting,
|
||||
nag: { title: messages.cleanUpSummary, ...commonNagPresentation },
|
||||
},
|
||||
},
|
||||
'project-summary-links': {
|
||||
severity: 'error',
|
||||
evaluate: ({ summary }) => ({
|
||||
valid: !summary || !containsProjectSummaryLinkOrIp(summary),
|
||||
}),
|
||||
presentation: {
|
||||
message: messages.links,
|
||||
nag: { title: messages.removeSummaryLinks, ...commonNagPresentation },
|
||||
},
|
||||
},
|
||||
} satisfies ValidationRuleSet<ProjectSummaryValidationInput>
|
||||
|
||||
export function validateProjectSummary(
|
||||
|
||||
@@ -15,7 +15,11 @@ import {
|
||||
} from './rules/description.ts'
|
||||
import { validateProjectGalleryDescription, validateProjectGalleryName } from './rules/gallery.ts'
|
||||
import { projectNameValidationRules, validateProjectNameField } from './rules/name.ts'
|
||||
import { projectSummaryMatchesName, validateProjectSummary } from './rules/summary.ts'
|
||||
import {
|
||||
hasProjectSummaryFormatting,
|
||||
projectSummaryMatchesName,
|
||||
validateProjectSummary,
|
||||
} from './rules/summary.ts'
|
||||
import { toFieldMessages } from './to-field-messages.ts'
|
||||
import { toNags } from './to-nags.ts'
|
||||
import type { ValidationRuleSet } from './types.ts'
|
||||
@@ -141,6 +145,32 @@ test('rejects repeated summary padding', () => {
|
||||
)
|
||||
})
|
||||
|
||||
test('detects Markdown and HTML formatting in project summaries', () => {
|
||||
for (const summary of [
|
||||
'Unknown <span>🩸</span>Unknown is a dark and unsettling horror-survival mod.',
|
||||
'<custom-element>Custom HTML content</custom-element>',
|
||||
'Visible content <!-- hidden HTML content -->',
|
||||
'**Bold text** in a detailed project summary',
|
||||
'# Heading in a detailed project summary',
|
||||
'- A list item in a detailed project summary',
|
||||
'`Inline code` in a detailed project summary',
|
||||
]) {
|
||||
assert.equal(hasProjectSummaryFormatting(summary), true, summary)
|
||||
}
|
||||
})
|
||||
|
||||
test('allows plain-text punctuation in project summaries', () => {
|
||||
for (const summary of [
|
||||
'A configuration value named file_name is supported.',
|
||||
'Use * to mark an important configuration value.',
|
||||
'The expression 2 < 3 is used as an example.',
|
||||
'First line\r\nSecond line',
|
||||
'First paragraph\n\nSecond paragraph',
|
||||
]) {
|
||||
assert.equal(hasProjectSummaryFormatting(summary), false, summary)
|
||||
}
|
||||
})
|
||||
|
||||
test('rejects every link and IP address in project summaries', () => {
|
||||
for (const summary of [
|
||||
'Visit https://example.dev for more information about this project',
|
||||
@@ -149,7 +179,7 @@ test('rejects every link and IP address in project summaries', () => {
|
||||
]) {
|
||||
assert.deepEqual(
|
||||
validateProjectSummary({ summary, name: 'Project title' }).map(({ code }) => code),
|
||||
['summary-special-formatting'],
|
||||
['project-summary-links'],
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -51,21 +51,8 @@ const FANCY_RANGES: ReadonlyArray<readonly [number, number]> = [
|
||||
]
|
||||
|
||||
const ALLOWED_FANCY_CODE_POINTS = new Set([
|
||||
0x02d6,
|
||||
0x02d7,
|
||||
0x02d8,
|
||||
0x02d9,
|
||||
0x02da,
|
||||
0x02db,
|
||||
0x02dc,
|
||||
0x02dd,
|
||||
0x207a,
|
||||
0x207b,
|
||||
0x208a,
|
||||
0x208b,
|
||||
0x2120,
|
||||
0x2122,
|
||||
0x2139,
|
||||
0x02d6, 0x02d7, 0x02d8, 0x02d9, 0x02da, 0x02db, 0x02dc, 0x02dd, 0x207a, 0x207b, 0x208a, 0x208b,
|
||||
0x2120, 0x2122, 0x2139,
|
||||
])
|
||||
|
||||
const MARK_PATTERN = /\p{M}/u
|
||||
|
||||
Reference in New Issue
Block a user