@imontene/mdrender 2.1.0 → 2.3.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,29 @@
1
1
  # Cambios
2
2
 
3
+ ## 2.3.0 — 2026-10-16
4
+
5
+ - **Manrope es la nueva tipografía por defecto** del texto, el encabezado, el pie y los
6
+ diagramas Mermaid. `--font inter` (o `font: inter`) conserva Inter, la de versiones
7
+ anteriores. Manrope no tiene cursivas (el navegador inclina la normal) y toma de Inter los
8
+ símbolos que le faltan (`→`, `✓`, `≤`…).
9
+
10
+ ## 2.2.0 — 2026-10-15
11
+
12
+ - **Autoajuste de diagramas de flujo Mermaid** (`flowchart`/`graph`): cada diagrama se dibuja
13
+ con varios anchos de ajuste de etiquetas y se elige el que imprime el texto más grande (a igual
14
+ legibilidad, el más bajo), nunca más pequeño que tal como está escrito. Si sigue por debajo del
15
+ 75 % de su tamaño, se prueba el otro eje (`LR` → `TB`, `TD` → `LR`) y se gira si gana al menos
16
+ 15 puntos. En el banco de pruebas, la escala media del texto pasa de 0,80 a 0,98 y ningún
17
+ diagrama queda por debajo de 0,6.
18
+ - `--mermaid-flip never` (`mermaidFlip`) conserva la dirección; `--no-mermaid-autofit`
19
+ (`mermaidAutofit: false`) lo desactiva. Se respetan los diagramas con `%%{init}%%` o
20
+ `config:` propios.
21
+ - `-v` informa la elección por diagrama; aviso si un diagrama se imprime por debajo del 50 %.
22
+ - Un título seguido de un diagrama o una imagen ya no queda solo al pie de una página: pasa con
23
+ ellos a la siguiente.
24
+ - `npm run bench:mermaid [-- archivo.md]` compara los diagramas de un documento con y sin
25
+ autoajuste.
26
+
3
27
  ## 2.1.0 — 2026-10-15
4
28
 
5
29
  - `mdrender doctor`: revisa instalación (versión, Node.js, `PATH`), navegador (cuál y por qué,
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Convierte archivos Markdown (`.md`) en PDF y HTML desde la línea de comandos, en Windows y
4
4
  Linux**, con ecuaciones LaTeX, diagramas Mermaid y la tipografía
5
- [Inter](https://fonts.google.com/specimen/Inter).
5
+ [Manrope](https://fonts.google.com/specimen/Manrope) (o [Inter](https://fonts.google.com/specimen/Inter)).
6
6
 
7
7
  ```bash
8
8
  mdrender informe.md # → informe.pdf
@@ -15,7 +15,8 @@ Ejemplo: [`examples/ejemplo.md`](examples/ejemplo.md) → [`examples/ejemplo.pdf
15
15
  - 📄 **Markdown → PDF o HTML autocontenido** (fuentes, imágenes y diagramas incluidos).
16
16
  - ➗ **Ecuaciones** `$…$` y `$$…$$` con KaTeX, dibujadas al convertir (sin JavaScript en la salida).
17
17
  - 📊 **Diagramas Mermaid**: flujo, secuencia, clases, estados, ER, Gantt, torta, mapa mental…
18
- - 🔤 **Inter** para el texto y **JetBrains Mono** para el código, incrustadas en el PDF.
18
+ - 🔤 **Manrope** para el texto (o **Inter** con `--font inter`) y **JetBrains Mono** para el
19
+ código, incrustadas en el PDF.
19
20
  - 🧩 **GFM**: tablas, listas de tareas, tachado, notas al pie, alertas `> [!NOTE]`,
20
21
  resaltado de código para ~35 lenguajes.
21
22
  - 🖨️ Tamaño de página, márgenes, orientación, encabezado/pie con números de página,
@@ -134,37 +135,40 @@ equipo); `--port` fija el puerto y `--no-open` no abre el navegador.
134
135
 
135
136
  ### Opciones
136
137
 
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
- | `--lang <código>` | Idioma del documento; `en` para inglés (`pt`, `fr`, `de`…) | `es` |
160
- | `--title <texto>` | Título del documento (muestra un bloque de título) | primer `# título` |
161
- | `--config <archivo>` | Archivo de configuración | el más cercano |
162
- | `--no-config` | Ignorar `mdrender.config.json` | — |
163
- | `--browser <ruta>` | Ruta a Chrome/Edge/Chromium (o variable `MDRENDER_BROWSER`) | autodetección |
164
- | `--allow-remote` | Permitir que el PDF descargue imágenes/CSS de internet | bloqueado |
165
- | `-w, --watch` | Regenerar al guardar | — |
166
- | `-q, --quiet` | Mostrar solo advertencias y errores | — |
167
- | `-v, --verbose` | Mostrar también configuración, navegador y tiempos | — |
138
+ | Opción | Descripción | Por defecto |
139
+ | ------------------------------ | -------------------------------------------------------------------- | ------------------ |
140
+ | `-o, --output <ruta>` | Archivo de salida, o carpeta con varios archivos | junto al `.md` |
141
+ | `-f, --format <pdf\|html>` | Formato de salida | `pdf` |
142
+ | `--page-size <tamaño>` | `Letter`, `A4`, `Legal` (también `A3`, `A5`, `Tabloid`) | `Letter` |
143
+ | `--margin <valor>` | Las cuatro márgenes como en CSS: `10mm`, `15mm 20mm`… | `10mm` |
144
+ | `--margin-top <medida>` | Margen superior, p. ej. `15mm` (prevalece sobre `--margin`) | `10mm` |
145
+ | `--margin-right <medida>` | Margen derecho | `10mm` |
146
+ | `--margin-bottom <medida>` | Margen inferior | `10mm` |
147
+ | `--margin-left <medida>` | Margen izquierdo | `10mm` |
148
+ | `--orientation <orientación>` | `portrait` (vertical) o `landscape` (horizontal) | `portrait` |
149
+ | `--landscape` | Atajo de `--orientation landscape` | — |
150
+ | `--toc` | Tabla de contenidos al inicio (o donde pongas `[[toc]]`) | — |
151
+ | `--toc-depth <n>` | Nivel de título más profundo en la tabla de contenidos | `3` |
152
+ | `--header <texto>` | Encabezado de página (ver abajo) | — |
153
+ | `--footer <texto>` | Pie de página (ver abajo) | `{page} / {pages}` |
154
+ | `--no-page-numbers` | Sin el pie con números de página | — |
155
+ | `--font <manrope\|inter>` | Tipografía del texto (ver abajo) | `manrope` |
156
+ | `--density <densidad>` | `normal` o `compact` (menos espacio entre líneas y bloques) | `normal` |
157
+ | `--font-size <tamaño>` | Tamaño del texto en `pt` o `px`, de 6 a 24 pt; p. ej. `10pt` | `10.5pt` |
158
+ | `--theme <light\|dark\|css>` | `light`, `dark` o un `.css` que reemplaza el tema | `light` |
159
+ | `--css <archivo.css>` | CSS adicional (se puede repetir) | — |
160
+ | `--mermaid-theme <tema>` | `default`, `neutral`, `dark`, `forest`, `base` | `neutral` / `dark` |
161
+ | `--mermaid-flip <auto\|never>` | Si el autoajuste puede girar un diagrama de flujo de eje (ver abajo) | `auto` |
162
+ | `--no-mermaid-autofit` | Dibujar los diagramas de flujo tal como están escritos | — |
163
+ | `--lang <código>` | Idioma del documento; `en` para inglés (`pt`, `fr`, `de`…) | `es` |
164
+ | `--title <texto>` | Título del documento (muestra un bloque de título) | primer `# título` |
165
+ | `--config <archivo>` | Archivo de configuración | el más cercano |
166
+ | `--no-config` | Ignorar `mdrender.config.json` | — |
167
+ | `--browser <ruta>` | Ruta a Chrome/Edge/Chromium (o variable `MDRENDER_BROWSER`) | autodetección |
168
+ | `--allow-remote` | Permitir que el PDF descargue imágenes/CSS de internet | bloqueado |
169
+ | `-w, --watch` | Regenerar al guardar | — |
170
+ | `-q, --quiet` | Mostrar solo advertencias y errores | — |
171
+ | `-v, --verbose` | Mostrar también configuración, navegador y tiempos | — |
168
172
 
169
173
  **Página**: por defecto hoja **Letter vertical con márgenes de 10 mm**. Las medidas aceptan
170
174
  `mm`, `cm`, `in` o `px`. `--margin` fija las cuatro márgenes a la vez y `--margin-top`,
@@ -173,6 +177,13 @@ equipo); `--port` fija el puerto y `--no-open` no abre el navegador.
173
177
  El encabezado y el pie se dibujan dentro de las márgenes superior e inferior: con menos de
174
178
  10 mm pueden tocar el texto y mdrender lo advierte.
175
179
 
180
+ **Tipografía**: el texto usa [Manrope](https://fonts.google.com/specimen/Manrope);
181
+ `--font inter` (o `font: inter` en el front matter o en `mdrender.config.json`) vuelve a
182
+ [Inter](https://fonts.google.com/specimen/Inter), la tipografía de las versiones anteriores.
183
+ Ambas van incrustadas en el PDF, también en el encabezado, el pie y los diagramas. Manrope no
184
+ tiene cursivas (el navegador inclina la normal) ni algunos símbolos como `→`, `✓` o `≤`, que
185
+ se toman de Inter.
186
+
176
187
  **Aprovechar la página**: `--density compact` reduce interlineado, espacios entre párrafos y
177
188
  títulos, y `--font-size 10pt` achica el texto (los diagramas Mermaid siguen el mismo tamaño).
178
189
  Juntos ahorran entre un 15 y un 25 % de páginas. Además, siempre:
@@ -237,6 +248,25 @@ fecha). Las rutas de `css` y `theme` son relativas al archivo donde aparecen.
237
248
  - Si una fórmula o diagrama tiene un error, el documento se genera igual con una caja roja en
238
249
  su lugar, se informa `error: archivo.md:LÍNEA: mensaje` y el comando termina con código `2`.
239
250
 
251
+ **Autoajuste de diagramas de flujo**: en el PDF, un diagrama más ancho que la columna o más alto
252
+ que la página se reduce entero, y su texto puede quedar diminuto. Por eso mdrender dibuja cada
253
+ `flowchart`/`graph` con varios anchos de ajuste de las etiquetas y se queda con el que imprime el
254
+ texto más grande; a igual legibilidad, el que ocupa menos alto. Nunca lo imprime más pequeño que
255
+ tal como está escrito.
256
+
257
+ - Si el diagrama sigue por debajo del 75 % de su tamaño, prueba además el otro eje (`LR` → `TB`,
258
+ o `TD` → `LR`) y lo gira si gana al menos 15 puntos. Una cadena horizontal de 8 cajas, por
259
+ ejemplo, pasa de imprimirse al 37 % a su tamaño real. `--mermaid-flip never` (o
260
+ `mermaidFlip: never`) conserva siempre la dirección escrita.
261
+ - No se tocan los diagramas con configuración propia (`%%{init: …}%%` o `config:` en su front
262
+ matter) ni los de otros tipos (secuencia, torta, clases…).
263
+ - `-v` informa la elección de cada diagrama; si alguno queda por debajo del 50 %, mdrender avisa:
264
+ conviene partirlo o acortar sus etiquetas.
265
+ - `--no-mermaid-autofit` (o `mermaidAutofit: false`) lo desactiva.
266
+
267
+ Un título seguido de un diagrama o una imagen pasa con ellos a la página siguiente cuando no
268
+ caben, en lugar de quedar solo al pie de la anterior.
269
+
240
270
  ## Más Markdown
241
271
 
242
272
  - **Alertas** estilo GitHub: `> [!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]`,
@@ -297,6 +327,6 @@ cambios: [CHANGELOG.md](CHANGELOG.md).
297
327
 
298
328
  ## Licencia
299
329
 
300
- [MIT](LICENSE). Inter y JetBrains Mono se distribuyen bajo la
330
+ [MIT](LICENSE). Manrope, Inter y JetBrains Mono se distribuyen bajo la
301
331
  [SIL Open Font License 1.1](https://openfontlicense.org); los ejecutables incluyen
302
332
  `THIRD-PARTY-NOTICES.txt` con las licencias de todos los componentes.
@@ -1,7 +1,8 @@
1
- /* mdrender light theme — Inter for text, JetBrains Mono for code. */
1
+ /* mdrender light theme — Manrope (or Inter) for text, JetBrains Mono for code. */
2
2
 
3
3
  :root {
4
- --font-text: "Inter", system-ui, sans-serif;
4
+ /* mdrender sets both after the theme for the chosen --font (render/fonts.ts). */
5
+ --font-text: "Manrope", "Inter", system-ui, sans-serif;
5
6
  /* Inter after the mono font covers symbols (arrows, math) missing from the code font. */
6
7
  --font-code: "JetBrains Mono", "Inter", ui-monospace, monospace;
7
8
  --color-text: #1f2328;
@@ -352,6 +353,21 @@ div.math-error {
352
353
  .mermaid-diagram svg {
353
354
  max-height: calc(var(--page-content-height) - 6mm);
354
355
  }
356
+
357
+ /* Next to its heading: leave room for it (HEADING_ALLOWANCE_EM in render/mermaid.ts). */
358
+ .keep-with-heading img,
359
+ .keep-with-heading .mermaid-diagram svg {
360
+ max-height: calc(var(--page-content-height) - 6mm - 5rem);
361
+ }
362
+ }
363
+
364
+ /* A heading and the diagram or image right after it go to the same page. */
365
+ .keep-with-heading {
366
+ break-inside: avoid;
367
+ }
368
+
369
+ .markdown-body > .keep-with-heading:first-child > :first-child {
370
+ margin-top: 0;
355
371
  }
356
372
 
357
373
  /* Mermaid's HTML labels are <p>/<span> inside <foreignObject>: keep document styles out. */
package/dist/assets.d.ts CHANGED
@@ -31,6 +31,8 @@ export declare const INTER_FACES: readonly [{
31
31
  readonly style: "italic";
32
32
  readonly file: "Inter-BoldItalic.woff2";
33
33
  }];
34
+ /** Manrope faces in use (Fontsource, split into unicode-range subsets); it has no italics. */
35
+ export declare const MANROPE_STYLESHEETS: string[];
34
36
  export declare const MONO_STYLESHEETS: string[];
35
37
  export declare const KATEX_CSS = "katex/dist/katex.min.css";
36
38
  export declare const MERMAID_JS = "mermaid/dist/mermaid.min.js";
package/dist/assets.js CHANGED
@@ -78,6 +78,12 @@ export const INTER_FACES = [
78
78
  { weight: 700, style: "normal", file: "Inter-Bold.woff2" },
79
79
  { weight: 700, style: "italic", file: "Inter-BoldItalic.woff2" },
80
80
  ];
81
+ /** Manrope faces in use (Fontsource, split into unicode-range subsets); it has no italics. */
82
+ export const MANROPE_STYLESHEETS = [
83
+ "@fontsource/manrope/400.css",
84
+ "@fontsource/manrope/600.css",
85
+ "@fontsource/manrope/700.css",
86
+ ];
81
87
  export const MONO_STYLESHEETS = [
82
88
  "@fontsource/jetbrains-mono/400.css",
83
89
  "@fontsource/jetbrains-mono/400-italic.css",
@@ -103,7 +109,7 @@ export function assetManifest() {
103
109
  const keys = new Set([PACKAGE_JSON, ...THEMES, MERMAID_JS]);
104
110
  for (const { file } of INTER_FACES)
105
111
  keys.add(`${INTER_DIR}/${file}`);
106
- for (const cssKey of [...MONO_STYLESHEETS, KATEX_CSS]) {
112
+ for (const cssKey of [...MANROPE_STYLESHEETS, ...MONO_STYLESHEETS, KATEX_CSS]) {
107
113
  keys.add(cssKey);
108
114
  for (const font of woff2References(cssKey, readTextAsset(cssKey)))
109
115
  keys.add(font);
package/dist/cli.js CHANGED
@@ -25,11 +25,14 @@ const DOCUMENT_FLAGS = [
25
25
  "header",
26
26
  "footer",
27
27
  "pageNumbers",
28
+ "font",
28
29
  "density",
29
30
  "fontSize",
30
31
  "css",
31
32
  "theme",
32
33
  "mermaidTheme",
34
+ "mermaidAutofit",
35
+ "mermaidFlip",
33
36
  "lang",
34
37
  "title",
35
38
  ];
@@ -55,11 +58,14 @@ function addDocumentOptions(command) {
55
58
  .option("--header <text>", 'page header, e.g. "{title} | | {date}"')
56
59
  .option("--footer <text>", 'page footer (default: "{page} / {pages}")')
57
60
  .option("--no-page-numbers", "no default page-number footer")
61
+ .option("--font <font>", "text typeface: manrope or inter (default: manrope)")
58
62
  .option("--density <density>", "normal or compact (tighter spacing) (default: normal)")
59
63
  .option("--font-size <size>", "text size, e.g. 10pt or 11pt (default: 10.5pt)")
60
64
  .option("--theme <theme>", "light, dark, or a .css file replacing the theme (default: light)")
61
65
  .option("--css <file>", "extra stylesheet (repeatable)", collect)
62
66
  .option("--mermaid-theme <theme>", `Mermaid theme: ${MERMAID_THEMES.join(", ")} (default: neutral; dark with --theme dark)`)
67
+ .option("--no-mermaid-autofit", "draw flowcharts exactly as written (no layout search)")
68
+ .option("--mermaid-flip <mode>", "auto: turn a flowchart to the other axis if its text prints too small; never (default: auto)")
63
69
  .option("--lang <tag>", "document language, e.g. es or en (default: es)")
64
70
  .option("--title <text>", "document title (shown as a title block)")
65
71
  .option("--config <file>", "config file (default: nearest mdrender.config.json)")
@@ -73,7 +79,7 @@ function configure(command, io) {
73
79
  function buildRenderProgram(io) {
74
80
  const program = new Command()
75
81
  .name("mdrender")
76
- .description("Render Markdown files to PDF (or self-contained HTML) using the Inter typeface.")
82
+ .description("Render Markdown files to PDF (or self-contained HTML) set in Manrope (or Inter).")
77
83
  .version(version, "-V, --version")
78
84
  .argument("<inputs...>", "Markdown files, folders or glob patterns (e.g. docs/**/*.md)")
79
85
  .option("-o, --output <path>", "output file, or folder for several inputs")
@@ -111,7 +117,11 @@ function cliOverrides(program) {
111
117
  if (program.getOptionValueSource(key) === "cli")
112
118
  raw[key] = program.getOptionValue(key);
113
119
  }
114
- return normalizeOptions(raw, "command line", process.cwd(), (key) => key === "pageNumbers" ? "--no-page-numbers" : toFlag(key));
120
+ return normalizeOptions(raw, "command line", process.cwd(), (key) => key === "pageNumbers"
121
+ ? "--no-page-numbers"
122
+ : key === "mermaidAutofit"
123
+ ? "--no-mermaid-autofit"
124
+ : toFlag(key));
115
125
  }
116
126
  function settings(program, options) {
117
127
  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): 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
@@ -11,6 +11,8 @@ 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";
15
+ import { FONT_FAMILIES, fontStack } from "./render/fonts.js";
14
16
  import { contentHeightPx, contentWidthPx, lengthToPx } from "./render/page.js";
15
17
  import { buildHtmlDocument, builtinThemeCss, densityCss } from "./render/template.js";
16
18
  import { writeFileAtomic } from "./write.js";
@@ -81,11 +83,33 @@ export function headerFooterSpaceWarnings(margin, hasHeader, hasFooter) {
81
83
  /** Font size, if set, and the page's text height (used to fit images and diagrams). */
82
84
  function layoutCss(options, margin) {
83
85
  const height = contentHeightPx(options.pageSize, options.orientation === "landscape", margin);
84
- const rules = [`:root { --page-content-height: ${height}px; }`];
86
+ const family = FONT_FAMILIES[options.font];
87
+ const rules = [
88
+ `:root { --page-content-height: ${height}px; }`,
89
+ `:root { --font-text: ${fontStack(options.font)}; ` +
90
+ `--font-code: "JetBrains Mono", "${family}", "Inter", ui-monospace, monospace; }`,
91
+ ];
92
+ // Manrope's glyphs are taller than the headings' 1.25 line height (about 1.37 em). When a
93
+ // heading is pushed to the next page, Chrome then repeats its text in the PDF bookmark.
94
+ if (options.font === "manrope")
95
+ rules.push("h1, h2, h3, h4, h5, h6 { line-height: 1.4; }");
85
96
  if (options.fontSize)
86
97
  rules.push(`html { font-size: ${options.fontSize}; }`);
87
98
  return rules.join("\n");
88
99
  }
100
+ const fixed2 = (n) => n.toFixed(2);
101
+ /** "Mermaid autofit: wrap 300, turned LR→TB, scale 0.33 → 1.00, height 81 → 903 px". */
102
+ export function describeLayout(layout) {
103
+ const changes = [
104
+ layout.wrap === undefined ? undefined : `wrap ${layout.wrap}`,
105
+ layout.turned ? `turned ${layout.turned}` : undefined,
106
+ ].filter(Boolean);
107
+ const scale = `scale ${fixed2(layout.baseScale)} → ${fixed2(layout.scale)}`;
108
+ const height = `height ${layout.baseHeight} → ${layout.height} px`;
109
+ return changes.length === 0
110
+ ? `Mermaid autofit: kept as written (scale ${fixed2(layout.scale)}, ${layout.tried} layouts tried)`
111
+ : `Mermaid autofit: ${changes.join(", ")}, ${scale}, ${height} (${layout.tried} layouts tried)`;
112
+ }
89
113
  /** Mermaid theme: explicit choice, else dark diagrams for the dark page theme. */
90
114
  export function effectiveMermaidTheme(options) {
91
115
  return options.mermaidTheme ?? (options.theme === "dark" ? "dark" : "neutral");
@@ -94,7 +118,9 @@ export function effectiveMermaidTheme(options) {
94
118
  * Render a Markdown file to a complete HTML document. A browser is only started (through
95
119
  * `browsers`) when the document has Mermaid diagrams.
96
120
  */
97
- export async function renderDocument(inputPath, settings, browsers) {
121
+ export async function renderDocument(inputPath, settings, browsers,
122
+ /** Printed on pages (PDF): diagrams are fitted to the page height, not only the width. */
123
+ paged = false) {
98
124
  const input = path.resolve(inputPath);
99
125
  const baseDir = path.dirname(input);
100
126
  const source = await readFile(input, "utf8");
@@ -114,6 +140,7 @@ export async function renderDocument(inputPath, settings, browsers) {
114
140
  ...config.warnings.map((message) => ({ severity: "warning", message })),
115
141
  ...md.diagnostics,
116
142
  ];
143
+ const notes = [];
117
144
  const title = options.title ?? md.title ?? path.parse(input).name;
118
145
  // Table of contents: at the [[toc]] marker, or at the start with --toc.
119
146
  let body = md.html;
@@ -130,10 +157,37 @@ export async function renderDocument(inputPath, settings, browsers) {
130
157
  const width = contentWidthPx(options.pageSize, options.orientation === "landscape", margin);
131
158
  const theme = effectiveMermaidTheme(options);
132
159
  const fontPx = fontSizePx(options.fontSize ?? DEFAULT_FONT_SIZE);
133
- const results = await renderMermaid(session, md.mermaid, theme, width, fontPx);
160
+ const landscape = options.orientation === "landscape";
161
+ const results = await renderMermaid(session, md.mermaid, {
162
+ theme,
163
+ widthPx: width,
164
+ fontPx,
165
+ // Same limit as the print stylesheet: the page's text height minus 6 mm.
166
+ pageHeightPx: paged
167
+ ? contentHeightPx(options.pageSize, landscape, margin) - lengthToPx("6mm")
168
+ : Infinity,
169
+ autofit: options.mermaidAutofit,
170
+ flip: options.mermaidFlip,
171
+ font: options.font,
172
+ });
134
173
  const filled = fillMermaid(body, md.nonce, md.mermaid, results);
135
174
  body = filled.html;
136
175
  diagnostics.push(...filled.diagnostics);
176
+ md.mermaid.forEach((block, i) => {
177
+ const result = results[i];
178
+ if (!result || "error" in result)
179
+ return;
180
+ if (result.layout)
181
+ notes.push({ line: block.line, message: describeLayout(result.layout) });
182
+ if (paged && result.scale !== undefined && result.scale < SMALL_SCALE) {
183
+ diagnostics.push({
184
+ severity: "warning",
185
+ line: block.line,
186
+ message: `Mermaid diagram printed at ${Math.round(result.scale * 100)}% of its size; ` +
187
+ "split it or shorten its labels to make it readable",
188
+ });
189
+ }
190
+ });
137
191
  }
138
192
  const html = buildHtmlDocument({
139
193
  title,
@@ -145,6 +199,7 @@ export async function renderDocument(inputPath, settings, browsers) {
145
199
  date: options.date,
146
200
  showTitle: options.title !== undefined,
147
201
  themeCss,
202
+ font: options.font,
148
203
  layoutCss: [...densityCss(options.density), layoutCss(options, margin)],
149
204
  extraCss,
150
205
  });
@@ -154,14 +209,14 @@ export async function renderDocument(inputPath, settings, browsers) {
154
209
  ...(options.theme.endsWith(".css") ? [options.theme] : []),
155
210
  ...options.css,
156
211
  ];
157
- return { html, title, diagnostics, options, configFile, dependencies };
212
+ return { html, title, diagnostics, notes, options, configFile, dependencies };
158
213
  }
159
214
  /** Read a Markdown file and write it as a self-contained HTML document or a PDF. */
160
215
  export async function convertFile(convert) {
161
216
  const output = path.resolve(convert.output);
162
217
  const browsers = convert.browsers ?? new BrowserPool(convert.browser);
163
218
  try {
164
- const doc = await renderDocument(convert.input, convert, browsers);
219
+ const doc = await renderDocument(convert.input, convert, browsers, convert.format === "pdf");
165
220
  const { options } = doc;
166
221
  if (convert.format === "pdf") {
167
222
  const margin = resolveMargins(options);
@@ -175,9 +230,11 @@ export async function convertFile(convert) {
175
230
  landscape: options.orientation === "landscape",
176
231
  margin,
177
232
  headerTemplate: hasHeaderFooter
178
- ? headerFooterTemplate(options.header, values, margin)
233
+ ? headerFooterTemplate(options.header, values, margin, options.font)
234
+ : undefined,
235
+ footerTemplate: hasHeaderFooter
236
+ ? headerFooterTemplate(footer, values, margin, options.font)
179
237
  : undefined,
180
- footerTemplate: hasHeaderFooter ? headerFooterTemplate(footer, values, margin) : undefined,
181
238
  allowRemote: convert.allowRemote,
182
239
  });
183
240
  for (const url of blocked) {
@@ -195,6 +252,7 @@ export async function convertFile(convert) {
195
252
  return {
196
253
  output,
197
254
  diagnostics: doc.diagnostics,
255
+ notes: doc.notes,
198
256
  options,
199
257
  configFile: doc.configFile,
200
258
  dependencies: doc.dependencies,
package/dist/doctor.js CHANGED
@@ -135,8 +135,9 @@ function settingsCheck(options) {
135
135
  name: "Settings",
136
136
  status: "info",
137
137
  detail: `${options.pageSize} ${options.orientation}, margins ${m.top} ${m.right} ${m.bottom} ` +
138
- `${m.left} (top right bottom left), ${options.density} density, ` +
139
- `${options.fontSize ?? DEFAULT_FONT_SIZE} text, language ${options.lang}, theme ` +
138
+ `${m.left} (top right bottom left), ${options.font} ` +
139
+ `${options.fontSize ?? DEFAULT_FONT_SIZE}, ${options.density} density, ` +
140
+ `language ${options.lang}, theme ` +
140
141
  path.basename(options.theme),
141
142
  };
142
143
  }
@@ -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 {
@@ -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 (`![alt](img.png)`). */
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
@@ -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 index = env.mermaid.push({ source: token.content, line: tokenLine(token) }) - 1;
18
+ const block = { source: token.content, line: tokenLine(token) };
19
+ const afterHeading = token.meta?.afterHeading;
20
+ const index = env.mermaid.push(afterHeading ? { ...block, afterHeading } : block) - 1;
19
21
  return `<figure class="mermaid-diagram">${mermaidPlaceholder(env.nonce, index)}</figure>\n`;
20
22
  };
21
23
  }
package/dist/options.d.ts CHANGED
@@ -11,7 +11,11 @@ export type Margins = {
11
11
  };
12
12
  export declare const MERMAID_THEMES: readonly ["default", "neutral", "dark", "forest", "base"];
13
13
  export type MermaidTheme = (typeof MERMAID_THEMES)[number];
14
+ export declare const MERMAID_FLIPS: readonly ["auto", "never"];
15
+ export type MermaidFlip = (typeof MERMAID_FLIPS)[number];
14
16
  export declare const BUILTIN_THEMES: readonly ["light", "dark"];
17
+ export declare const TEXT_FONTS: readonly ["manrope", "inter"];
18
+ export type TextFont = (typeof TEXT_FONTS)[number];
15
19
  export declare const DENSITIES: readonly ["normal", "compact"];
16
20
  export type Density = (typeof DENSITIES)[number];
17
21
  /** Rendering options, merged from defaults < config file < front matter < command line. */
@@ -35,6 +39,12 @@ export interface DocumentOptions {
35
39
  footer?: string;
36
40
  /** Mermaid theme; when unset, "neutral" for light pages and "dark" for the dark theme. */
37
41
  mermaidTheme?: MermaidTheme;
42
+ /** Try several layouts of each flowchart and keep the one whose text prints largest. */
43
+ mermaidAutofit: boolean;
44
+ /** Whether autofit may turn a flowchart to the other axis (LR ↔ TB) when it reads far better. */
45
+ mermaidFlip: MermaidFlip;
46
+ /** Typeface of the text: Manrope or Inter (code is always JetBrains Mono). */
47
+ font: TextFont;
38
48
  /** Vertical spacing: "compact" tightens line height, paragraph and heading spacing. */
39
49
  density: Density;
40
50
  /** Base font size of the text, e.g. "10pt"; when unset, the theme's (10.5pt). */