@imontene/mdrender 2.1.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 +17 -0
- package/README.md +52 -31
- package/dist/assets/theme.css +15 -0
- package/dist/cli.js +9 -1
- package/dist/convert.d.ts +12 -1
- package/dist/convert.js +48 -4
- package/dist/markdown/env.d.ts +2 -0
- package/dist/markdown/index.js +2 -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 +6 -0
- package/dist/options.js +5 -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 -8
- package/dist/render/mermaid.js +99 -30
- package/dist/run.js +6 -0
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Cambios
|
|
2
2
|
|
|
3
|
+
## 2.2.0 — 2026-10-15
|
|
4
|
+
|
|
5
|
+
- **Autoajuste de diagramas de flujo Mermaid** (`flowchart`/`graph`): cada diagrama se dibuja
|
|
6
|
+
con varios anchos de ajuste de etiquetas y se elige el que imprime el texto más grande (a igual
|
|
7
|
+
legibilidad, el más bajo), nunca más pequeño que tal como está escrito. Si sigue por debajo del
|
|
8
|
+
75 % de su tamaño, se prueba el otro eje (`LR` → `TB`, `TD` → `LR`) y se gira si gana al menos
|
|
9
|
+
15 puntos. En el banco de pruebas, la escala media del texto pasa de 0,80 a 0,98 y ningún
|
|
10
|
+
diagrama queda por debajo de 0,6.
|
|
11
|
+
- `--mermaid-flip never` (`mermaidFlip`) conserva la dirección; `--no-mermaid-autofit`
|
|
12
|
+
(`mermaidAutofit: false`) lo desactiva. Se respetan los diagramas con `%%{init}%%` o
|
|
13
|
+
`config:` propios.
|
|
14
|
+
- `-v` informa la elección por diagrama; aviso si un diagrama se imprime por debajo del 50 %.
|
|
15
|
+
- Un título seguido de un diagrama o una imagen ya no queda solo al pie de una página: pasa con
|
|
16
|
+
ellos a la siguiente.
|
|
17
|
+
- `npm run bench:mermaid [-- archivo.md]` compara los diagramas de un documento con y sin
|
|
18
|
+
autoajuste.
|
|
19
|
+
|
|
3
20
|
## 2.1.0 — 2026-10-15
|
|
4
21
|
|
|
5
22
|
- `mdrender doctor`: revisa instalación (versión, Node.js, `PATH`), navegador (cuál y por qué,
|
package/README.md
CHANGED
|
@@ -134,37 +134,39 @@ equipo); `--port` fija el puerto y `--no-open` no abre el navegador.
|
|
|
134
134
|
|
|
135
135
|
### Opciones
|
|
136
136
|
|
|
137
|
-
| Opción
|
|
138
|
-
|
|
|
139
|
-
| `-o, --output <ruta>`
|
|
140
|
-
| `-f, --format <pdf\|html>`
|
|
141
|
-
| `--page-size <tamaño>`
|
|
142
|
-
| `--margin <valor>`
|
|
143
|
-
| `--margin-top <medida>`
|
|
144
|
-
| `--margin-right <medida>`
|
|
145
|
-
| `--margin-bottom <medida>`
|
|
146
|
-
| `--margin-left <medida>`
|
|
147
|
-
| `--orientation <orientación>`
|
|
148
|
-
| `--landscape`
|
|
149
|
-
| `--toc`
|
|
150
|
-
| `--toc-depth <n>`
|
|
151
|
-
| `--header <texto>`
|
|
152
|
-
| `--footer <texto>`
|
|
153
|
-
| `--no-page-numbers`
|
|
154
|
-
| `--density <densidad>`
|
|
155
|
-
| `--font-size <tamaño>`
|
|
156
|
-
| `--theme <light\|dark\|css>`
|
|
157
|
-
| `--css <archivo.css>`
|
|
158
|
-
| `--mermaid-theme <tema>`
|
|
159
|
-
| `--
|
|
160
|
-
| `--
|
|
161
|
-
| `--
|
|
162
|
-
| `--
|
|
163
|
-
| `--
|
|
164
|
-
| `--
|
|
165
|
-
|
|
|
166
|
-
|
|
|
167
|
-
| `-
|
|
137
|
+
| Opción | Descripción | Por defecto |
|
|
138
|
+
| ------------------------------ | -------------------------------------------------------------------- | ------------------ |
|
|
139
|
+
| `-o, --output <ruta>` | Archivo de salida, o carpeta con varios archivos | junto al `.md` |
|
|
140
|
+
| `-f, --format <pdf\|html>` | Formato de salida | `pdf` |
|
|
141
|
+
| `--page-size <tamaño>` | `Letter`, `A4`, `Legal` (también `A3`, `A5`, `Tabloid`) | `Letter` |
|
|
142
|
+
| `--margin <valor>` | Las cuatro márgenes como en CSS: `10mm`, `15mm 20mm`… | `10mm` |
|
|
143
|
+
| `--margin-top <medida>` | Margen superior, p. ej. `15mm` (prevalece sobre `--margin`) | `10mm` |
|
|
144
|
+
| `--margin-right <medida>` | Margen derecho | `10mm` |
|
|
145
|
+
| `--margin-bottom <medida>` | Margen inferior | `10mm` |
|
|
146
|
+
| `--margin-left <medida>` | Margen izquierdo | `10mm` |
|
|
147
|
+
| `--orientation <orientación>` | `portrait` (vertical) o `landscape` (horizontal) | `portrait` |
|
|
148
|
+
| `--landscape` | Atajo de `--orientation landscape` | — |
|
|
149
|
+
| `--toc` | Tabla de contenidos al inicio (o donde pongas `[[toc]]`) | — |
|
|
150
|
+
| `--toc-depth <n>` | Nivel de título más profundo en la tabla de contenidos | `3` |
|
|
151
|
+
| `--header <texto>` | Encabezado de página (ver abajo) | — |
|
|
152
|
+
| `--footer <texto>` | Pie de página (ver abajo) | `{page} / {pages}` |
|
|
153
|
+
| `--no-page-numbers` | Sin el pie con números de página | — |
|
|
154
|
+
| `--density <densidad>` | `normal` o `compact` (menos espacio entre líneas y bloques) | `normal` |
|
|
155
|
+
| `--font-size <tamaño>` | Tamaño del texto en `pt` o `px`, de 6 a 24 pt; p. ej. `10pt` | `10.5pt` |
|
|
156
|
+
| `--theme <light\|dark\|css>` | `light`, `dark` o un `.css` que reemplaza el tema | `light` |
|
|
157
|
+
| `--css <archivo.css>` | CSS adicional (se puede repetir) | — |
|
|
158
|
+
| `--mermaid-theme <tema>` | `default`, `neutral`, `dark`, `forest`, `base` | `neutral` / `dark` |
|
|
159
|
+
| `--mermaid-flip <auto\|never>` | Si el autoajuste puede girar un diagrama de flujo de eje (ver abajo) | `auto` |
|
|
160
|
+
| `--no-mermaid-autofit` | Dibujar los diagramas de flujo tal como están escritos | — |
|
|
161
|
+
| `--lang <código>` | Idioma del documento; `en` para inglés (`pt`, `fr`, `de`…) | `es` |
|
|
162
|
+
| `--title <texto>` | Título del documento (muestra un bloque de título) | primer `# título` |
|
|
163
|
+
| `--config <archivo>` | Archivo de configuración | el más cercano |
|
|
164
|
+
| `--no-config` | Ignorar `mdrender.config.json` | — |
|
|
165
|
+
| `--browser <ruta>` | Ruta a Chrome/Edge/Chromium (o variable `MDRENDER_BROWSER`) | autodetección |
|
|
166
|
+
| `--allow-remote` | Permitir que el PDF descargue imágenes/CSS de internet | bloqueado |
|
|
167
|
+
| `-w, --watch` | Regenerar al guardar | — |
|
|
168
|
+
| `-q, --quiet` | Mostrar solo advertencias y errores | — |
|
|
169
|
+
| `-v, --verbose` | Mostrar también configuración, navegador y tiempos | — |
|
|
168
170
|
|
|
169
171
|
**Página**: por defecto hoja **Letter vertical con márgenes de 10 mm**. Las medidas aceptan
|
|
170
172
|
`mm`, `cm`, `in` o `px`. `--margin` fija las cuatro márgenes a la vez y `--margin-top`,
|
|
@@ -237,6 +239,25 @@ fecha). Las rutas de `css` y `theme` son relativas al archivo donde aparecen.
|
|
|
237
239
|
- Si una fórmula o diagrama tiene un error, el documento se genera igual con una caja roja en
|
|
238
240
|
su lugar, se informa `error: archivo.md:LÍNEA: mensaje` y el comando termina con código `2`.
|
|
239
241
|
|
|
242
|
+
**Autoajuste de diagramas de flujo**: en el PDF, un diagrama más ancho que la columna o más alto
|
|
243
|
+
que la página se reduce entero, y su texto puede quedar diminuto. Por eso mdrender dibuja cada
|
|
244
|
+
`flowchart`/`graph` con varios anchos de ajuste de las etiquetas y se queda con el que imprime el
|
|
245
|
+
texto más grande; a igual legibilidad, el que ocupa menos alto. Nunca lo imprime más pequeño que
|
|
246
|
+
tal como está escrito.
|
|
247
|
+
|
|
248
|
+
- Si el diagrama sigue por debajo del 75 % de su tamaño, prueba además el otro eje (`LR` → `TB`,
|
|
249
|
+
o `TD` → `LR`) y lo gira si gana al menos 15 puntos. Una cadena horizontal de 8 cajas, por
|
|
250
|
+
ejemplo, pasa de imprimirse al 37 % a su tamaño real. `--mermaid-flip never` (o
|
|
251
|
+
`mermaidFlip: never`) conserva siempre la dirección escrita.
|
|
252
|
+
- No se tocan los diagramas con configuración propia (`%%{init: …}%%` o `config:` en su front
|
|
253
|
+
matter) ni los de otros tipos (secuencia, torta, clases…).
|
|
254
|
+
- `-v` informa la elección de cada diagrama; si alguno queda por debajo del 50 %, mdrender avisa:
|
|
255
|
+
conviene partirlo o acortar sus etiquetas.
|
|
256
|
+
- `--no-mermaid-autofit` (o `mermaidAutofit: false`) lo desactiva.
|
|
257
|
+
|
|
258
|
+
Un título seguido de un diagrama o una imagen pasa con ellos a la página siguiente cuando no
|
|
259
|
+
caben, en lugar de quedar solo al pie de la anterior.
|
|
260
|
+
|
|
240
261
|
## Más Markdown
|
|
241
262
|
|
|
242
263
|
- **Alertas** estilo GitHub: `> [!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]`,
|
package/dist/assets/theme.css
CHANGED
|
@@ -352,6 +352,21 @@ div.math-error {
|
|
|
352
352
|
.mermaid-diagram svg {
|
|
353
353
|
max-height: calc(var(--page-content-height) - 6mm);
|
|
354
354
|
}
|
|
355
|
+
|
|
356
|
+
/* Next to its heading: leave room for it (HEADING_ALLOWANCE_EM in render/mermaid.ts). */
|
|
357
|
+
.keep-with-heading img,
|
|
358
|
+
.keep-with-heading .mermaid-diagram svg {
|
|
359
|
+
max-height: calc(var(--page-content-height) - 6mm - 5rem);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/* A heading and the diagram or image right after it go to the same page. */
|
|
364
|
+
.keep-with-heading {
|
|
365
|
+
break-inside: avoid;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.markdown-body > .keep-with-heading:first-child > :first-child {
|
|
369
|
+
margin-top: 0;
|
|
355
370
|
}
|
|
356
371
|
|
|
357
372
|
/* Mermaid's HTML labels are <p>/<span> inside <foreignObject>: keep document styles out. */
|
package/dist/cli.js
CHANGED
|
@@ -30,6 +30,8 @@ const DOCUMENT_FLAGS = [
|
|
|
30
30
|
"css",
|
|
31
31
|
"theme",
|
|
32
32
|
"mermaidTheme",
|
|
33
|
+
"mermaidAutofit",
|
|
34
|
+
"mermaidFlip",
|
|
33
35
|
"lang",
|
|
34
36
|
"title",
|
|
35
37
|
];
|
|
@@ -60,6 +62,8 @@ function addDocumentOptions(command) {
|
|
|
60
62
|
.option("--theme <theme>", "light, dark, or a .css file replacing the theme (default: light)")
|
|
61
63
|
.option("--css <file>", "extra stylesheet (repeatable)", collect)
|
|
62
64
|
.option("--mermaid-theme <theme>", `Mermaid theme: ${MERMAID_THEMES.join(", ")} (default: neutral; dark with --theme dark)`)
|
|
65
|
+
.option("--no-mermaid-autofit", "draw flowcharts exactly as written (no layout search)")
|
|
66
|
+
.option("--mermaid-flip <mode>", "auto: turn a flowchart to the other axis if its text prints too small; never (default: auto)")
|
|
63
67
|
.option("--lang <tag>", "document language, e.g. es or en (default: es)")
|
|
64
68
|
.option("--title <text>", "document title (shown as a title block)")
|
|
65
69
|
.option("--config <file>", "config file (default: nearest mdrender.config.json)")
|
|
@@ -111,7 +115,11 @@ function cliOverrides(program) {
|
|
|
111
115
|
if (program.getOptionValueSource(key) === "cli")
|
|
112
116
|
raw[key] = program.getOptionValue(key);
|
|
113
117
|
}
|
|
114
|
-
return normalizeOptions(raw, "command line", process.cwd(), (key) => key === "pageNumbers"
|
|
118
|
+
return normalizeOptions(raw, "command line", process.cwd(), (key) => key === "pageNumbers"
|
|
119
|
+
? "--no-page-numbers"
|
|
120
|
+
: key === "mermaidAutofit"
|
|
121
|
+
? "--no-mermaid-autofit"
|
|
122
|
+
: toFlag(key));
|
|
115
123
|
}
|
|
116
124
|
function settings(program, options) {
|
|
117
125
|
return {
|
package/dist/convert.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Diagnostic } from "./diagnostics.js";
|
|
|
2
2
|
import { type DocumentOptions, type Margins, type MermaidTheme } from "./options.js";
|
|
3
3
|
import type { OutputFormat } from "./paths.js";
|
|
4
4
|
import { BrowserSession } from "./render/browser.js";
|
|
5
|
+
import { type DiagramLayout } from "./render/mermaid.js";
|
|
5
6
|
/** Settings shared by every file of a run (batch, watch or preview). */
|
|
6
7
|
export interface RenderSettings {
|
|
7
8
|
/** Explicit browser executable. */
|
|
@@ -30,6 +31,8 @@ export interface ConvertResult {
|
|
|
30
31
|
output: string;
|
|
31
32
|
/** Problems found while rendering; the output is still written, with errors shown inline. */
|
|
32
33
|
diagnostics: Diagnostic[];
|
|
34
|
+
/** Details for --verbose, such as the layout chosen for each Mermaid flowchart. */
|
|
35
|
+
notes: Note[];
|
|
33
36
|
/** The effective options after merging config file, front matter and command line. */
|
|
34
37
|
options: DocumentOptions;
|
|
35
38
|
/** Config file that was applied, if any. */
|
|
@@ -59,12 +62,20 @@ export declare class BrowserPool {
|
|
|
59
62
|
*/
|
|
60
63
|
export declare const MIN_HEADER_FOOTER_MARGIN = "10mm";
|
|
61
64
|
export declare function headerFooterSpaceWarnings(margin: Margins, hasHeader: boolean, hasFooter: boolean): Diagnostic[];
|
|
65
|
+
export interface Note {
|
|
66
|
+
line?: number;
|
|
67
|
+
message: string;
|
|
68
|
+
}
|
|
69
|
+
/** "Mermaid autofit: wrap 300, turned LR→TB, scale 0.33 → 1.00, height 81 → 903 px". */
|
|
70
|
+
export declare function describeLayout(layout: DiagramLayout): string;
|
|
62
71
|
/** Mermaid theme: explicit choice, else dark diagrams for the dark page theme. */
|
|
63
72
|
export declare function effectiveMermaidTheme(options: DocumentOptions): MermaidTheme;
|
|
64
73
|
/**
|
|
65
74
|
* Render a Markdown file to a complete HTML document. A browser is only started (through
|
|
66
75
|
* `browsers`) when the document has Mermaid diagrams.
|
|
67
76
|
*/
|
|
68
|
-
export declare function renderDocument(inputPath: string, settings: RenderSettings, browsers: BrowserPool
|
|
77
|
+
export declare function renderDocument(inputPath: string, settings: RenderSettings, browsers: BrowserPool,
|
|
78
|
+
/** Printed on pages (PDF): diagrams are fitted to the page height, not only the width. */
|
|
79
|
+
paged?: boolean): Promise<RenderedDocument>;
|
|
69
80
|
/** Read a Markdown file and write it as a self-contained HTML document or a PDF. */
|
|
70
81
|
export declare function convertFile(convert: ConvertOptions): Promise<ConvertResult>;
|
package/dist/convert.js
CHANGED
|
@@ -11,6 +11,7 @@ import { DEFAULT_FONT_SIZE, fontSizePx, mergeOptions, normalizeOptions, OptionsE
|
|
|
11
11
|
import { BrowserSession } from "./render/browser.js";
|
|
12
12
|
import { DEFAULT_FOOTER, headerFooterTemplate } from "./render/header-footer.js";
|
|
13
13
|
import { fillMermaid, renderMermaid } from "./render/mermaid.js";
|
|
14
|
+
import { SMALL_SCALE } from "./render/mermaid-fit.js";
|
|
14
15
|
import { contentHeightPx, contentWidthPx, lengthToPx } from "./render/page.js";
|
|
15
16
|
import { buildHtmlDocument, builtinThemeCss, densityCss } from "./render/template.js";
|
|
16
17
|
import { writeFileAtomic } from "./write.js";
|
|
@@ -86,6 +87,19 @@ function layoutCss(options, margin) {
|
|
|
86
87
|
rules.push(`html { font-size: ${options.fontSize}; }`);
|
|
87
88
|
return rules.join("\n");
|
|
88
89
|
}
|
|
90
|
+
const fixed2 = (n) => n.toFixed(2);
|
|
91
|
+
/** "Mermaid autofit: wrap 300, turned LR→TB, scale 0.33 → 1.00, height 81 → 903 px". */
|
|
92
|
+
export function describeLayout(layout) {
|
|
93
|
+
const changes = [
|
|
94
|
+
layout.wrap === undefined ? undefined : `wrap ${layout.wrap}`,
|
|
95
|
+
layout.turned ? `turned ${layout.turned}` : undefined,
|
|
96
|
+
].filter(Boolean);
|
|
97
|
+
const scale = `scale ${fixed2(layout.baseScale)} → ${fixed2(layout.scale)}`;
|
|
98
|
+
const height = `height ${layout.baseHeight} → ${layout.height} px`;
|
|
99
|
+
return changes.length === 0
|
|
100
|
+
? `Mermaid autofit: kept as written (scale ${fixed2(layout.scale)}, ${layout.tried} layouts tried)`
|
|
101
|
+
: `Mermaid autofit: ${changes.join(", ")}, ${scale}, ${height} (${layout.tried} layouts tried)`;
|
|
102
|
+
}
|
|
89
103
|
/** Mermaid theme: explicit choice, else dark diagrams for the dark page theme. */
|
|
90
104
|
export function effectiveMermaidTheme(options) {
|
|
91
105
|
return options.mermaidTheme ?? (options.theme === "dark" ? "dark" : "neutral");
|
|
@@ -94,7 +108,9 @@ export function effectiveMermaidTheme(options) {
|
|
|
94
108
|
* Render a Markdown file to a complete HTML document. A browser is only started (through
|
|
95
109
|
* `browsers`) when the document has Mermaid diagrams.
|
|
96
110
|
*/
|
|
97
|
-
export async function renderDocument(inputPath, settings, browsers
|
|
111
|
+
export async function renderDocument(inputPath, settings, browsers,
|
|
112
|
+
/** Printed on pages (PDF): diagrams are fitted to the page height, not only the width. */
|
|
113
|
+
paged = false) {
|
|
98
114
|
const input = path.resolve(inputPath);
|
|
99
115
|
const baseDir = path.dirname(input);
|
|
100
116
|
const source = await readFile(input, "utf8");
|
|
@@ -114,6 +130,7 @@ export async function renderDocument(inputPath, settings, browsers) {
|
|
|
114
130
|
...config.warnings.map((message) => ({ severity: "warning", message })),
|
|
115
131
|
...md.diagnostics,
|
|
116
132
|
];
|
|
133
|
+
const notes = [];
|
|
117
134
|
const title = options.title ?? md.title ?? path.parse(input).name;
|
|
118
135
|
// Table of contents: at the [[toc]] marker, or at the start with --toc.
|
|
119
136
|
let body = md.html;
|
|
@@ -130,10 +147,36 @@ export async function renderDocument(inputPath, settings, browsers) {
|
|
|
130
147
|
const width = contentWidthPx(options.pageSize, options.orientation === "landscape", margin);
|
|
131
148
|
const theme = effectiveMermaidTheme(options);
|
|
132
149
|
const fontPx = fontSizePx(options.fontSize ?? DEFAULT_FONT_SIZE);
|
|
133
|
-
const
|
|
150
|
+
const landscape = options.orientation === "landscape";
|
|
151
|
+
const results = await renderMermaid(session, md.mermaid, {
|
|
152
|
+
theme,
|
|
153
|
+
widthPx: width,
|
|
154
|
+
fontPx,
|
|
155
|
+
// Same limit as the print stylesheet: the page's text height minus 6 mm.
|
|
156
|
+
pageHeightPx: paged
|
|
157
|
+
? contentHeightPx(options.pageSize, landscape, margin) - lengthToPx("6mm")
|
|
158
|
+
: Infinity,
|
|
159
|
+
autofit: options.mermaidAutofit,
|
|
160
|
+
flip: options.mermaidFlip,
|
|
161
|
+
});
|
|
134
162
|
const filled = fillMermaid(body, md.nonce, md.mermaid, results);
|
|
135
163
|
body = filled.html;
|
|
136
164
|
diagnostics.push(...filled.diagnostics);
|
|
165
|
+
md.mermaid.forEach((block, i) => {
|
|
166
|
+
const result = results[i];
|
|
167
|
+
if (!result || "error" in result)
|
|
168
|
+
return;
|
|
169
|
+
if (result.layout)
|
|
170
|
+
notes.push({ line: block.line, message: describeLayout(result.layout) });
|
|
171
|
+
if (paged && result.scale !== undefined && result.scale < SMALL_SCALE) {
|
|
172
|
+
diagnostics.push({
|
|
173
|
+
severity: "warning",
|
|
174
|
+
line: block.line,
|
|
175
|
+
message: `Mermaid diagram printed at ${Math.round(result.scale * 100)}% of its size; ` +
|
|
176
|
+
"split it or shorten its labels to make it readable",
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
});
|
|
137
180
|
}
|
|
138
181
|
const html = buildHtmlDocument({
|
|
139
182
|
title,
|
|
@@ -154,14 +197,14 @@ export async function renderDocument(inputPath, settings, browsers) {
|
|
|
154
197
|
...(options.theme.endsWith(".css") ? [options.theme] : []),
|
|
155
198
|
...options.css,
|
|
156
199
|
];
|
|
157
|
-
return { html, title, diagnostics, options, configFile, dependencies };
|
|
200
|
+
return { html, title, diagnostics, notes, options, configFile, dependencies };
|
|
158
201
|
}
|
|
159
202
|
/** Read a Markdown file and write it as a self-contained HTML document or a PDF. */
|
|
160
203
|
export async function convertFile(convert) {
|
|
161
204
|
const output = path.resolve(convert.output);
|
|
162
205
|
const browsers = convert.browsers ?? new BrowserPool(convert.browser);
|
|
163
206
|
try {
|
|
164
|
-
const doc = await renderDocument(convert.input, convert, browsers);
|
|
207
|
+
const doc = await renderDocument(convert.input, convert, browsers, convert.format === "pdf");
|
|
165
208
|
const { options } = doc;
|
|
166
209
|
if (convert.format === "pdf") {
|
|
167
210
|
const margin = resolveMargins(options);
|
|
@@ -195,6 +238,7 @@ export async function convertFile(convert) {
|
|
|
195
238
|
return {
|
|
196
239
|
output,
|
|
197
240
|
diagnostics: doc.diagnostics,
|
|
241
|
+
notes: doc.notes,
|
|
198
242
|
options,
|
|
199
243
|
configFile: doc.configFile,
|
|
200
244
|
dependencies: doc.dependencies,
|
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
|
@@ -8,6 +8,7 @@ import { codeBlocks } from "./code-blocks.js";
|
|
|
8
8
|
import { inlineChildLines } from "./env.js";
|
|
9
9
|
import { highlightCode } from "./highlight.js";
|
|
10
10
|
import { inlineLocalImages } from "./images.js";
|
|
11
|
+
import { keepWithHeading } from "./keep-with-heading.js";
|
|
11
12
|
import { math } from "./math.js";
|
|
12
13
|
import { mermaid } from "./mermaid.js";
|
|
13
14
|
import { taskLists } from "./task-lists.js";
|
|
@@ -24,6 +25,7 @@ export function createMarkdown() {
|
|
|
24
25
|
md.use(taskLists);
|
|
25
26
|
md.use(alerts);
|
|
26
27
|
md.use(tocMarker);
|
|
28
|
+
md.use(keepWithHeading);
|
|
27
29
|
md.use(inlineChildLines);
|
|
28
30
|
md.use(inlineLocalImages);
|
|
29
31
|
// Before math and mermaid, which hand their non-code fences on to this renderer.
|
|
@@ -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,6 +11,8 @@ 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"];
|
|
15
17
|
export declare const DENSITIES: readonly ["normal", "compact"];
|
|
16
18
|
export type Density = (typeof DENSITIES)[number];
|
|
@@ -35,6 +37,10 @@ export interface DocumentOptions {
|
|
|
35
37
|
footer?: string;
|
|
36
38
|
/** Mermaid theme; when unset, "neutral" for light pages and "dark" for the dark theme. */
|
|
37
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;
|
|
38
44
|
/** Vertical spacing: "compact" tightens line height, paragraph and heading spacing. */
|
|
39
45
|
density: Density;
|
|
40
46
|
/** Base font size of the text, e.g. "10pt"; when unset, the theme's (10.5pt). */
|
package/dist/options.js
CHANGED
|
@@ -5,6 +5,7 @@ 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"];
|
|
9
10
|
export const DENSITIES = ["normal", "compact"];
|
|
10
11
|
export const DEFAULT_OPTIONS = {
|
|
@@ -14,6 +15,8 @@ export const DEFAULT_OPTIONS = {
|
|
|
14
15
|
toc: false,
|
|
15
16
|
tocDepth: 3,
|
|
16
17
|
pageNumbers: true,
|
|
18
|
+
mermaidAutofit: true,
|
|
19
|
+
mermaidFlip: "auto",
|
|
17
20
|
density: "normal",
|
|
18
21
|
theme: "light",
|
|
19
22
|
css: [],
|
|
@@ -110,6 +113,8 @@ const VALIDATORS = {
|
|
|
110
113
|
header: text,
|
|
111
114
|
footer: text,
|
|
112
115
|
mermaidTheme: oneOf(MERMAID_THEMES),
|
|
116
|
+
mermaidAutofit: bool,
|
|
117
|
+
mermaidFlip: oneOf(MERMAID_FLIPS),
|
|
113
118
|
density: oneOf(DENSITIES),
|
|
114
119
|
fontSize,
|
|
115
120
|
theme: (value, describe, baseDir) => {
|
|
@@ -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;
|
|
@@ -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,22 +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,
|
|
18
|
-
/** Text size of the document in CSS pixels, so diagram labels match the body text. */
|
|
19
|
-
fontPx?: number): Promise<DiagramResult[]>;
|
|
51
|
+
export declare function renderMermaid(session: BrowserSession, blocks: MermaidBlock[], options?: MermaidRenderOptions): Promise<DiagramResult[]>;
|
|
20
52
|
/**
|
|
21
53
|
* Replace the Mermaid placeholders in `html` with rendered SVGs, or with a visible error box.
|
|
22
54
|
* Returns the new HTML and one error diagnostic per failed diagram.
|
package/dist/render/mermaid.js
CHANGED
|
@@ -2,21 +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;
|
|
8
9
|
/** 10.5pt, the built-in themes' text size (Mermaid's own default is 16px). */
|
|
9
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;
|
|
10
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
|
+
}
|
|
11
32
|
/**
|
|
12
33
|
* 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"`.
|
|
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.
|
|
14
36
|
*/
|
|
15
|
-
export async function renderMermaid(session, blocks,
|
|
16
|
-
|
|
17
|
-
widthPx = DEFAULT_WIDTH_PX,
|
|
18
|
-
/** Text size of the document in CSS pixels, so diagram labels match the body text. */
|
|
19
|
-
fontPx = DEFAULT_FONT_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;
|
|
20
39
|
mermaidJs ??= readTextAsset(MERMAID_JS);
|
|
21
40
|
const page = await session.newPage();
|
|
22
41
|
try {
|
|
@@ -24,35 +43,85 @@ fontPx = DEFAULT_FONT_PX) {
|
|
|
24
43
|
body { margin: 0; width: ${widthPx}px; font-family: "Inter", sans-serif; }</style>
|
|
25
44
|
</head><body></body></html>`);
|
|
26
45
|
await page.evaluate(mermaidJs);
|
|
27
|
-
|
|
46
|
+
await page.evaluate(async () => {
|
|
28
47
|
// Runs in the browser. The project is compiled without DOM types, hence PageGlobals.
|
|
29
|
-
const { document
|
|
48
|
+
const { document } = globalThis;
|
|
30
49
|
// Load the font weights Mermaid uses before it measures any text.
|
|
31
50
|
await Promise.all(["400", "600", "700"].map((w) => document.fonts.load(`${w} 16px Inter`)));
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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;
|
|
52
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);
|
|
53
107
|
}
|
|
54
|
-
|
|
55
|
-
|
|
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;
|
|
56
125
|
}
|
|
57
126
|
finally {
|
|
58
127
|
await page.close();
|
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",
|