mirror of
https://github.com/modrinth/code.git
synced 2026-09-05 06:19:11 +00:00
fix: summary html tag thats opening tag only, allow name to have version number when port or fork
This commit is contained in:
@@ -77,6 +77,12 @@
|
||||
"nags.description-too-short.title": {
|
||||
"defaultMessage": "Expand the description"
|
||||
},
|
||||
"nags.disclosures-special-formatting.description": {
|
||||
"defaultMessage": "Content disclosures cannot contain HTML, as they display in plain text."
|
||||
},
|
||||
"nags.disclosures-special-formatting.title": {
|
||||
"defaultMessage": "Remove HTML from content disclosures"
|
||||
},
|
||||
"nags.edit-description.title": {
|
||||
"defaultMessage": "Edit description"
|
||||
},
|
||||
@@ -282,7 +288,7 @@
|
||||
"defaultMessage": "Review the project summary"
|
||||
},
|
||||
"nags.project-summary-links.description": {
|
||||
"defaultMessage": "Links, URLs, or IPs are not allowed in the project summary."
|
||||
"defaultMessage": "Links, URLs, or IPs are not allowed in the project summary. Detected: {value}"
|
||||
},
|
||||
"nags.project-summary-links.title": {
|
||||
"defaultMessage": "Remove links from the summary"
|
||||
|
||||
@@ -105,11 +105,9 @@ export const projectNameValidationRules = {
|
||||
severity: 'error',
|
||||
evaluate: (projectName) => {
|
||||
const normalizedName = projectName.normalize('NFC').toLowerCase()
|
||||
const includesVersionNumber = [...normalizedName.matchAll(/\d+(?:\.\d+)+/g)].some((match) => {
|
||||
const textAfterVersion = normalizedName.slice((match.index ?? 0) + match[0].length)
|
||||
return !/\b(?:port|fork)\b/.test(textAfterVersion)
|
||||
})
|
||||
return { valid: !includesVersionNumber }
|
||||
const isPortOrFork = normalizedName.includes('port') || normalizedName.includes('fork')
|
||||
const includesVersionNumber = /\d+(?:\.\d+)+/.test(normalizedName)
|
||||
return { valid: !includesVersionNumber || isPortOrFork }
|
||||
},
|
||||
presentation: {
|
||||
message: messages.versionNumber,
|
||||
|
||||
@@ -84,7 +84,7 @@ const messages = defineMessages({
|
||||
},
|
||||
links: {
|
||||
id: 'nags.project-summary-links.description',
|
||||
defaultMessage: 'Links, URLs, or IPs are not allowed in the project summary.',
|
||||
defaultMessage: 'Links, URLs, or IPs are not allowed in the project summary. Detected: {value}',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -101,8 +101,8 @@ const summaryLinkify = new LinkifyIt({
|
||||
fuzzyLink: true,
|
||||
}).tlds(tlds)
|
||||
|
||||
function containsProjectSummaryLinkOrIp(summary: string): boolean {
|
||||
return summaryLinkify.test(summary)
|
||||
function findProjectSummaryLinkOrIp(summary: string): string | null {
|
||||
return summaryLinkify.match(summary)?.[0].raw ?? null
|
||||
}
|
||||
|
||||
export function projectSummaryMatchesName(summary: string, name: string) {
|
||||
@@ -153,7 +153,7 @@ export const projectSummaryValidationRules = {
|
||||
if (
|
||||
!normalized ||
|
||||
normalized.length < MIN_SUMMARY_CHARS ||
|
||||
containsProjectSummaryLinkOrIp(normalized) ||
|
||||
findProjectSummaryLinkOrIp(normalized) !== null ||
|
||||
!validateSpam(normalized).valid
|
||||
) {
|
||||
return { valid: true }
|
||||
@@ -170,7 +170,7 @@ export const projectSummaryValidationRules = {
|
||||
evaluate: ({ summary, name }) => ({
|
||||
valid:
|
||||
!summary ||
|
||||
containsProjectSummaryLinkOrIp(summary) ||
|
||||
findProjectSummaryLinkOrIp(summary) !== null ||
|
||||
!name ||
|
||||
!projectSummaryMatchesName(summary, name),
|
||||
}),
|
||||
@@ -182,7 +182,7 @@ export const projectSummaryValidationRules = {
|
||||
'summary-too-short': {
|
||||
severity: 'error',
|
||||
evaluate: ({ summary }) => {
|
||||
if (!summary || containsProjectSummaryLinkOrIp(summary)) return { valid: true }
|
||||
if (!summary || findProjectSummaryLinkOrIp(summary) !== null) return { valid: true }
|
||||
const length = normalizeProjectFieldText(summary).length
|
||||
return length < MIN_SUMMARY_CHARS
|
||||
? { valid: false, values: { length, minChars: MIN_SUMMARY_CHARS } }
|
||||
@@ -215,9 +215,10 @@ export const projectSummaryValidationRules = {
|
||||
},
|
||||
'project-summary-links': {
|
||||
severity: 'error',
|
||||
evaluate: ({ summary }) => ({
|
||||
valid: !summary || !containsProjectSummaryLinkOrIp(summary),
|
||||
}),
|
||||
evaluate: ({ summary }) => {
|
||||
const match = summary ? findProjectSummaryLinkOrIp(summary) : null
|
||||
return match ? { valid: false, values: { value: match } } : { valid: true }
|
||||
},
|
||||
presentation: {
|
||||
message: messages.links,
|
||||
nag: { title: messages.removeSummaryLinks, ...commonNagPresentation },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { formatCategory } from '@modrinth/ui/src/utils/tag-messages.ts'
|
||||
import { defineMessages } from '@modrinth/ui/i18n'
|
||||
import { formatCategory } from '@modrinth/ui/src/utils/tag-messages.ts'
|
||||
|
||||
import type { Nag, ProjectValidationContext } from '../../types/nags.ts'
|
||||
import { evaluateRules } from '../evaluate-rules.ts'
|
||||
|
||||
@@ -81,7 +81,31 @@ test('evaluates matching rules in definition order and converts them to field me
|
||||
|
||||
test('allows clean project names and versioned ports', () => {
|
||||
assert.deepEqual(evaluateRules('Sodium Extras', projectNameValidationRules), [])
|
||||
assert.deepEqual(evaluateRules('Sodium 1.20 Port', projectNameValidationRules), [])
|
||||
|
||||
for (const name of [
|
||||
'Sodium 1.20 Port',
|
||||
'Port Sodium 1.20',
|
||||
'Sodium Fork Edition 1.20',
|
||||
'1.20 Sodium Fork',
|
||||
'Sodium 1.20 Forked',
|
||||
'Sodium 1.20 Teleport',
|
||||
'Sodium 1.20 Port:',
|
||||
]) {
|
||||
assert.equal(
|
||||
evaluateRules(name, projectNameValidationRules).some(
|
||||
({ code }) => code === 'project-name-version',
|
||||
),
|
||||
false,
|
||||
name,
|
||||
)
|
||||
}
|
||||
|
||||
assert.equal(
|
||||
evaluateRules('Sodium Edition 1.20', projectNameValidationRules).some(
|
||||
({ code }) => code === 'project-name-version',
|
||||
),
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
test('collects every matching project name rule', () => {
|
||||
@@ -186,7 +210,6 @@ 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',
|
||||
@@ -196,6 +219,17 @@ test('detects Markdown and HTML formatting in project summaries', () => {
|
||||
}
|
||||
})
|
||||
|
||||
test('requires paired HTML tags in project summaries', () => {
|
||||
for (const summary of [
|
||||
'<b>Bold summary',
|
||||
'Bold summary</strong>',
|
||||
'A summary with a line break<br>',
|
||||
'Visible content <!-- hidden HTML content -->',
|
||||
]) {
|
||||
assert.equal(hasProjectSummaryFormatting(summary), false, summary)
|
||||
}
|
||||
})
|
||||
|
||||
test('allows plain-text punctuation in project summaries', () => {
|
||||
for (const summary of [
|
||||
'A configuration value named file_name is supported.',
|
||||
@@ -273,14 +307,17 @@ test('rejects HTML in disclosure text', () => {
|
||||
})
|
||||
|
||||
test('rejects every link and IP address in project summaries', () => {
|
||||
for (const summary of [
|
||||
'Visit https://example.dev for more information about this project',
|
||||
'Visit example.dev for more information about this project',
|
||||
'Join 127.0.0.1:25565 for more information about this project',
|
||||
for (const [summary, value] of [
|
||||
['Visit https://example.dev for more information about this project', 'https://example.dev'],
|
||||
['Visit example.dev for more information about this project', 'example.dev'],
|
||||
['Join 127.0.0.1:25565 for more information about this project', '127.0.0.1:25565'],
|
||||
]) {
|
||||
assert.deepEqual(
|
||||
validateProjectSummary({ summary, name: 'Project title' }).map(({ code }) => code),
|
||||
['project-summary-links'],
|
||||
validateProjectSummary({ summary, name: 'Project title' }).map(({ code, values }) => ({
|
||||
code,
|
||||
values,
|
||||
})),
|
||||
[{ code: 'project-summary-links', values: { value } }],
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,13 +18,19 @@ const allowedPlainTextBlockTokenTypes = new Set(['paragraph_open', 'inline', 'pa
|
||||
const allowedPlainTextInlineTokenTypes = new Set(['text', 'softbreak', 'hardbreak'])
|
||||
|
||||
export function hasProjectTextFormatting(text: string) {
|
||||
return projectPlainTextMarkdown.parse(text, {}).some((token) => {
|
||||
const hasMarkdownFormatting = projectPlainTextMarkdown.parse(text, {}).some((token) => {
|
||||
if (token.type === 'html_block') return false
|
||||
if (!allowedPlainTextBlockTokenTypes.has(token.type)) return true
|
||||
|
||||
return (
|
||||
token.children?.some((child) => !allowedPlainTextInlineTokenTypes.has(child.type)) ?? false
|
||||
token.children?.some(
|
||||
(child) =>
|
||||
child.type !== 'html_inline' && !allowedPlainTextInlineTokenTypes.has(child.type),
|
||||
) ?? false
|
||||
)
|
||||
})
|
||||
|
||||
return hasMarkdownFormatting || hasProjectTextHtmlFormatting(text)
|
||||
}
|
||||
|
||||
const pairedHtmlTagPattern = /<([a-z][\w:-]*)\b[^>]*>[\s\S]*?<\/\1\s*>/i
|
||||
|
||||
Reference in New Issue
Block a user