feat: start work implementing validation checks using new nag system
This commit is contained in:
1 parent
59ad8eb426
commit
729d584757
9 files changed
+270
-7
No files matched your search
@@ -1,4 +1,6 @@
|
||||
import type { Nag } from '../types/nags'
|
||||
import { coreNags } from './nags/core'
|
||||
import { descriptionNags } from './nags/description'
|
||||
import { linksNags } from './nags/links'
|
||||
|
||||
export default [...coreNags] as Nag[]
|
||||
export default [...coreNags, ...linksNags, ...descriptionNags] as Nag[]
|
||||
@@ -0,0 +1,104 @@
|
||||
import type { Nag, NagContext } from '../../types/nags'
|
||||
|
||||
export const MIN_DESCRIPTION_CHARS = 500
|
||||
export const MIN_SUMMARY_CHARS = 125
|
||||
|
||||
function analyzeImageContent(markdown: string): { imageHeavy: boolean; hasEmptyAltText: boolean } {
|
||||
if (!markdown) return { imageHeavy: false, hasEmptyAltText: false }
|
||||
|
||||
const withoutCodeBlocks = markdown.replace(/```[\s\S]*?```/g, '').replace(/`[^`]*`/g, '')
|
||||
|
||||
const imageRegex = /!\[([^\]]*)\]\([^)]+\)/g
|
||||
const images = [...withoutCodeBlocks.matchAll(imageRegex)]
|
||||
|
||||
const htmlImageRegex = /<img[^>]*>/gi
|
||||
const htmlImages = [...withoutCodeBlocks.matchAll(htmlImageRegex)]
|
||||
|
||||
const totalImages = images.length + htmlImages.length
|
||||
if (totalImages === 0) return { imageHeavy: false, hasEmptyAltText: false }
|
||||
|
||||
const textWithoutImages = withoutCodeBlocks
|
||||
.replace(/!\[([^\]]*)\]\([^)]+\)/g, '')
|
||||
.replace(/<img[^>]*>/gi, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
|
||||
const textLength = textWithoutImages.length
|
||||
const imageHeavy = textLength < 100 || (totalImages >= 3 && textLength < 200)
|
||||
|
||||
const hasEmptyAltText =
|
||||
images.some((match) => !match[1]?.trim()) ||
|
||||
htmlImages.some((match) => {
|
||||
const altMatch = match[0].match(/alt\s*=\s*["']([^"']*)["']/i)
|
||||
return !altMatch || !altMatch[1]?.trim()
|
||||
})
|
||||
|
||||
return { imageHeavy, hasEmptyAltText }
|
||||
}
|
||||
|
||||
export const descriptionNags: Nag[] = [
|
||||
{
|
||||
id: 'description-too-short',
|
||||
title: 'Description may be insufficient',
|
||||
description: (context: NagContext) =>
|
||||
`Your description is ${context.project.body?.length || 0} characters. It's recommended to have at least ${MIN_DESCRIPTION_CHARS} characters to provide users with enough information about your project.`,
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const bodyLength = context.project.body?.trim()?.length || 0
|
||||
return bodyLength < MIN_DESCRIPTION_CHARS && bodyLength !== 0
|
||||
},
|
||||
link: {
|
||||
path: 'settings/description',
|
||||
title: 'Edit description',
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'summary-too-short',
|
||||
title: 'Summary may be insufficient',
|
||||
description: (context: NagContext) =>
|
||||
`Your summary is ${context.project.description?.length || 0} characters. It's recommended to have at least ${MIN_SUMMARY_CHARS} characters to provide users with enough information about your project.`,
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const summaryLength = context.project.description?.trim()?.length || 0
|
||||
return summaryLength < MIN_SUMMARY_CHARS && summaryLength !== 0
|
||||
},
|
||||
link: {
|
||||
path: 'settings',
|
||||
title: 'Edit summary',
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'image-heavy-description',
|
||||
title: 'Description is mostly images',
|
||||
description: () =>
|
||||
`Please add more descriptive text to help users understand your project, especially those using screen readers or with slow internet connections.`,
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const { imageHeavy } = analyzeImageContent(context.project.body || '')
|
||||
return imageHeavy
|
||||
},
|
||||
link: {
|
||||
path: 'settings/description',
|
||||
title: 'Edit description',
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'missing-alt-text',
|
||||
title: 'Images missing alt text',
|
||||
description: () =>
|
||||
`Some of your images are missing alt text, which is important for accessibility, especially for visually impaired users.`,
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const { hasEmptyAltText } = analyzeImageContent(context.project.body || '')
|
||||
return hasEmptyAltText
|
||||
},
|
||||
link: {
|
||||
path: 'settings/description',
|
||||
title: 'Edit description',
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-description',
|
||||
},
|
||||
},
|
||||
]
|
||||
Whitespace-only changes.
@@ -0,0 +1,3 @@
|
||||
export * from './core'
|
||||
export * from './links'
|
||||
export * from './description'
|
||||
@@ -0,0 +1,82 @@
|
||||
import type { Nag, NagContext } from '../../types/nags'
|
||||
import { formatProjectType } from '@modrinth/utils'
|
||||
|
||||
export const commonLinkDomains = {
|
||||
source: ['github.com', 'gitlab.com', 'bitbucket.org', 'codeberg.org', 'git.sr.ht'],
|
||||
issues: ['github.com', 'gitlab.com', 'bitbucket.org', 'codeberg.org'],
|
||||
discord: ['discord.gg', 'discord.com'],
|
||||
}
|
||||
|
||||
export function isCommonUrl(url: string | undefined, commonDomains: string[]): boolean {
|
||||
if (!url) return false
|
||||
try {
|
||||
const domain = new URL(url).hostname.toLowerCase()
|
||||
console.log(domain)
|
||||
return commonDomains.some((allowed) => domain.includes(allowed))
|
||||
} catch {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export const linksNags: Nag[] = [
|
||||
{
|
||||
id: 'verify-external-links',
|
||||
title: 'Verify external links',
|
||||
description: () =>
|
||||
`Some of your external links may be using domains that aren't recognized as common for their link type.`,
|
||||
status: 'warning',
|
||||
shouldShow: (context: NagContext) => {
|
||||
return (
|
||||
!isCommonUrl(context.project.source_url, commonLinkDomains.source) ||
|
||||
!isCommonUrl(context.project.issues_url, commonLinkDomains.issues) ||
|
||||
!isCommonUrl(context.project.discord_url, commonLinkDomains.discord)
|
||||
)
|
||||
},
|
||||
link: {
|
||||
path: 'settings/links',
|
||||
title: 'Visit links settings',
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-links',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'gpl-license-source-required',
|
||||
title: 'GPL license requires source',
|
||||
description: (context: NagContext) =>
|
||||
`Your ${formatProjectType(context.project.project_type).toLowerCase()} uses a GPL license which requires source code to be available. Please provide a source code link or consider using a different license.`,
|
||||
status: 'required',
|
||||
shouldShow: (context: NagContext) => {
|
||||
const gplLicenses = [
|
||||
'GPL-2.0',
|
||||
'GPL-2.0+',
|
||||
'GPL-2.0-only',
|
||||
'GPL-2.0-or-later',
|
||||
'GPL-3.0',
|
||||
'GPL-3.0+',
|
||||
'GPL-3.0-only',
|
||||
'GPL-3.0-or-later',
|
||||
'LGPL-2.1',
|
||||
'LGPL-2.1+',
|
||||
'LGPL-2.1-only',
|
||||
'LGPL-2.1-or-later',
|
||||
'LGPL-3.0',
|
||||
'LGPL-3.0+',
|
||||
'LGPL-3.0-only',
|
||||
'LGPL-3.0-or-later',
|
||||
'AGPL-3.0',
|
||||
'AGPL-3.0+',
|
||||
'AGPL-3.0-only',
|
||||
'AGPL-3.0-or-later',
|
||||
]
|
||||
|
||||
const isGplLicense = gplLicenses.includes(context.project.license.id)
|
||||
const hasSourceUrl = !!context.project.source_url
|
||||
|
||||
return isGplLicense && !hasSourceUrl
|
||||
},
|
||||
link: {
|
||||
path: 'settings/links',
|
||||
title: 'Visit links settings',
|
||||
shouldShow: (context: NagContext) => context.currentRoute !== 'type-id-settings-links',
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -5,6 +5,7 @@ export * from './types/keybinds'
|
||||
export * from './types/nags'
|
||||
export * from './utils'
|
||||
|
||||
export * from './data/nags/index'
|
||||
export { default as checklist } from './data/checklist'
|
||||
export { default as keybinds } from './data/keybinds'
|
||||
export { default as nags } from './data/nags'
|
||||
Reference in new issue
Block a user