@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,167 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { findBrowser } from "./browser/detect.js";
5
+ import { findConfigFile, loadConfig } from "./config.js";
6
+ import { extractFrontMatter } from "./frontmatter.js";
7
+ import { strings } from "./i18n.js";
8
+ import { renderMarkdown } from "./markdown/index.js";
9
+ import { buildToc, tocPlaceholder } from "./markdown/toc.js";
10
+ import { mergeOptions, normalizeOptions, OptionsError, parseMargin, } from "./options.js";
11
+ import { BrowserSession } from "./render/browser.js";
12
+ import { DEFAULT_FOOTER, headerFooterTemplate } from "./render/header-footer.js";
13
+ import { fillMermaid, renderMermaid } from "./render/mermaid.js";
14
+ import { contentWidthPx } from "./render/page.js";
15
+ import { buildHtmlDocument, builtinThemeCss } from "./render/template.js";
16
+ /** Starts one browser on first use and keeps it for later conversions. */
17
+ export class BrowserPool {
18
+ executable;
19
+ log;
20
+ session;
21
+ constructor(executable,
22
+ /** Receives notes about the browser (which one started, sandbox fallback) for --verbose. */
23
+ log) {
24
+ this.executable = executable;
25
+ this.log = log;
26
+ }
27
+ get() {
28
+ if (!this.session) {
29
+ const executablePath = findBrowser(this.executable);
30
+ this.log?.(`browser: ${executablePath}`);
31
+ this.session = BrowserSession.open(executablePath, undefined, () => this.log?.("browser: sandbox unavailable on this system, running without it"));
32
+ }
33
+ // A failed start is not cached: the next conversion tries again.
34
+ this.session.catch(() => (this.session = undefined));
35
+ return this.session;
36
+ }
37
+ async close() {
38
+ const session = await this.session?.catch(() => undefined);
39
+ this.session = undefined;
40
+ await session?.close();
41
+ }
42
+ }
43
+ function readStylesheet(file) {
44
+ try {
45
+ return readFileSync(file, "utf8");
46
+ }
47
+ catch {
48
+ throw new OptionsError(`stylesheet not found: ${file}`);
49
+ }
50
+ }
51
+ function themeStylesheets(theme) {
52
+ return theme === "light" || theme === "dark" ? builtinThemeCss(theme) : [readStylesheet(theme)];
53
+ }
54
+ /** Mermaid theme: explicit choice, else dark diagrams for the dark page theme. */
55
+ export function effectiveMermaidTheme(options) {
56
+ return options.mermaidTheme ?? (options.theme === "dark" ? "dark" : "neutral");
57
+ }
58
+ /**
59
+ * Render a Markdown file to a complete HTML document. A browser is only started (through
60
+ * `browsers`) when the document has Mermaid diagrams.
61
+ */
62
+ export async function renderDocument(inputPath, settings, browsers) {
63
+ const input = path.resolve(inputPath);
64
+ const baseDir = path.dirname(input);
65
+ const source = await readFile(input, "utf8");
66
+ const frontMatter = extractFrontMatter(source, input);
67
+ const configFile = settings.config === false
68
+ ? undefined
69
+ : settings.config
70
+ ? path.resolve(settings.config)
71
+ : findConfigFile(baseDir);
72
+ const options = mergeOptions(configFile ? loadConfig(configFile) : {}, normalizeOptions(frontMatter.data, `${input} (front matter)`, baseDir), settings.overrides ?? {});
73
+ const margin = parseMargin(options.margin);
74
+ const themeCss = themeStylesheets(options.theme);
75
+ const extraCss = options.css.map(readStylesheet);
76
+ const md = renderMarkdown(frontMatter.body, { baseDir, lang: options.lang });
77
+ const diagnostics = [...md.diagnostics];
78
+ const title = options.title ?? md.title ?? path.parse(input).name;
79
+ // Table of contents: at the [[toc]] marker, or at the start with --toc.
80
+ let body = md.html;
81
+ const toc = buildToc(md.headings, options.tocDepth, strings(options.lang).toc);
82
+ if (md.hasTocMarker) {
83
+ body = body.replace(tocPlaceholder(md.nonce), () => toc);
84
+ }
85
+ else if (options.toc) {
86
+ body = toc + body;
87
+ }
88
+ // Mermaid needs a real DOM to lay out diagrams, also for HTML output.
89
+ if (md.mermaid.length > 0) {
90
+ const session = await browsers.get();
91
+ const width = contentWidthPx(options.pageSize, options.landscape, margin);
92
+ const theme = effectiveMermaidTheme(options);
93
+ const results = await renderMermaid(session, md.mermaid, theme, width);
94
+ const filled = fillMermaid(body, md.nonce, md.mermaid, results);
95
+ body = filled.html;
96
+ diagnostics.push(...filled.diagnostics);
97
+ }
98
+ const html = buildHtmlDocument({
99
+ title,
100
+ body,
101
+ math: md.hasMath,
102
+ lang: options.lang,
103
+ subtitle: options.subtitle,
104
+ author: options.author,
105
+ date: options.date,
106
+ showTitle: options.title !== undefined,
107
+ themeCss,
108
+ extraCss,
109
+ });
110
+ diagnostics.sort((a, b) => (a.line ?? 0) - (b.line ?? 0));
111
+ const dependencies = [
112
+ ...(configFile ? [configFile] : []),
113
+ ...(options.theme.endsWith(".css") ? [options.theme] : []),
114
+ ...options.css,
115
+ ];
116
+ return { html, title, diagnostics, options, configFile, dependencies };
117
+ }
118
+ /** Read a Markdown file and write it as a self-contained HTML document or a PDF. */
119
+ export async function convertFile(convert) {
120
+ const output = path.resolve(convert.output);
121
+ const browsers = convert.browsers ?? new BrowserPool(convert.browser);
122
+ try {
123
+ const doc = await renderDocument(convert.input, convert, browsers);
124
+ const { options } = doc;
125
+ await mkdir(path.dirname(output), { recursive: true });
126
+ if (convert.format === "pdf") {
127
+ const margin = parseMargin(options.margin);
128
+ const values = { title: doc.title, author: options.author, date: options.date };
129
+ const footer = options.footer ?? (options.pageNumbers ? DEFAULT_FOOTER : undefined);
130
+ const hasHeaderFooter = options.header !== undefined || footer !== undefined;
131
+ const session = await browsers.get();
132
+ const { blocked } = await session.printPdf(doc.html, {
133
+ output,
134
+ format: options.pageSize,
135
+ landscape: options.landscape,
136
+ margin,
137
+ headerTemplate: hasHeaderFooter
138
+ ? headerFooterTemplate(options.header, values, margin)
139
+ : undefined,
140
+ footerTemplate: hasHeaderFooter ? headerFooterTemplate(footer, values, margin) : undefined,
141
+ allowRemote: convert.allowRemote,
142
+ });
143
+ for (const url of blocked) {
144
+ const hint = /^https?:/i.test(url) ? " (use --allow-remote to download it)" : "";
145
+ doc.diagnostics.push({
146
+ severity: "warning",
147
+ message: `resource not loaded: ${url}${hint}`,
148
+ });
149
+ }
150
+ }
151
+ else {
152
+ await writeFile(output, doc.html, "utf8");
153
+ }
154
+ return {
155
+ output,
156
+ diagnostics: doc.diagnostics,
157
+ options,
158
+ configFile: doc.configFile,
159
+ dependencies: doc.dependencies,
160
+ };
161
+ }
162
+ finally {
163
+ if (!convert.browsers)
164
+ await browsers.close();
165
+ }
166
+ }
167
+ //# sourceMappingURL=convert.js.map
@@ -0,0 +1,8 @@
1
+ export interface Diagnostic {
2
+ severity: "warning" | "error";
3
+ message: string;
4
+ /** 1-based line in the Markdown source, when known. */
5
+ line?: number;
6
+ }
7
+ /** `file:line: message`, the conventional compiler-style location prefix. */
8
+ export declare function formatDiagnostic(diagnostic: Diagnostic, file?: string): string;
@@ -0,0 +1,6 @@
1
+ /** `file:line: message`, the conventional compiler-style location prefix. */
2
+ export function formatDiagnostic(diagnostic, file) {
3
+ const location = [file, diagnostic.line].filter((part) => part !== undefined).join(":");
4
+ return `${diagnostic.severity}: ${location ? `${location}: ` : ""}${diagnostic.message}`;
5
+ }
6
+ //# sourceMappingURL=diagnostics.js.map
@@ -0,0 +1,12 @@
1
+ import { ExitCode } from "./exit-codes.js";
2
+ /** An expected failure with a user-facing message and a specific exit code. */
3
+ export declare class MdRenderError extends Error {
4
+ readonly exitCode: ExitCode;
5
+ constructor(message: string, exitCode: ExitCode);
6
+ }
7
+ export declare class BrowserNotFoundError extends MdRenderError {
8
+ constructor(message: string);
9
+ }
10
+ export declare class RenderError extends MdRenderError {
11
+ constructor(message: string);
12
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,21 @@
1
+ import { ExitCode } from "./exit-codes.js";
2
+ /** An expected failure with a user-facing message and a specific exit code. */
3
+ export class MdRenderError extends Error {
4
+ exitCode;
5
+ constructor(message, exitCode) {
6
+ super(message);
7
+ this.exitCode = exitCode;
8
+ this.name = new.target.name;
9
+ }
10
+ }
11
+ export class BrowserNotFoundError extends MdRenderError {
12
+ constructor(message) {
13
+ super(message, ExitCode.BrowserNotFound);
14
+ }
15
+ }
16
+ export class RenderError extends MdRenderError {
17
+ constructor(message) {
18
+ super(message, ExitCode.Render);
19
+ }
20
+ }
21
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1,8 @@
1
+ /** Process exit codes, as documented in docs/PLAN_MAESTRO.md §3. */
2
+ export declare const ExitCode: {
3
+ readonly Ok: 0;
4
+ readonly Usage: 1;
5
+ readonly Render: 2;
6
+ readonly BrowserNotFound: 3;
7
+ };
8
+ export type ExitCode = (typeof ExitCode)[keyof typeof ExitCode];
@@ -0,0 +1,8 @@
1
+ /** Process exit codes, as documented in docs/PLAN_MAESTRO.md §3. */
2
+ export const ExitCode = {
3
+ Ok: 0,
4
+ Usage: 1,
5
+ Render: 2,
6
+ BrowserNotFound: 3,
7
+ };
8
+ //# sourceMappingURL=exit-codes.js.map
@@ -0,0 +1,10 @@
1
+ export interface FrontMatter {
2
+ data: Record<string, unknown>;
3
+ /**
4
+ * The Markdown without the front matter block. The block is replaced by empty lines so that
5
+ * line numbers in diagnostics still match the original file.
6
+ */
7
+ body: string;
8
+ }
9
+ /** Split a leading YAML front matter block (`---` ... `---`) from the Markdown source. */
10
+ export declare function extractFrontMatter(source: string, file?: string): FrontMatter;
@@ -0,0 +1,26 @@
1
+ import { parse } from "yaml";
2
+ import { OptionsError } from "./options.js";
3
+ const BLOCK = /^---[ \t]*\r?\n([\s\S]*?)\r?\n(?:---|\.\.\.)[ \t]*(?:\r?\n|$)/;
4
+ /** Split a leading YAML front matter block (`---` ... `---`) from the Markdown source. */
5
+ export function extractFrontMatter(source, file = "front matter") {
6
+ const text = source.replace(/^\uFEFF/, "");
7
+ const match = BLOCK.exec(text);
8
+ if (!match)
9
+ return { data: {}, body: text };
10
+ let data;
11
+ try {
12
+ data = parse(match[1]) ?? {};
13
+ }
14
+ catch (error) {
15
+ throw new OptionsError(`${file}: invalid YAML front matter: ${error.message}`);
16
+ }
17
+ if (typeof data !== "object" || Array.isArray(data)) {
18
+ throw new OptionsError(`${file}: front matter must be a set of "key: value" pairs`);
19
+ }
20
+ const lines = match[0].split("\n").length - 1;
21
+ return {
22
+ data: data,
23
+ body: "\n".repeat(lines) + text.slice(match[0].length),
24
+ };
25
+ }
26
+ //# sourceMappingURL=frontmatter.js.map
package/dist/html.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ /** Escape text for use in HTML content and attribute values. */
2
+ export declare function escapeHtml(text: string): string;
package/dist/html.js ADDED
@@ -0,0 +1,12 @@
1
+ const ENTITIES = {
2
+ "&": "&amp;",
3
+ "<": "&lt;",
4
+ ">": "&gt;",
5
+ '"': "&quot;",
6
+ "'": "&#39;",
7
+ };
8
+ /** Escape text for use in HTML content and attribute values. */
9
+ export function escapeHtml(text) {
10
+ return text.replace(/[&<>"']/g, (c) => ENTITIES[c]);
11
+ }
12
+ //# sourceMappingURL=html.js.map
package/dist/i18n.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ export type AlertType = "note" | "tip" | "important" | "warning" | "caution";
2
+ export interface Strings {
3
+ toc: string;
4
+ alerts: Record<AlertType, string>;
5
+ }
6
+ /** Default document language: Spanish. English and others are available with `lang`. */
7
+ export declare const DEFAULT_LANG = "es";
8
+ /** UI strings for a BCP 47 language tag ("es-AR" uses "es"); Spanish as fallback. */
9
+ export declare function strings(lang: string): Strings;
package/dist/i18n.js ADDED
@@ -0,0 +1,59 @@
1
+ const STRINGS = {
2
+ en: {
3
+ toc: "Contents",
4
+ alerts: {
5
+ note: "Note",
6
+ tip: "Tip",
7
+ important: "Important",
8
+ warning: "Warning",
9
+ caution: "Caution",
10
+ },
11
+ },
12
+ es: {
13
+ toc: "Contenido",
14
+ alerts: {
15
+ note: "Nota",
16
+ tip: "Consejo",
17
+ important: "Importante",
18
+ warning: "Advertencia",
19
+ caution: "Precaución",
20
+ },
21
+ },
22
+ pt: {
23
+ toc: "Sumário",
24
+ alerts: {
25
+ note: "Nota",
26
+ tip: "Dica",
27
+ important: "Importante",
28
+ warning: "Aviso",
29
+ caution: "Cuidado",
30
+ },
31
+ },
32
+ fr: {
33
+ toc: "Sommaire",
34
+ alerts: {
35
+ note: "Remarque",
36
+ tip: "Astuce",
37
+ important: "Important",
38
+ warning: "Avertissement",
39
+ caution: "Attention",
40
+ },
41
+ },
42
+ de: {
43
+ toc: "Inhalt",
44
+ alerts: {
45
+ note: "Hinweis",
46
+ tip: "Tipp",
47
+ important: "Wichtig",
48
+ warning: "Warnung",
49
+ caution: "Vorsicht",
50
+ },
51
+ },
52
+ };
53
+ /** Default document language: Spanish. English and others are available with `lang`. */
54
+ export const DEFAULT_LANG = "es";
55
+ /** UI strings for a BCP 47 language tag ("es-AR" uses "es"); Spanish as fallback. */
56
+ export function strings(lang) {
57
+ return STRINGS[lang.toLowerCase().split("-")[0]] ?? STRINGS[DEFAULT_LANG];
58
+ }
59
+ //# sourceMappingURL=i18n.js.map
@@ -0,0 +1,17 @@
1
+ export { findBrowser, browserCandidates } from "./browser/detect.js";
2
+ export { main } from "./cli.js";
3
+ export { CONFIG_FILE, findConfigFile, loadConfig } from "./config.js";
4
+ export { BrowserPool, convertFile, renderDocument, type ConvertOptions, type ConvertResult, type RenderSettings, } from "./convert.js";
5
+ export { formatDiagnostic, type Diagnostic } from "./diagnostics.js";
6
+ export { BrowserNotFoundError, MdRenderError, RenderError } from "./errors.js";
7
+ export { ExitCode } from "./exit-codes.js";
8
+ export { renderMarkdown, type MarkdownResult } from "./markdown/index.js";
9
+ export { extractFrontMatter } from "./frontmatter.js";
10
+ export { DEFAULT_OPTIONS, mergeOptions, normalizeOptions, OptionsError, type DocumentOptions, } from "./options.js";
11
+ export { expandInputs, planOutputs, type InputFile, type Job } from "./inputs.js";
12
+ export { isMarkdownFile, type OutputFormat } from "./paths.js";
13
+ export { preview } from "./preview.js";
14
+ export { runJobs } from "./run.js";
15
+ export { watchAndConvert } from "./watch.js";
16
+ export { buildHtmlDocument } from "./render/template.js";
17
+ export { version } from "./version.js";
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ export { findBrowser, browserCandidates } from "./browser/detect.js";
2
+ export { main } from "./cli.js";
3
+ export { CONFIG_FILE, findConfigFile, loadConfig } from "./config.js";
4
+ export { BrowserPool, convertFile, renderDocument, } from "./convert.js";
5
+ export { formatDiagnostic } from "./diagnostics.js";
6
+ export { BrowserNotFoundError, MdRenderError, RenderError } from "./errors.js";
7
+ export { ExitCode } from "./exit-codes.js";
8
+ export { renderMarkdown } from "./markdown/index.js";
9
+ export { extractFrontMatter } from "./frontmatter.js";
10
+ export { DEFAULT_OPTIONS, mergeOptions, normalizeOptions, OptionsError, } from "./options.js";
11
+ export { expandInputs, planOutputs } from "./inputs.js";
12
+ export { isMarkdownFile } from "./paths.js";
13
+ export { preview } from "./preview.js";
14
+ export { runJobs } from "./run.js";
15
+ export { watchAndConvert } from "./watch.js";
16
+ export { buildHtmlDocument } from "./render/template.js";
17
+ export { version } from "./version.js";
18
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,26 @@
1
+ import { type OutputFormat } from "./paths.js";
2
+ /** A Markdown file to convert, with the folder its output path is relative to. */
3
+ export interface InputFile {
4
+ file: string;
5
+ /** Directory or glob base the file was found under; undefined for files named directly. */
6
+ root?: string;
7
+ }
8
+ /**
9
+ * Expand command-line inputs into Markdown files: files as given, folders searched recursively
10
+ * (skipping hidden folders and node_modules), and glob patterns (`docs/**\/*.md`) expanded here
11
+ * so they also work in Windows shells, which do not expand them.
12
+ */
13
+ export declare function expandInputs(inputs: string[], cwd?: string): InputFile[];
14
+ export interface Job {
15
+ input: string;
16
+ output: string;
17
+ }
18
+ /**
19
+ * Decide each output path.
20
+ *
21
+ * - No `output`: next to each Markdown file.
22
+ * - One input file and `output` is not a folder: `output` is the file to write.
23
+ * - Otherwise `output` is a folder; files found under a folder or glob keep their relative
24
+ * path inside it, so `docs/a/x.md` → `out/a/x.pdf`.
25
+ */
26
+ export declare function planOutputs(inputs: InputFile[], format: OutputFormat, output?: string, cwd?: string): Job[];
package/dist/inputs.js ADDED
@@ -0,0 +1,118 @@
1
+ import { existsSync, readdirSync, statSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { globSync, isDynamicPattern } from "tinyglobby";
4
+ import { OptionsError } from "./options.js";
5
+ import { isMarkdownFile } from "./paths.js";
6
+ /** Folders never searched when an input is a directory or a glob. */
7
+ const SKIPPED_DIRS = new Set(["node_modules"]);
8
+ function walk(dir, found) {
9
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
10
+ if (entry.name.startsWith(".") || SKIPPED_DIRS.has(entry.name))
11
+ continue;
12
+ const full = path.join(dir, entry.name);
13
+ if (entry.isDirectory())
14
+ walk(full, found);
15
+ else if (entry.isFile() && isMarkdownFile(entry.name))
16
+ found.push(full);
17
+ }
18
+ }
19
+ /** Leading part of a glob without wildcards, e.g. "docs/" for "docs/**\/*.md". */
20
+ function globBase(pattern) {
21
+ const parts = pattern.split("/");
22
+ const fixed = [];
23
+ for (const part of parts.slice(0, -1)) {
24
+ if (isDynamicPattern(part))
25
+ break;
26
+ fixed.push(part);
27
+ }
28
+ return fixed.join("/") || ".";
29
+ }
30
+ /**
31
+ * Expand command-line inputs into Markdown files: files as given, folders searched recursively
32
+ * (skipping hidden folders and node_modules), and glob patterns (`docs/**\/*.md`) expanded here
33
+ * so they also work in Windows shells, which do not expand them.
34
+ */
35
+ export function expandInputs(inputs, cwd = process.cwd()) {
36
+ const files = [];
37
+ for (const input of inputs) {
38
+ const resolved = path.resolve(cwd, input);
39
+ if (existsSync(resolved)) {
40
+ if (statSync(resolved).isDirectory()) {
41
+ const found = [];
42
+ walk(resolved, found);
43
+ if (found.length === 0)
44
+ throw new OptionsError(`no Markdown files in folder: ${input}`);
45
+ files.push(...found.sort().map((file) => ({ file, root: resolved })));
46
+ }
47
+ else if (isMarkdownFile(resolved)) {
48
+ files.push({ file: resolved });
49
+ }
50
+ else {
51
+ throw new OptionsError(`input is not a Markdown file (.md, .markdown): ${input}`);
52
+ }
53
+ continue;
54
+ }
55
+ const pattern = input.replace(/\\/g, "/");
56
+ if (isDynamicPattern(pattern)) {
57
+ const matches = globSync(pattern, {
58
+ cwd,
59
+ absolute: true,
60
+ onlyFiles: true,
61
+ ignore: ["**/node_modules/**", "**/.*/**"],
62
+ })
63
+ .filter(isMarkdownFile)
64
+ .map((file) => path.resolve(file))
65
+ .sort();
66
+ if (matches.length === 0)
67
+ throw new OptionsError(`no Markdown files match: ${input}`);
68
+ const root = path.resolve(cwd, globBase(pattern));
69
+ files.push(...matches.map((file) => ({ file, root })));
70
+ continue;
71
+ }
72
+ throw new OptionsError(`input file not found: ${input}`);
73
+ }
74
+ // The same file may be reached twice (e.g. a folder and a glob): keep the first.
75
+ const seen = new Set();
76
+ return files.filter(({ file }) => !seen.has(file) && seen.add(file));
77
+ }
78
+ /**
79
+ * Decide each output path.
80
+ *
81
+ * - No `output`: next to each Markdown file.
82
+ * - One input file and `output` is not a folder: `output` is the file to write.
83
+ * - Otherwise `output` is a folder; files found under a folder or glob keep their relative
84
+ * path inside it, so `docs/a/x.md` → `out/a/x.pdf`.
85
+ */
86
+ export function planOutputs(inputs, format, output, cwd = process.cwd()) {
87
+ const ext = `.${format}`;
88
+ const outDir = output !== undefined ? path.resolve(cwd, output) : undefined;
89
+ const several = inputs.length > 1 || inputs[0]?.root !== undefined;
90
+ const looksLikeFile = output !== undefined && /\.(pdf|html?)$/i.test(output);
91
+ const outputIsDir = outDir !== undefined &&
92
+ (/[\\/]$/.test(output) ||
93
+ (existsSync(outDir) ? statSync(outDir).isDirectory() : several && !looksLikeFile));
94
+ if (outDir && !outputIsDir) {
95
+ if (several) {
96
+ throw new OptionsError(`with several input files, --output must be a folder: ${output}`);
97
+ }
98
+ return [{ input: inputs[0].file, output: outDir }];
99
+ }
100
+ const jobs = inputs.map(({ file, root }) => {
101
+ const name = path.parse(file).name + ext;
102
+ if (!outDir)
103
+ return { input: file, output: path.join(path.dirname(file), name) };
104
+ const relativeDir = root ? path.relative(root, path.dirname(file)) : "";
105
+ return { input: file, output: path.join(outDir, relativeDir, name) };
106
+ });
107
+ const targets = new Map();
108
+ for (const job of jobs) {
109
+ const key = process.platform === "win32" ? job.output.toLowerCase() : job.output;
110
+ const other = targets.get(key);
111
+ if (other) {
112
+ throw new OptionsError(`${other} and ${job.input} would both be written to ${job.output}`);
113
+ }
114
+ targets.set(key, job.input);
115
+ }
116
+ return jobs;
117
+ }
118
+ //# sourceMappingURL=inputs.js.map
@@ -0,0 +1,6 @@
1
+ import type { MarkdownIt } from "markdown-it";
2
+ /**
3
+ * GitHub alerts: a blockquote whose first line is `[!NOTE]`, `[!TIP]`, `[!IMPORTANT]`,
4
+ * `[!WARNING]` or `[!CAUTION]` becomes a styled callout with a title.
5
+ */
6
+ export declare function alerts(md: MarkdownIt): void;
@@ -0,0 +1,35 @@
1
+ import { DEFAULT_LANG, strings } from "../i18n.js";
2
+ const MARKER = /^\[!(note|tip|important|warning|caution)\][ \t]*$/i;
3
+ /**
4
+ * GitHub alerts: a blockquote whose first line is `[!NOTE]`, `[!TIP]`, `[!IMPORTANT]`,
5
+ * `[!WARNING]` or `[!CAUTION]` becomes a styled callout with a title.
6
+ */
7
+ export function alerts(md) {
8
+ md.core.ruler.after("inline", "github_alerts", (state) => {
9
+ const tokens = state.tokens;
10
+ const titles = strings(state.env.lang ?? DEFAULT_LANG).alerts;
11
+ for (let i = 0; i + 2 < tokens.length; i++) {
12
+ const open = tokens[i];
13
+ const inline = tokens[i + 2];
14
+ if (open.type !== "blockquote_open" || tokens[i + 1].type !== "paragraph_open")
15
+ continue;
16
+ const first = inline.children?.[0];
17
+ const match = first?.type === "text" ? MARKER.exec(first.content) : null;
18
+ if (!match)
19
+ continue;
20
+ const type = match[1].toLowerCase();
21
+ open.attrJoin("class", `markdown-alert markdown-alert-${type}`);
22
+ // Drop the marker (and the line break after it); drop the paragraph if nothing is left.
23
+ const children = inline.children;
24
+ children.splice(0, children[1]?.type === "softbreak" ? 2 : 1);
25
+ inline.content = inline.content.replace(/^[^\n]*\n?/, "");
26
+ if (children.length === 0) {
27
+ tokens.splice(i + 1, 3);
28
+ }
29
+ const title = new state.Token("html_block", "", 0);
30
+ title.content = `<p class="markdown-alert-title">${titles[type]}</p>\n`;
31
+ tokens.splice(i + 1, 0, title);
32
+ }
33
+ });
34
+ }
35
+ //# sourceMappingURL=alerts.js.map
@@ -0,0 +1,29 @@
1
+ import type { Env, MarkdownIt, Token } from "markdown-it";
2
+ import type { Diagnostic } from "../diagnostics.js";
3
+ export interface MermaidBlock {
4
+ source: string;
5
+ /** 1-based source line of the opening fence. */
6
+ line?: number;
7
+ }
8
+ /** Per-document state shared by the Markdown plugins during one render. */
9
+ export interface RenderEnv extends Env {
10
+ /** Directory that relative image paths are resolved against (the .md file's folder). */
11
+ baseDir: string;
12
+ diagnostics: Diagnostic[];
13
+ /** Mermaid diagrams found in the document, rendered later in a browser. */
14
+ mermaid: MermaidBlock[];
15
+ /** Random token that makes Mermaid placeholders impossible to forge from Markdown. */
16
+ nonce: string;
17
+ /** KaTeX macros, shared by every formula so `\newcommand`/`\def` work across the document. */
18
+ macros: Record<string, string | object>;
19
+ /** Set when the document contains at least one formula. */
20
+ hasMath: boolean;
21
+ /** Document language, for translated labels (alert titles). */
22
+ lang: string;
23
+ /** Set when the document places its table of contents with a `[[toc]]` marker. */
24
+ hasTocMarker: boolean;
25
+ }
26
+ /** 1-based line of a token, from markdown-it's 0-based `[start, end)` line map. */
27
+ export declare function tokenLine(token: Token): number | undefined;
28
+ /** Copy each inline token's line map to its children so inline errors can report a line. */
29
+ export declare function inlineChildLines(md: MarkdownIt): void;
@@ -0,0 +1,16 @@
1
+ /** 1-based line of a token, from markdown-it's 0-based `[start, end)` line map. */
2
+ export function tokenLine(token) {
3
+ return token.map ? token.map[0] + 1 : undefined;
4
+ }
5
+ /** Copy each inline token's line map to its children so inline errors can report a line. */
6
+ export function inlineChildLines(md) {
7
+ md.core.ruler.push("inline_child_lines", (state) => {
8
+ for (const token of state.tokens) {
9
+ if (token.type !== "inline" || !token.map)
10
+ continue;
11
+ for (const child of token.children ?? [])
12
+ child.map ??= token.map;
13
+ }
14
+ });
15
+ }
16
+ //# sourceMappingURL=env.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Syntax highlighting for fenced code with a language (` ```ts `). highlight.js's "common" set
3
+ * covers ~35 popular languages. Unknown or missing languages are shown as plain text: no
4
+ * auto-detection, so output never changes with heuristics.
5
+ */
6
+ export declare function highlightCode(code: string, lang: string): string;