Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/lib/frontmatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const FRONTMATTER_DELIMITER = /^---[ \t]*$/;

function lineWithoutCarriageReturn(src: string, start: number, end: number): string {
const line = src.slice(start, end);
return line.endsWith("\r") ? line.slice(0, -1) : line;
}

/**
* Hide a leading YAML frontmatter block without changing source positions.
*
* Replacing non-newline characters with spaces keeps markdown-it token maps
* aligned with the original document for preview-to-source selection sync.
*/
export function maskYamlFrontmatter(src: string): string {
const firstNewline = src.indexOf("\n");
const firstLineEnd = firstNewline === -1 ? src.length : firstNewline;
const firstLine = lineWithoutCarriageReturn(src, 0, firstLineEnd);

if (!FRONTMATTER_DELIMITER.test(firstLine) || firstNewline === -1) {
return src;
}

let lineStart = firstNewline + 1;
while (lineStart <= src.length) {
const newline = src.indexOf("\n", lineStart);
const lineEnd = newline === -1 ? src.length : newline;
const line = lineWithoutCarriageReturn(src, lineStart, lineEnd);

if (FRONTMATTER_DELIMITER.test(line)) {
const frontmatterEnd = newline === -1 ? lineEnd : newline + 1;
const masked = src.slice(0, frontmatterEnd).replace(/[^\r\n]/g, " ");
return masked + src.slice(frontmatterEnd);
}

if (newline === -1) break;
lineStart = newline + 1;
}

return src;
}
6 changes: 4 additions & 2 deletions src/lib/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import MarkdownIt from "markdown-it";
import mark from "markdown-it-mark";
import taskLists from "markdown-it-task-lists";
import type { Highlighter } from "shiki";
import { maskYamlFrontmatter } from "./frontmatter";
import { plantUmlUrl } from "./plantuml";
import type { Theme } from "./theme";

Expand Down Expand Up @@ -171,13 +172,14 @@ export async function ensureMarkdownReady(): Promise<void> {
}

export async function renderMarkdown(src: string, theme: Theme): Promise<string> {
const langs = extractLangs(src);
const renderSource = maskYamlFrontmatter(src);
const langs = extractLangs(renderSource);
if (langs.length > 0) {
const h = await getHighlighter();
const shikiTheme = THEMES[theme];
await ensureThemeLoaded(h, shikiTheme);
await ensureLangsLoaded(h, langs);
activeShikiTheme = shikiTheme;
}
return md.render(src);
return md.render(renderSource);
}
40 changes: 40 additions & 0 deletions tests/markdown.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { expect, test } from "bun:test";
import { renderMarkdown } from "../src/lib/markdown";

test("hides leading YAML frontmatter and preserves source line mapping", async () => {
const source = [
"---",
"created: 2026-07-27",
"updated: 2026-07-31",
"type: reference",
"status: complete",
"tags: [workbook, principles, defaults]",
"---",
"",
"# Your Context Block: your defaults",
"",
"Body text here.",
].join("\n");

const html = await renderMarkdown(source, "latte");

expect(html).not.toContain("created: 2026-07-27");
expect(html).not.toContain("<hr");
expect(html).toContain(
'<h1 data-sline="8" data-eline="9" id="your-context-block-your-defaults">',
);
expect(html).toContain("Body text here.");
});

test("keeps thematic breaks outside leading frontmatter", async () => {
const html = await renderMarkdown("# Heading\n\n---\n\nBody", "latte");

expect(html).toContain('<hr data-sline="2" data-eline="3">');
});

test("leaves an unterminated opening delimiter as ordinary markdown", async () => {
const html = await renderMarkdown("---\nkey: value\n# Heading", "latte");

expect(html).toContain("<hr");
expect(html).toContain("key: value");
});