@imontene/mdrender 2.0.0 → 2.2.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.
- package/CHANGELOG.md +33 -0
- package/README.md +89 -29
- package/dist/assets/density-compact.css +74 -0
- package/dist/assets/theme.css +34 -0
- package/dist/assets.js +5 -1
- package/dist/cli.js +43 -1
- package/dist/convert.d.ts +12 -1
- package/dist/convert.js +61 -7
- package/dist/doctor.d.ts +30 -0
- package/dist/doctor.js +278 -0
- package/dist/markdown/code-blocks.d.ts +9 -0
- package/dist/markdown/code-blocks.js +21 -0
- package/dist/markdown/env.d.ts +2 -0
- package/dist/markdown/index.js +5 -0
- package/dist/markdown/keep-with-heading.d.ts +8 -0
- package/dist/markdown/keep-with-heading.js +44 -0
- package/dist/markdown/mermaid.js +3 -1
- package/dist/options.d.ts +16 -0
- package/dist/options.js +30 -0
- package/dist/render/browser.d.ts +2 -0
- package/dist/render/browser.js +4 -0
- package/dist/render/mermaid-fit.d.ts +81 -0
- package/dist/render/mermaid-fit.js +135 -0
- package/dist/render/mermaid.d.ts +40 -6
- package/dist/render/mermaid.js +101 -24
- package/dist/render/page.d.ts +5 -0
- package/dist/render/page.js +6 -0
- package/dist/render/template.d.ts +5 -1
- package/dist/render/template.js +12 -2
- package/dist/run.js +6 -0
- package/package.json +3 -2
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { existsSync, mkdtempSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import sea from "node:sea";
|
|
5
|
+
import { assetPath, PACKAGE_JSON } from "./assets.js";
|
|
6
|
+
import { cacheDir, installedBrowser } from "./browser/cache.js";
|
|
7
|
+
import { findBrowser } from "./browser/detect.js";
|
|
8
|
+
import { findConfigFile, readConfig } from "./config.js";
|
|
9
|
+
import { BrowserPool, convertFile } from "./convert.js";
|
|
10
|
+
import { DEFAULT_FONT_SIZE, mergeOptions, resolveMargins } from "./options.js";
|
|
11
|
+
import { version } from "./version.js";
|
|
12
|
+
const MIN_NODE = [22, 12];
|
|
13
|
+
function runningAsExecutable() {
|
|
14
|
+
try {
|
|
15
|
+
return sea.isSea();
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function isFile(file) {
|
|
22
|
+
try {
|
|
23
|
+
return statSync(file).isFile();
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Where `mdrender` resolves on the PATH, like `which`/`where`. */
|
|
30
|
+
export function findOnPath(env, platform = process.platform) {
|
|
31
|
+
const pathValue = Object.entries(env).find(([key]) => key.toUpperCase() === "PATH")?.[1] ?? "";
|
|
32
|
+
const separator = platform === "win32" ? ";" : ":";
|
|
33
|
+
const names = platform === "win32"
|
|
34
|
+
? ["mdrender.exe", "mdrender.cmd", "mdrender.ps1", "mdrender"]
|
|
35
|
+
: ["mdrender"];
|
|
36
|
+
const join = platform === "win32" ? path.win32.join : path.posix.join;
|
|
37
|
+
for (const dir of pathValue.split(separator).filter(Boolean)) {
|
|
38
|
+
for (const name of names) {
|
|
39
|
+
const candidate = join(dir, name);
|
|
40
|
+
if (isFile(candidate))
|
|
41
|
+
return candidate;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
function installationChecks(env, platform) {
|
|
47
|
+
const checks = [];
|
|
48
|
+
const executable = runningAsExecutable();
|
|
49
|
+
checks.push({
|
|
50
|
+
name: "mdrender",
|
|
51
|
+
status: "ok",
|
|
52
|
+
detail: executable
|
|
53
|
+
? `${version}, standalone executable (${process.execPath})`
|
|
54
|
+
: `${version}, npm package (${path.dirname(assetPath(PACKAGE_JSON))})`,
|
|
55
|
+
});
|
|
56
|
+
if (!executable) {
|
|
57
|
+
const [major = 0, minor = 0] = process.versions.node.split(".").map(Number);
|
|
58
|
+
const [needMajor, needMinor] = MIN_NODE;
|
|
59
|
+
const supported = major > needMajor || (major === needMajor && minor >= needMinor);
|
|
60
|
+
checks.push({
|
|
61
|
+
name: "Node.js",
|
|
62
|
+
status: supported ? "ok" : "fail",
|
|
63
|
+
detail: `${process.versions.node} (${process.execPath})`,
|
|
64
|
+
hint: supported ? undefined : `mdrender needs Node.js ${needMajor}.${needMinor} or newer`,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
checks.push({
|
|
68
|
+
name: "System",
|
|
69
|
+
status: "info",
|
|
70
|
+
detail: `${platform} ${process.arch}, ${os.release()}`,
|
|
71
|
+
});
|
|
72
|
+
const onPath = findOnPath(env, platform);
|
|
73
|
+
checks.push(onPath
|
|
74
|
+
? { name: "PATH", status: "ok", detail: `mdrender found at ${onPath}` }
|
|
75
|
+
: {
|
|
76
|
+
name: "PATH",
|
|
77
|
+
status: "warn",
|
|
78
|
+
detail: "the mdrender command is not in any PATH folder",
|
|
79
|
+
hint: "you can only run it by its full path or with npx; add its folder to PATH " +
|
|
80
|
+
"(for npm: the output of `npm prefix -g`, plus /bin on Linux)",
|
|
81
|
+
});
|
|
82
|
+
return checks;
|
|
83
|
+
}
|
|
84
|
+
function browserSource(found, explicit, env, downloaded) {
|
|
85
|
+
if (explicit)
|
|
86
|
+
return "from --browser";
|
|
87
|
+
if (env.MDRENDER_BROWSER)
|
|
88
|
+
return "from MDRENDER_BROWSER";
|
|
89
|
+
if (downloaded && path.resolve(found) === path.resolve(downloaded)) {
|
|
90
|
+
return "downloaded by mdrender setup";
|
|
91
|
+
}
|
|
92
|
+
return "installed on this system";
|
|
93
|
+
}
|
|
94
|
+
function configChecks(options, cwd) {
|
|
95
|
+
if (options.config === false) {
|
|
96
|
+
return [{ name: "Config", status: "info", detail: "config files disabled (--no-config)" }];
|
|
97
|
+
}
|
|
98
|
+
const file = options.config ? path.resolve(cwd, options.config) : findConfigFile(cwd);
|
|
99
|
+
if (!file) {
|
|
100
|
+
const defaults = mergeOptions();
|
|
101
|
+
return [
|
|
102
|
+
{
|
|
103
|
+
name: "Config",
|
|
104
|
+
status: "info",
|
|
105
|
+
detail: `no mdrender.config.json in ${cwd} or its parent folders; defaults apply`,
|
|
106
|
+
},
|
|
107
|
+
settingsCheck(defaults),
|
|
108
|
+
];
|
|
109
|
+
}
|
|
110
|
+
if (!existsSync(file)) {
|
|
111
|
+
return [{ name: "Config", status: "fail", detail: `not found: ${file}` }];
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const config = readConfig(file);
|
|
115
|
+
const checks = [
|
|
116
|
+
config.warnings.length
|
|
117
|
+
? {
|
|
118
|
+
name: "Config",
|
|
119
|
+
status: "warn",
|
|
120
|
+
detail: `${file}\n${config.warnings.map((w) => ` - ${w}`).join("\n")}`,
|
|
121
|
+
hint: "fix or remove the options above; they are ignored",
|
|
122
|
+
}
|
|
123
|
+
: { name: "Config", status: "ok", detail: file },
|
|
124
|
+
];
|
|
125
|
+
checks.push(settingsCheck(mergeOptions(config.options)));
|
|
126
|
+
return checks;
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
return [{ name: "Config", status: "fail", detail: error.message }];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function settingsCheck(options) {
|
|
133
|
+
const m = resolveMargins(options);
|
|
134
|
+
return {
|
|
135
|
+
name: "Settings",
|
|
136
|
+
status: "info",
|
|
137
|
+
detail: `${options.pageSize} ${options.orientation}, margins ${m.top} ${m.right} ${m.bottom} ` +
|
|
138
|
+
`${m.left} (top right bottom left), ${options.density} density, ` +
|
|
139
|
+
`${options.fontSize ?? DEFAULT_FONT_SIZE} text, language ${options.lang}, theme ` +
|
|
140
|
+
path.basename(options.theme),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const SAMPLE = `# mdrender doctor
|
|
144
|
+
|
|
145
|
+
Equation: $e^{i\\pi} + 1 = 0$
|
|
146
|
+
|
|
147
|
+
\`\`\`mermaid
|
|
148
|
+
flowchart LR
|
|
149
|
+
A[Markdown] --> B[PDF]
|
|
150
|
+
\`\`\`
|
|
151
|
+
`;
|
|
152
|
+
async function browserChecks(options, env, platform) {
|
|
153
|
+
const checks = [];
|
|
154
|
+
const dir = cacheDir(env, platform);
|
|
155
|
+
const downloaded = installedBrowser(dir);
|
|
156
|
+
checks.push({
|
|
157
|
+
name: "Setup cache",
|
|
158
|
+
status: "info",
|
|
159
|
+
detail: downloaded ? `${dir} (contains ${downloaded})` : `${dir} (no downloaded browser)`,
|
|
160
|
+
});
|
|
161
|
+
let executable;
|
|
162
|
+
try {
|
|
163
|
+
executable = findBrowser(options.browser, {
|
|
164
|
+
env,
|
|
165
|
+
platform,
|
|
166
|
+
downloaded: () => downloaded,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
checks.push({
|
|
171
|
+
name: "Browser",
|
|
172
|
+
status: "fail",
|
|
173
|
+
detail: error.message.split("\n")[0],
|
|
174
|
+
hint: "run `mdrender setup` to download one, install Chrome/Edge/Chromium, or point to " +
|
|
175
|
+
"one with --browser <path> or MDRENDER_BROWSER",
|
|
176
|
+
});
|
|
177
|
+
return checks;
|
|
178
|
+
}
|
|
179
|
+
checks.push({
|
|
180
|
+
name: "Browser",
|
|
181
|
+
status: "ok",
|
|
182
|
+
detail: `${executable} (${browserSource(executable, options.browser, env, downloaded)})`,
|
|
183
|
+
});
|
|
184
|
+
if (options.render === false)
|
|
185
|
+
return checks;
|
|
186
|
+
const notes = [];
|
|
187
|
+
const pool = new BrowserPool(executable, (message) => notes.push(message));
|
|
188
|
+
const work = mkdtempSync(path.join(os.tmpdir(), "mdrender-doctor-"));
|
|
189
|
+
try {
|
|
190
|
+
let started = Date.now();
|
|
191
|
+
const session = await pool.get();
|
|
192
|
+
const browserVersion = await session.version();
|
|
193
|
+
const unsandboxed = notes.some((note) => note.includes("sandbox"));
|
|
194
|
+
checks.push({
|
|
195
|
+
name: "Browser start",
|
|
196
|
+
status: unsandboxed ? "warn" : "ok",
|
|
197
|
+
detail: `${browserVersion} in ${Date.now() - started} ms`,
|
|
198
|
+
hint: unsandboxed
|
|
199
|
+
? "started without Chrome's sandbox (blocked by the system, e.g. Ubuntu 23.10+); " +
|
|
200
|
+
"conversions still work. A browser installed from a package avoids it"
|
|
201
|
+
: undefined,
|
|
202
|
+
});
|
|
203
|
+
const input = path.join(work, "doctor.md");
|
|
204
|
+
writeFileSync(input, SAMPLE);
|
|
205
|
+
started = Date.now();
|
|
206
|
+
const result = await convertFile({
|
|
207
|
+
input,
|
|
208
|
+
output: path.join(work, "doctor.pdf"),
|
|
209
|
+
format: "pdf",
|
|
210
|
+
config: false,
|
|
211
|
+
browsers: pool,
|
|
212
|
+
});
|
|
213
|
+
const problems = result.diagnostics.map((d) => d.message);
|
|
214
|
+
checks.push({
|
|
215
|
+
name: "Test conversion",
|
|
216
|
+
status: problems.length ? "fail" : "ok",
|
|
217
|
+
detail: problems.length
|
|
218
|
+
? problems.join("; ")
|
|
219
|
+
: `PDF with an equation and a Mermaid diagram in ${Date.now() - started} ms`,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
checks.push({
|
|
224
|
+
name: "Browser start",
|
|
225
|
+
status: "fail",
|
|
226
|
+
detail: error.message.split("\n")[0],
|
|
227
|
+
hint: "try another browser with --browser <path>, or `mdrender setup --force`",
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
finally {
|
|
231
|
+
await pool.close();
|
|
232
|
+
rmSync(work, { recursive: true, force: true });
|
|
233
|
+
}
|
|
234
|
+
return checks;
|
|
235
|
+
}
|
|
236
|
+
function networkChecks(env) {
|
|
237
|
+
const proxy = ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"].find((k) => env[k]);
|
|
238
|
+
return proxy
|
|
239
|
+
? [{ name: "Proxy", status: "info", detail: `${proxy} is set (used by mdrender setup)` }]
|
|
240
|
+
: [];
|
|
241
|
+
}
|
|
242
|
+
/** Inspect the installation, browser and configuration, and try a real conversion. */
|
|
243
|
+
export async function runDoctor(options = {}) {
|
|
244
|
+
const env = options.env ?? process.env;
|
|
245
|
+
const platform = options.platform ?? process.platform;
|
|
246
|
+
const cwd = options.cwd ?? process.cwd();
|
|
247
|
+
const checks = [
|
|
248
|
+
...installationChecks(env, platform),
|
|
249
|
+
...(await browserChecks(options, env, platform)),
|
|
250
|
+
...configChecks(options, cwd),
|
|
251
|
+
...networkChecks(env),
|
|
252
|
+
];
|
|
253
|
+
return { version, checks, ok: !checks.some((check) => check.status === "fail") };
|
|
254
|
+
}
|
|
255
|
+
const LABEL = {
|
|
256
|
+
ok: "[ok] ",
|
|
257
|
+
warn: "[warn]",
|
|
258
|
+
fail: "[fail]",
|
|
259
|
+
info: "[info]",
|
|
260
|
+
};
|
|
261
|
+
/** Plain-text report; ASCII markers so it reads well in any Windows console. */
|
|
262
|
+
export function formatReport(report) {
|
|
263
|
+
const width = Math.max(...report.checks.map((check) => check.name.length));
|
|
264
|
+
const lines = report.checks.map((check) => {
|
|
265
|
+
const [first, ...rest] = check.detail.split("\n");
|
|
266
|
+
const head = `${LABEL[check.status]} ${check.name.padEnd(width)} ${first}`;
|
|
267
|
+
const more = rest.map((line) => `${" ".repeat(width + 9)}${line}`);
|
|
268
|
+
const hint = check.hint ? [`${" ".repeat(width + 9)}-> ${check.hint}`] : [];
|
|
269
|
+
return [head, ...more, ...hint].join("\n");
|
|
270
|
+
});
|
|
271
|
+
const failures = report.checks.filter((c) => c.status === "fail").length;
|
|
272
|
+
const warnings = report.checks.filter((c) => c.status === "warn").length;
|
|
273
|
+
const summary = report.ok
|
|
274
|
+
? `All good${warnings ? ` (${warnings} warning${warnings > 1 ? "s" : ""})` : ""}.`
|
|
275
|
+
: `${failures} problem${failures > 1 ? "s" : ""} found.`;
|
|
276
|
+
return `mdrender doctor\n\n${lines.join("\n")}\n\n${summary}\n`;
|
|
277
|
+
}
|
|
278
|
+
//# sourceMappingURL=doctor.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { MarkdownIt } from "markdown-it";
|
|
2
|
+
/**
|
|
3
|
+
* Code blocks up to this many lines are kept on one page; longer ones may break across pages
|
|
4
|
+
* (the theme keeps at least a few lines on each side), so a long listing does not push itself
|
|
5
|
+
* to the next page and leave a large gap behind.
|
|
6
|
+
*/
|
|
7
|
+
export declare const SHORT_CODE_LINES = 12;
|
|
8
|
+
/** Mark short fenced and indented code blocks with `keep-together` (see SHORT_CODE_LINES). */
|
|
9
|
+
export declare function codeBlocks(md: MarkdownIt): void;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code blocks up to this many lines are kept on one page; longer ones may break across pages
|
|
3
|
+
* (the theme keeps at least a few lines on each side), so a long listing does not push itself
|
|
4
|
+
* to the next page and leave a large gap behind.
|
|
5
|
+
*/
|
|
6
|
+
export const SHORT_CODE_LINES = 12;
|
|
7
|
+
const KEEP_CLASS = "keep-together";
|
|
8
|
+
function markShort(html, content) {
|
|
9
|
+
const lines = content.replace(/\n$/, "").split("\n").length;
|
|
10
|
+
if (lines > SHORT_CODE_LINES || !html.startsWith("<pre"))
|
|
11
|
+
return html;
|
|
12
|
+
return html.replace(/^<pre(?: class="([^"]*)")?/, (_m, classes) => classes ? `<pre class="${classes} ${KEEP_CLASS}"` : `<pre class="${KEEP_CLASS}"`);
|
|
13
|
+
}
|
|
14
|
+
/** Mark short fenced and indented code blocks with `keep-together` (see SHORT_CODE_LINES). */
|
|
15
|
+
export function codeBlocks(md) {
|
|
16
|
+
for (const rule of ["fence", "code_block"]) {
|
|
17
|
+
const render = md.renderer.rules[rule];
|
|
18
|
+
md.renderer.rules[rule] = (tokens, idx, options, env, self) => markShort(render(tokens, idx, options, env, self), tokens[idx].content);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=code-blocks.js.map
|
package/dist/markdown/env.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export interface MermaidBlock {
|
|
|
4
4
|
source: string;
|
|
5
5
|
/** 1-based source line of the opening fence. */
|
|
6
6
|
line?: number;
|
|
7
|
+
/** Printed together with the heading just above it (see markdown/keep-with-heading.ts). */
|
|
8
|
+
afterHeading?: boolean;
|
|
7
9
|
}
|
|
8
10
|
/** Per-document state shared by the Markdown plugins during one render. */
|
|
9
11
|
export interface RenderEnv extends Env {
|
package/dist/markdown/index.js
CHANGED
|
@@ -4,9 +4,11 @@ import anchor from "markdown-it-anchor";
|
|
|
4
4
|
import footnote from "markdown-it-footnote";
|
|
5
5
|
import { DEFAULT_LANG } from "../i18n.js";
|
|
6
6
|
import { alerts } from "./alerts.js";
|
|
7
|
+
import { codeBlocks } from "./code-blocks.js";
|
|
7
8
|
import { inlineChildLines } from "./env.js";
|
|
8
9
|
import { highlightCode } from "./highlight.js";
|
|
9
10
|
import { inlineLocalImages } from "./images.js";
|
|
11
|
+
import { keepWithHeading } from "./keep-with-heading.js";
|
|
10
12
|
import { math } from "./math.js";
|
|
11
13
|
import { mermaid } from "./mermaid.js";
|
|
12
14
|
import { taskLists } from "./task-lists.js";
|
|
@@ -23,8 +25,11 @@ export function createMarkdown() {
|
|
|
23
25
|
md.use(taskLists);
|
|
24
26
|
md.use(alerts);
|
|
25
27
|
md.use(tocMarker);
|
|
28
|
+
md.use(keepWithHeading);
|
|
26
29
|
md.use(inlineChildLines);
|
|
27
30
|
md.use(inlineLocalImages);
|
|
31
|
+
// Before math and mermaid, which hand their non-code fences on to this renderer.
|
|
32
|
+
md.use(codeBlocks);
|
|
28
33
|
md.use(math);
|
|
29
34
|
md.use(mermaid);
|
|
30
35
|
return md;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { MarkdownIt } from "markdown-it";
|
|
2
|
+
export declare const KEEP_WITH_HEADING_CLASS = "keep-with-heading";
|
|
3
|
+
/**
|
|
4
|
+
* Wrap a top-level heading and the diagram or lone image right after it in a block that is not
|
|
5
|
+
* split across pages (`.keep-with-heading` in theme.css). Otherwise a figure that does not fit at
|
|
6
|
+
* the bottom of a page moves to the next one and leaves its heading alone at the bottom.
|
|
7
|
+
*/
|
|
8
|
+
export declare function keepWithHeading(md: MarkdownIt): void;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export const KEEP_WITH_HEADING_CLASS = "keep-with-heading";
|
|
2
|
+
const isMermaidFence = (token) => token?.type === "fence" && token.info.trim().toLowerCase() === "mermaid";
|
|
3
|
+
/** A paragraph holding nothing but one image (``). */
|
|
4
|
+
function imageParagraphEnd(tokens, i) {
|
|
5
|
+
if (tokens[i]?.type !== "paragraph_open" || tokens[i + 2]?.type !== "paragraph_close") {
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
const children = tokens[i + 1]?.children?.filter((child) => !(child.type === "text" && child.content.trim() === ""));
|
|
9
|
+
return children?.length === 1 && children[0].type === "image" ? i + 2 : undefined;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Wrap a top-level heading and the diagram or lone image right after it in a block that is not
|
|
13
|
+
* split across pages (`.keep-with-heading` in theme.css). Otherwise a figure that does not fit at
|
|
14
|
+
* the bottom of a page moves to the next one and leaves its heading alone at the bottom.
|
|
15
|
+
*/
|
|
16
|
+
export function keepWithHeading(md) {
|
|
17
|
+
md.core.ruler.push("keep_with_heading", (state) => {
|
|
18
|
+
const tokens = state.tokens;
|
|
19
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
20
|
+
const open = tokens[i];
|
|
21
|
+
if (open.type !== "heading_open" || open.level !== 0)
|
|
22
|
+
continue;
|
|
23
|
+
const next = i + 3; // heading_open, inline, heading_close
|
|
24
|
+
let end;
|
|
25
|
+
if (isMermaidFence(tokens[next])) {
|
|
26
|
+
end = next;
|
|
27
|
+
tokens[next].meta = { ...tokens[next].meta, afterHeading: true };
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
end = imageParagraphEnd(tokens, next);
|
|
31
|
+
}
|
|
32
|
+
if (end === undefined)
|
|
33
|
+
continue;
|
|
34
|
+
const before = new state.Token("html_block", "", 0);
|
|
35
|
+
before.content = `<div class="${KEEP_WITH_HEADING_CLASS}">\n`;
|
|
36
|
+
const after = new state.Token("html_block", "", 0);
|
|
37
|
+
after.content = "</div>\n";
|
|
38
|
+
tokens.splice(end + 1, 0, after);
|
|
39
|
+
tokens.splice(i, 0, before);
|
|
40
|
+
i = end + 2;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=keep-with-heading.js.map
|
package/dist/markdown/mermaid.js
CHANGED
|
@@ -15,7 +15,9 @@ export function mermaid(md) {
|
|
|
15
15
|
return fence(tokens, idx, options, rawEnv, self);
|
|
16
16
|
}
|
|
17
17
|
const env = rawEnv;
|
|
18
|
-
const
|
|
18
|
+
const block = { source: token.content, line: tokenLine(token) };
|
|
19
|
+
const afterHeading = token.meta?.afterHeading;
|
|
20
|
+
const index = env.mermaid.push(afterHeading ? { ...block, afterHeading } : block) - 1;
|
|
19
21
|
return `<figure class="mermaid-diagram">${mermaidPlaceholder(env.nonce, index)}</figure>\n`;
|
|
20
22
|
};
|
|
21
23
|
}
|
package/dist/options.d.ts
CHANGED
|
@@ -11,7 +11,11 @@ export type Margins = {
|
|
|
11
11
|
};
|
|
12
12
|
export declare const MERMAID_THEMES: readonly ["default", "neutral", "dark", "forest", "base"];
|
|
13
13
|
export type MermaidTheme = (typeof MERMAID_THEMES)[number];
|
|
14
|
+
export declare const MERMAID_FLIPS: readonly ["auto", "never"];
|
|
15
|
+
export type MermaidFlip = (typeof MERMAID_FLIPS)[number];
|
|
14
16
|
export declare const BUILTIN_THEMES: readonly ["light", "dark"];
|
|
17
|
+
export declare const DENSITIES: readonly ["normal", "compact"];
|
|
18
|
+
export type Density = (typeof DENSITIES)[number];
|
|
15
19
|
/** Rendering options, merged from defaults < config file < front matter < command line. */
|
|
16
20
|
export interface DocumentOptions {
|
|
17
21
|
pageSize: PageSize;
|
|
@@ -33,6 +37,14 @@ export interface DocumentOptions {
|
|
|
33
37
|
footer?: string;
|
|
34
38
|
/** Mermaid theme; when unset, "neutral" for light pages and "dark" for the dark theme. */
|
|
35
39
|
mermaidTheme?: MermaidTheme;
|
|
40
|
+
/** Try several layouts of each flowchart and keep the one whose text prints largest. */
|
|
41
|
+
mermaidAutofit: boolean;
|
|
42
|
+
/** Whether autofit may turn a flowchart to the other axis (LR ↔ TB) when it reads far better. */
|
|
43
|
+
mermaidFlip: MermaidFlip;
|
|
44
|
+
/** Vertical spacing: "compact" tightens line height, paragraph and heading spacing. */
|
|
45
|
+
density: Density;
|
|
46
|
+
/** Base font size of the text, e.g. "10pt"; when unset, the theme's (10.5pt). */
|
|
47
|
+
fontSize?: string;
|
|
36
48
|
/** "light", "dark", or the absolute path of a stylesheet that replaces the built-in theme. */
|
|
37
49
|
theme: string;
|
|
38
50
|
/** Extra stylesheets (absolute paths), applied after the built-in theme. */
|
|
@@ -50,6 +62,10 @@ export declare class OptionsError extends MdRenderError {
|
|
|
50
62
|
}
|
|
51
63
|
/** Parse a CSS margin shorthand into the four sides, or throw. */
|
|
52
64
|
export declare function parseMargin(margin: string): Margins;
|
|
65
|
+
/** Text size of the built-in themes (theme.css), when --font-size is not given. */
|
|
66
|
+
export declare const DEFAULT_FONT_SIZE = "10.5pt";
|
|
67
|
+
/** A font size in CSS pixels ("10.5pt" → 14). */
|
|
68
|
+
export declare function fontSizePx(size: string): number;
|
|
53
69
|
/**
|
|
54
70
|
* Validate options from a config file, front matter or the command line.
|
|
55
71
|
*
|
package/dist/options.js
CHANGED
|
@@ -5,7 +5,9 @@ import { DEFAULT_LANG } from "./i18n.js";
|
|
|
5
5
|
export const PAGE_SIZES = ["A3", "A4", "A5", "Letter", "Legal", "Tabloid"];
|
|
6
6
|
export const ORIENTATIONS = ["portrait", "landscape"];
|
|
7
7
|
export const MERMAID_THEMES = ["default", "neutral", "dark", "forest", "base"];
|
|
8
|
+
export const MERMAID_FLIPS = ["auto", "never"];
|
|
8
9
|
export const BUILTIN_THEMES = ["light", "dark"];
|
|
10
|
+
export const DENSITIES = ["normal", "compact"];
|
|
9
11
|
export const DEFAULT_OPTIONS = {
|
|
10
12
|
pageSize: "Letter",
|
|
11
13
|
margin: "10mm",
|
|
@@ -13,6 +15,9 @@ export const DEFAULT_OPTIONS = {
|
|
|
13
15
|
toc: false,
|
|
14
16
|
tocDepth: 3,
|
|
15
17
|
pageNumbers: true,
|
|
18
|
+
mermaidAutofit: true,
|
|
19
|
+
mermaidFlip: "auto",
|
|
20
|
+
density: "normal",
|
|
16
21
|
theme: "light",
|
|
17
22
|
css: [],
|
|
18
23
|
lang: DEFAULT_LANG,
|
|
@@ -59,6 +64,27 @@ const length = (value, describe) => {
|
|
|
59
64
|
return trimmed;
|
|
60
65
|
throw new OptionsError(`${describe} must be a length in mm, cm, in or px, e.g. "10mm"`);
|
|
61
66
|
};
|
|
67
|
+
/** Text size of the built-in themes (theme.css), when --font-size is not given. */
|
|
68
|
+
export const DEFAULT_FONT_SIZE = "10.5pt";
|
|
69
|
+
/** Font sizes accepted by --font-size, in points or pixels. */
|
|
70
|
+
const FONT_SIZE = /^(\d+(?:\.\d+)?)(pt|px)$/;
|
|
71
|
+
const FONT_SIZE_PT = { min: 6, max: 24 };
|
|
72
|
+
/** A font size in CSS pixels ("10.5pt" → 14). */
|
|
73
|
+
export function fontSizePx(size) {
|
|
74
|
+
const match = FONT_SIZE.exec(size);
|
|
75
|
+
if (!match)
|
|
76
|
+
throw new Error(`invalid font size: ${size}`);
|
|
77
|
+
return match[2] === "pt" ? (Number(match[1]) * 4) / 3 : Number(match[1]);
|
|
78
|
+
}
|
|
79
|
+
const fontSize = (value, describe) => {
|
|
80
|
+
const size = text(value, describe, "").trim();
|
|
81
|
+
if (FONT_SIZE.test(size)) {
|
|
82
|
+
const pt = (fontSizePx(size) * 3) / 4;
|
|
83
|
+
if (pt >= FONT_SIZE_PT.min && pt <= FONT_SIZE_PT.max)
|
|
84
|
+
return size;
|
|
85
|
+
}
|
|
86
|
+
throw new OptionsError(`${describe} must be a size in pt or px from ${FONT_SIZE_PT.min}pt to ${FONT_SIZE_PT.max}pt, e.g. "10pt"`);
|
|
87
|
+
};
|
|
62
88
|
const VALIDATORS = {
|
|
63
89
|
pageSize: oneOf(PAGE_SIZES),
|
|
64
90
|
margin: (value, describe) => {
|
|
@@ -87,6 +113,10 @@ const VALIDATORS = {
|
|
|
87
113
|
header: text,
|
|
88
114
|
footer: text,
|
|
89
115
|
mermaidTheme: oneOf(MERMAID_THEMES),
|
|
116
|
+
mermaidAutofit: bool,
|
|
117
|
+
mermaidFlip: oneOf(MERMAID_FLIPS),
|
|
118
|
+
density: oneOf(DENSITIES),
|
|
119
|
+
fontSize,
|
|
90
120
|
theme: (value, describe, baseDir) => {
|
|
91
121
|
const theme = text(value, describe, baseDir);
|
|
92
122
|
const builtin = BUILTIN_THEMES.find((t) => t === theme.toLowerCase());
|
package/dist/render/browser.d.ts
CHANGED
package/dist/render/browser.js
CHANGED
|
@@ -127,6 +127,10 @@ export class BrowserSession {
|
|
|
127
127
|
await page.close();
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
|
+
/** Browser name and version, e.g. "HeadlessChrome/141.0.7390.37". */
|
|
131
|
+
version() {
|
|
132
|
+
return this.browser.version();
|
|
133
|
+
}
|
|
130
134
|
async close() {
|
|
131
135
|
await this.browser.close();
|
|
132
136
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Autofit for Mermaid flowcharts: pick, among several layouts of the same diagram, the one whose
|
|
3
|
+
* text prints largest once the diagram is scaled to the text column and the page height.
|
|
4
|
+
*
|
|
5
|
+
* Mermaid wraps label lines at `flowchart.wrappingWidth`, and a flowchart laid out along one axis
|
|
6
|
+
* can be far wider (or taller) than the page; the theme then shrinks the whole drawing, text
|
|
7
|
+
* included. Wider wrapping makes tall diagrams shorter; turning a long left-to-right chain
|
|
8
|
+
* top-to-bottom makes it fit the column. Neither has one value that suits every diagram, so each
|
|
9
|
+
* flowchart is rendered a few times and measured. Everything here is pure; the rendering loop is
|
|
10
|
+
* in render/mermaid.ts.
|
|
11
|
+
*/
|
|
12
|
+
export declare const FLOW_DIRECTIONS: readonly ["TB", "TD", "BT", "LR", "RL"];
|
|
13
|
+
export type FlowDirection = (typeof FLOW_DIRECTIONS)[number];
|
|
14
|
+
/** Label wrapping widths tried, in px; undefined is Mermaid's own default (the layout as today). */
|
|
15
|
+
export declare const WRAPPING_WIDTHS: readonly (number | undefined)[];
|
|
16
|
+
/** Candidates within this scale of the best one read the same; the shorter one wins. */
|
|
17
|
+
export declare const SCALE_TOLERANCE = 0.05;
|
|
18
|
+
/** Only turn a diagram whose best scale in its own direction is below this (text under ~10 px)… */
|
|
19
|
+
export declare const FLIP_BELOW = 0.75;
|
|
20
|
+
/** …and only when turning it raises the scale by at least this much. */
|
|
21
|
+
export declare const FLIP_GAIN = 0.15;
|
|
22
|
+
/** Below this scale the diagram is reported: no layout makes it readable, its content must change. */
|
|
23
|
+
export declare const SMALL_SCALE = 0.5;
|
|
24
|
+
export interface FlowchartHeader {
|
|
25
|
+
/** Index of the header line in `source.split("\n")`. */
|
|
26
|
+
line: number;
|
|
27
|
+
/** Direction as written; flowcharts without one are top-to-bottom. */
|
|
28
|
+
direction: FlowDirection;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The flowchart's header line and main direction, or undefined when the source is another kind
|
|
32
|
+
* of diagram. Blank lines and `%%` comments before the header are skipped.
|
|
33
|
+
*/
|
|
34
|
+
export declare function flowchartHeader(source: string): FlowchartHeader | undefined;
|
|
35
|
+
/** The source with its main direction replaced; subgraph directions are left alone. */
|
|
36
|
+
export declare function withDirection(source: string, header: FlowchartHeader, direction: FlowDirection): string;
|
|
37
|
+
export declare function flippedDirection(direction: FlowDirection): FlowDirection;
|
|
38
|
+
/**
|
|
39
|
+
* The diagram sets its own Mermaid configuration (an `%%{init: …}%%` directive or `config:` in
|
|
40
|
+
* its front matter): it is drawn exactly as written.
|
|
41
|
+
*/
|
|
42
|
+
export declare function authorConfigured(source: string): boolean;
|
|
43
|
+
/** Width and height of an SVG from its viewBox. */
|
|
44
|
+
export declare function measureSvg(svg: string): {
|
|
45
|
+
width: number;
|
|
46
|
+
height: number;
|
|
47
|
+
} | undefined;
|
|
48
|
+
export interface Fit {
|
|
49
|
+
/** Text column width in CSS px. */
|
|
50
|
+
width: number;
|
|
51
|
+
/** Largest printed height in CSS px; Infinity without pages (HTML). */
|
|
52
|
+
height: number;
|
|
53
|
+
}
|
|
54
|
+
/** How much the diagram is shrunk when printed: 1 is its natural size. */
|
|
55
|
+
export declare function printScale(size: {
|
|
56
|
+
width: number;
|
|
57
|
+
height: number;
|
|
58
|
+
}, fit: Fit): number;
|
|
59
|
+
export interface Candidate {
|
|
60
|
+
svg: string;
|
|
61
|
+
/** Label wrapping width in px; undefined is Mermaid's default. */
|
|
62
|
+
wrap: number | undefined;
|
|
63
|
+
flipped: boolean;
|
|
64
|
+
width: number;
|
|
65
|
+
height: number;
|
|
66
|
+
scale: number;
|
|
67
|
+
}
|
|
68
|
+
export declare function candidate(svg: string, wrap: number | undefined, flipped: boolean, fit: Fit): Candidate | undefined;
|
|
69
|
+
/** Whether a turned layout is worth trying, given the best layout in the written direction. */
|
|
70
|
+
export declare const flipWorthTrying: (best: Candidate) => boolean;
|
|
71
|
+
/**
|
|
72
|
+
* The chosen layout. `base` is the diagram as written (Mermaid's default wrapping, its own
|
|
73
|
+
* direction); the result is never smaller than it. A turned layout wins only when the written
|
|
74
|
+
* direction stays below FLIP_BELOW and turning gains at least FLIP_GAIN.
|
|
75
|
+
*/
|
|
76
|
+
export declare function chooseCandidate(candidates: Candidate[], base: Candidate): Candidate;
|
|
77
|
+
/**
|
|
78
|
+
* Whether wider wrapping in this direction can still help: once the diagram is wider than the
|
|
79
|
+
* column and its scale went down, wider labels only make it wider.
|
|
80
|
+
*/
|
|
81
|
+
export declare function wrapsExhausted(current: Candidate, previous: Candidate | undefined, fit: Fit): boolean;
|