@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
|
@@ -0,0 +1,135 @@
|
|
|
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 const FLOW_DIRECTIONS = ["TB", "TD", "BT", "LR", "RL"];
|
|
13
|
+
/** Label wrapping widths tried, in px; undefined is Mermaid's own default (the layout as today). */
|
|
14
|
+
export const WRAPPING_WIDTHS = [
|
|
15
|
+
undefined,
|
|
16
|
+
200,
|
|
17
|
+
300,
|
|
18
|
+
400,
|
|
19
|
+
600,
|
|
20
|
+
900,
|
|
21
|
+
];
|
|
22
|
+
/** Candidates within this scale of the best one read the same; the shorter one wins. */
|
|
23
|
+
export const SCALE_TOLERANCE = 0.05;
|
|
24
|
+
/** Only turn a diagram whose best scale in its own direction is below this (text under ~10 px)… */
|
|
25
|
+
export const FLIP_BELOW = 0.75;
|
|
26
|
+
/** …and only when turning it raises the scale by at least this much. */
|
|
27
|
+
export const FLIP_GAIN = 0.15;
|
|
28
|
+
/** Below this scale the diagram is reported: no layout makes it readable, its content must change. */
|
|
29
|
+
export const SMALL_SCALE = 0.5;
|
|
30
|
+
/** The axis-swapped direction: horizontal flowcharts go top-to-bottom, vertical ones left-to-right. */
|
|
31
|
+
const FLIPPED = {
|
|
32
|
+
LR: "TB",
|
|
33
|
+
RL: "TB",
|
|
34
|
+
TB: "LR",
|
|
35
|
+
TD: "LR",
|
|
36
|
+
BT: "LR",
|
|
37
|
+
};
|
|
38
|
+
const HEADER = /^(\s*(?:flowchart|graph))(?:[ \t]+(TB|TD|BT|LR|RL))?(?=\s|;|$)/i;
|
|
39
|
+
/** Lines of the diagram after Mermaid's optional front matter (`---` … `---`). */
|
|
40
|
+
function bodyStart(lines) {
|
|
41
|
+
if (lines[0]?.trim() !== "---")
|
|
42
|
+
return 0;
|
|
43
|
+
const end = lines.findIndex((line, i) => i > 0 && line.trim() === "---");
|
|
44
|
+
return end === -1 ? 0 : end + 1;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The flowchart's header line and main direction, or undefined when the source is another kind
|
|
48
|
+
* of diagram. Blank lines and `%%` comments before the header are skipped.
|
|
49
|
+
*/
|
|
50
|
+
export function flowchartHeader(source) {
|
|
51
|
+
const lines = source.split("\n");
|
|
52
|
+
for (let i = bodyStart(lines); i < lines.length; i++) {
|
|
53
|
+
const text = lines[i].trim();
|
|
54
|
+
if (text === "" || text.startsWith("%%"))
|
|
55
|
+
continue;
|
|
56
|
+
const match = HEADER.exec(lines[i]);
|
|
57
|
+
if (!match)
|
|
58
|
+
return undefined;
|
|
59
|
+
const direction = (match[2]?.toUpperCase() ?? "TB");
|
|
60
|
+
return { line: i, direction };
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
/** The source with its main direction replaced; subgraph directions are left alone. */
|
|
65
|
+
export function withDirection(source, header, direction) {
|
|
66
|
+
const lines = source.split("\n");
|
|
67
|
+
lines[header.line] = lines[header.line].replace(HEADER, (_m, keyword) => `${keyword} ${direction}`);
|
|
68
|
+
return lines.join("\n");
|
|
69
|
+
}
|
|
70
|
+
export function flippedDirection(direction) {
|
|
71
|
+
return FLIPPED[direction];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The diagram sets its own Mermaid configuration (an `%%{init: …}%%` directive or `config:` in
|
|
75
|
+
* its front matter): it is drawn exactly as written.
|
|
76
|
+
*/
|
|
77
|
+
export function authorConfigured(source) {
|
|
78
|
+
if (/%%\{\s*(?:init|initialize)\b/i.test(source))
|
|
79
|
+
return true;
|
|
80
|
+
const lines = source.split("\n");
|
|
81
|
+
const start = bodyStart(lines);
|
|
82
|
+
return start > 0 && lines.slice(1, start - 1).some((line) => /^\s*config\s*:/.test(line));
|
|
83
|
+
}
|
|
84
|
+
/** Width and height of an SVG from its viewBox. */
|
|
85
|
+
export function measureSvg(svg) {
|
|
86
|
+
const match = /viewBox="\s*[-\d.e]+[\s,]+[-\d.e]+[\s,]+([\d.e]+)[\s,]+([\d.e]+)\s*"/.exec(svg);
|
|
87
|
+
if (!match)
|
|
88
|
+
return undefined;
|
|
89
|
+
const width = Number(match[1]);
|
|
90
|
+
const height = Number(match[2]);
|
|
91
|
+
return width > 0 && height > 0 ? { width, height } : undefined;
|
|
92
|
+
}
|
|
93
|
+
/** How much the diagram is shrunk when printed: 1 is its natural size. */
|
|
94
|
+
export function printScale(size, fit) {
|
|
95
|
+
return Math.min(1, fit.width / size.width, fit.height / size.height);
|
|
96
|
+
}
|
|
97
|
+
export function candidate(svg, wrap, flipped, fit) {
|
|
98
|
+
const size = measureSvg(svg);
|
|
99
|
+
return size && { svg, wrap, flipped, ...size, scale: printScale(size, fit) };
|
|
100
|
+
}
|
|
101
|
+
const printedHeight = (c) => Math.round(c.height * c.scale);
|
|
102
|
+
/**
|
|
103
|
+
* Best of one direction's candidates: those within SCALE_TOLERANCE of the top scale and never
|
|
104
|
+
* below the layout as written (`base`), then the shortest printed, then the largest scale.
|
|
105
|
+
*/
|
|
106
|
+
function bestOf(candidates, base) {
|
|
107
|
+
const top = Math.max(...candidates.map((c) => c.scale));
|
|
108
|
+
return candidates
|
|
109
|
+
.filter((c) => c.scale >= top - SCALE_TOLERANCE && c.scale >= base.scale - 1e-9)
|
|
110
|
+
.sort((a, b) => printedHeight(a) - printedHeight(b) || b.scale - a.scale)[0];
|
|
111
|
+
}
|
|
112
|
+
/** Whether a turned layout is worth trying, given the best layout in the written direction. */
|
|
113
|
+
export const flipWorthTrying = (best) => best.scale < FLIP_BELOW - 1e-9;
|
|
114
|
+
/**
|
|
115
|
+
* The chosen layout. `base` is the diagram as written (Mermaid's default wrapping, its own
|
|
116
|
+
* direction); the result is never smaller than it. A turned layout wins only when the written
|
|
117
|
+
* direction stays below FLIP_BELOW and turning gains at least FLIP_GAIN.
|
|
118
|
+
*/
|
|
119
|
+
export function chooseCandidate(candidates, base) {
|
|
120
|
+
const written = bestOf(candidates.filter((c) => !c.flipped), base);
|
|
121
|
+
const best = written ?? base;
|
|
122
|
+
const turned = candidates.filter((c) => c.flipped);
|
|
123
|
+
if (turned.length === 0 || !flipWorthTrying(best))
|
|
124
|
+
return best;
|
|
125
|
+
const flipped = bestOf(turned, base);
|
|
126
|
+
return flipped && flipped.scale >= best.scale + FLIP_GAIN - 1e-9 ? flipped : best;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Whether wider wrapping in this direction can still help: once the diagram is wider than the
|
|
130
|
+
* column and its scale went down, wider labels only make it wider.
|
|
131
|
+
*/
|
|
132
|
+
export function wrapsExhausted(current, previous, fit) {
|
|
133
|
+
return previous !== undefined && current.width > fit.width && current.scale < previous.scale;
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=mermaid-fit.js.map
|
package/dist/render/mermaid.d.ts
CHANGED
|
@@ -1,20 +1,54 @@
|
|
|
1
1
|
import type { Diagnostic } from "../diagnostics.js";
|
|
2
2
|
import type { MermaidBlock } from "../markdown/env.js";
|
|
3
|
-
import type { MermaidTheme } from "../options.js";
|
|
3
|
+
import type { MermaidFlip, MermaidTheme } from "../options.js";
|
|
4
4
|
import type { BrowserSession } from "./browser.js";
|
|
5
5
|
export { MERMAID_THEMES, type MermaidTheme } from "../options.js";
|
|
6
|
-
|
|
6
|
+
/** How autofit laid out a flowchart (see render/mermaid-fit.ts). */
|
|
7
|
+
export interface DiagramLayout {
|
|
8
|
+
/** Label wrapping width in px; undefined is Mermaid's default. */
|
|
9
|
+
wrap: number | undefined;
|
|
10
|
+
/** Main direction turned to the other axis, e.g. "LR→TB"; undefined when kept. */
|
|
11
|
+
turned?: string;
|
|
12
|
+
/** Printed scale and height (CSS px) as written and as chosen. */
|
|
13
|
+
baseScale: number;
|
|
14
|
+
scale: number;
|
|
15
|
+
baseHeight: number;
|
|
16
|
+
height: number;
|
|
17
|
+
/** Layouts rendered to choose this one. */
|
|
18
|
+
tried: number;
|
|
19
|
+
}
|
|
20
|
+
export type DiagramResult = {
|
|
7
21
|
svg: string;
|
|
22
|
+
/** Printed scale (1 = natural size), when the SVG could be measured. */
|
|
23
|
+
scale?: number;
|
|
24
|
+
/** Set for flowcharts considered by autofit. */
|
|
25
|
+
layout?: DiagramLayout;
|
|
8
26
|
} | {
|
|
9
27
|
error: string;
|
|
10
28
|
};
|
|
29
|
+
export interface MermaidRenderOptions {
|
|
30
|
+
theme?: MermaidTheme;
|
|
31
|
+
/** Width of the text column, so diagrams are laid out at their printed size. */
|
|
32
|
+
widthPx?: number;
|
|
33
|
+
/** Text size of the document in CSS pixels, so diagram labels match the body text. */
|
|
34
|
+
fontPx?: number;
|
|
35
|
+
/** Tallest a diagram may print (the page's text height), in CSS px; Infinity for HTML. */
|
|
36
|
+
pageHeightPx?: number;
|
|
37
|
+
/** Search label wrapping widths (and, with `flip`, the other axis) for each flowchart. */
|
|
38
|
+
autofit?: boolean;
|
|
39
|
+
flip?: MermaidFlip;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Room kept for a heading printed together with the diagram below it (see keep-with-heading in
|
|
43
|
+
* theme.css, where the same allowance is subtracted), in multiples of the text size.
|
|
44
|
+
*/
|
|
45
|
+
export declare const HEADING_ALLOWANCE_EM = 5;
|
|
11
46
|
/**
|
|
12
47
|
* Render Mermaid diagrams to SVG in a blank page that holds no user content: only the diagram
|
|
13
|
-
* sources are passed in, as data. Mermaid runs with `securityLevel: "strict"`.
|
|
48
|
+
* sources are passed in, as data. Mermaid runs with `securityLevel: "strict"`. With `autofit`,
|
|
49
|
+
* flowcharts are rendered in several layouts and the one whose text prints largest is kept.
|
|
14
50
|
*/
|
|
15
|
-
export declare function renderMermaid(session: BrowserSession, blocks: MermaidBlock[],
|
|
16
|
-
/** Width of the text column, so diagrams are laid out at their printed size. */
|
|
17
|
-
widthPx?: number): Promise<DiagramResult[]>;
|
|
51
|
+
export declare function renderMermaid(session: BrowserSession, blocks: MermaidBlock[], options?: MermaidRenderOptions): Promise<DiagramResult[]>;
|
|
18
52
|
/**
|
|
19
53
|
* Replace the Mermaid placeholders in `html` with rendered SVGs, or with a visible error box.
|
|
20
54
|
* Returns the new HTML and one error diagnostic per failed diagram.
|
package/dist/render/mermaid.js
CHANGED
|
@@ -2,17 +2,40 @@ import { MERMAID_JS, readTextAsset } from "../assets.js";
|
|
|
2
2
|
import { mermaidPlaceholder } from "../markdown/mermaid.js";
|
|
3
3
|
import { fontFaceCss } from "./fonts.js";
|
|
4
4
|
import { escapeHtml } from "../html.js";
|
|
5
|
+
import { authorConfigured, candidate, chooseCandidate, flipWorthTrying, flippedDirection, flowchartHeader, withDirection, WRAPPING_WIDTHS, wrapsExhausted, } from "./mermaid-fit.js";
|
|
5
6
|
export { MERMAID_THEMES } from "../options.js";
|
|
6
7
|
/** Width of the text column on A4 with 20 mm margins. */
|
|
7
8
|
const DEFAULT_WIDTH_PX = 642;
|
|
9
|
+
/** 10.5pt, the built-in themes' text size (Mermaid's own default is 16px). */
|
|
10
|
+
const DEFAULT_FONT_PX = 14;
|
|
11
|
+
/**
|
|
12
|
+
* Room kept for a heading printed together with the diagram below it (see keep-with-heading in
|
|
13
|
+
* theme.css, where the same allowance is subtracted), in multiples of the text size.
|
|
14
|
+
*/
|
|
15
|
+
export const HEADING_ALLOWANCE_EM = 5;
|
|
8
16
|
let mermaidJs;
|
|
17
|
+
function mermaidConfig(theme, fontPx, wrap) {
|
|
18
|
+
return {
|
|
19
|
+
startOnLoad: false,
|
|
20
|
+
securityLevel: "strict",
|
|
21
|
+
theme,
|
|
22
|
+
fontFamily: "Inter, sans-serif",
|
|
23
|
+
// Sequence diagrams take their text sizes from here, the others from themeVariables.
|
|
24
|
+
fontSize: fontPx,
|
|
25
|
+
themeVariables: { fontFamily: "Inter, sans-serif", fontSize: `${fontPx}px` },
|
|
26
|
+
htmlLabels: true,
|
|
27
|
+
// Actors only at the top of sequence diagrams: repeating them below costs height.
|
|
28
|
+
sequence: { mirrorActors: false },
|
|
29
|
+
...(wrap === undefined ? {} : { flowchart: { wrappingWidth: wrap } }),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
9
32
|
/**
|
|
10
33
|
* Render Mermaid diagrams to SVG in a blank page that holds no user content: only the diagram
|
|
11
|
-
* sources are passed in, as data. Mermaid runs with `securityLevel: "strict"`.
|
|
34
|
+
* sources are passed in, as data. Mermaid runs with `securityLevel: "strict"`. With `autofit`,
|
|
35
|
+
* flowcharts are rendered in several layouts and the one whose text prints largest is kept.
|
|
12
36
|
*/
|
|
13
|
-
export async function renderMermaid(session, blocks,
|
|
14
|
-
|
|
15
|
-
widthPx = DEFAULT_WIDTH_PX) {
|
|
37
|
+
export async function renderMermaid(session, blocks, options = {}) {
|
|
38
|
+
const { theme = "neutral", widthPx = DEFAULT_WIDTH_PX, fontPx = DEFAULT_FONT_PX, pageHeightPx = Infinity, autofit = true, flip = "auto", } = options;
|
|
16
39
|
mermaidJs ??= readTextAsset(MERMAID_JS);
|
|
17
40
|
const page = await session.newPage();
|
|
18
41
|
try {
|
|
@@ -20,31 +43,85 @@ widthPx = DEFAULT_WIDTH_PX) {
|
|
|
20
43
|
body { margin: 0; width: ${widthPx}px; font-family: "Inter", sans-serif; }</style>
|
|
21
44
|
</head><body></body></html>`);
|
|
22
45
|
await page.evaluate(mermaidJs);
|
|
23
|
-
|
|
46
|
+
await page.evaluate(async () => {
|
|
24
47
|
// Runs in the browser. The project is compiled without DOM types, hence PageGlobals.
|
|
25
|
-
const { document
|
|
48
|
+
const { document } = globalThis;
|
|
26
49
|
// Load the font weights Mermaid uses before it measures any text.
|
|
27
50
|
await Promise.all(["400", "600", "700"].map((w) => document.fonts.load(`${w} 16px Inter`)));
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
51
|
+
});
|
|
52
|
+
const render = (source, id, wrap) => page.evaluate(async (source, id, config) => {
|
|
53
|
+
const { mermaid } = globalThis;
|
|
54
|
+
// Each render sets the whole configuration, so one layout never leaks into the next.
|
|
55
|
+
mermaid.initialize(config);
|
|
56
|
+
try {
|
|
57
|
+
return { svg: (await mermaid.render(id, source)).svg };
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
return { error: String(error?.message ?? error) };
|
|
61
|
+
}
|
|
62
|
+
}, source, id, mermaidConfig(theme, fontPx, wrap));
|
|
63
|
+
const results = [];
|
|
64
|
+
for (const [i, block] of blocks.entries()) {
|
|
65
|
+
const id = `mermaid-${i + 1}`;
|
|
66
|
+
const fit = {
|
|
67
|
+
width: widthPx,
|
|
68
|
+
height: block.afterHeading ? pageHeightPx - HEADING_ALLOWANCE_EM * fontPx : pageHeightPx,
|
|
69
|
+
};
|
|
70
|
+
const written = await render(block.source, id, undefined);
|
|
71
|
+
if ("error" in written) {
|
|
72
|
+
results.push(written);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const base = candidate(written.svg, undefined, false, fit);
|
|
76
|
+
const header = flowchartHeader(block.source);
|
|
77
|
+
if (!autofit || !base || !header || authorConfigured(block.source)) {
|
|
78
|
+
results.push({ svg: written.svg, scale: base?.scale });
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
let tried = 1;
|
|
82
|
+
const candidates = [base];
|
|
83
|
+
const tryWraps = async (source, flipped) => {
|
|
84
|
+
let previous = flipped ? undefined : base;
|
|
85
|
+
for (const wrap of WRAPPING_WIDTHS) {
|
|
86
|
+
if (!flipped && wrap === undefined)
|
|
87
|
+
continue; // that is `base`
|
|
88
|
+
const rendered = await render(source, `${id}-${tried}`, wrap);
|
|
89
|
+
tried++;
|
|
90
|
+
if ("error" in rendered)
|
|
91
|
+
continue;
|
|
92
|
+
const next = candidate(rendered.svg, wrap, flipped, fit);
|
|
93
|
+
if (!next)
|
|
94
|
+
continue;
|
|
95
|
+
candidates.push(next);
|
|
96
|
+
if (wrapsExhausted(next, previous, fit))
|
|
97
|
+
break;
|
|
98
|
+
previous = next;
|
|
44
99
|
}
|
|
100
|
+
};
|
|
101
|
+
await tryWraps(block.source, false);
|
|
102
|
+
let chosen = chooseCandidate(candidates, base);
|
|
103
|
+
const turnedTo = flippedDirection(header.direction);
|
|
104
|
+
if (flip === "auto" && flipWorthTrying(chosen)) {
|
|
105
|
+
await tryWraps(withDirection(block.source, header, turnedTo), true);
|
|
106
|
+
chosen = chooseCandidate(candidates, base);
|
|
45
107
|
}
|
|
46
|
-
|
|
47
|
-
|
|
108
|
+
// Candidate ids ("mermaid-2-3") stay unique in the document, and its styles are scoped
|
|
109
|
+
// to that id, so the chosen SVG is used as rendered.
|
|
110
|
+
results.push({
|
|
111
|
+
svg: chosen.svg,
|
|
112
|
+
scale: chosen.scale,
|
|
113
|
+
layout: {
|
|
114
|
+
wrap: chosen.wrap,
|
|
115
|
+
turned: chosen.flipped ? `${header.direction}→${turnedTo}` : undefined,
|
|
116
|
+
baseScale: base.scale,
|
|
117
|
+
scale: chosen.scale,
|
|
118
|
+
baseHeight: Math.round(base.height * base.scale),
|
|
119
|
+
height: Math.round(chosen.height * chosen.scale),
|
|
120
|
+
tried,
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return results;
|
|
48
125
|
}
|
|
49
126
|
finally {
|
|
50
127
|
await page.close();
|
package/dist/render/page.d.ts
CHANGED
|
@@ -5,3 +5,8 @@ export declare function contentWidthPx(pageSize: PageSize, landscape: boolean, m
|
|
|
5
5
|
left: string;
|
|
6
6
|
right: string;
|
|
7
7
|
}): number;
|
|
8
|
+
/** Height of the text area in CSS pixels: the paper minus the top and bottom margins. */
|
|
9
|
+
export declare function contentHeightPx(pageSize: PageSize, landscape: boolean, margin: {
|
|
10
|
+
top: string;
|
|
11
|
+
bottom: string;
|
|
12
|
+
}): number;
|
package/dist/render/page.js
CHANGED
|
@@ -20,4 +20,10 @@ export function contentWidthPx(pageSize, landscape, margin) {
|
|
|
20
20
|
const paperPx = ((landscape ? height : width) * 96) / 25.4;
|
|
21
21
|
return Math.round(paperPx - lengthToPx(margin.left) - lengthToPx(margin.right));
|
|
22
22
|
}
|
|
23
|
+
/** Height of the text area in CSS pixels: the paper minus the top and bottom margins. */
|
|
24
|
+
export function contentHeightPx(pageSize, landscape, margin) {
|
|
25
|
+
const [width, height] = PAPER_MM[pageSize];
|
|
26
|
+
const paperPx = ((landscape ? width : height) * 96) / 25.4;
|
|
27
|
+
return Math.round(paperPx - lengthToPx(margin.top) - lengthToPx(margin.bottom));
|
|
28
|
+
}
|
|
23
29
|
//# sourceMappingURL=page.js.map
|
|
@@ -2,6 +2,8 @@ import { escapeHtml } from "../html.js";
|
|
|
2
2
|
export { escapeHtml };
|
|
3
3
|
/** Stylesheets of a built-in theme ("light" or "dark"). */
|
|
4
4
|
export declare function builtinThemeCss(theme: "light" | "dark"): string[];
|
|
5
|
+
/** Stylesheet for a density other than "normal", applied after the theme. */
|
|
6
|
+
export declare function densityCss(density: "normal" | "compact"): string[];
|
|
5
7
|
export interface DocumentParts {
|
|
6
8
|
title: string;
|
|
7
9
|
body: string;
|
|
@@ -15,7 +17,9 @@ export interface DocumentParts {
|
|
|
15
17
|
showTitle?: boolean;
|
|
16
18
|
/** Theme stylesheet contents. Defaults to the built-in light theme. */
|
|
17
19
|
themeCss?: string[];
|
|
18
|
-
/**
|
|
20
|
+
/** Layout stylesheets (density, font size, page height), applied after the theme. */
|
|
21
|
+
layoutCss?: string[];
|
|
22
|
+
/** Extra stylesheet contents, applied after the theme and layout. */
|
|
19
23
|
extraCss?: string[];
|
|
20
24
|
}
|
|
21
25
|
/** Wrap a rendered Markdown fragment in a complete, self-contained HTML document. */
|
package/dist/render/template.js
CHANGED
|
@@ -17,6 +17,10 @@ function asset(name) {
|
|
|
17
17
|
export function builtinThemeCss(theme) {
|
|
18
18
|
return theme === "dark" ? [asset("theme.css"), asset("theme-dark.css")] : [asset("theme.css")];
|
|
19
19
|
}
|
|
20
|
+
/** Stylesheet for a density other than "normal", applied after the theme. */
|
|
21
|
+
export function densityCss(density) {
|
|
22
|
+
return density === "compact" ? [asset("density-compact.css")] : [];
|
|
23
|
+
}
|
|
20
24
|
function titleBlock({ title, subtitle, author, date }) {
|
|
21
25
|
const meta = [author, date].filter(Boolean).map((part) => escapeHtml(part));
|
|
22
26
|
return [
|
|
@@ -31,9 +35,15 @@ function titleBlock({ title, subtitle, author, date }) {
|
|
|
31
35
|
}
|
|
32
36
|
/** Wrap a rendered Markdown fragment in a complete, self-contained HTML document. */
|
|
33
37
|
export function buildHtmlDocument(parts) {
|
|
34
|
-
const { title, body, math = false, lang, author, extraCss = [] } = parts;
|
|
38
|
+
const { title, body, math = false, lang, author, layoutCss = [], extraCss = [] } = parts;
|
|
35
39
|
const themeCss = parts.themeCss ?? builtinThemeCss("light");
|
|
36
|
-
const styles = [
|
|
40
|
+
const styles = [
|
|
41
|
+
fontFaceCss(),
|
|
42
|
+
...(math ? [katexCss()] : []),
|
|
43
|
+
...themeCss,
|
|
44
|
+
...layoutCss,
|
|
45
|
+
...extraCss,
|
|
46
|
+
];
|
|
37
47
|
return `<!doctype html>
|
|
38
48
|
<html${lang ? ` lang="${escapeHtml(lang)}"` : ""}>
|
|
39
49
|
<head>
|
package/dist/run.js
CHANGED
|
@@ -26,6 +26,12 @@ export async function runJob(job, format, settings, browsers, io) {
|
|
|
26
26
|
}
|
|
27
27
|
if (io.verbose && result.configFile)
|
|
28
28
|
io.err(`config: ${result.configFile}\n`);
|
|
29
|
+
if (io.verbose) {
|
|
30
|
+
for (const note of result.notes) {
|
|
31
|
+
const location = [displayPath(job.input), note.line].filter((p) => p !== undefined);
|
|
32
|
+
io.err(`${location.join(":")}: ${note.message}\n`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
29
35
|
if (!io.quiet)
|
|
30
36
|
io.out(`${result.output}\n`);
|
|
31
37
|
if (io.verbose)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imontene/mdrender",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Command-line app for Windows and Linux that renders Markdown — with LaTeX equations and Mermaid diagrams — to PDF and HTML using the Inter typeface",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|
|
@@ -60,7 +60,8 @@
|
|
|
60
60
|
"check": "npm run format:check && npm run lint && npm run typecheck && npm test",
|
|
61
61
|
"build:exe": "npm run build && node scripts/build-sea.mjs",
|
|
62
62
|
"prepublishOnly": "npm run check && npm run build",
|
|
63
|
-
"test:coverage": "vitest run --coverage"
|
|
63
|
+
"test:coverage": "vitest run --coverage",
|
|
64
|
+
"bench:mermaid": "tsx tests/bench/mermaid-autofit.ts"
|
|
64
65
|
},
|
|
65
66
|
"dependencies": {
|
|
66
67
|
"@fontsource/jetbrains-mono": "^5.3.0",
|