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 ©",
"==============",
"",
"---",
" 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("").map((line) => line.text),
["▧ bold"],
);
assertEquals(
renderMarkdownLines("").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(
' 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(" 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 = [
"",
].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(
'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 = [
"",
].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(
"