mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(tui): hide HTML comments in Markdown rendering (#11755)
Fixes #11710. ## Problem The tui `Markdown` component printed `<!-- ... -->` comments verbatim. marked lexes them as `html` tokens and both the block and inline `html` cases pushed `token.raw` as text, so comments leaked into every Markdown surface (Extension Updates, What's New, `/changelog`, messages). ``` ## [1.0.0] <!-- release-notes:start --> Fixed a bug. See <!-- todo: link --> the docs. ``` rendered both comments. ## Fix - After lexing (next to `trimPartialClosingFences`, before tokens are cached), remove block `html` tokens that contain only comments, recursing into lists and blockquotes. If the removed comment sat between two blank lines, or at the start/end, one blank line is dropped too, so no extra gap remains. - Block HTML with other content keeps rendering as text, with comments stripped (`<!-- x --> text` renders `text`). - Inline comments are skipped; if text before ended with a space, the next text's leading space is dropped (`See <!-- c --> the docs.` renders `See the docs.`). - An unclosed block comment runs to the end of the block (CommonMark), so streamed comments stay hidden. - Other HTML still renders as text (`with <tag> markers`). Parsing stays in marked; the change only filters tokens marked already produced. Cost: one pass over top-level tokens per lex (cached), a regex on `html` tokens only. ## Tests `packages/tui/test/markdown.test.ts`, "HTML comments": compares rendering with comments against the same Markdown without them (issue repro, blank-line handling, multi-line/unclosed, blockquotes and lists, other HTML kept). This PR is AI-generated.
This commit is contained in:
parent
ace560c302
commit
54280c4ff0
3 changed files with 93 additions and 5 deletions
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed Markdown rendering HTML comments (`<!-- ... -->`) as visible text ([#11710](https://github.com/earendil-works/pi/issues/11710))
|
||||
|
||||
## [1.5.0] - 2026-11-02
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -197,6 +197,36 @@ function trimPartialClosingFences(tokens: readonly Token[]): void {
|
|||
token.text = token.text.slice(0, -lastLine.length).replace(/\n$/, "");
|
||||
}
|
||||
|
||||
// An unclosed comment runs to the end of an HTML block, as in CommonMark, so streamed comments stay hidden.
|
||||
const HTML_COMMENT_REGEX = /<!--[\s\S]*?(?:-->|$)/g;
|
||||
|
||||
function stripHtmlComments(html: string): string {
|
||||
return html.replace(HTML_COMMENT_REGEX, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove block HTML tokens that hold only comments, so they render no line. A removed comment between two blank
|
||||
* lines, or at the start or end, also drops a blank line, so it leaves no extra gap.
|
||||
* See https://github.com/earendil-works/pi/issues/11710.
|
||||
*/
|
||||
function removeHtmlCommentBlocks(tokens: Token[]): void {
|
||||
for (let i = tokens.length - 1; i >= 0; i--) {
|
||||
const token = tokens[i];
|
||||
if (token.type === "list") {
|
||||
for (const item of token.items) removeHtmlCommentBlocks(item.tokens);
|
||||
} else if (token.type === "blockquote") {
|
||||
removeHtmlCommentBlocks(token.tokens ?? []);
|
||||
} else if (token.type === "html" && stripHtmlComments(token.raw).trim() === "") {
|
||||
tokens.splice(i, 1);
|
||||
if (tokens[i]?.type === "space" && (i === 0 || tokens[i - 1]?.type === "space")) {
|
||||
tokens.splice(i, 1);
|
||||
} else if (i === tokens.length && tokens[i - 1]?.type === "space") {
|
||||
tokens.splice(--i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const markdownParser = new Marked();
|
||||
markdownParser.setOptions({
|
||||
tokenizer: new MarkdownTokenizer(),
|
||||
|
|
@ -365,6 +395,7 @@ export class Markdown implements Component {
|
|||
if (!tokens) {
|
||||
tokens = markdownParser.lexer(normalizedText);
|
||||
trimPartialClosingFences(tokens);
|
||||
removeHtmlCommentBlocks(tokens);
|
||||
this.cachedTokens = new WeakRef({ source: normalizedText, tokens });
|
||||
}
|
||||
|
||||
|
|
@ -715,9 +746,9 @@ export class Markdown implements Component {
|
|||
break;
|
||||
|
||||
case "html":
|
||||
// Render HTML as plain text (escaped for terminal)
|
||||
// Render HTML as plain text (escaped for terminal), without its comments
|
||||
if ("raw" in token && typeof token.raw === "string") {
|
||||
lines.push(this.applyDefaultStyle(token.raw.trim()));
|
||||
lines.push(this.applyDefaultStyle(stripHtmlComments(token.raw).trim()));
|
||||
}
|
||||
break;
|
||||
|
||||
|
|
@ -745,7 +776,15 @@ export class Markdown implements Component {
|
|||
return segments.map((segment: string) => applyText(segment)).join("\n");
|
||||
};
|
||||
|
||||
for (const token of tokens) {
|
||||
// After a skipped HTML comment, drop the next text's leading space if the text before ended with one,
|
||||
// so "a <!-- c --> b" renders as "a b".
|
||||
let dropLeadingSpace = false;
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
let token = tokens[i];
|
||||
if (dropLeadingSpace && token.type === "text" && !token.tokens?.length && /^[ \t]/.test(token.text)) {
|
||||
token = { ...token, text: token.text.replace(/^[ \t]+/, "") };
|
||||
}
|
||||
dropLeadingSpace = false;
|
||||
switch (token.type) {
|
||||
case "latex": {
|
||||
const latexToken = token as LatexToken;
|
||||
|
|
@ -824,8 +863,10 @@ export class Markdown implements Component {
|
|||
}
|
||||
|
||||
case "html":
|
||||
// Render inline HTML as plain text
|
||||
if ("raw" in token && typeof token.raw === "string") {
|
||||
// Render inline HTML as plain text; comments are hidden
|
||||
if (token.raw.startsWith("<!--")) {
|
||||
dropLeadingSpace = i === 0 || /[ \t]$/.test(tokens[i - 1].raw);
|
||||
} else {
|
||||
result += applyTextWithNewlines(token.raw);
|
||||
}
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -1973,6 +1973,49 @@ bar`,
|
|||
});
|
||||
});
|
||||
|
||||
describe("HTML comments", () => {
|
||||
// Regression tests for https://github.com/earendil-works/pi/issues/11710
|
||||
function renderPlain(text: string): string[] {
|
||||
return new Markdown(text, 0, 0, defaultMarkdownTheme).render(60).map((line) => stripAnsi(line).trimEnd());
|
||||
}
|
||||
|
||||
it("hides block and inline comments without extra spacing", () => {
|
||||
assert.deepStrictEqual(
|
||||
renderPlain(
|
||||
"## [1.0.0]\n\n<!-- release-notes:start -->\nFixed a bug.\n\nSee <!-- todo: link --> the docs.",
|
||||
),
|
||||
renderPlain("## [1.0.0]\n\nFixed a bug.\n\nSee the docs."),
|
||||
);
|
||||
});
|
||||
|
||||
it("drops one blank line around comment-only blocks", () => {
|
||||
assert.deepStrictEqual(
|
||||
renderPlain(
|
||||
"<!-- start -->\n\n## [1.0.0]\n\n<!-- a -->\n\n- item\n\n<!-- b -->\n\n## [0.9.0]\n\n<!-- end -->",
|
||||
),
|
||||
renderPlain("## [1.0.0]\n\n- item\n\n## [0.9.0]"),
|
||||
);
|
||||
});
|
||||
|
||||
it("hides multi-line and unclosed comments", () => {
|
||||
assert.deepStrictEqual(renderPlain("<!--\nmulti\nline\n-->\ntext"), renderPlain("text"));
|
||||
assert.deepStrictEqual(renderPlain("text\n\n<!-- streaming"), renderPlain("text"));
|
||||
});
|
||||
|
||||
it("hides comments in blockquotes and list items", () => {
|
||||
assert.deepStrictEqual(
|
||||
renderPlain("> q\n>\n> <!-- c -->\n>\n> r\n\n- a\n\n <!-- c -->\n\n b\n- <!-- c --> item"),
|
||||
renderPlain("> q\n>\n> r\n\n- a\n\n b\n- item"),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps other HTML and the text after a comment in a block", () => {
|
||||
assert.deepStrictEqual(renderPlain("with <tag> markers"), ["with <tag> markers"]);
|
||||
assert.deepStrictEqual(renderPlain("<!-- c --> trailing text"), ["trailing text"]);
|
||||
assert.deepStrictEqual(renderPlain("<div>\n<!-- c -->a</div>"), ["<div>", "a</div>"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Streaming code fences", () => {
|
||||
it("stabilizes partial closing fence rendering", () => {
|
||||
const cases = [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue