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, "");
const html = renderMarkdownToHtml(tree, { failOnUnknown: true });
expect(html).toEqual(
'',
);
});
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,
'',
);
const html = renderMarkdownToHtml(tree, { failOnUnknown: true });
expect(html).toEqual(
'',
);
});
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,
"",
);
const html = renderMarkdownToHtml(tree, { failOnUnknown: true });
expect(html).toBe("");
});
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,
"",
);
const html = renderMarkdownToHtml(tree, { failOnUnknown: true });
expect(html).toBe(
"",
);
});
test("Self-closing
inside block HTML td renders as HTML", () => {
const tree = parse(
extendedMarkdownLanguage,
"",
);
const html = renderMarkdownToHtml(tree, { failOnUnknown: true });
expect(html).toBe(
"",
);
});
test("Wiki link inside block HTML td is rendered", () => {
const tree = parse(
extendedMarkdownLanguage,
"",
);
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 = ``;
const tree = parse(extendedMarkdownLanguage, md);
const html = renderMarkdownToHtml(tree, { failOnUnknown: true });
expect(html).toContain("| name | age |
");
expect(html).toContain("Alice | 30 | ");
});
test("Block HTML with data attributes and wiki links", () => {
const md = ``;
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,
'',
);
const html = renderMarkdownToHtml(tree, { failOnUnknown: true });
expect(html).toBe(
'',
);
});
test("Nested block HTML tables", () => {
const tree = parse(
extendedMarkdownLanguage,
"",
);
const html = renderMarkdownToHtml(tree, { failOnUnknown: true });
expect(html).toContain("");
// 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("");
});