From db5274b48c89329a27ba74cb89fe0fbbea353efb Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 16 Apr 2026 12:32:51 +0200 Subject: [PATCH] fix(markdown): require double-tilde strikethrough delimiters --- .../src/core/export-html/template.js | 14 ++++++++++ packages/tui/src/components/markdown.ts | 28 +++++++++++++++++-- packages/tui/test/markdown.test.ts | 25 +++++++++++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/src/core/export-html/template.js b/packages/coding-agent/src/core/export-html/template.js index 6b53516b..57bdb811 100644 --- a/packages/coding-agent/src/core/export-html/template.js +++ b/packages/coding-agent/src/core/export-html/template.js @@ -1448,9 +1448,23 @@ } // Configure marked with syntax highlighting and HTML escaping for text + const strictStrikethroughRegex = /^(~~)(?=[^\s~])((?:\\.|[^\\])*?(?:\\.|[^\s~\\]))\1(?=[^~]|$)/; + marked.use({ breaks: true, gfm: true, + tokenizer: { + del(src) { + const match = strictStrikethroughRegex.exec(src); + if (!match) return undefined; + return { + type: 'del', + raw: match[0], + text: match[2], + tokens: this.lexer.inlineTokens(match[2]) + }; + } + }, renderer: { // Code blocks: syntax highlight, no HTML escaping code(token) { diff --git a/packages/tui/src/components/markdown.ts b/packages/tui/src/components/markdown.ts index 3cdf6373..4399ee53 100644 --- a/packages/tui/src/components/markdown.ts +++ b/packages/tui/src/components/markdown.ts @@ -1,8 +1,32 @@ -import { marked, type Token } from "marked"; +import { Marked, type Token, Tokenizer, type Tokens } from "marked"; import { isImageLine } from "../terminal-image.js"; import type { Component } from "../tui.js"; import { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from "../utils.js"; +const STRICT_STRIKETHROUGH_REGEX = /^(~~)(?=[^\s~])((?:\\.|[^\\])*?(?:\\.|[^\s~\\]))\1(?=[^~]|$)/; + +class StrictStrikethroughTokenizer extends Tokenizer { + override del(src: string): Tokens.Del | undefined { + const match = STRICT_STRIKETHROUGH_REGEX.exec(src); + if (!match) { + return undefined; + } + + const text = match[2]; + return { + type: "del", + raw: match[0], + text, + tokens: this.lexer.inlineTokens(text), + }; + } +} + +const markdownParser = new Marked(); +markdownParser.setOptions({ + tokenizer: new StrictStrikethroughTokenizer(), +}); + /** * Default text styling for markdown content. * Applied to all text unless overridden by markdown formatting. @@ -112,7 +136,7 @@ export class Markdown implements Component { const normalizedText = this.text.replace(/\t/g, " "); // Parse markdown to HTML-like tokens - const tokens = marked.lexer(normalizedText); + const tokens = markdownParser.lexer(normalizedText); // Convert tokens to styled terminal output const renderedLines: string[] = []; diff --git a/packages/tui/test/markdown.test.ts b/packages/tui/test/markdown.test.ts index e8f0d023..fd265187 100644 --- a/packages/tui/test/markdown.test.ts +++ b/packages/tui/test/markdown.test.ts @@ -1061,6 +1061,31 @@ bar`, }); }); + describe("Strikethrough syntax", () => { + it("should render ~~text~~ as strikethrough", () => { + const markdown = new Markdown("Use ~~strikethrough~~ here", 0, 0, defaultMarkdownTheme); + + const lines = markdown.render(80); + const joinedOutput = lines.join("\n"); + const joinedPlain = lines.map((line) => line.replace(/\x1b\[[0-9;]*m/g, "")).join(" "); + + assert.ok(joinedOutput.includes("\x1b[9m"), "Should apply strikethrough styling"); + assert.ok(joinedPlain.includes("strikethrough"), "Should include struck text content"); + assert.ok(!joinedPlain.includes("~~strikethrough~~"), "Should not render delimiters as text"); + }); + + it("should keep ~text~ as plain text", () => { + const markdown = new Markdown("Use ~strikethrough~ literally", 0, 0, defaultMarkdownTheme); + + const lines = markdown.render(80); + const joinedOutput = lines.join("\n"); + const joinedPlain = lines.map((line) => line.replace(/\x1b\[[0-9;]*m/g, "")).join(" "); + + assert.ok(joinedPlain.includes("~strikethrough~"), "Single-tilde delimiters should remain visible"); + assert.ok(!joinedOutput.includes("\x1b[9m"), "Single-tilde text should not use strikethrough styling"); + }); + }); + describe("Links", () => { it("should not duplicate URL for autolinked emails", () => { const markdown = new Markdown("Contact user@example.com for help", 0, 0, defaultMarkdownTheme);