fix(markdown): require double-tilde strikethrough delimiters
This commit is contained in:
@@ -1448,9 +1448,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Configure marked with syntax highlighting and HTML escaping for text
|
// Configure marked with syntax highlighting and HTML escaping for text
|
||||||
|
const strictStrikethroughRegex = /^(~~)(?=[^\s~])((?:\\.|[^\\])*?(?:\\.|[^\s~\\]))\1(?=[^~]|$)/;
|
||||||
|
|
||||||
marked.use({
|
marked.use({
|
||||||
breaks: true,
|
breaks: true,
|
||||||
gfm: 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: {
|
renderer: {
|
||||||
// Code blocks: syntax highlight, no HTML escaping
|
// Code blocks: syntax highlight, no HTML escaping
|
||||||
code(token) {
|
code(token) {
|
||||||
|
|||||||
@@ -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 { isImageLine } from "../terminal-image.js";
|
||||||
import type { Component } from "../tui.js";
|
import type { Component } from "../tui.js";
|
||||||
import { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from "../utils.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.
|
* Default text styling for markdown content.
|
||||||
* Applied to all text unless overridden by markdown formatting.
|
* 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, " ");
|
const normalizedText = this.text.replace(/\t/g, " ");
|
||||||
|
|
||||||
// Parse markdown to HTML-like tokens
|
// Parse markdown to HTML-like tokens
|
||||||
const tokens = marked.lexer(normalizedText);
|
const tokens = markdownParser.lexer(normalizedText);
|
||||||
|
|
||||||
// Convert tokens to styled terminal output
|
// Convert tokens to styled terminal output
|
||||||
const renderedLines: string[] = [];
|
const renderedLines: string[] = [];
|
||||||
|
|||||||
@@ -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", () => {
|
describe("Links", () => {
|
||||||
it("should not duplicate URL for autolinked emails", () => {
|
it("should not duplicate URL for autolinked emails", () => {
|
||||||
const markdown = new Markdown("Contact user@example.com for help", 0, 0, defaultMarkdownTheme);
|
const markdown = new Markdown("Contact user@example.com for help", 0, 0, defaultMarkdownTheme);
|
||||||
|
|||||||
Reference in New Issue
Block a user