Skip to content

Commit ce3ea2d

Browse files
committed
Escape inline markdown output
1 parent e7a760c commit ce3ea2d

File tree

1 file changed

+36
-20
lines changed

1 file changed

+36
-20
lines changed

src/lib/markdown.ts

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,53 @@
11
export function renderInlineMarkdown(text: string): string {
22
if (!text) return "";
33

4-
// Helper function to escape HTML entities
5-
const escapeHtml = (str: string): string => {
6-
return str
4+
const escapeHtml = (str: string): string =>
5+
str
76
.replace(/&/g, "&")
87
.replace(/</g, "&lt;")
98
.replace(/>/g, "&gt;")
109
.replace(/"/g, "&quot;")
1110
.replace(/'/g, "&#039;");
12-
};
1311

14-
// Process inline markdown patterns
15-
let html = text
16-
// Code: `text` - escape content inside backticks, then wrap in <code>
17-
.replace(/`([^`]+)`/g, (_, content) => `<code>${escapeHtml(content)}</code>`)
12+
const pattern = /(`([^`]+)`)|(\*\*([^*]+)\*\*)|(__([^_]+)__)|(~~([^~]+)~~)|(?<!\*)\*([^*]+)\*(?!\*)|(?<!_)_([^_]+)_(?!_)/g;
1813

19-
// Bold: **text** or __text__ - escape content, then wrap in <strong>
20-
.replace(/\*\*([^*]+)\*\*/g, (_, content) => `<strong>${escapeHtml(content)}</strong>`)
21-
.replace(/__([^_]+)__/g, (_, content) => `<strong>${escapeHtml(content)}</strong>`)
14+
let result = "";
15+
let lastIndex = 0;
2216

23-
// Italic: *text* or _text_ (but not part of bold) - escape content, then wrap in <em>
24-
.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, (_, content) => `<em>${escapeHtml(content)}</em>`)
25-
.replace(/(?<!_)_([^_]+)_(?!_)/g, (_, content) => `<em>${escapeHtml(content)}</em>`)
17+
for (const match of text.matchAll(pattern)) {
18+
const matchIndex = match.index ?? 0;
19+
if (matchIndex > lastIndex) {
20+
result += escapeHtml(text.slice(lastIndex, matchIndex));
21+
}
2622

27-
// Strikethrough: ~~text~~ - escape content, then wrap in <del>
28-
.replace(/~~([^~]+)~~/g, (_, content) => `<del>${escapeHtml(content)}</del>`);
23+
if (match[1]) {
24+
// `code`
25+
result += `<code>${escapeHtml(match[2] ?? "")}</code>`;
26+
} else if (match[3]) {
27+
// **bold**
28+
result += `<strong>${escapeHtml(match[4] ?? "")}</strong>`;
29+
} else if (match[5]) {
30+
// __bold__
31+
result += `<strong>${escapeHtml(match[6] ?? "")}</strong>`;
32+
} else if (match[7]) {
33+
// ~~strikethrough~~
34+
result += `<del>${escapeHtml(match[8] ?? "")}</del>`;
35+
} else if (match[9]) {
36+
// *italic*
37+
result += `<em>${escapeHtml(match[10] ?? "")}</em>`;
38+
} else if (match[11]) {
39+
// _italic_
40+
result += `<em>${escapeHtml(match[12] ?? "")}</em>`;
41+
}
2942

30-
// Escape any remaining unprocessed text (text outside of markdown patterns)
31-
// This is tricky because we need to avoid escaping the HTML we just created
32-
// For now, we'll leave plain text unescaped since Astro should handle it
43+
lastIndex = matchIndex + match[0].length;
44+
}
3345

34-
return html;
46+
if (lastIndex < text.length) {
47+
result += escapeHtml(text.slice(lastIndex));
48+
}
49+
50+
return result;
3551
}
3652

3753
/**

0 commit comments

Comments
 (0)