Skip to content

Commit 8e7e77f

Browse files
betegonclaude
andauthored
fix(formatters): fix HTML entities and escaped underscores in table output (#313)
## Summary Fixes two rendering bugs introduced in `efb59ba` (the markdown rendering pipeline): 1. **`<unknown>` displays as `&lt;unknown&gt;`** — `escapeMarkdownInline` used HTML entities to escape angle brackets, but `marked` keeps them as-is in text tokens, so they showed up literally in terminal output. 2. **`/_astro/` displays as `/\_astro/`** — escaped underscores in URLs got captured by GFM autolink detection, which doesn't process backslash escapes, leaving the backslash visible. ## Changes - `escapeMarkdownInline` / `escapeMarkdownCell`: use CommonMark backslash escapes (`\<`/`\>`) instead of HTML entities. `marked` correctly parses these as `escape` tokens with the decoded character. - `renderOneInline` link case: detect GFM autolinks via `link.raw` (starts with URL scheme, not `[`) and strip backslash-escape artifacts from both link text and href. - Tests: added angle bracket escape tests for both functions, plus round-trip tests verifying `<unknown>` and URLs with underscores render correctly through the full pipeline. ## Test plan - `bun test test/lib/formatters/markdown.test.ts` — 77 tests pass (3 new) - Manual: `bun src/bin.ts issue list` against projects with `<unknown>` titles and URL-containing titles --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent efb59ba commit 8e7e77f

File tree

2 files changed

+47
-10
lines changed

2 files changed

+47
-10
lines changed

src/lib/formatters/markdown.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export function isPlainOutput(): boolean {
7878
* Escape a string for safe use inside a markdown table cell.
7979
*
8080
* Collapses newlines, escapes backslashes, pipes, and angle brackets.
81-
* Angle brackets must be escaped to `&lt;`/`&gt;` so that user-supplied
81+
* Angle brackets are backslash-escaped (`\<`/`\>`) so that user-supplied
8282
* content (e.g. `Expected <string>`) is not parsed as an HTML tag by
8383
* `marked`, which would silently drop the content via `renderHtmlToken`.
8484
*/
@@ -87,17 +87,17 @@ export function escapeMarkdownCell(value: string): string {
8787
.replace(/\n/g, " ")
8888
.replace(/\\/g, "\\\\")
8989
.replace(/\|/g, "\\|")
90-
.replace(/</g, "&lt;")
91-
.replace(/>/g, "&gt;");
90+
.replace(/</g, "\\<")
91+
.replace(/>/g, "\\>");
9292
}
9393

9494
/**
9595
* Escape CommonMark inline emphasis characters.
9696
*
9797
* Prevents `_`, `*`, `` ` ``, `[`, `]`, `<`, and `>` from being consumed
98-
* by the parser. Angle brackets are HTML-escaped so that user-supplied
99-
* content (e.g. `Expected <string> got <number>`) is not silently dropped
100-
* when `marked` parses the text as HTML tokens.
98+
* by the parser. Angle brackets are backslash-escaped (`\<`/`\>`) so that
99+
* user-supplied content (e.g. `Expected <string> got <number>`) is not
100+
* silently dropped when `marked` parses the text as HTML tokens.
101101
*/
102102
export function escapeMarkdownInline(value: string): string {
103103
return value
@@ -107,8 +107,8 @@ export function escapeMarkdownInline(value: string): string {
107107
.replace(/`/g, "\\`")
108108
.replace(/\[/g, "\\[")
109109
.replace(/\]/g, "\\]")
110-
.replace(/</g, "&lt;")
111-
.replace(/>/g, "&gt;");
110+
.replace(/</g, "\\<")
111+
.replace(/>/g, "\\>");
112112
}
113113

114114
/**
@@ -320,9 +320,19 @@ function renderOneInline(token: Token): string {
320320
return chalk.hex(COLORS.yellow)((token as Tokens.Codespan).text);
321321
case "link": {
322322
const link = token as Tokens.Link;
323-
const linkText = renderInline(link.tokens);
323+
let linkText = renderInline(link.tokens);
324+
let href = link.href ?? "";
325+
// GFM autolinks absorb backslash-escapes literally (\_astro → \_astro).
326+
// Detect autolinks via link.raw (starts with URL, not "[") and strip artifacts.
327+
const raw = link.raw ?? "";
328+
if (raw.startsWith("http://") || raw.startsWith("https://")) {
329+
const stripEscapes = (s: string) =>
330+
s.replace(/\\([_*`[\]<>\\])/g, "$1");
331+
linkText = stripEscapes(linkText);
332+
href = stripEscapes(href);
333+
}
324334
const styled = chalk.hex(COLORS.blue)(linkText);
325-
return link.href ? terminalLink(styled, link.href) : styled;
335+
return href ? terminalLink(styled, href) : styled;
326336
}
327337
case "del":
328338
return chalk.dim.gray.strikethrough(

test/lib/formatters/markdown.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,10 @@ describe("escapeMarkdownCell", () => {
313313
const result = escapeMarkdownCell("a|b|c");
314314
expect(result).toBe("a\\|b\\|c");
315315
});
316+
317+
test("escapes angle brackets with backslash", () => {
318+
expect(escapeMarkdownCell("<html>")).toBe("\\<html\\>");
319+
});
316320
});
317321

318322
// ---------------------------------------------------------------------------
@@ -479,6 +483,29 @@ describe("escapeMarkdownInline", () => {
479483
test("returns unchanged string with no special chars", () => {
480484
expect(escapeMarkdownInline("hello world")).toBe("hello world");
481485
});
486+
487+
test("escapes angle brackets with backslash", () => {
488+
expect(escapeMarkdownInline("<unknown>")).toBe("\\<unknown\\>");
489+
});
490+
491+
test("angle brackets survive round-trip through renderInlineMarkdown", () => {
492+
withEnv({ SENTRY_PLAIN_OUTPUT: "0", NO_COLOR: undefined }, false, () => {
493+
const escaped = escapeMarkdownInline("<unknown>");
494+
const result = stripAnsi(renderInlineMarkdown(escaped));
495+
expect(result).toBe("<unknown>");
496+
});
497+
});
498+
499+
test("URLs with underscores render without backslashes", () => {
500+
withEnv({ SENTRY_PLAIN_OUTPUT: "0", NO_COLOR: undefined }, false, () => {
501+
const escaped = escapeMarkdownInline(
502+
"https://spotlightjs.com/_astro/ui-core.js"
503+
);
504+
const result = stripAnsi(renderInlineMarkdown(escaped));
505+
expect(result).not.toContain("\\_");
506+
expect(result).toContain("_astro");
507+
});
508+
});
482509
});
483510

484511
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)