forked from npmx-dev/npmx.dev
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMarkdownText.vue
More file actions
111 lines (93 loc) · 4.1 KB
/
MarkdownText.vue
File metadata and controls
111 lines (93 loc) · 4.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
<script setup lang="ts">
import { decodeHtmlEntities } from '~/utils/formatters'
const props = defineProps<{
text: string
/** When true, renders link text without the anchor tag (useful when inside another link) */
plain?: boolean
/** Package name to strip from the beginning of the description (if present) */
packageName?: string
}>()
// Strip markdown image badges from text
function stripMarkdownImages(text: string): string {
// Remove linked images: [](link-url) - handles incomplete URLs too
// Using {0,500} instead of * to prevent ReDoS on pathological inputs
text = text.replace(/\[!\[[^\]]{0,500}\]\([^)]{0,2000}\)\]\([^)]{0,2000}\)?/g, '')
// Remove standalone images: 
text = text.replace(/!\[[^\]]{0,500}\]\([^)]{0,2000}\)/g, '')
// Remove any leftover empty links or broken markdown link syntax
text = text.replace(/\[\]\([^)]{0,2000}\)?/g, '')
return text.trim()
}
// Strip HTML tags and escape remaining HTML to prevent XSS
function stripAndEscapeHtml(text: string): string {
// First decode any HTML entities in the input
let stripped = decodeHtmlEntities(text)
// Check if original text has HTML tags or markdown images BEFORE stripping
// Only strip package name for these "badge-style" descriptions
const hasHtmlTags = /<\/?[a-z][^>]*>/i.test(stripped)
const hasMarkdownImages = /!\[[^\]]*\]\([^)]*\)/.test(stripped)
// Then strip markdown image badges
stripped = stripMarkdownImages(stripped)
// Then strip actual HTML tags (keep their text content)
// Only match tags that start with a letter or / (to avoid matching things like "a < b > c")
stripped = stripped.replace(/<\/?[a-z][^>]*>/gi, '')
// Only strip package name if original text had HTML tags or markdown images
// Normal descriptions like "Nuxt is a framework..." should keep the package name
if ((hasHtmlTags || hasMarkdownImages) && props.packageName) {
// Trim first to handle leading/trailing whitespace from stripped HTML
stripped = stripped.trim()
// Collapse multiple whitespace into single space
stripped = stripped.replace(/\s+/g, ' ')
// Escape special regex characters in package name
const escapedName = props.packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
// Match package name at the start, optionally followed by: space, dash, colon, hyphen, or just space
const namePattern = new RegExp(`^${escapedName}\\s*[-:—]?\\s*`, 'i')
stripped = stripped.replace(namePattern, '').trim()
}
// Then escape any remaining HTML entities
return stripped
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
}
// Parse simple inline markdown to HTML
function parseMarkdown(text: string): string {
if (!text) return ''
// First strip HTML tags and escape remaining HTML
let html = stripAndEscapeHtml(text)
// Bold: **text** or __text__
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
html = html.replace(/__(.+?)__/g, '<strong>$1</strong>')
// Italic: *text* or _text_
html = html.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, '<em>$1</em>')
html = html.replace(/\b_(.+?)_\b/g, '<em>$1</em>')
// Inline code: `code`
html = html.replace(/`([^`]+)`/g, '<code>$1</code>')
// Strikethrough: ~~text~~
html = html.replace(/~~(.+?)~~/g, '<del>$1</del>')
// Links: [text](url) - only allow https, mailto
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => {
// In plain mode, just render the link text without the anchor
if (props.plain) {
return text
}
const decodedUrl = url.replace(/&/g, '&')
try {
const { protocol, href } = new URL(decodedUrl)
if (['https:', 'mailto:'].includes(protocol)) {
const safeUrl = href.replace(/"/g, '"')
return `<a href="${safeUrl}" rel="nofollow noreferrer noopener" target="_blank">${text}</a>`
}
} catch {}
return `${text} (${url})`
})
return html
}
const html = computed(() => parseMarkdown(props.text))
</script>
<template>
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-html="html" />
</template>