import { expect, test } from "vitest"; import { parse } from "../markdown_parser/parse_tree.ts"; import { renderMarkdownToHtml } from "./markdown_render.ts"; import { buildExtendedMarkdownLanguage, extendedMarkdownLanguage, } from "../markdown_parser/parser.ts"; import { CustomSyntaxRenderedHtmlType, expandMarkdown } from "./inline.ts"; import type { Space } from "../space.ts"; import type { SpaceLuaEnvironment } from "../space_lua.ts"; import { LuaEnv } from "../space_lua/runtime.ts"; const sampleMarkdown = `--- name: Sup --- # Hello world This is **bold** and _italic_, or _italic_. And a **_mix_**. And ==highlight==! Lists: - This - Is a - list - And here we go nested 1. This is a numbered 2. Two - And different - Bla - More bla And: 1. Numbered 2. Two ## Second heading And some And like this: More code Bla And a blockquote: > Sup yo Empty line\ > Second part And more custom stuff [[Page link]] {[Command button]} - [ ] #next Task - [x] #next Task 2 ## Tables | type | actor_login | created_at | payload_ref | | --------- | ----------- | -------------------- | ---------------------- | | PushEvent | avb | 2022-10-27T08:27:48Z | refs/heads/master | | PushEvent | avb | 2022-10-27T04:31:27Z | refs/heads/jitterSched | Here is something --- A new thing. `; test("Markdown render", () => { const tree = parse(extendedMarkdownLanguage, sampleMarkdown); renderMarkdownToHtml(tree, { failOnUnknown: true, }); }); test("Wiki link with embedded image path", () => { // This particular one caused an infinite regex loop previously, adding it here as a regression to avoid in the future const example = `![[Inbox/2026-01-08/CleanShot 2026-01-01 at 12.36.23.png]]`; const tree = parse(extendedMarkdownLanguage, example); renderMarkdownToHtml(tree, { failOnUnknown: true, }); }); test("Smart hard break test", () => { const example = `**Hello** *world!*`; const tree = parse(extendedMarkdownLanguage, example); const html = renderMarkdownToHtml(tree, { failOnUnknown: true, smartHardBreak: true, }); expect(html).toEqual( `Hello
world!
`, ); const example2 = `This is going to be a text. With a new line. And another * and a list * with a second item ### [[Bla]] Url: something Server: something else 📅 last_updated - [Release notes](release_notes_url)`; const tree2 = parse(extendedMarkdownLanguage, example2); const html2 = renderMarkdownToHtml(tree2, { failOnUnknown: true, smartHardBreak: true, }); console.log(html2); }); test("Inline HTML tags render as proper elements", () => { const tree = parse(extendedMarkdownLanguage, "Hello"); const html = renderMarkdownToHtml(tree, { failOnUnknown: true }); expect(html).toEqual( 'Hello', ); }); test("Inline HTML tags with attributes", () => { const tree = parse( extendedMarkdownLanguage, 'red text', ); const html = renderMarkdownToHtml(tree, { failOnUnknown: true }); expect(html).toEqual( 'red text', ); }); test("Inline HTML tags with markdown content", () => { const tree = parse( extendedMarkdownLanguage, 'Hello **there**', ); const html = renderMarkdownToHtml(tree, { failOnUnknown: true }); expect(html).toEqual( 'Hello there', ); }); test("Inline HTML mixed with text", () => { const tree = parse( extendedMarkdownLanguage, "Before bold after", ); const html = renderMarkdownToHtml(tree, { failOnUnknown: true }); expect(html).toEqual( 'Before bold after', ); }); test("Nested same-name HTML tags", () => { const tree = parse( extendedMarkdownLanguage, "nested", ); const html = renderMarkdownToHtml(tree, { failOnUnknown: true }); expect(html).toEqual( 'nested', ); }); test("Inline HTML with wiki link", () => { const tree = parse( extendedMarkdownLanguage, "hello [[there]]", ); const html = renderMarkdownToHtml(tree, { failOnUnknown: true }); expect(html).toEqual( 'hello there', ); }); test("Unmatched HTML tags render as literal text", () => { const tree = parse( extendedMarkdownLanguage, "text unclosed", ); const html = renderMarkdownToHtml(tree, { failOnUnknown: true }); expect(html).toEqual( 'text <b>unclosed', ); }); test("CustomSyntaxRenderedHtml renders raw HTML", () => { // Directly test the renderer with a synthetic parse tree const tree = { type: "Document", children: [ { type: "Paragraph", children: [ { text: "Before " }, { type: CustomSyntaxRenderedHtmlType, children: [{ text: "rendered" }], }, { text: " after" }, ], }, ], }; const html = renderMarkdownToHtml(tree, { failOnUnknown: true }); expect(html).toEqual( 'Before rendered after', ); }); // Minimal stubs for expandMarkdown tests const stubSpace = {} as Space; const stubSle = { env: new LuaEnv() } as SpaceLuaEnvironment; const defaultExpandOpts = { expandTransclusions: false, expandLuaDirectives: false, rewriteTasks: false, }; const latexInlineSpec = { name: "LatexInline", startMarker: "\\$", endMarker: "\\$", mode: "inline" as const, }; const latexBlockSpec = { name: "LatexBlock", startMarker: "^\\$\\$$", endMarker: "^\\$\\$$", mode: "block" as const, }; const customSpec = { name: "Custom", startMarker: "<<", endMarker: ">>", mode: "inline" as const, }; test("expandMarkdown resolves inline custom syntax renderHtml", async () => { const lang = buildExtendedMarkdownLanguage({ LatexInline: latexInlineSpec, }); const tree = parse(lang, "Hello $E=mc^2$ world"); const expanded = await expandMarkdown(stubSpace, "test", tree, stubSle, { ...defaultExpandOpts, syntaxExtensions: { LatexInline: { ...latexInlineSpec, renderHtml: (body, _pageName) => `${body}`, }, }, }); const html = renderMarkdownToHtml(expanded, { failOnUnknown: true }); expect(html).toEqual( 'Hello E=mc^2 world', ); }); test("expandMarkdown resolves block custom syntax renderHtml", async () => { const lang = buildExtendedMarkdownLanguage({ LatexBlock: latexBlockSpec, }); const tree = parse(lang, "$$\nE=mc^2\n$$"); const expanded = await expandMarkdown(stubSpace, "test", tree, stubSle, { ...defaultExpandOpts, syntaxExtensions: { LatexBlock: { ...latexBlockSpec, renderHtml: (body, _pageName) => `
${body.trim()}
`, }, }, }); const html = renderMarkdownToHtml(expanded, { failOnUnknown: true }); expect(html).toEqual('
E=mc^2
'); }); test("expandMarkdown passes pageName to renderHtml", async () => { const lang = buildExtendedMarkdownLanguage({ Custom: customSpec }); const tree = parse(lang, "Hello <> world"); let receivedPageName: string | undefined; await expandMarkdown(stubSpace, "MyPage", tree, stubSle, { ...defaultExpandOpts, syntaxExtensions: { Custom: { ...customSpec, renderHtml: (_body, pageName) => { receivedPageName = pageName; return "ok"; }, }, }, }); expect(receivedPageName).toEqual("MyPage"); }); test("expandMarkdown handles renderHtml errors gracefully", async () => { const lang = buildExtendedMarkdownLanguage({ Custom: customSpec }); const tree = parse(lang, "Hello <> world"); const expanded = await expandMarkdown(stubSpace, "test", tree, stubSle, { ...defaultExpandOpts, syntaxExtensions: { Custom: { ...customSpec, renderHtml: () => { throw new Error("something failed"); }, }, }, }); const html = renderMarkdownToHtml(expanded); expect(html).toContain("Error in Custom renderHtml: something failed"); expect(html).toContain(''); }); test("expandMarkdown skips custom syntax without renderHtml", async () => { const lang = buildExtendedMarkdownLanguage({ Custom: customSpec }); const tree = parse(lang, "Hello <> world"); const expanded = await expandMarkdown(stubSpace, "test", tree, stubSle, { ...defaultExpandOpts, syntaxExtensions: { Custom: { ...customSpec, // No renderHtml callback }, }, }); const html = renderMarkdownToHtml(expanded); // Should fall through to default rendering (raw text, HTML-escaped) expect(html).toContain("<<content>>"); }); // ── Block-level HTML rendering ───────────────────────────────────── test("Block HTML table renders correctly", () => { const tree = parse( extendedMarkdownLanguage, "
hello
", ); const html = renderMarkdownToHtml(tree, { failOnUnknown: true }); expect(html).toBe("
hello
"); }); test("Block HTML preserves data attributes", () => { const tree = parse( extendedMarkdownLanguage, '42', ); const html = renderMarkdownToHtml(tree, { failOnUnknown: true }); expect(html).toBe('42'); }); test("Markdown inside block HTML td is rendered", () => { const tree = parse( extendedMarkdownLanguage, "
hello **world**
", ); const html = renderMarkdownToHtml(tree, { failOnUnknown: true }); expect(html).toBe( "
hello world
", ); }); test("Self-closing
inside block HTML td renders as HTML", () => { const tree = parse( extendedMarkdownLanguage, "
Hello
there
", ); const html = renderMarkdownToHtml(tree, { failOnUnknown: true }); expect(html).toBe( "
Hello
there
", ); }); test("Wiki link inside block HTML td is rendered", () => { const tree = parse( extendedMarkdownLanguage, "
see [[MyPage]]
", ); const html = renderMarkdownToHtml(tree, { failOnUnknown: true }); expect(html).toContain(' { const tree = parse( extendedMarkdownLanguage, "


", ); const html = renderMarkdownToHtml(tree, { failOnUnknown: true }); expect(html).toBe("


"); }); test("Multi-line block HTML table", () => { const md = `
nameage
Alice30
`; const tree = parse(extendedMarkdownLanguage, md); const html = renderMarkdownToHtml(tree, { failOnUnknown: true }); expect(html).toContain("nameage"); expect(html).toContain("Alice30"); }); test("Block HTML with data attributes and wiki links", () => { const md = `
name
[[Alice]]
`; const tree = parse(extendedMarkdownLanguage, md); const html = renderMarkdownToHtml(tree, { failOnUnknown: true }); expect(html).toContain('data-table-cell-type="string"'); expect(html).toContain('
{ const tree = parse( extendedMarkdownLanguage, '
  • hello
', ); const html = renderMarkdownToHtml(tree, { failOnUnknown: true }); expect(html).toBe( '
  • hello
', ); }); test("Nested block HTML tables", () => { const tree = parse( extendedMarkdownLanguage, "
inner
", ); const html = renderMarkdownToHtml(tree, { failOnUnknown: true }); expect(html).toContain("
inner
"); // Should have two table open/close pairs expect(html.match(//g)).toHaveLength(2); expect(html.match(/<\/table>/g)).toHaveLength(2); }); test("Empty block HTML table", () => { const tree = parse( extendedMarkdownLanguage, '
', ); const html = renderMarkdownToHtml(tree, { failOnUnknown: true }); expect(html).toContain("data-table-empty"); }); test("HTML comment is still removed", () => { const tree = parse(extendedMarkdownLanguage, ""); const html = renderMarkdownToHtml(tree, { failOnUnknown: true }); expect(html).toBe(""); });