fix: description nag

This commit is contained in:
IMB11 2025-07-29 13:23:16 +01:00
parent dc258de3c2
commit ad1fed91cf
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 = 500
export const MAX_HEADER_LENGTH = 100 export const MAX_HEADER_LENGTH = 100
export const MIN_SUMMARY_CHARS = 35 export const MIN_SUMMARY_CHARS = 35
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, '')
@ -35,7 +39,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, '')
@ -68,6 +75,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',
@ -77,23 +103,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. It's recommended to have at least {minChars} characters to provide users with enough information about your project.", "Your description is {length} readable characters. It's recommended to have at least {minChars} readable characters to provide users with enough information about your project.",
}), }),
{ {
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',