Merge branch 'cal/dev-124-project-validation' of https://github.com/modrinth/code into cal/dev-124-project-validation

This commit is contained in:
coolbot100s 2025-07-29 13:30:25 -07:00
commit b5f44ac5cd
2 changed files with 36 additions and 9 deletions

View File

@ -43,7 +43,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { SaveIcon, TriangleAlertIcon } from "@modrinth/assets"; import { SaveIcon, TriangleAlertIcon } from "@modrinth/assets";
import { MIN_DESCRIPTION_CHARS } from "@modrinth/moderation"; import { countText, MIN_DESCRIPTION_CHARS } from "@modrinth/moderation";
import { MarkdownEditor } from "@modrinth/ui"; import { MarkdownEditor } from "@modrinth/ui";
import { type Project, type TeamMember, TeamMemberPermission } from "@modrinth/utils"; import { type Project, type TeamMember, TeamMemberPermission } from "@modrinth/utils";
import { computed, ref } from "vue"; import { computed, ref } from "vue";
@ -60,10 +60,10 @@ const description = ref(props.project.body);
const descriptionWarning = computed(() => { const descriptionWarning = computed(() => {
const text = description.value?.trim() || ""; const text = description.value?.trim() || "";
const charCount = text.length; const charCount = countText(text);
if (charCount < MIN_DESCRIPTION_CHARS) { if (charCount < MIN_DESCRIPTION_CHARS) {
return `It's recommended to have a description with at least ${MIN_DESCRIPTION_CHARS} characters. (${charCount}/${MIN_DESCRIPTION_CHARS})`; return `It's recommended to have a description with at least ${MIN_DESCRIPTION_CHARS} readable characters. (${charCount}/${MIN_DESCRIPTION_CHARS})`;
} }
return null; return null;

View File

@ -1,3 +1,4 @@
import { renderHighlightedString } from '@modrinth/utils'
import type { Nag, NagContext } from '../../types/nags' import type { Nag, NagContext } from '../../types/nags'
import { useVIntl, defineMessage } from '@vintl/vintl' import { useVIntl, defineMessage } from '@vintl/vintl'
@ -5,7 +6,10 @@ export const MIN_DESCRIPTION_CHARS = 200
export const MAX_HEADER_LENGTH = 80 export const MAX_HEADER_LENGTH = 80
export const MIN_SUMMARY_CHARS = 30 export const MIN_SUMMARY_CHARS = 30
function analyzeHeaderLength(markdown: string): { hasLongHeaders: boolean; longHeaders: string[] } { export function analyzeHeaderLength(markdown: string): {
hasLongHeaders: boolean
longHeaders: string[]
} {
if (!markdown) return { hasLongHeaders: false, longHeaders: [] } if (!markdown) return { hasLongHeaders: false, longHeaders: [] }
const withoutCodeBlocks = markdown.replace(/```[\s\S]*?```/g, '').replace(/`[^`]*`/g, '') const withoutCodeBlocks = markdown.replace(/```[\s\S]*?```/g, '').replace(/`[^`]*`/g, '')
@ -34,7 +38,10 @@ function analyzeHeaderLength(markdown: string): { hasLongHeaders: boolean; longH
} }
} }
function analyzeImageContent(markdown: string): { imageHeavy: boolean; hasEmptyAltText: boolean } { export function analyzeImageContent(markdown: string): {
imageHeavy: boolean
hasEmptyAltText: boolean
} {
if (!markdown) return { imageHeavy: false, hasEmptyAltText: false } if (!markdown) return { imageHeavy: false, hasEmptyAltText: false }
const withoutCodeBlocks = markdown.replace(/```[\s\S]*?```/g, '').replace(/`[^`]*`/g, '') const withoutCodeBlocks = markdown.replace(/```[\s\S]*?```/g, '').replace(/`[^`]*`/g, '')
@ -67,6 +74,25 @@ function analyzeImageContent(markdown: string): { imageHeavy: boolean; hasEmptyA
return { imageHeavy, hasEmptyAltText } return { imageHeavy, hasEmptyAltText }
} }
export function countText(markdown: string): number {
const htmlString = renderHighlightedString(markdown)
const parser = new DOMParser()
const doc = parser.parseFromString(htmlString, 'text/html')
const walker = document.createTreeWalker(doc, NodeFilter.SHOW_TEXT)
const textList: string[] = []
let currentNode: Node | null = walker.currentNode
while (currentNode) {
if (currentNode.textContent !== null) {
textList.push(currentNode.textContent)
}
currentNode = walker.nextNode()
}
return textList.join(' ').trim().length
}
export const descriptionNags: Nag[] = [ export const descriptionNags: Nag[] = [
{ {
id: 'description-too-short', id: 'description-too-short',
@ -76,23 +102,24 @@ export const descriptionNags: Nag[] = [
}), }),
description: (context: NagContext) => { description: (context: NagContext) => {
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const readableLength = countText(context.project.body || '')
return formatMessage( return formatMessage(
defineMessage({ defineMessage({
id: 'nags.description-too-short.description', id: 'nags.description-too-short.description',
defaultMessage: defaultMessage:
'Your description is {length} characters. At least {minChars} characters is recommended to create a clear and informative Description.', 'Your description is {length} readable characters. At least {minChars} characters is recommended to create a clear and informative Description.',
}), }),
{ {
length: context.project.body?.length || 0, length: readableLength,
minChars: MIN_DESCRIPTION_CHARS, minChars: MIN_DESCRIPTION_CHARS,
}, },
) )
}, },
status: 'warning', status: 'warning',
shouldShow: (context: NagContext) => { shouldShow: (context: NagContext) => {
const bodyLength = context.project.body?.trim()?.length || 0 const readableLength = countText(context.project.body || '')
return bodyLength < MIN_DESCRIPTION_CHARS && bodyLength !== 0 return readableLength < MIN_DESCRIPTION_CHARS && readableLength > 0
}, },
link: { link: {
path: 'settings/description', path: 'settings/description',