Skip to content

Commit 592982d

Browse files
DDeenisdanielroe
andauthored
fix: replace MarkdownText component with useMarkdown composable (#590)
Co-authored-by: Daniel Roe <daniel@roe.dev>
1 parent da2d3f2 commit 592982d

File tree

7 files changed

+358
-464
lines changed

7 files changed

+358
-464
lines changed

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ import { hasProtocol } from 'ufo'
187187

188188
| Type | Convention | Example |
189189
| ---------------- | ------------------------ | ------------------------------ |
190-
| Vue components | PascalCase | `MarkdownText.vue` |
190+
| Vue components | PascalCase | `DateTime.vue` |
191191
| Pages | kebab-case | `search.vue`, `[...name].vue` |
192192
| Composables | camelCase + `use` prefix | `useNpmRegistry.ts` |
193193
| Server routes | kebab-case + method | `search.get.ts` |

app/components/Package/Card.vue

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ const isExactMatch = computed(() => {
1919
const name = props.result.package.name.toLowerCase()
2020
return query === name
2121
})
22+
23+
// Process package description
24+
const pkgDescription = useMarkdown(() => ({
25+
text: props.result.package.description ?? '',
26+
plain: true,
27+
packageName: props.result.package.name,
28+
}))
2229
</script>
2330

2431
<template>
@@ -74,11 +81,8 @@ const isExactMatch = computed(() => {
7481
</div>
7582
<div class="flex justify-start items-start gap-4 sm:gap-8">
7683
<div class="min-w-0">
77-
<p
78-
v-if="result.package.description"
79-
class="text-fg-muted text-xs sm:text-sm line-clamp-2 mb-2 sm:mb-3"
80-
>
81-
<MarkdownText :text="result.package.description" plain />
84+
<p v-if="pkgDescription" class="text-fg-muted text-xs sm:text-sm line-clamp-2 mb-2 sm:mb-3">
85+
<span v-html="pkgDescription" />
8286
</p>
8387
<div class="flex flex-wrap items-center gap-x-3 sm:gap-x-4 gap-y-2 text-xs text-fg-subtle">
8488
<dl v-if="showPublisher || result.package.date" class="flex items-center gap-4 m-0">
Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1-
<script setup lang="ts">
21
import { decodeHtmlEntities } from '~/utils/formatters'
32

4-
const props = defineProps<{
3+
interface UseMarkdownOptions {
54
text: string
65
/** When true, renders link text without the anchor tag (useful when inside another link) */
76
plain?: boolean
87
/** Package name to strip from the beginning of the description (if present) */
98
packageName?: string
10-
}>()
9+
}
10+
11+
/** @public */
12+
export function useMarkdown(options: MaybeRefOrGetter<UseMarkdownOptions>) {
13+
return computed(() => parseMarkdown(toValue(options)))
14+
}
1115

1216
// Strip markdown image badges from text
1317
function stripMarkdownImages(text: string): string {
@@ -22,7 +26,7 @@ function stripMarkdownImages(text: string): string {
2226
}
2327

2428
// Strip HTML tags and escape remaining HTML to prevent XSS
25-
function stripAndEscapeHtml(text: string): string {
29+
function stripAndEscapeHtml(text: string, packageName?: string): string {
2630
// First decode any HTML entities in the input
2731
let stripped = decodeHtmlEntities(text)
2832

@@ -33,13 +37,13 @@ function stripAndEscapeHtml(text: string): string {
3337
// Only match tags that start with a letter or / (to avoid matching things like "a < b > c")
3438
stripped = stripped.replace(/<\/?[a-z][^>]*>/gi, '')
3539

36-
if (props.packageName) {
40+
if (packageName) {
3741
// Trim first to handle leading/trailing whitespace from stripped HTML
3842
stripped = stripped.trim()
3943
// Collapse multiple whitespace into single space
4044
stripped = stripped.replace(/\s+/g, ' ')
4145
// Escape special regex characters in package name
42-
const escapedName = props.packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
46+
const escapedName = packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
4347
// Match package name at the start, optionally followed by: space, dash, colon, hyphen, or just space
4448
const namePattern = new RegExp(`^${escapedName}\\s*[-:—]?\\s*`, 'i')
4549
stripped = stripped.replace(namePattern, '').trim()
@@ -55,11 +59,11 @@ function stripAndEscapeHtml(text: string): string {
5559
}
5660

5761
// Parse simple inline markdown to HTML
58-
function parseMarkdown(text: string): string {
62+
function parseMarkdown({ text, packageName, plain }: UseMarkdownOptions): string {
5963
if (!text) return ''
6064

6165
// First strip HTML tags and escape remaining HTML
62-
let html = stripAndEscapeHtml(text)
66+
let html = stripAndEscapeHtml(text, packageName)
6367

6468
// Bold: **text** or __text__
6569
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
@@ -78,7 +82,7 @@ function parseMarkdown(text: string): string {
7882
// Links: [text](url) - only allow https, mailto
7983
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => {
8084
// In plain mode, just render the link text without the anchor
81-
if (props.plain) {
85+
if (plain) {
8286
return text
8387
}
8488
const decodedUrl = url.replace(/&amp;/g, '&')
@@ -94,11 +98,3 @@ function parseMarkdown(text: string): string {
9498

9599
return html
96100
}
97-
98-
const html = computed(() => parseMarkdown(props.text))
99-
</script>
100-
101-
<template>
102-
<!-- eslint-disable-next-line vue/no-v-html -->
103-
<span v-html="html" />
104-
</template>

app/pages/[...package].vue

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,12 @@ const displayVersion = computed(() => {
122122
return pkg.value.versions[latestTag] ?? null
123123
})
124124
125+
// Process package description
126+
const pkgDescription = useMarkdown(() => ({
127+
text: pkg.value?.description ?? '',
128+
packageName: pkg.value?.name,
129+
}))
130+
125131
//copy package name
126132
const { copied: copiedPkgName, copy: copyPkgName } = useClipboard({
127133
source: packageName,
@@ -160,6 +166,10 @@ const deprecationNotice = computed(() => {
160166
return { type: 'version' as const, message: displayVersion.value.deprecated }
161167
})
162168
169+
const deprecationNoticeMessage = useMarkdown(() => ({
170+
text: deprecationNotice.value?.message ?? '',
171+
}))
172+
163173
const sizeTooltip = computed(() => {
164174
const chunks = [
165175
displayVersion.value &&
@@ -563,8 +573,8 @@ function handleClick(event: MouseEvent) {
563573
<div class="mb-4">
564574
<!-- Description container with min-height to prevent CLS -->
565575
<div class="max-w-2xl min-h-[4.5rem]">
566-
<p v-if="pkg.description" class="text-fg-muted text-base m-0">
567-
<MarkdownText :text="pkg.description" :package-name="pkg.name" />
576+
<p v-if="pkgDescription" class="text-fg-muted text-base m-0">
577+
<span v-html="pkgDescription" />
568578
</p>
569579
<p v-else class="text-fg-subtle text-base m-0 italic">
570580
{{ $t('package.no_description') }}
@@ -713,8 +723,8 @@ function handleClick(event: MouseEvent) {
713723
: $t('package.deprecation.version')
714724
}}
715725
</h2>
716-
<p v-if="deprecationNotice.message" class="text-base m-0">
717-
<MarkdownText :text="deprecationNotice.message" />
726+
<p v-if="deprecationNoticeMessage" class="text-base m-0">
727+
<span v-html="deprecationNoticeMessage" />
718728
</p>
719729
<p v-else class="text-base m-0 italic">
720730
{{ $t('package.deprecation.no_reason') }}

test/nuxt/a11y.spec.ts

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@ import {
7676
HeaderAccountMenu,
7777
LicenseDisplay,
7878
LoadingSpinner,
79-
MarkdownText,
8079
PackageChartModal,
8180
PackageClaimPackageModal,
8281
HeaderConnectorModal,
@@ -281,24 +280,6 @@ describe('component accessibility audits', () => {
281280
})
282281
})
283282

284-
describe('MarkdownText', () => {
285-
it('should have no accessibility violations with plain text', async () => {
286-
const component = await mountSuspended(MarkdownText, {
287-
props: { text: 'Simple text' },
288-
})
289-
const results = await runAxe(component)
290-
expect(results.violations).toEqual([])
291-
})
292-
293-
it('should have no accessibility violations with formatted text', async () => {
294-
const component = await mountSuspended(MarkdownText, {
295-
props: { text: '**Bold** and *italic* and `code`' },
296-
})
297-
const results = await runAxe(component)
298-
expect(results.violations).toEqual([])
299-
})
300-
})
301-
302283
describe('PackageSkeleton', () => {
303284
it('should have no accessibility violations', async () => {
304285
const component = await mountSuspended(PackageSkeleton)

0 commit comments

Comments
 (0)