import { assert, assertEquals } from "@std/assert"; import { Lexer } from "marked"; import { buildView } from "../lib/view/mod.ts"; import { Session } from "../lib/view/session.ts"; import type { Key } from "../lib/view/keys.ts"; import type { Line } from "../lib/view/model.ts"; import { _internal as markdownInternals, renderMarkdownLines, } from "../lib/view/languages/markdown/markdown.ts"; import { parseDiff } from "../lib/view/diff.ts"; import { _internal as diffDocumentInternals, buildDiffDocument, type DiffWorkspace, type WorkspaceCache, } from "../lib/view/diffdoc.ts"; import { diffSource } from "../lib/view/diffedit.ts"; import { renderLineColored } from "../lib/view/highlight.ts"; const MARKDOWN = [ "# **Title** &", "", "> A *quote* with [link](https://example.com) and `code`.", "- [x] Done", "1) First", "", "| Name | Value |", "| :--- | ---: |", "| one | **two** |", "", "```ts", "const x = 1;", "```", ].join("\n"); function press(session: Session, ...names: string[]): void { for (const name of names) { const key: Key = name.length === 1 && name >= " " ? { name, char: name } : { name }; session.handleKey(key); } } Deno.test("markdown rendered view formats blocks and inline content", () => { const lines = renderMarkdownLines(MARKDOWN); assertEquals(lines.length, MARKDOWN.split("\n").length); assertEquals(lines.map((line) => line.text), [ "Title &", "", "│ A quote with link and code.", "☑ Done", "1. First", "", "│ Name │ Value │", "├──────┼───────┤", "│ one │ two │", "", "", "const x = 1;", "", ]); for (const line of lines) { assertEquals( line.spans.map((span) => span.text).join(""), line.text, `spans reconstruct ${JSON.stringify(line.text)}`, ); } assert(lines[0].spans.some((span) => span.bold), "strong text is bold"); assert(lines[2].spans.some((span) => span.italic), "emphasis is italic"); assert( lines[2].spans.some((span) => span.underline), "link text is underlined", ); assert( renderLineColored(lines[0], true).includes("\x1b[1;"), "bold spans reach ANSI output", ); assert( renderLineColored(lines[2], true).includes("\x1b[4;"), "underlined spans reach ANSI output", ); assert( lines[2].spans.some((span) => span.cls === "string"), "inline code has code color", ); assert( lines[6].spans.some((span) => span.bold), "table headings are bold", ); assert(lines[0].renderedSourceHidden, "heading levels remain diff-visible"); }); Deno.test("markdown rendered view handles setext, rules, images, and fenced tables", () => { const lines = renderMarkdownLines([ "```", "a | b", "--- | ---", "```", "Setext heading ©", "==============", "", "---", "![diagram](diagram.png) and ~~retired~~", "HTML text", "", "- item", "---", "# Heading", "---", ].join("\n")); assertEquals(lines.map((line) => line.text), [ "", "a | b", "--- | ---", "", "Setext heading ©", "", "", "────────────────────────────────", "▧ diagram and retired", "HTML text", "", "• item", "────────────────────────────────", "Heading", "────────────────────────────────", ]); assert( lines[8].spans.some((span) => span.strikethrough), "deleted text is struck through", ); assert( renderLineColored(lines[8], true).includes("\x1b[9;"), "rich modifiers reach ANSI output", ); assertEquals( renderMarkdownLines("foo\nbar\n---").map((line) => line.text), ["foo", "bar", ""], ); assertEquals( renderMarkdownLines("![**bold**](x)").map((line) => line.text), ["▧ bold"], ); assertEquals( renderMarkdownLines("![a *b* `c`](x)").map((line) => line.text), ["▧ a b c"], ); }); Deno.test("markdown rendered view covers nested blocks and table alignment", () => { const lines = renderMarkdownLines([ "[target]: https://example.com", "> > nested **[quote][target]**", "- bullet", "2. [ ] pending", " indented code", "", "| Center | Left | Right |", "| :---: | :--- | ---: |", "| x\\|y | \\*literal\\* | `p|q` |", "", "\\*literal\\* and ", " ~~~ text", " fenced code", " ~~~~", ].join("\n")); assertEquals(lines[0].text, ""); assertEquals(lines[1].text, "│ │ nested quote"); assert(lines[1].spans.some((span) => span.underline)); assertEquals(lines[2].text, "• bullet"); assertEquals(lines[3].text, "2. ☐ pending"); assertEquals(lines[4].text, " indented code"); assert(lines[6].text.includes("Center")); assert(lines[7].text.includes("┼")); assert(lines[8].text.includes("x|y")); assert(lines[8].text.includes("*literal*")); assert(lines[8].text.includes("p|q")); assertEquals(lines[10].text, "*literal* and https://example.com"); assertEquals(lines[11].text, ""); assertEquals(lines[12].text, " fenced code"); assertEquals(lines[13].text, ""); }); Deno.test("markdown rendered view follows block containers and literal syntax", () => { const lines = renderMarkdownLines([ " a | b", " --- | ---", "", "> ```js", "> const x = 1;", "> ```", "", "`&`", 'visible', ].join("\n")); assertEquals(lines.map((line) => line.text), [ "a | b", "--- | ---", "", "│ ", "│ const x = 1;", "│ ", "", "&", "visible", ]); assertEquals(lines[0].spans[0]?.cls, "string"); assertEquals(lines[1].spans[0]?.cls, "string"); assertEquals(lines[4].spans[1]?.cls, "string"); }); Deno.test("markdown rendered view preserves nested and multiline structure", () => { const nested = renderMarkdownLines([ "- outer", " - inner", " - deep", ].join("\n")); assertEquals(nested.map((line) => line.text), [ "• outer", " • inner", " • deep", ]); assertEquals( renderMarkdownLines("-\n continuation").map((line) => line.text), ["• ", " continuation"], ); assertEquals( renderMarkdownLines("1.\n continuation").map((line) => line.text), ["1. ", " continuation"], ); const code = renderMarkdownLines([ "- outer", "", " code", ].join("\n")); assertEquals(code.map((line) => line.text), ["• outer", "", " code"]); assertEquals(code[2].spans[1]?.cls, "string"); const paragraph = renderMarkdownLines([ "This is *emphasis", "continued here*.", ].join("\n")); assertEquals(paragraph.map((line) => line.text), [ "This is emphasis", "continued here.", ]); assert(paragraph[0].spans.some((span) => span.italic)); assert(paragraph[1].spans.some((span) => span.italic)); const comment = renderMarkdownLines([""].join("\n")); assertEquals(comment.map((line) => line.text), ["", "", ""]); assert(comment.every((line) => line.renderedSourceHidden)); }); Deno.test("markdown rendered view keeps inline content on its source lines", () => { const code = renderMarkdownLines("`first\nsecond`"); assertEquals(code.map((line) => line.text), ["first", "second"]); assert(code.every((line) => line.spans[0]?.cls === "string")); const entity = renderMarkdownLines("before after"); assertEquals(entity.map((line) => line.text), ["before after"]); const html = renderMarkdownLines( 'before visible after', ); assertEquals(html.map((line) => line.text), ["before ", "visible after"]); const comment = renderMarkdownLines("before after"); assertEquals(comment.map((line) => line.text), ["before ", " after"]); const link = renderMarkdownLines( '[link](https://example.com\n "title") after', ); assertEquals(link.map((line) => line.text), ["link", " after"]); const image = renderMarkdownLines( '![alt](diagram.png\n "title") after', ); assertEquals(image.map((line) => line.text), ["▧ alt", " after"]); const heading = renderMarkdownLines("foo \nbar\n---"); assertEquals(heading.map((line) => line.text), ["foo", "bar", ""]); const imageBreak = renderMarkdownLines("![first \nsecond](x) after"); assertEquals(imageBreak.map((line) => line.text), [ "▧ first", "second after", ]); const loneCarriage = renderMarkdownLines("# hi\rbody"); assertEquals(loneCarriage.map((line) => line.text), ["hi body"]); assertEquals( renderMarkdownLines("#\rheading").map((line) => line.text), ["heading"], ); assertEquals( renderMarkdownLines("-\ritem").map((line) => line.text), ["• item"], ); assertEquals(renderMarkdownLines("a\r")[0].text, "a \uE000"); for ( const line of [ ...code, ...entity, ...html, ...comment, ...link, ...image, ...heading, ...imageBreak, ...loneCarriage, ] ) { assertEquals( line.spans.map((span) => span.text).join(""), line.text, `spans reconstruct ${JSON.stringify(line.text)}`, ); } }); Deno.test("markdown rendered view strips multiline block HTML", () => { const source = [ "', "**visible**", "", ].join("\n"); const lines = renderMarkdownLines(source); assertEquals(lines.map((line) => line.text), ["", "", "visible", ""]); assert(lines[0].renderedSourceHidden); assert(lines[1].renderedSourceHidden); assertEquals(lines[2].renderedSourceHidden, undefined); assert(lines[2].spans.some((span) => span.bold)); assert(lines[3].renderedSourceHidden); for (const line of lines) { assertEquals( line.spans.map((span) => span.text).join(""), line.text, `spans reconstruct ${JSON.stringify(line.text)}`, ); } const script = renderMarkdownLines([ "", ].join("\n")); assertEquals(script.map((line) => line.text), [ "", 'if (a < b) alert("safe");', "", ]); assertEquals(script[1].renderedSourceHidden, undefined); const rawScript = renderMarkdownLines([ "", ].join("\n")); assertEquals(rawScript.map((line) => line.text), [ "", 'if (ad) alert("safe");', "const template = `[x](url)`;", "", ]); assertEquals(rawScript[1].renderedSourceHidden, undefined); assertEquals(rawScript[2].renderedSourceHidden, undefined); const style = renderMarkdownLines([ "", ].join("\n")); assertEquals(style.map((line) => line.text), [ "", ".x{width:calc(1rem)}", "", ]); const closePrefix = renderMarkdownLines([ "", ].join("\n")); assertEquals(closePrefix.map((line) => line.text), [ "", 'const x = "";', "", ]); const expandingCaseFold = renderMarkdownLines([ "", ].join("\n")); assertEquals(expandingCaseFold.map((line) => line.text), [ "", "const İ = 1;", "", ]); const declaration = renderMarkdownLines( 'y">visible', ); assertEquals(declaration.map((line) => line.text), ["visible"]); const declarationSubset = renderMarkdownLines( "]>

visible

", ); assertEquals(declarationSubset.map((line) => line.text), ["visible"]); const multilineDeclarationSubset = renderMarkdownLines([ "", "]>", "

visible

", ].join("\n")); assertEquals(multilineDeclarationSubset.map((line) => line.text), [ "", "", "", "visible", ]); const commentedDeclarationSubset = renderMarkdownLines( " ]>

visible

", ); assertEquals( commentedDeclarationSubset.map((line) => line.text), ["visible"], ); const adjacentDeclarationSubset = renderMarkdownLines([ "", "]>", "

visible

", ].join("\n")); assertEquals(adjacentDeclarationSubset.map((line) => line.text), [ "", "", "", "visible", ]); const separatedDeclarationSubset = renderMarkdownLines( "" + "]>

visible

", ); assertEquals( separatedDeclarationSubset.map((line) => line.text), ["visible"], ); const declarationAfterText = renderMarkdownLines( " text " + "]>

visible

", ); assertEquals(declarationAfterText.map((line) => line.text), [ " text visible", ]); }); Deno.test("rendered Markdown diff keeps changed multiline HTML tags visible", () => { const oldSource = [ "', "**visible**", "", ].join("\n"); const newSource = oldSource.replace('class="old"', 'class="new"'); const diffText = `diff --git a/notes.md b/notes.md index 1234..5678 100644 --- a/notes.md +++ b/notes.md @@ -1,4 +1,4 @@
+ class="new"> **visible**
`; const model = parseDiff(diffText); assert(model); const workspace: DiffWorkspace = { resolve: () => "/workspace/notes.md", read: () => newSource, readBlob: (object) => object === "1234" ? oldSource : null, }; const doc = buildDiffDocument( diffText, model, workspace, new Map(), "rendered", ).doc; assertEquals(doc.lines[6].text, '- class="old">'); assertEquals(doc.lines[7].text, '+ class="new">'); assertEquals(doc.lines[6].renderedSourceHidden, undefined); assertEquals(doc.lines[7].renderedSourceHidden, undefined); }); Deno.test("rendered Markdown diff keeps contextless fragments in source form", () => { const diffText = `diff --git a/notes.md b/notes.md index 1234..5678 100644 --- a/notes.md +++ b/notes.md @@ -10 +10 @@ -**old** +**new**`; const model = parseDiff(diffText); assert(model); const workspace: DiffWorkspace = { resolve: () => "/missing/notes.md", read: () => null, }; const doc = buildDiffDocument( diffText, model, workspace, new Map(), "rendered", ).doc; assertEquals(doc.lines[5].text, "-**old**"); assertEquals(doc.lines[6].text, "+**new**"); }); Deno.test("markdown rendered view composes lines without prebuilt spans", () => { const composed = new markdownInternals.RichLine(); composed.appendLine({ text: "plain", spans: [], renderedSourceHidden: true, }); assertEquals(composed.line(), { text: "plain", spans: [{ col: 0, text: "plain", cls: "plain" }], renderedSourceHidden: true, }); const padded = markdownInternals.splitRichLine({ text: "visible", spans: [{ col: 0, text: "visible", cls: "plain" }], renderedSourceHidden: true, }, 3); assertEquals(padded.map((line) => line.text), ["visible", "", ""]); assert(padded.every((line) => line.renderedSourceHidden)); }); Deno.test("markdown rendered view retains readable source when lexing fails", () => { const originalStaticLex = Lexer.lex; const originalInstanceLex = Lexer.prototype.lex; const throwLex = () => { throw new Error("lexer unavailable"); }; Lexer.lex = throwLex as typeof Lexer.lex; Lexer.prototype.lex = throwLex as typeof Lexer.prototype.lex; try { assertEquals( renderMarkdownLines("plain **source**").map((line) => line.text), ["plain source"], ); } finally { Lexer.lex = originalStaticLex; Lexer.prototype.lex = originalInstanceLex; } const originalInline = Lexer.lexInline; Lexer.lexInline = throwLex as typeof Lexer.lexInline; try { assertEquals( markdownInternals.renderInlineLine("plain **source**").text, "plain **source**", ); } finally { Lexer.lexInline = originalInline; } const throwingInlineLexer = { inlineTokens: throwLex, } as unknown as Lexer; assertEquals( markdownInternals.renderParagraphBlock( { type: "paragraph", raw: "fallback" }, ["fallback"], throwingInlineLexer, ).map((line) => line.text), ["fallback"], ); }); Deno.test("markdown rendered view handles incomplete block token data", () => { assertEquals( markdownInternals.renderBlockToken( { type: "space", raw: "\n" }, [""], ).map((line) => line.text), [""], ); assertEquals( markdownInternals.renderBlockToken( { type: "extension", raw: "plain" }, ["plain"], ).map((line) => line.text), ["plain"], ); assertEquals( markdownInternals.renderCodeBlock( { type: "code", raw: "body", text: "first\nsecond" }, ["first", "second"], ), [ { text: "first", spans: [{ col: 0, text: "first", cls: "string" }], renderedSourceHidden: true, }, { text: "second", spans: [{ col: 0, text: "second", cls: "string" }], renderedSourceHidden: true, }, ], ); assertEquals( markdownInternals.renderHtmlBlock(["plain"]).map((line) => line.text), ["plain"], ); const originalLex = Lexer.lex; Lexer.lex = (() => [{ type: "space", raw: "not present", }]) as unknown as typeof Lexer.lex; try { assertEquals( markdownInternals.renderMarkdownBlocks("source").map((line) => line.text), ["source"], ); } finally { Lexer.lex = originalLex; } assertEquals(markdownInternals.tokenLineRange("\n\n", 0, [0, 1, 2]), null); }); Deno.test("markdown rendered view handles incomplete list token data", () => { assertEquals( markdownInternals.renderListBlock( { type: "list", raw: "- item" }, ["- item"], ).map((line) => line.text), ["• item"], ); assertEquals( markdownInternals.renderListBlock( { type: "list", raw: "- item", items: [{ type: "list_item", raw: "missing", text: "item" }], }, ["- item"], ).map((line) => line.text), [""], ); assertEquals( markdownInternals.renderListBlock( { type: "list", raw: "\n\n", items: [{ type: "list_item", raw: "\n\n", text: "" }], }, ["", ""], ).map((line) => line.text), ["", ""], ); assertEquals( markdownInternals.renderListBlock( { type: "list", raw: "plain", items: [{ type: "list_item", raw: "plain", text: "plain" }], }, ["plain"], ).map((line) => line.text), [""], ); assertEquals( markdownInternals.renderListBlock( { type: "list", raw: "- first\n second", items: [{ type: "list_item", raw: "- first\n second", text: "first", }], }, ["- first", " second"], ).map((line) => line.text), ["• first", " second"], ); }); Deno.test("markdown rendered view handles uncommon inline token shapes", () => { const line = new markdownInternals.RichLine(); markdownInternals.appendInlineTokens( line, [ { type: "br", raw: " \n" }, { type: "extension", raw: "outer", tokens: [{ type: "text", raw: "nested", text: "nested" }], }, ], { cls: "plain" }, true, ); assertEquals(line.line().text, "\nnested"); assertEquals( markdownInternals.codeSpanText( { type: "codespan", raw: "invalid", text: "fallback" }, true, ), "fallback", ); assertEquals( markdownInternals.codeSpanText( { type: "codespan", raw: "` padded\ncode `", text: "" }, true, ), "padded\ncode", ); const child = new markdownInternals.RichLine(); markdownInternals.appendTokenChildren( child, { type: "strong", raw: "fallback", text: "fallback" }, { cls: "plain", bold: true }, ); assertEquals(child.line().spans, [{ col: 0, text: "fallback", cls: "plain", bold: true, }]); }); Deno.test("markdown rendered view handles incomplete HTML, fences, and tables", () => { assertEquals( markdownInternals.stripHtmlTags("plain visible', ), "visible", ); assertEquals( markdownInternals.stripHtmlTags("if (a < b && c > d)"), "if (a < b && c > d)", ); assertEquals( renderMarkdownLines("").map((line) => line.text), [""], ); assertEquals( renderMarkdownLines(" still instruction\n?>").map((line) => line.text ), ["", "", ""], ); assertEquals( renderMarkdownLines("\n?>").map((line) => line.text), ["", "", ""], ); assertEquals(markdownInternals.openingFence("plain"), null); assertEquals(markdownInternals.openingFence("```bad`info"), null); assertEquals(markdownInternals.tableAlignment("plain"), null); assertEquals(markdownInternals.tableAlignment("bad | ---"), null); assertEquals(markdownInternals.splitTableCells("plain"), null); assertEquals(markdownInternals.splitTableCells("a | ``b|c``"), [ "a ", " ``b|c``", ]); assertEquals( markdownInternals.renderTableBlock([ "a | b", "--- | ---", "only one cell", ]).map((line) => line.text), ["a | b", "--- | ---", "only one cell"], ); const literalBackslashes = renderMarkdownLines([ "code | value", "--- | ---", "`a\\\\b` | x", ].join("\n")); assert(literalBackslashes[2].text.includes("a\\\\b")); }); Deno.test("markdown rendered view handles malformed HTML boundaries", () => { assertEquals( markdownInternals.htmlDeclarationRanges( "