@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 CHANGED
@@ -1,5 +1,38 @@
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
+
20
+ ## 2.1.0 — 2026-10-15
21
+
22
+ - `mdrender doctor`: revisa instalación (versión, Node.js, `PATH`), navegador (cuál y por qué,
23
+ arranque y versión, sandbox), una conversión real de prueba, el `mdrender.config.json`
24
+ aplicado (errores, opciones mal escritas, valores efectivos), la caché de `setup` y el proxy.
25
+ `--quick`, `--json`; código 1 si encuentra un problema.
26
+ - Mejor uso de la página:
27
+ - Los bloques de código largos se parten entre páginas (al menos 4 líneas a cada lado) en
28
+ vez de saltar enteros y dejar hasta media página en blanco; los de hasta 12 líneas siguen
29
+ juntos.
30
+ - `--density compact` (o `density: compact`): interlineado y espacios más ajustados.
31
+ - `--font-size <tamaño>` (o `fontSize`): tamaño del texto, por defecto 10.5pt.
32
+ - Mermaid usa el tamaño de letra del documento (antes 16 px) y los diagramas de secuencia no
33
+ repiten los actores al pie.
34
+ - Imágenes y diagramas más altos que la página se reducen para caber en una sola.
35
+
3
36
  ## 2.0.0 — 2026-10-13
4
37
 
5
38
  - Márgenes por lado: `--margin-top`, `--margin-right`, `--margin-bottom`, `--margin-left`
package/README.md CHANGED
@@ -89,14 +89,39 @@ mdrender docs/ -o pdf/ --watch
89
89
  # Vista previa en el navegador que se recarga sola al guardar
90
90
  mdrender preview documento.md
91
91
 
92
+ # Más contenido por página
93
+ mdrender documento.md --density compact --font-size 10pt
94
+
92
95
  # Tema oscuro, o un CSS propio que reemplaza el tema
93
96
  mdrender documento.md --theme dark
94
97
  mdrender documento.md --theme mi-tema.css
95
98
 
96
99
  # Descargar un navegador si no tienes Chrome, Edge ni Chromium
97
100
  mdrender setup
101
+
102
+ # Diagnóstico: instalación, navegador, configuración y una conversión de prueba
103
+ mdrender doctor
104
+ ```
105
+
106
+ ### Si algo no funciona: `mdrender doctor`
107
+
108
+ Revisa la versión y el Node.js, si `mdrender` está en el `PATH`, qué navegador se usa (y por
109
+ qué), lo arranca y convierte un documento de prueba con una ecuación y un diagrama, e indica
110
+ qué `mdrender.config.json` se aplica, con sus opciones mal escritas y los valores efectivos.
111
+ Cada problema viene con su solución sugerida; termina con código 1 si encuentra alguno.
112
+
113
+ ```text
114
+ [ok] mdrender 2.0.0, npm package (/usr/lib/node_modules/@imontene/mdrender)
115
+ [ok] Node.js 22.23.3
116
+ [warn] PATH the mdrender command is not in any PATH folder
117
+ -> add its folder to PATH (for npm: the output of `npm prefix -g`…)
118
+ [ok] Browser C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe
119
+ [ok] Test conversion PDF with an equation and a Mermaid diagram in 1840 ms
98
120
  ```
99
121
 
122
+ `--quick` omite el arranque del navegador y la conversión; `--json` produce el informe en JSON
123
+ para compartirlo.
124
+
100
125
  En Windows (PowerShell o CMD) los comandos son idénticos:
101
126
 
102
127
  ```powershell
@@ -109,35 +134,39 @@ equipo); `--port` fija el puerto y `--no-open` no abre el navegador.
109
134
 
110
135
  ### Opciones
111
136
 
112
- | Opción | Descripción | Por defecto |
113
- | ----------------------------- | ----------------------------------------------------------- | ------------------ |
114
- | `-o, --output <ruta>` | Archivo de salida, o carpeta con varios archivos | junto al `.md` |
115
- | `-f, --format <pdf\|html>` | Formato de salida | `pdf` |
116
- | `--page-size <tamaño>` | `Letter`, `A4`, `Legal` (también `A3`, `A5`, `Tabloid`) | `Letter` |
117
- | `--margin <valor>` | Las cuatro márgenes como en CSS: `10mm`, `15mm 20mm`… | `10mm` |
118
- | `--margin-top <medida>` | Margen superior, p. ej. `15mm` (prevalece sobre `--margin`) | `10mm` |
119
- | `--margin-right <medida>` | Margen derecho | `10mm` |
120
- | `--margin-bottom <medida>` | Margen inferior | `10mm` |
121
- | `--margin-left <medida>` | Margen izquierdo | `10mm` |
122
- | `--orientation <orientación>` | `portrait` (vertical) o `landscape` (horizontal) | `portrait` |
123
- | `--landscape` | Atajo de `--orientation landscape` | — |
124
- | `--toc` | Tabla de contenidos al inicio (o donde pongas `[[toc]]`) | — |
125
- | `--toc-depth <n>` | Nivel de título más profundo en la tabla de contenidos | `3` |
126
- | `--header <texto>` | Encabezado de página (ver abajo) | — |
127
- | `--footer <texto>` | Pie de página (ver abajo) | `{page} / {pages}` |
128
- | `--no-page-numbers` | Sin el pie con números de página | — |
129
- | `--theme <light\|dark\|css>` | `light`, `dark` o un `.css` que reemplaza el tema | `light` |
130
- | `--css <archivo.css>` | CSS adicional (se puede repetir) | — |
131
- | `--mermaid-theme <tema>` | `default`, `neutral`, `dark`, `forest`, `base` | `neutral` / `dark` |
132
- | `--lang <código>` | Idioma del documento; `en` para inglés (`pt`, `fr`, `de`…) | `es` |
133
- | `--title <texto>` | Título del documento (muestra un bloque de título) | primer `# título` |
134
- | `--config <archivo>` | Archivo de configuración | el más cercano |
135
- | `--no-config` | Ignorar `mdrender.config.json` | — |
136
- | `--browser <ruta>` | Ruta a Chrome/Edge/Chromium (o variable `MDRENDER_BROWSER`) | autodetección |
137
- | `--allow-remote` | Permitir que el PDF descargue imágenes/CSS de internet | bloqueado |
138
- | `-w, --watch` | Regenerar al guardar | — |
139
- | `-q, --quiet` | Mostrar solo advertencias y errores | — |
140
- | `-v, --verbose` | Mostrar también configuración, navegador y tiempos | — |
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 | — |
141
170
 
142
171
  **Página**: por defecto hoja **Letter vertical con márgenes de 10 mm**. Las medidas aceptan
143
172
  `mm`, `cm`, `in` o `px`. `--margin` fija las cuatro márgenes a la vez y `--margin-top`,
@@ -146,6 +175,16 @@ equipo); `--port` fija el puerto y `--no-open` no abre el navegador.
146
175
  El encabezado y el pie se dibujan dentro de las márgenes superior e inferior: con menos de
147
176
  10 mm pueden tocar el texto y mdrender lo advierte.
148
177
 
178
+ **Aprovechar la página**: `--density compact` reduce interlineado, espacios entre párrafos y
179
+ títulos, y `--font-size 10pt` achica el texto (los diagramas Mermaid siguen el mismo tamaño).
180
+ Juntos ahorran entre un 15 y un 25 % de páginas. Además, siempre:
181
+
182
+ - Los bloques de código largos continúan en la página siguiente (con al menos 4 líneas a cada
183
+ lado del corte) en lugar de saltar enteros y dejar un hueco; los de hasta 12 líneas no se
184
+ parten.
185
+ - Las imágenes y diagramas más altos que la página se reducen para caber en una sola.
186
+ - Los diagramas de secuencia no repiten los actores al pie.
187
+
149
188
  **Encabezado y pie**: texto con marcadores `{page}`, `{pages}`, `{title}`, `{author}` y
150
189
  `{date}`; `|` separa columnas izquierda, centro y derecha. Ejemplos:
151
190
  `--header "{title} | | {date}"`, `--footer "Confidencial | {page} de {pages}"`.
@@ -176,6 +215,8 @@ header: "{title} | | {date}"
176
215
  "pageSize": "Letter",
177
216
  "margin": "15mm",
178
217
  "marginLeft": "25mm",
218
+ "density": "compact",
219
+ "fontSize": "10pt",
179
220
  "css": "estilos/empresa.css"
180
221
  }
181
222
  ```
@@ -198,6 +239,25 @@ fecha). Las rutas de `css` y `theme` son relativas al archivo donde aparecen.
198
239
  - Si una fórmula o diagrama tiene un error, el documento se genera igual con una caja roja en
199
240
  su lugar, se informa `error: archivo.md:LÍNEA: mensaje` y el comando termina con código `2`.
200
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
+
201
261
  ## Más Markdown
202
262
 
203
263
  - **Alertas** estilo GitHub: `> [!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]`,
@@ -0,0 +1,74 @@
1
+ /* mdrender compact density (--density compact): loaded after the theme, tightens vertical spacing. */
2
+
3
+ body {
4
+ line-height: 1.5;
5
+ }
6
+
7
+ p,
8
+ ul,
9
+ ol,
10
+ blockquote,
11
+ pre,
12
+ table,
13
+ dl {
14
+ margin-bottom: 0.8em;
15
+ }
16
+
17
+ h1,
18
+ h2,
19
+ h3,
20
+ h4,
21
+ h5,
22
+ h6 {
23
+ margin: 1.3em 0 0.5em;
24
+ }
25
+
26
+ h1 {
27
+ font-size: 1.9em;
28
+ }
29
+
30
+ h2 {
31
+ font-size: 1.4em;
32
+ }
33
+
34
+ h3 {
35
+ font-size: 1.18em;
36
+ }
37
+
38
+ li + li {
39
+ margin-top: 0.15em;
40
+ }
41
+
42
+ pre {
43
+ padding: 0.7em 0.9em;
44
+ }
45
+
46
+ th,
47
+ td {
48
+ padding: 0.35em 0.7em;
49
+ }
50
+
51
+ .math-display {
52
+ margin: 0.8em 0;
53
+ }
54
+
55
+ .mermaid-diagram {
56
+ margin: 0.9em 0;
57
+ }
58
+
59
+ .doc-header {
60
+ margin-bottom: 1.5em;
61
+ }
62
+
63
+ .toc {
64
+ margin-bottom: 1.5em;
65
+ padding: 0.8em 1.2em;
66
+ }
67
+
68
+ hr {
69
+ margin: 1.5em 0;
70
+ }
71
+
72
+ hr.footnotes-sep {
73
+ margin: 1.8em 0 0.8em;
74
+ }
@@ -243,6 +243,14 @@ pre {
243
243
  background: var(--color-subtle-bg);
244
244
  white-space: pre-wrap;
245
245
  overflow-wrap: anywhere;
246
+ /* Long listings may continue on the next page, keeping a few lines on each side. */
247
+ break-inside: auto;
248
+ orphans: 4;
249
+ widows: 4;
250
+ }
251
+
252
+ /* Short blocks (marked by mdrender) stay on one page. */
253
+ pre.keep-together {
246
254
  break-inside: avoid;
247
255
  }
248
256
 
@@ -335,6 +343,32 @@ div.math-error {
335
343
  height: auto;
336
344
  }
337
345
 
346
+ /*
347
+ * In the PDF, images and diagrams are scaled down to fit the page height (mdrender sets
348
+ * --page-content-height), so they are never cut across pages.
349
+ */
350
+ @media print {
351
+ img,
352
+ .mermaid-diagram svg {
353
+ max-height: calc(var(--page-content-height) - 6mm);
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;
370
+ }
371
+
338
372
  /* Mermaid's HTML labels are <p>/<span> inside <foreignObject>: keep document styles out. */
339
373
  .mermaid-diagram foreignObject p {
340
374
  margin: 0;
package/dist/assets.js CHANGED
@@ -85,7 +85,11 @@ export const MONO_STYLESHEETS = [
85
85
  ];
86
86
  export const KATEX_CSS = "katex/dist/katex.min.css";
87
87
  export const MERMAID_JS = "mermaid/dist/mermaid.min.js";
88
- export const THEMES = ["mdrender/assets/theme.css", "mdrender/assets/theme-dark.css"];
88
+ export const THEMES = [
89
+ "mdrender/assets/theme.css",
90
+ "mdrender/assets/theme-dark.css",
91
+ "mdrender/assets/density-compact.css",
92
+ ];
89
93
  export const PACKAGE_JSON = "mdrender/package.json";
90
94
  /** Fonts referenced by a stylesheet as url(...woff2), as asset keys. */
91
95
  export function woff2References(cssKey, css) {
package/dist/cli.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Command, CommanderError, Option } from "commander";
2
2
  import { setup } from "./browser/setup.js";
3
+ import { formatReport, runDoctor } from "./doctor.js";
3
4
  import { MdRenderError } from "./errors.js";
4
5
  import { ExitCode } from "./exit-codes.js";
5
6
  import { expandInputs, planOutputs } from "./inputs.js";
@@ -24,9 +25,13 @@ const DOCUMENT_FLAGS = [
24
25
  "header",
25
26
  "footer",
26
27
  "pageNumbers",
28
+ "density",
29
+ "fontSize",
27
30
  "css",
28
31
  "theme",
29
32
  "mermaidTheme",
33
+ "mermaidAutofit",
34
+ "mermaidFlip",
30
35
  "lang",
31
36
  "title",
32
37
  ];
@@ -52,9 +57,13 @@ function addDocumentOptions(command) {
52
57
  .option("--header <text>", 'page header, e.g. "{title} | | {date}"')
53
58
  .option("--footer <text>", 'page footer (default: "{page} / {pages}")')
54
59
  .option("--no-page-numbers", "no default page-number footer")
60
+ .option("--density <density>", "normal or compact (tighter spacing) (default: normal)")
61
+ .option("--font-size <size>", "text size, e.g. 10pt or 11pt (default: 10.5pt)")
55
62
  .option("--theme <theme>", "light, dark, or a .css file replacing the theme (default: light)")
56
63
  .option("--css <file>", "extra stylesheet (repeatable)", collect)
57
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)")
58
67
  .option("--lang <tag>", "document language, e.g. es or en (default: es)")
59
68
  .option("--title <text>", "document title (shown as a title block)")
60
69
  .option("--config <file>", "config file (default: nearest mdrender.config.json)")
@@ -80,6 +89,7 @@ function buildRenderProgram(io) {
80
89
  Commands:
81
90
  mdrender preview <file> live HTML preview in the browser (see: mdrender preview --help)
82
91
  mdrender setup download a headless Chrome if no browser is installed
92
+ mdrender doctor check the installation, browser and configuration
83
93
 
84
94
  Header/footer placeholders: {page} {pages} {title} {author} {date}; "|" separates
85
95
  left | center | right columns.
@@ -105,7 +115,11 @@ function cliOverrides(program) {
105
115
  if (program.getOptionValueSource(key) === "cli")
106
116
  raw[key] = program.getOptionValue(key);
107
117
  }
108
- return normalizeOptions(raw, "command line", process.cwd(), (key) => key === "pageNumbers" ? "--no-page-numbers" : toFlag(key));
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));
109
123
  }
110
124
  function settings(program, options) {
111
125
  return {
@@ -143,6 +157,32 @@ async function runSetup(argv, io) {
143
157
  return parsed;
144
158
  return setup({ io, force: Boolean(program.opts().force) });
145
159
  }
160
+ function buildDoctorProgram(io) {
161
+ const program = new Command()
162
+ .name("mdrender doctor")
163
+ .description("Check the installation, browser and configuration, and try a real conversion.\n" +
164
+ "Exits with 1 when a problem is found.")
165
+ .option("--browser <path>", "check this browser instead of the detected one")
166
+ .option("--config <file>", "check this config file (default: nearest mdrender.config.json)")
167
+ .option("--no-config", "ignore mdrender.config.json files")
168
+ .option("--quick", "skip starting the browser and the test conversion")
169
+ .option("--json", "print the report as JSON");
170
+ return configure(program, io);
171
+ }
172
+ async function runDoctorCommand(argv, io) {
173
+ const program = buildDoctorProgram(io);
174
+ const parsed = await parse(program, argv);
175
+ if (parsed !== undefined)
176
+ return parsed;
177
+ const options = program.opts();
178
+ const report = await runDoctor({
179
+ browser: options.browser,
180
+ config: options.config,
181
+ render: !options.quick,
182
+ });
183
+ io.out(options.json ? `${JSON.stringify(report, null, 2)}\n` : formatReport(report));
184
+ return report.ok ? ExitCode.Ok : ExitCode.Usage;
185
+ }
146
186
  async function runPreview(argv, io, signal) {
147
187
  const program = buildPreviewProgram(io);
148
188
  const parsed = await parse(program, argv);
@@ -192,6 +232,8 @@ export async function main(argv, io = defaultIo, { signal = new AbortController(
192
232
  return await runPreview(argv.slice(1), io, signal);
193
233
  if (argv[0] === "setup")
194
234
  return await runSetup(argv.slice(1), io);
235
+ if (argv[0] === "doctor")
236
+ return await runDoctorCommand(argv.slice(1), io);
195
237
  return await runRender(argv, io, signal);
196
238
  }
197
239
  catch (error) {
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): Promise<RenderedDocument>;
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
@@ -7,12 +7,13 @@ import { extractFrontMatter } from "./frontmatter.js";
7
7
  import { strings } from "./i18n.js";
8
8
  import { renderMarkdown } from "./markdown/index.js";
9
9
  import { buildToc, tocPlaceholder } from "./markdown/toc.js";
10
- import { mergeOptions, normalizeOptions, OptionsError, resolveMargins, } from "./options.js";
10
+ import { DEFAULT_FONT_SIZE, fontSizePx, mergeOptions, normalizeOptions, OptionsError, resolveMargins, } from "./options.js";
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 { contentWidthPx, lengthToPx } from "./render/page.js";
15
- import { buildHtmlDocument, builtinThemeCss } from "./render/template.js";
14
+ import { SMALL_SCALE } from "./render/mermaid-fit.js";
15
+ import { contentHeightPx, contentWidthPx, lengthToPx } from "./render/page.js";
16
+ import { buildHtmlDocument, builtinThemeCss, densityCss } from "./render/template.js";
16
17
  import { writeFileAtomic } from "./write.js";
17
18
  /** Starts one browser on first use and keeps it for later conversions. */
18
19
  export class BrowserPool {
@@ -78,6 +79,27 @@ export function headerFooterSpaceWarnings(margin, hasHeader, hasFooter) {
78
79
  }
79
80
  return warnings;
80
81
  }
82
+ /** Font size, if set, and the page's text height (used to fit images and diagrams). */
83
+ function layoutCss(options, margin) {
84
+ const height = contentHeightPx(options.pageSize, options.orientation === "landscape", margin);
85
+ const rules = [`:root { --page-content-height: ${height}px; }`];
86
+ if (options.fontSize)
87
+ rules.push(`html { font-size: ${options.fontSize}; }`);
88
+ return rules.join("\n");
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
+ }
81
103
  /** Mermaid theme: explicit choice, else dark diagrams for the dark page theme. */
82
104
  export function effectiveMermaidTheme(options) {
83
105
  return options.mermaidTheme ?? (options.theme === "dark" ? "dark" : "neutral");
@@ -86,7 +108,9 @@ export function effectiveMermaidTheme(options) {
86
108
  * Render a Markdown file to a complete HTML document. A browser is only started (through
87
109
  * `browsers`) when the document has Mermaid diagrams.
88
110
  */
89
- 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) {
90
114
  const input = path.resolve(inputPath);
91
115
  const baseDir = path.dirname(input);
92
116
  const source = await readFile(input, "utf8");
@@ -106,6 +130,7 @@ export async function renderDocument(inputPath, settings, browsers) {
106
130
  ...config.warnings.map((message) => ({ severity: "warning", message })),
107
131
  ...md.diagnostics,
108
132
  ];
133
+ const notes = [];
109
134
  const title = options.title ?? md.title ?? path.parse(input).name;
110
135
  // Table of contents: at the [[toc]] marker, or at the start with --toc.
111
136
  let body = md.html;
@@ -121,10 +146,37 @@ export async function renderDocument(inputPath, settings, browsers) {
121
146
  const session = await browsers.get();
122
147
  const width = contentWidthPx(options.pageSize, options.orientation === "landscape", margin);
123
148
  const theme = effectiveMermaidTheme(options);
124
- const results = await renderMermaid(session, md.mermaid, theme, width);
149
+ const fontPx = fontSizePx(options.fontSize ?? DEFAULT_FONT_SIZE);
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
+ });
125
162
  const filled = fillMermaid(body, md.nonce, md.mermaid, results);
126
163
  body = filled.html;
127
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
+ });
128
180
  }
129
181
  const html = buildHtmlDocument({
130
182
  title,
@@ -136,6 +188,7 @@ export async function renderDocument(inputPath, settings, browsers) {
136
188
  date: options.date,
137
189
  showTitle: options.title !== undefined,
138
190
  themeCss,
191
+ layoutCss: [...densityCss(options.density), layoutCss(options, margin)],
139
192
  extraCss,
140
193
  });
141
194
  diagnostics.sort((a, b) => (a.line ?? 0) - (b.line ?? 0));
@@ -144,14 +197,14 @@ export async function renderDocument(inputPath, settings, browsers) {
144
197
  ...(options.theme.endsWith(".css") ? [options.theme] : []),
145
198
  ...options.css,
146
199
  ];
147
- return { html, title, diagnostics, options, configFile, dependencies };
200
+ return { html, title, diagnostics, notes, options, configFile, dependencies };
148
201
  }
149
202
  /** Read a Markdown file and write it as a self-contained HTML document or a PDF. */
150
203
  export async function convertFile(convert) {
151
204
  const output = path.resolve(convert.output);
152
205
  const browsers = convert.browsers ?? new BrowserPool(convert.browser);
153
206
  try {
154
- const doc = await renderDocument(convert.input, convert, browsers);
207
+ const doc = await renderDocument(convert.input, convert, browsers, convert.format === "pdf");
155
208
  const { options } = doc;
156
209
  if (convert.format === "pdf") {
157
210
  const margin = resolveMargins(options);
@@ -185,6 +238,7 @@ export async function convertFile(convert) {
185
238
  return {
186
239
  output,
187
240
  diagnostics: doc.diagnostics,
241
+ notes: doc.notes,
188
242
  options,
189
243
  configFile: doc.configFile,
190
244
  dependencies: doc.dependencies,
@@ -0,0 +1,30 @@
1
+ export type CheckStatus = "ok" | "warn" | "fail" | "info";
2
+ export interface Check {
3
+ name: string;
4
+ status: CheckStatus;
5
+ detail: string;
6
+ /** What to do about a warning or failure. */
7
+ hint?: string;
8
+ }
9
+ export interface DoctorOptions {
10
+ /** Explicit browser executable (as with --browser). */
11
+ browser?: string;
12
+ /** Config file to check; undefined: nearest to `cwd`; false: none. */
13
+ config?: string | false;
14
+ /** Start the browser and convert a sample document (the slow part). Default: true. */
15
+ render?: boolean;
16
+ cwd?: string;
17
+ env?: Record<string, string | undefined>;
18
+ platform?: NodeJS.Platform;
19
+ }
20
+ export interface DoctorReport {
21
+ version: string;
22
+ checks: Check[];
23
+ ok: boolean;
24
+ }
25
+ /** Where `mdrender` resolves on the PATH, like `which`/`where`. */
26
+ export declare function findOnPath(env: Record<string, string | undefined>, platform?: NodeJS.Platform): string | undefined;
27
+ /** Inspect the installation, browser and configuration, and try a real conversion. */
28
+ export declare function runDoctor(options?: DoctorOptions): Promise<DoctorReport>;
29
+ /** Plain-text report; ASCII markers so it reads well in any Windows console. */
30
+ export declare function formatReport(report: DoctorReport): string;