@imontene/mdrender 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/LICENSE +21 -0
  3. package/README.md +243 -0
  4. package/dist/assets/theme-dark.css +40 -0
  5. package/dist/assets/theme.css +545 -0
  6. package/dist/assets.d.ts +45 -0
  7. package/dist/assets.js +109 -0
  8. package/dist/bin.d.ts +2 -0
  9. package/dist/bin.js +16 -0
  10. package/dist/browser/cache.d.ts +15 -0
  11. package/dist/browser/cache.js +30 -0
  12. package/dist/browser/detect.d.ts +17 -0
  13. package/dist/browser/detect.js +90 -0
  14. package/dist/browser/setup.d.ts +13 -0
  15. package/dist/browser/setup.js +69 -0
  16. package/dist/cli.d.ts +8 -0
  17. package/dist/cli.js +194 -0
  18. package/dist/config.d.ts +6 -0
  19. package/dist/config.js +32 -0
  20. package/dist/convert.d.ts +64 -0
  21. package/dist/convert.js +167 -0
  22. package/dist/diagnostics.d.ts +8 -0
  23. package/dist/diagnostics.js +6 -0
  24. package/dist/errors.d.ts +12 -0
  25. package/dist/errors.js +21 -0
  26. package/dist/exit-codes.d.ts +8 -0
  27. package/dist/exit-codes.js +8 -0
  28. package/dist/frontmatter.d.ts +10 -0
  29. package/dist/frontmatter.js +26 -0
  30. package/dist/html.d.ts +2 -0
  31. package/dist/html.js +12 -0
  32. package/dist/i18n.d.ts +9 -0
  33. package/dist/i18n.js +59 -0
  34. package/dist/index.d.ts +17 -0
  35. package/dist/index.js +18 -0
  36. package/dist/inputs.d.ts +26 -0
  37. package/dist/inputs.js +118 -0
  38. package/dist/markdown/alerts.d.ts +6 -0
  39. package/dist/markdown/alerts.js +35 -0
  40. package/dist/markdown/env.d.ts +29 -0
  41. package/dist/markdown/env.js +16 -0
  42. package/dist/markdown/highlight.d.ts +6 -0
  43. package/dist/markdown/highlight.js +17 -0
  44. package/dist/markdown/images.d.ts +9 -0
  45. package/dist/markdown/images.js +93 -0
  46. package/dist/markdown/index.d.ts +27 -0
  47. package/dist/markdown/index.js +69 -0
  48. package/dist/markdown/math.d.ts +9 -0
  49. package/dist/markdown/math.js +147 -0
  50. package/dist/markdown/mermaid.d.ts +8 -0
  51. package/dist/markdown/mermaid.js +22 -0
  52. package/dist/markdown/task-lists.d.ts +6 -0
  53. package/dist/markdown/task-lists.js +40 -0
  54. package/dist/markdown/toc.d.ts +22 -0
  55. package/dist/markdown/toc.js +101 -0
  56. package/dist/options.d.ts +55 -0
  57. package/dist/options.js +131 -0
  58. package/dist/paths.d.ts +2 -0
  59. package/dist/paths.js +4 -0
  60. package/dist/preview.d.ts +22 -0
  61. package/dist/preview.js +126 -0
  62. package/dist/render/browser.d.ts +48 -0
  63. package/dist/render/browser.js +134 -0
  64. package/dist/render/fonts.d.ts +10 -0
  65. package/dist/render/fonts.js +47 -0
  66. package/dist/render/header-footer.d.ts +22 -0
  67. package/dist/render/header-footer.js +39 -0
  68. package/dist/render/katex.d.ts +2 -0
  69. package/dist/render/katex.js +19 -0
  70. package/dist/render/mermaid.d.ts +25 -0
  71. package/dist/render/mermaid.js +91 -0
  72. package/dist/render/page.d.ts +7 -0
  73. package/dist/render/page.js +23 -0
  74. package/dist/render/template.d.ts +22 -0
  75. package/dist/render/template.js +54 -0
  76. package/dist/run.d.ts +26 -0
  77. package/dist/run.js +71 -0
  78. package/dist/version.d.ts +1 -0
  79. package/dist/version.js +4 -0
  80. package/dist/watch.d.ts +25 -0
  81. package/dist/watch.js +128 -0
  82. package/package.json +97 -0
@@ -0,0 +1,17 @@
1
+ import hljs from "highlight.js/lib/common";
2
+ import { escapeHtml } from "../html.js";
3
+ /**
4
+ * Syntax highlighting for fenced code with a language (` ```ts `). highlight.js's "common" set
5
+ * covers ~35 popular languages. Unknown or missing languages are shown as plain text: no
6
+ * auto-detection, so output never changes with heuristics.
7
+ */
8
+ export function highlightCode(code, lang) {
9
+ const language = lang.trim().split(/\s+/)[0]?.toLowerCase() ?? "";
10
+ if (language && hljs.getLanguage(language)) {
11
+ const { value } = hljs.highlight(code, { language, ignoreIllegals: true });
12
+ return `<pre class="hljs"><code class="language-${escapeHtml(language)}">${value}</code></pre>`;
13
+ }
14
+ // Empty string: markdown-it falls back to its default escaped <pre><code> block.
15
+ return "";
16
+ }
17
+ //# sourceMappingURL=highlight.js.map
@@ -0,0 +1,9 @@
1
+ import type { MarkdownIt } from "markdown-it";
2
+ /** Resolve an image `src` to a local file path, or undefined for remote/data URLs. */
3
+ export declare function localImagePath(src: string, baseDir: string): string | undefined;
4
+ /**
5
+ * Embed local images as data URIs so the output is self-contained and the browser never needs
6
+ * file system access: Markdown images (`![alt](img.png)`) and `<img src="img.png">` in raw HTML.
7
+ * Remote images are left untouched.
8
+ */
9
+ export declare function inlineLocalImages(md: MarkdownIt): void;
@@ -0,0 +1,93 @@
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { tokenLine } from "./env.js";
5
+ const MIME_TYPES = {
6
+ ".png": "image/png",
7
+ ".jpg": "image/jpeg",
8
+ ".jpeg": "image/jpeg",
9
+ ".gif": "image/gif",
10
+ ".svg": "image/svg+xml",
11
+ ".webp": "image/webp",
12
+ ".avif": "image/avif",
13
+ ".bmp": "image/bmp",
14
+ ".ico": "image/x-icon",
15
+ };
16
+ /** Resolve an image `src` to a local file path, or undefined for remote/data URLs. */
17
+ export function localImagePath(src, baseDir) {
18
+ if (/^data:/i.test(src))
19
+ return undefined;
20
+ if (/^file:/i.test(src))
21
+ return fileURLToPath(src);
22
+ // Any other URL scheme (http:, https:, ...) but not a Windows drive letter like C:\
23
+ if (/^[a-z][a-z0-9+.-]+:/i.test(src) && !/^[a-z]:[\\/]/i.test(src))
24
+ return undefined;
25
+ const withoutQuery = src.replace(/[?#].*$/, "");
26
+ let decoded = withoutQuery;
27
+ try {
28
+ decoded = decodeURI(withoutQuery);
29
+ }
30
+ catch {
31
+ // keep the raw path if it is not valid percent-encoding
32
+ }
33
+ return path.resolve(baseDir, decoded);
34
+ }
35
+ function toDataUri(file) {
36
+ const mime = MIME_TYPES[path.extname(file).toLowerCase()];
37
+ if (!mime)
38
+ return undefined;
39
+ return `data:${mime};base64,${readFileSync(file).toString("base64")}`;
40
+ }
41
+ function warn(env, message, line) {
42
+ env.diagnostics.push({ severity: "warning", message, line });
43
+ }
44
+ /**
45
+ * Replace a local image reference by a data URI. Returns the new src, or undefined to keep the
46
+ * original (remote URL, data URI, or a problem that was reported as a warning).
47
+ */
48
+ function embed(src, env, line) {
49
+ const file = localImagePath(src, env.baseDir);
50
+ if (!file)
51
+ return undefined;
52
+ if (!existsSync(file) || !statSync(file).isFile()) {
53
+ warn(env, `image not found: ${src}`, line);
54
+ return undefined;
55
+ }
56
+ const dataUri = toDataUri(file);
57
+ if (!dataUri)
58
+ warn(env, `unsupported image type: ${src}`, line);
59
+ return dataUri;
60
+ }
61
+ const HTML_IMG_SRC = /(<img\b[^>]*?\bsrc\s*=\s*)(["'])(.*?)\2/gi;
62
+ /**
63
+ * Embed local images as data URIs so the output is self-contained and the browser never needs
64
+ * file system access: Markdown images (`![alt](img.png)`) and `<img src="img.png">` in raw HTML.
65
+ * Remote images are left untouched.
66
+ */
67
+ export function inlineLocalImages(md) {
68
+ const fallback = md.renderer.rules.image;
69
+ md.renderer.rules.image = (tokens, idx, options, rawEnv, self) => {
70
+ const env = rawEnv;
71
+ const token = tokens[idx];
72
+ const src = String(token.attrGet("src") ?? "");
73
+ const dataUri = src && env?.baseDir ? embed(src, env, tokenLine(token)) : undefined;
74
+ if (dataUri)
75
+ token.attrSet("src", dataUri);
76
+ return fallback(tokens, idx, options, env, self);
77
+ };
78
+ for (const rule of ["html_block", "html_inline"]) {
79
+ const original = md.renderer.rules[rule];
80
+ md.renderer.rules[rule] = (tokens, idx, options, rawEnv, self) => {
81
+ const env = rawEnv;
82
+ const token = tokens[idx];
83
+ if (env?.baseDir && /<img\b/i.test(token.content)) {
84
+ token.content = token.content.replace(HTML_IMG_SRC, (match, prefix, quote, src) => {
85
+ const dataUri = embed(md.utils.unescapeAll(src), env, tokenLine(token));
86
+ return dataUri ? `${prefix}${quote}${dataUri}${quote}` : match;
87
+ });
88
+ }
89
+ return original(tokens, idx, options, env, self);
90
+ };
91
+ }
92
+ }
93
+ //# sourceMappingURL=images.js.map
@@ -0,0 +1,27 @@
1
+ import { type MarkdownIt } from "markdown-it";
2
+ import type { Diagnostic } from "../diagnostics.js";
3
+ import { type MermaidBlock } from "./env.js";
4
+ import { type Heading } from "./toc.js";
5
+ export interface MarkdownResult {
6
+ /** HTML fragment for the document body; Mermaid diagrams are still placeholders. */
7
+ html: string;
8
+ /** Text of the first level-1 heading, if any. */
9
+ title?: string;
10
+ diagnostics: Diagnostic[];
11
+ /** Mermaid sources, in document order, to render with `fillMermaid`. */
12
+ mermaid: MermaidBlock[];
13
+ /** Token identifying this render's placeholders (Mermaid diagrams, table of contents). */
14
+ nonce: string;
15
+ hasMath: boolean;
16
+ headings: Heading[];
17
+ /** The document has a `[[toc]]` marker (its placeholder is in `html`). */
18
+ hasTocMarker: boolean;
19
+ }
20
+ export interface MarkdownOptions {
21
+ /** Directory used to resolve relative image paths. Defaults to the current directory. */
22
+ baseDir?: string;
23
+ /** Document language for translated labels. Defaults to Spanish. */
24
+ lang?: string;
25
+ }
26
+ export declare function createMarkdown(): MarkdownIt;
27
+ export declare function renderMarkdown(source: string, options?: MarkdownOptions): MarkdownResult;
@@ -0,0 +1,69 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import markdownIt, {} from "markdown-it";
3
+ import anchor from "markdown-it-anchor";
4
+ import footnote from "markdown-it-footnote";
5
+ import { DEFAULT_LANG } from "../i18n.js";
6
+ import { alerts } from "./alerts.js";
7
+ import { inlineChildLines } from "./env.js";
8
+ import { highlightCode } from "./highlight.js";
9
+ import { inlineLocalImages } from "./images.js";
10
+ import { math } from "./math.js";
11
+ import { mermaid } from "./mermaid.js";
12
+ import { taskLists } from "./task-lists.js";
13
+ import { collectHeadings, tocMarker } from "./toc.js";
14
+ export function createMarkdown() {
15
+ const md = markdownIt({
16
+ html: true,
17
+ linkify: true,
18
+ typographer: false,
19
+ highlight: highlightCode,
20
+ });
21
+ md.use(anchor, { tabIndex: false });
22
+ md.use(footnote);
23
+ md.use(taskLists);
24
+ md.use(alerts);
25
+ md.use(tocMarker);
26
+ md.use(inlineChildLines);
27
+ md.use(inlineLocalImages);
28
+ md.use(math);
29
+ md.use(mermaid);
30
+ return md;
31
+ }
32
+ let shared;
33
+ export function renderMarkdown(source, options = {}) {
34
+ const md = (shared ??= createMarkdown());
35
+ const env = {
36
+ baseDir: options.baseDir ?? process.cwd(),
37
+ diagnostics: [],
38
+ mermaid: [],
39
+ nonce: randomBytes(8).toString("hex"),
40
+ macros: {},
41
+ hasMath: false,
42
+ lang: options.lang ?? DEFAULT_LANG,
43
+ hasTocMarker: false,
44
+ };
45
+ const tokens = md.parse(source, env);
46
+ let title;
47
+ const h1 = tokens.findIndex((t) => t.type === "heading_open" && t.tag === "h1");
48
+ if (h1 !== -1) {
49
+ title = tokens[h1 + 1]?.children
50
+ ?.filter((t) => t.type === "text" || t.type === "code_inline")
51
+ .map((t) => t.content)
52
+ .join("")
53
+ .trim();
54
+ }
55
+ const html = md.renderer.render(tokens, md.options, env);
56
+ // Separate diagnostics: headings are rendered a second time for the table of contents.
57
+ const headings = collectHeadings(md, tokens, { ...env, diagnostics: [] });
58
+ return {
59
+ html,
60
+ title: title || undefined,
61
+ diagnostics: env.diagnostics,
62
+ mermaid: env.mermaid,
63
+ nonce: env.nonce,
64
+ hasMath: env.hasMath,
65
+ headings,
66
+ hasTocMarker: env.hasTocMarker,
67
+ };
68
+ }
69
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,9 @@
1
+ import type { MarkdownIt, Token } from "markdown-it";
2
+ import { type RenderEnv } from "./env.js";
3
+ /** Render TeX with KaTeX; on failure record an error and show the source highlighted instead. */
4
+ export declare function renderTex(tex: string, displayMode: boolean, env: RenderEnv, token: Token): string;
5
+ /**
6
+ * LaTeX math rendered at build time with KaTeX (no JavaScript needed in the output):
7
+ * `$inline$`, `$$display$$` blocks and ```` ```math ```` fences (GitHub syntax).
8
+ */
9
+ export declare function math(md: MarkdownIt): void;
@@ -0,0 +1,147 @@
1
+ import katex from "katex";
2
+ import { escapeHtml } from "../html.js";
3
+ import { tokenLine } from "./env.js";
4
+ const DOLLAR = 0x24;
5
+ const BACKSLASH = 0x5c;
6
+ const isWhitespace = (ch) => ch === undefined || /\s/.test(ch);
7
+ const isDigit = (ch) => ch !== undefined && ch >= "0" && ch <= "9";
8
+ /** Index of the next `$` (or `$$`) at or after `from` that is not backslash-escaped, or -1. */
9
+ function findClosing(src, from, delimiter) {
10
+ for (let i = from; i < src.length; i++) {
11
+ if (src.charCodeAt(i) === BACKSLASH) {
12
+ i++;
13
+ continue;
14
+ }
15
+ if (src.startsWith(delimiter, i))
16
+ return i;
17
+ }
18
+ return -1;
19
+ }
20
+ /**
21
+ * Inline math: `$...$` and `$$...$$` inside a paragraph.
22
+ *
23
+ * Follows Pandoc's rules so prices like "$5 and $10" stay text: the opening `$` must not be
24
+ * followed by whitespace, and the closing `$` must not be preceded by whitespace nor followed by
25
+ * a digit.
26
+ */
27
+ function mathInline(state, silent) {
28
+ const { src, pos } = state;
29
+ if (src.charCodeAt(pos) !== DOLLAR)
30
+ return false;
31
+ const display = src.charCodeAt(pos + 1) === DOLLAR;
32
+ const delimiter = display ? "$$" : "$";
33
+ const start = pos + delimiter.length;
34
+ if (!display && isWhitespace(src[start]))
35
+ return false;
36
+ const end = findClosing(src, start, delimiter);
37
+ if (end === -1 || end === start)
38
+ return false;
39
+ if (!display && (isWhitespace(src[end - 1]) || isDigit(src[end + 1])))
40
+ return false;
41
+ if (!silent) {
42
+ const token = state.push(display ? "math_inline_display" : "math_inline", "math", 0);
43
+ token.content = src.slice(start, end);
44
+ token.markup = delimiter;
45
+ }
46
+ state.pos = end + delimiter.length;
47
+ return true;
48
+ }
49
+ /** Block math: a paragraph opened by `$$`, on one line (`$$ x $$`) or spanning several. */
50
+ function mathBlock(state, startLine, endLine, silent) {
51
+ let pos = state.bMarks[startLine] + state.tShift[startLine];
52
+ let max = state.eMarks[startLine];
53
+ if (state.sCount[startLine] - state.blkIndent >= 4)
54
+ return false;
55
+ if (!state.src.startsWith("$$", pos))
56
+ return false;
57
+ pos += 2;
58
+ let firstLine = state.src.slice(pos, max);
59
+ let lastLine = "";
60
+ let nextLine = startLine;
61
+ let found = false;
62
+ if (firstLine.trim().endsWith("$$")) {
63
+ // Single line: $$ ... $$
64
+ firstLine = firstLine.trim().slice(0, -2);
65
+ found = true;
66
+ }
67
+ else {
68
+ while (++nextLine < endLine) {
69
+ pos = state.bMarks[nextLine] + state.tShift[nextLine];
70
+ max = state.eMarks[nextLine];
71
+ if (pos < max && state.sCount[nextLine] < state.blkIndent)
72
+ break;
73
+ const text = state.src.slice(pos, max).trimEnd();
74
+ if (text.endsWith("$$")) {
75
+ lastLine = text.slice(0, -2);
76
+ found = true;
77
+ break;
78
+ }
79
+ }
80
+ }
81
+ if (!found)
82
+ return false;
83
+ if (silent)
84
+ return true;
85
+ const body = nextLine === startLine
86
+ ? firstLine
87
+ : [
88
+ firstLine,
89
+ state.getLines(startLine + 1, nextLine, state.tShift[startLine], false),
90
+ lastLine,
91
+ ].join("\n");
92
+ const token = state.push("math_block", "math", 0);
93
+ token.block = true;
94
+ token.content = body.trim();
95
+ token.map = [startLine, nextLine + 1];
96
+ token.markup = "$$";
97
+ state.line = nextLine + 1;
98
+ return true;
99
+ }
100
+ /** Render TeX with KaTeX; on failure record an error and show the source highlighted instead. */
101
+ export function renderTex(tex, displayMode, env, token) {
102
+ env.hasMath = true;
103
+ try {
104
+ return katex.renderToString(tex, {
105
+ displayMode,
106
+ throwOnError: true,
107
+ output: "html",
108
+ strict: "ignore",
109
+ trust: false,
110
+ globalGroup: true,
111
+ macros: env.macros,
112
+ });
113
+ }
114
+ catch (error) {
115
+ const message = error instanceof katex.ParseError ? error.rawMessage : String(error);
116
+ env.diagnostics.push({
117
+ severity: "error",
118
+ message: `LaTeX: ${message}`,
119
+ line: tokenLine(token),
120
+ });
121
+ const tag = displayMode ? "div" : "span";
122
+ return `<${tag} class="math-error" title="${escapeHtml(message)}">${escapeHtml(tex)}</${tag}>`;
123
+ }
124
+ }
125
+ /**
126
+ * LaTeX math rendered at build time with KaTeX (no JavaScript needed in the output):
127
+ * `$inline$`, `$$display$$` blocks and ```` ```math ```` fences (GitHub syntax).
128
+ */
129
+ export function math(md) {
130
+ md.inline.ruler.after("escape", "math_inline", mathInline);
131
+ md.block.ruler.before("fence", "math_block", mathBlock, {
132
+ alt: ["paragraph", "reference", "blockquote", "list"],
133
+ });
134
+ const rules = md.renderer.rules;
135
+ rules.math_inline = (tokens, idx, _options, env) => renderTex(tokens[idx].content, false, env, tokens[idx]);
136
+ rules.math_inline_display = (tokens, idx, _options, env) => renderTex(tokens[idx].content, true, env, tokens[idx]);
137
+ rules.math_block = (tokens, idx, _options, env) => `<div class="math-display">${renderTex(tokens[idx].content, true, env, tokens[idx])}</div>\n`;
138
+ const fence = rules.fence;
139
+ rules.fence = (tokens, idx, options, env, self) => {
140
+ const token = tokens[idx];
141
+ if (token.info.trim().toLowerCase() === "math") {
142
+ return `<div class="math-display">${renderTex(token.content, true, env, token)}</div>\n`;
143
+ }
144
+ return fence(tokens, idx, options, env, self);
145
+ };
146
+ }
147
+ //# sourceMappingURL=math.js.map
@@ -0,0 +1,8 @@
1
+ import type { MarkdownIt } from "markdown-it";
2
+ /** Placeholder left in the HTML for diagram `index`; replaced once the browser renders it. */
3
+ export declare function mermaidPlaceholder(nonce: string, index: number): string;
4
+ /**
5
+ * ```` ```mermaid ```` fences are collected into `env.mermaid` and replaced by a placeholder.
6
+ * Rendering needs a real DOM, so it happens later in the browser (see render/mermaid.ts).
7
+ */
8
+ export declare function mermaid(md: MarkdownIt): void;
@@ -0,0 +1,22 @@
1
+ import { tokenLine } from "./env.js";
2
+ /** Placeholder left in the HTML for diagram `index`; replaced once the browser renders it. */
3
+ export function mermaidPlaceholder(nonce, index) {
4
+ return `<!--mermaid:${nonce}:${index}-->`;
5
+ }
6
+ /**
7
+ * ```` ```mermaid ```` fences are collected into `env.mermaid` and replaced by a placeholder.
8
+ * Rendering needs a real DOM, so it happens later in the browser (see render/mermaid.ts).
9
+ */
10
+ export function mermaid(md) {
11
+ const fence = md.renderer.rules.fence;
12
+ md.renderer.rules.fence = (tokens, idx, options, rawEnv, self) => {
13
+ const token = tokens[idx];
14
+ if (token.info.trim().toLowerCase() !== "mermaid") {
15
+ return fence(tokens, idx, options, rawEnv, self);
16
+ }
17
+ const env = rawEnv;
18
+ const index = env.mermaid.push({ source: token.content, line: tokenLine(token) }) - 1;
19
+ return `<figure class="mermaid-diagram">${mermaidPlaceholder(env.nonce, index)}</figure>\n`;
20
+ };
21
+ }
22
+ //# sourceMappingURL=mermaid.js.map
@@ -0,0 +1,6 @@
1
+ import type { MarkdownIt } from "markdown-it";
2
+ /**
3
+ * GFM task lists: `- [ ] todo` / `- [x] done` become disabled checkboxes.
4
+ * Adds `task-list-item` to the <li> and `contains-task-list` to the parent list, like GitHub.
5
+ */
6
+ export declare function taskLists(md: MarkdownIt): void;
@@ -0,0 +1,40 @@
1
+ const TASK_MARKER = /^\[([ xX])\](?:[ \u00a0]|$)/;
2
+ /**
3
+ * GFM task lists: `- [ ] todo` / `- [x] done` become disabled checkboxes.
4
+ * Adds `task-list-item` to the <li> and `contains-task-list` to the parent list, like GitHub.
5
+ */
6
+ export function taskLists(md) {
7
+ md.core.ruler.after("inline", "task_lists", (state) => {
8
+ const tokens = state.tokens;
9
+ for (let i = 2; i < tokens.length; i++) {
10
+ const inline = tokens[i];
11
+ const item = tokens[i - 2];
12
+ if (inline.type !== "inline" ||
13
+ tokens[i - 1].type !== "paragraph_open" ||
14
+ item.type !== "list_item_open") {
15
+ continue;
16
+ }
17
+ const first = inline.children?.[0];
18
+ const match = first?.type === "text" ? TASK_MARKER.exec(first.content) : null;
19
+ if (!first || !match)
20
+ continue;
21
+ first.content = first.content.slice(match[0].length);
22
+ const checkbox = new state.Token("html_inline", "", 0);
23
+ const checked = match[1] === " " ? "" : " checked";
24
+ checkbox.content = `<input type="checkbox" class="task-list-item-checkbox" disabled${checked}> `;
25
+ inline.children.unshift(checkbox);
26
+ item.attrJoin("class", "task-list-item");
27
+ for (let j = i - 3; j >= 0; j--) {
28
+ const list = tokens[j];
29
+ const isList = list.type === "bullet_list_open" || list.type === "ordered_list_open";
30
+ if (isList && list.level === item.level - 1) {
31
+ if (!String(list.attrGet("class") ?? "").includes("contains-task-list")) {
32
+ list.attrJoin("class", "contains-task-list");
33
+ }
34
+ break;
35
+ }
36
+ }
37
+ }
38
+ });
39
+ }
40
+ //# sourceMappingURL=task-lists.js.map
@@ -0,0 +1,22 @@
1
+ import type { MarkdownIt, Token } from "markdown-it";
2
+ import type { RenderEnv } from "./env.js";
3
+ export interface Heading {
4
+ level: number;
5
+ id: string;
6
+ /** Heading content as HTML (text, inline code and math), without links. */
7
+ html: string;
8
+ }
9
+ /** Placeholder for the table of contents, filled in after rendering. */
10
+ export declare function tocPlaceholder(nonce: string): string;
11
+ /**
12
+ * A paragraph containing only `[[toc]]` or `[toc]` marks where the table of contents goes.
13
+ * Without a marker, `--toc` puts it at the start of the document.
14
+ */
15
+ export declare function tocMarker(md: MarkdownIt): void;
16
+ /** Collect headings (with the ids set by markdown-it-anchor) from parsed tokens. */
17
+ export declare function collectHeadings(md: MarkdownIt, tokens: Token[], env: RenderEnv): Heading[];
18
+ /**
19
+ * Nested list of links to the headings up to `depth`. A single level-1 heading is the document
20
+ * title and is left out. Skipped levels (h2 → h4) nest under an empty item.
21
+ */
22
+ export declare function buildToc(headings: Heading[], depth: number, title: string): string;
@@ -0,0 +1,101 @@
1
+ import { escapeHtml } from "../html.js";
2
+ const MARKER = /^\[\[?toc\]?\]$/i;
3
+ /** Placeholder for the table of contents, filled in after rendering. */
4
+ export function tocPlaceholder(nonce) {
5
+ return `<!--toc:${nonce}-->`;
6
+ }
7
+ /**
8
+ * A paragraph containing only `[[toc]]` or `[toc]` marks where the table of contents goes.
9
+ * Without a marker, `--toc` puts it at the start of the document.
10
+ */
11
+ export function tocMarker(md) {
12
+ md.block.ruler.before("paragraph", "toc_marker", (state, startLine, _endLine, silent) => {
13
+ const start = state.bMarks[startLine] + state.tShift[startLine];
14
+ const line = state.src.slice(start, state.eMarks[startLine]).trim();
15
+ if (!MARKER.test(line))
16
+ return false;
17
+ if (!silent) {
18
+ const token = state.push("toc_marker", "", 0);
19
+ token.map = [startLine, startLine + 1];
20
+ state.line = startLine + 1;
21
+ }
22
+ return true;
23
+ });
24
+ md.renderer.rules.toc_marker = (_tokens, _idx, _options, env) => {
25
+ const renderEnv = env;
26
+ renderEnv.hasTocMarker = true;
27
+ return tocPlaceholder(renderEnv.nonce) + "\n";
28
+ };
29
+ }
30
+ /** Collect headings (with the ids set by markdown-it-anchor) from parsed tokens. */
31
+ export function collectHeadings(md, tokens, env) {
32
+ const headings = [];
33
+ tokens.forEach((token, i) => {
34
+ if (token.type !== "heading_open")
35
+ return;
36
+ const id = token.attrGet("id");
37
+ const inline = tokens[i + 1];
38
+ if (!id || !inline?.children)
39
+ return;
40
+ const html = inline.children
41
+ .map((child) => {
42
+ switch (child.type) {
43
+ case "text":
44
+ return escapeHtml(child.content);
45
+ case "code_inline":
46
+ return `<code>${escapeHtml(child.content)}</code>`;
47
+ case "math_inline":
48
+ case "softbreak":
49
+ return md.renderer.renderInline([child], md.options, env);
50
+ default:
51
+ return "";
52
+ }
53
+ })
54
+ .join("")
55
+ .trim();
56
+ headings.push({ level: Number(token.tag.slice(1)), id: String(id), html });
57
+ });
58
+ return headings;
59
+ }
60
+ function renderNodes(nodes) {
61
+ const items = nodes.map((node) => {
62
+ const link = node.heading
63
+ ? `<a href="#${escapeHtml(node.heading.id)}">${node.heading.html}</a>`
64
+ : "";
65
+ return `<li>${link}${node.children.length ? renderNodes(node.children) : ""}</li>`;
66
+ });
67
+ return `<ul>${items.join("")}</ul>`;
68
+ }
69
+ /**
70
+ * Nested list of links to the headings up to `depth`. A single level-1 heading is the document
71
+ * title and is left out. Skipped levels (h2 → h4) nest under an empty item.
72
+ */
73
+ export function buildToc(headings, depth, title) {
74
+ let items = headings.filter((h) => h.level <= depth);
75
+ if (headings.filter((h) => h.level === 1).length === 1)
76
+ items = items.filter((h) => h.level > 1);
77
+ if (items.length === 0)
78
+ return "";
79
+ const base = Math.min(...items.map((h) => h.level));
80
+ const root = { children: [] };
81
+ // stack[k] is the node whose children hold level base + k.
82
+ const stack = [root];
83
+ for (const heading of items) {
84
+ const depthIndex = heading.level - base;
85
+ stack.length = Math.min(stack.length, depthIndex + 1);
86
+ while (stack.length <= depthIndex) {
87
+ const parent = stack[stack.length - 1];
88
+ let last = parent.children[parent.children.length - 1];
89
+ if (!last) {
90
+ last = { children: [] };
91
+ parent.children.push(last);
92
+ }
93
+ stack.push(last);
94
+ }
95
+ const node = { heading, children: [] };
96
+ stack[depthIndex].children.push(node);
97
+ stack.push(node);
98
+ }
99
+ return `<nav class="toc"><p class="toc-title">${escapeHtml(title)}</p>\n${renderNodes(root.children)}\n</nav>\n`;
100
+ }
101
+ //# sourceMappingURL=toc.js.map
@@ -0,0 +1,55 @@
1
+ import { MdRenderError } from "./errors.js";
2
+ export declare const PAGE_SIZES: readonly ["A3", "A4", "A5", "Letter", "Legal", "Tabloid"];
3
+ export type PageSize = (typeof PAGE_SIZES)[number];
4
+ export declare const MERMAID_THEMES: readonly ["default", "neutral", "dark", "forest", "base"];
5
+ export type MermaidTheme = (typeof MERMAID_THEMES)[number];
6
+ export declare const BUILTIN_THEMES: readonly ["light", "dark"];
7
+ /** Rendering options, merged from defaults < config file < front matter < command line. */
8
+ export interface DocumentOptions {
9
+ pageSize: PageSize;
10
+ /** CSS margin shorthand: 1 to 4 lengths in mm, cm, in or px. */
11
+ margin: string;
12
+ landscape: boolean;
13
+ toc: boolean;
14
+ /** Deepest heading level listed in the table of contents. */
15
+ tocDepth: number;
16
+ /** Default footer with "page / pages". Ignored when `footer` is set. */
17
+ pageNumbers: boolean;
18
+ /** Header/footer text; see render/header-footer.ts for placeholders and `|` columns. */
19
+ header?: string;
20
+ footer?: string;
21
+ /** Mermaid theme; when unset, "neutral" for light pages and "dark" for the dark theme. */
22
+ mermaidTheme?: MermaidTheme;
23
+ /** "light", "dark", or the absolute path of a stylesheet that replaces the built-in theme. */
24
+ theme: string;
25
+ /** Extra stylesheets (absolute paths), applied after the built-in theme. */
26
+ css: string[];
27
+ /** BCP 47 language of the document, e.g. "es" or "en". */
28
+ lang: string;
29
+ title?: string;
30
+ subtitle?: string;
31
+ author?: string;
32
+ date?: string;
33
+ }
34
+ export declare const DEFAULT_OPTIONS: DocumentOptions;
35
+ export declare class OptionsError extends MdRenderError {
36
+ constructor(message: string);
37
+ }
38
+ /** Parse a CSS margin shorthand into the four sides, or throw. */
39
+ export declare function parseMargin(margin: string): {
40
+ top: string;
41
+ right: string;
42
+ bottom: string;
43
+ left: string;
44
+ };
45
+ /**
46
+ * Validate options from a config file, front matter or the command line.
47
+ *
48
+ * Keys may be camelCase or kebab-case (`page-size`). Unknown keys are ignored so front matter can
49
+ * hold other metadata. Relative CSS paths resolve against `baseDir`.
50
+ */
51
+ export declare function normalizeOptions(raw: Record<string, unknown>, source: string, baseDir: string,
52
+ /** How to name a key in error messages (e.g. as a command-line flag). */
53
+ label?: (key: string) => string): Partial<DocumentOptions>;
54
+ /** Merge option layers, later ones winning. CSS lists accumulate instead of replacing. */
55
+ export declare function mergeOptions(...layers: Partial<DocumentOptions>[]): DocumentOptions;