···11-<script setup lang="ts">
21import { decodeHtmlEntities } from '~/utils/formatters'
3244-const props = defineProps<{
33+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
1010-}>()
99+}
1010+1111+/** @public */
1212+export function useMarkdown(options: MaybeRefOrGetter<UseMarkdownOptions>) {
1313+ return computed(() => parseMarkdown(toValue(options)))
1414+}
11151216// Strip markdown image badges from text
1317function stripMarkdownImages(text: string): string {
···2226}
23272428// Strip HTML tags and escape remaining HTML to prevent XSS
2525-function stripAndEscapeHtml(text: string): string {
2929+function stripAndEscapeHtml(text: string, packageName?: string): string {
2630 // First decode any HTML entities in the input
2731 let stripped = decodeHtmlEntities(text)
2832···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, '')
35393636- if (props.packageName) {
4040+ 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
4242- const escapedName = props.packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
4646+ 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()
···5559}
56605761// Parse simple inline markdown to HTML
5858-function parseMarkdown(text: string): string {
6262+function parseMarkdown({ text, packageName, plain }: UseMarkdownOptions): string {
5963 if (!text) return ''
60646165 // First strip HTML tags and escape remaining HTML
6262- let html = stripAndEscapeHtml(text)
6666+ let html = stripAndEscapeHtml(text, packageName)
63676468 // Bold: **text** or __text__
6569 html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
···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
8181- if (props.plain) {
8585+ if (plain) {
8286 return text
8387 }
8488 const decodedUrl = url.replace(/&/g, '&')
···94989599 return html
96100}
9797-9898-const html = computed(() => parseMarkdown(props.text))
9999-</script>
100100-101101-<template>
102102- <!-- eslint-disable-next-line vue/no-v-html -->
103103- <span v-html="html" />
104104-</template>