fix(markdown): require double-tilde strikethrough delimiters

This commit is contained in:
Armin Ronacher
2026-04-16 12:32:51 +02:00
parent 624a7f794f
commit db5274b48c
3 changed files with 65 additions and 2 deletions

View File

@@ -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) {

View File

@@ -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[] = [];

View File

@@ -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);