@imontene/mdrender 1.0.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/README.md +88 -29
- package/dist/assets/density-compact.css +74 -0
- package/dist/assets/theme.css +19 -0
- package/dist/assets.js +5 -1
- package/dist/cli.js +48 -3
- package/dist/config.d.ts +7 -0
- package/dist/config.js +11 -2
- package/dist/convert.d.ts +7 -1
- package/dist/convert.js +57 -16
- package/dist/doctor.d.ts +30 -0
- package/dist/doctor.js +278 -0
- package/dist/inputs.js +15 -2
- package/dist/markdown/code-blocks.d.ts +9 -0
- package/dist/markdown/code-blocks.js +21 -0
- package/dist/markdown/index.js +3 -0
- package/dist/options.d.ts +44 -8
- package/dist/options.js +107 -5
- package/dist/render/browser.d.ts +4 -1
- package/dist/render/browser.js +8 -4
- package/dist/render/mermaid.d.ts +3 -1
- package/dist/render/mermaid.js +12 -4
- package/dist/render/page.d.ts +5 -0
- package/dist/render/page.js +6 -0
- package/dist/render/template.d.ts +5 -1
- package/dist/render/template.js +12 -2
- package/dist/write.d.ts +9 -0
- package/dist/write.js +41 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Cambios
|
|
2
2
|
|
|
3
|
+
## 2.1.0 — 2026-10-15
|
|
4
|
+
|
|
5
|
+
- `mdrender doctor`: revisa instalación (versión, Node.js, `PATH`), navegador (cuál y por qué,
|
|
6
|
+
arranque y versión, sandbox), una conversión real de prueba, el `mdrender.config.json`
|
|
7
|
+
aplicado (errores, opciones mal escritas, valores efectivos), la caché de `setup` y el proxy.
|
|
8
|
+
`--quick`, `--json`; código 1 si encuentra un problema.
|
|
9
|
+
- Mejor uso de la página:
|
|
10
|
+
- Los bloques de código largos se parten entre páginas (al menos 4 líneas a cada lado) en
|
|
11
|
+
vez de saltar enteros y dejar hasta media página en blanco; los de hasta 12 líneas siguen
|
|
12
|
+
juntos.
|
|
13
|
+
- `--density compact` (o `density: compact`): interlineado y espacios más ajustados.
|
|
14
|
+
- `--font-size <tamaño>` (o `fontSize`): tamaño del texto, por defecto 10.5pt.
|
|
15
|
+
- Mermaid usa el tamaño de letra del documento (antes 16 px) y los diagramas de secuencia no
|
|
16
|
+
repiten los actores al pie.
|
|
17
|
+
- Imágenes y diagramas más altos que la página se reducen para caber en una sola.
|
|
18
|
+
|
|
19
|
+
## 2.0.0 — 2026-10-13
|
|
20
|
+
|
|
21
|
+
- Márgenes por lado: `--margin-top`, `--margin-right`, `--margin-bottom`, `--margin-left`
|
|
22
|
+
(también `marginTop`… en front matter y `mdrender.config.json`), por encima de `--margin`.
|
|
23
|
+
- Orientación con `--orientation portrait|landscape` (`--landscape` sigue funcionando).
|
|
24
|
+
- **Cambian los valores por defecto**: hoja **Letter** (antes A4) y márgenes de **10 mm**
|
|
25
|
+
(antes 20 mm). Para el formato anterior: `--page-size A4 --margin 20mm`, o ponerlo en
|
|
26
|
+
`mdrender.config.json`.
|
|
27
|
+
- El proceso de publicación falla al inicio, con un error claro, si falta `NPM_TOKEN`.
|
|
28
|
+
- Nunca sobrescribe el `.md` de entrada: `mdrender doc.md -o doc.md` termina con un error.
|
|
29
|
+
- Avisa de opciones desconocidas en `mdrender.config.json` y sugiere la correcta
|
|
30
|
+
(`"margn"` → `"margin"`).
|
|
31
|
+
- Escritura segura: el PDF/HTML se escribe en un archivo temporal y se renombra al final; un
|
|
32
|
+
fallo no deja archivos a medias y conserva la versión anterior. Mensaje claro si el archivo
|
|
33
|
+
está abierto en otro programa.
|
|
34
|
+
- Avisa si un encabezado o pie no cabe (margen superior o inferior menor de 10 mm).
|
|
35
|
+
|
|
3
36
|
## 1.0.1 — 2026-10-12
|
|
4
37
|
|
|
5
38
|
- Corrige un error EPIPE sin capturar cuando la salida se corta con una tubería
|
package/README.md
CHANGED
|
@@ -73,7 +73,7 @@ npm link # deja disponible el comando `mdrender`
|
|
|
73
73
|
mdrender documento.md
|
|
74
74
|
|
|
75
75
|
# Elegir salida, tamaño de página y tabla de contenidos
|
|
76
|
-
mdrender documento.md -o salida/informe.pdf --page-size
|
|
76
|
+
mdrender documento.md -o salida/informe.pdf --page-size A4 --toc
|
|
77
77
|
|
|
78
78
|
# HTML autocontenido
|
|
79
79
|
mdrender documento.md --format html
|
|
@@ -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
|
|
98
104
|
```
|
|
99
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
|
|
120
|
+
```
|
|
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,30 +134,54 @@ equipo); `--port` fija el puerto y `--no-open` no abre el navegador.
|
|
|
109
134
|
|
|
110
135
|
### Opciones
|
|
111
136
|
|
|
112
|
-
| Opción
|
|
113
|
-
|
|
|
114
|
-
| `-o, --output <ruta>`
|
|
115
|
-
| `-f, --format <pdf\|html>`
|
|
116
|
-
| `--page-size <tamaño>`
|
|
117
|
-
| `--margin <valor>`
|
|
118
|
-
| `--
|
|
119
|
-
| `--
|
|
120
|
-
| `--
|
|
121
|
-
| `--
|
|
122
|
-
| `--
|
|
123
|
-
| `--
|
|
124
|
-
| `--
|
|
125
|
-
| `--
|
|
126
|
-
| `--
|
|
127
|
-
| `--
|
|
128
|
-
| `--
|
|
129
|
-
| `--
|
|
130
|
-
| `--
|
|
131
|
-
| `--
|
|
132
|
-
| `--
|
|
133
|
-
|
|
|
134
|
-
|
|
|
135
|
-
|
|
|
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 | — |
|
|
168
|
+
|
|
169
|
+
**Página**: por defecto hoja **Letter vertical con márgenes de 10 mm**. Las medidas aceptan
|
|
170
|
+
`mm`, `cm`, `in` o `px`. `--margin` fija las cuatro márgenes a la vez y `--margin-top`,
|
|
171
|
+
`--margin-right`, `--margin-bottom` y `--margin-left` ajustan una sola, por encima de
|
|
172
|
+
`--margin`. Ejemplo: `--page-size A4 --orientation landscape --margin 15mm --margin-left 25mm`.
|
|
173
|
+
El encabezado y el pie se dibujan dentro de las márgenes superior e inferior: con menos de
|
|
174
|
+
10 mm pueden tocar el texto y mdrender lo advierte.
|
|
175
|
+
|
|
176
|
+
**Aprovechar la página**: `--density compact` reduce interlineado, espacios entre párrafos y
|
|
177
|
+
títulos, y `--font-size 10pt` achica el texto (los diagramas Mermaid siguen el mismo tamaño).
|
|
178
|
+
Juntos ahorran entre un 15 y un 25 % de páginas. Además, siempre:
|
|
179
|
+
|
|
180
|
+
- Los bloques de código largos continúan en la página siguiente (con al menos 4 líneas a cada
|
|
181
|
+
lado del corte) en lugar de saltar enteros y dejar un hueco; los de hasta 12 líneas no se
|
|
182
|
+
parten.
|
|
183
|
+
- Las imágenes y diagramas más altos que la página se reducen para caber en una sola.
|
|
184
|
+
- Los diagramas de secuencia no repiten los actores al pie.
|
|
136
185
|
|
|
137
186
|
**Encabezado y pie**: texto con marcadores `{page}`, `{pages}`, `{title}`, `{author}` y
|
|
138
187
|
`{date}`; `|` separa columnas izquierda, centro y derecha. Ejemplos:
|
|
@@ -151,7 +200,9 @@ subtitle: Resultados del trimestre
|
|
|
151
200
|
author: Ana Pérez
|
|
152
201
|
date: 2026-10-04
|
|
153
202
|
toc: true
|
|
154
|
-
page-size:
|
|
203
|
+
page-size: A4
|
|
204
|
+
orientation: landscape
|
|
205
|
+
margin-left: 25mm
|
|
155
206
|
header: "{title} | | {date}"
|
|
156
207
|
---
|
|
157
208
|
```
|
|
@@ -159,8 +210,11 @@ header: "{title} | | {date}"
|
|
|
159
210
|
```json
|
|
160
211
|
{
|
|
161
212
|
"lang": "es",
|
|
162
|
-
"pageSize": "
|
|
163
|
-
"margin": "
|
|
213
|
+
"pageSize": "Letter",
|
|
214
|
+
"margin": "15mm",
|
|
215
|
+
"marginLeft": "25mm",
|
|
216
|
+
"density": "compact",
|
|
217
|
+
"fontSize": "10pt",
|
|
164
218
|
"css": "estilos/empresa.css"
|
|
165
219
|
}
|
|
166
220
|
```
|
|
@@ -199,6 +253,10 @@ fecha). Las rutas de `css` y `theme` son relativas al archivo donde aparecen.
|
|
|
199
253
|
como imágenes referenciadas desde el `.md`.
|
|
200
254
|
- Mermaid corre en una página aparte, sin contenido del usuario, con `securityLevel: "strict"`.
|
|
201
255
|
- La vista previa solo escucha en `127.0.0.1`.
|
|
256
|
+
- Nunca sobrescribe un `.md` de entrada, y escribe cada PDF/HTML en un archivo temporal que
|
|
257
|
+
renombra al final: si algo falla, el archivo anterior queda intacto.
|
|
258
|
+
- Las opciones mal escritas en `mdrender.config.json` generan una advertencia con la opción
|
|
259
|
+
correcta sugerida.
|
|
202
260
|
- En Ubuntu 23.10+ el navegador descargado con `mdrender setup` no puede usar el sandbox de
|
|
203
261
|
Chrome (restricción de AppArmor); mdrender lo inicia entonces sin sandbox, lo que es seguro
|
|
204
262
|
aquí porque las páginas no ejecutan JavaScript del documento ni acceden a la red
|
|
@@ -231,7 +289,8 @@ El CI (GitHub Actions) prueba en **Windows y Linux** con Node 22 y 24, construye
|
|
|
231
289
|
ejecutable y valida `mdrender setup`. Para publicar una versión, sube una etiqueta `v*` o
|
|
232
290
|
lanza a mano **Actions → Release → Run workflow** con la versión (debe coincidir con
|
|
233
291
|
`package.json`): construye los ejecutables, crea la etiqueta y el GitHub Release con sus sumas
|
|
234
|
-
SHA-256, y publica en npm
|
|
292
|
+
SHA-256, y publica en npm. Requiere el secreto de repositorio `NPM_TOKEN`; si falta, la
|
|
293
|
+
publicación se detiene al inicio, antes de publicar nada.
|
|
235
294
|
|
|
236
295
|
Plan completo y decisiones de diseño: [docs/PLAN_MAESTRO.md](docs/PLAN_MAESTRO.md) ·
|
|
237
296
|
cambios: [CHANGELOG.md](CHANGELOG.md).
|
|
@@ -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
|
+
}
|
package/dist/assets/theme.css
CHANGED
|
@@ -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,17 @@ 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
|
+
|
|
338
357
|
/* Mermaid's HTML labels are <p>/<span> inside <foreignObject>: keep document styles out. */
|
|
339
358
|
.mermaid-diagram foreignObject p {
|
|
340
359
|
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 = [
|
|
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";
|
|
@@ -12,12 +13,20 @@ import { watchAndConvert } from "./watch.js";
|
|
|
12
13
|
const DOCUMENT_FLAGS = [
|
|
13
14
|
"pageSize",
|
|
14
15
|
"margin",
|
|
16
|
+
"marginTop",
|
|
17
|
+
"marginRight",
|
|
18
|
+
"marginBottom",
|
|
19
|
+
"marginLeft",
|
|
20
|
+
// --landscape before --orientation so an explicit --orientation wins.
|
|
15
21
|
"landscape",
|
|
22
|
+
"orientation",
|
|
16
23
|
"toc",
|
|
17
24
|
"tocDepth",
|
|
18
25
|
"header",
|
|
19
26
|
"footer",
|
|
20
27
|
"pageNumbers",
|
|
28
|
+
"density",
|
|
29
|
+
"fontSize",
|
|
21
30
|
"css",
|
|
22
31
|
"theme",
|
|
23
32
|
"mermaidTheme",
|
|
@@ -33,14 +42,21 @@ const collect = (value, previous = []) => [...previous, value];
|
|
|
33
42
|
/** Options shared by `mdrender` (render) and `mdrender preview`. */
|
|
34
43
|
function addDocumentOptions(command) {
|
|
35
44
|
return command
|
|
36
|
-
.option("--page-size <size>", `paper size: ${PAGE_SIZES.join(", ")} (default:
|
|
37
|
-
.option("--
|
|
38
|
-
.option("--landscape", "
|
|
45
|
+
.option("--page-size <size>", `paper size: ${PAGE_SIZES.join(", ")} (default: Letter)`)
|
|
46
|
+
.option("--orientation <orientation>", "portrait or landscape (default: portrait)")
|
|
47
|
+
.option("--landscape", "same as --orientation landscape")
|
|
48
|
+
.option("--margin <margin>", 'all page margins, CSS-style: "10mm", "15mm 20mm"… (default: 10mm)')
|
|
49
|
+
.option("--margin-top <length>", "top margin, e.g. 15mm (overrides --margin)")
|
|
50
|
+
.option("--margin-right <length>", "right margin")
|
|
51
|
+
.option("--margin-bottom <length>", "bottom margin")
|
|
52
|
+
.option("--margin-left <length>", "left margin")
|
|
39
53
|
.option("--toc", "insert a table of contents (or place it with [[toc]])")
|
|
40
54
|
.option("--toc-depth <n>", "deepest heading level in the table of contents (default: 3)")
|
|
41
55
|
.option("--header <text>", 'page header, e.g. "{title} | | {date}"')
|
|
42
56
|
.option("--footer <text>", 'page footer (default: "{page} / {pages}")')
|
|
43
57
|
.option("--no-page-numbers", "no default page-number footer")
|
|
58
|
+
.option("--density <density>", "normal or compact (tighter spacing) (default: normal)")
|
|
59
|
+
.option("--font-size <size>", "text size, e.g. 10pt or 11pt (default: 10.5pt)")
|
|
44
60
|
.option("--theme <theme>", "light, dark, or a .css file replacing the theme (default: light)")
|
|
45
61
|
.option("--css <file>", "extra stylesheet (repeatable)", collect)
|
|
46
62
|
.option("--mermaid-theme <theme>", `Mermaid theme: ${MERMAID_THEMES.join(", ")} (default: neutral; dark with --theme dark)`)
|
|
@@ -69,6 +85,7 @@ function buildRenderProgram(io) {
|
|
|
69
85
|
Commands:
|
|
70
86
|
mdrender preview <file> live HTML preview in the browser (see: mdrender preview --help)
|
|
71
87
|
mdrender setup download a headless Chrome if no browser is installed
|
|
88
|
+
mdrender doctor check the installation, browser and configuration
|
|
72
89
|
|
|
73
90
|
Header/footer placeholders: {page} {pages} {title} {author} {date}; "|" separates
|
|
74
91
|
left | center | right columns.
|
|
@@ -132,6 +149,32 @@ async function runSetup(argv, io) {
|
|
|
132
149
|
return parsed;
|
|
133
150
|
return setup({ io, force: Boolean(program.opts().force) });
|
|
134
151
|
}
|
|
152
|
+
function buildDoctorProgram(io) {
|
|
153
|
+
const program = new Command()
|
|
154
|
+
.name("mdrender doctor")
|
|
155
|
+
.description("Check the installation, browser and configuration, and try a real conversion.\n" +
|
|
156
|
+
"Exits with 1 when a problem is found.")
|
|
157
|
+
.option("--browser <path>", "check this browser instead of the detected one")
|
|
158
|
+
.option("--config <file>", "check this config file (default: nearest mdrender.config.json)")
|
|
159
|
+
.option("--no-config", "ignore mdrender.config.json files")
|
|
160
|
+
.option("--quick", "skip starting the browser and the test conversion")
|
|
161
|
+
.option("--json", "print the report as JSON");
|
|
162
|
+
return configure(program, io);
|
|
163
|
+
}
|
|
164
|
+
async function runDoctorCommand(argv, io) {
|
|
165
|
+
const program = buildDoctorProgram(io);
|
|
166
|
+
const parsed = await parse(program, argv);
|
|
167
|
+
if (parsed !== undefined)
|
|
168
|
+
return parsed;
|
|
169
|
+
const options = program.opts();
|
|
170
|
+
const report = await runDoctor({
|
|
171
|
+
browser: options.browser,
|
|
172
|
+
config: options.config,
|
|
173
|
+
render: !options.quick,
|
|
174
|
+
});
|
|
175
|
+
io.out(options.json ? `${JSON.stringify(report, null, 2)}\n` : formatReport(report));
|
|
176
|
+
return report.ok ? ExitCode.Ok : ExitCode.Usage;
|
|
177
|
+
}
|
|
135
178
|
async function runPreview(argv, io, signal) {
|
|
136
179
|
const program = buildPreviewProgram(io);
|
|
137
180
|
const parsed = await parse(program, argv);
|
|
@@ -181,6 +224,8 @@ export async function main(argv, io = defaultIo, { signal = new AbortController(
|
|
|
181
224
|
return await runPreview(argv.slice(1), io, signal);
|
|
182
225
|
if (argv[0] === "setup")
|
|
183
226
|
return await runSetup(argv.slice(1), io);
|
|
227
|
+
if (argv[0] === "doctor")
|
|
228
|
+
return await runDoctorCommand(argv.slice(1), io);
|
|
184
229
|
return await runRender(argv, io, signal);
|
|
185
230
|
}
|
|
186
231
|
catch (error) {
|
package/dist/config.d.ts
CHANGED
|
@@ -2,5 +2,12 @@ import { type DocumentOptions } from "./options.js";
|
|
|
2
2
|
export declare const CONFIG_FILE = "mdrender.config.json";
|
|
3
3
|
/** Find `mdrender.config.json` in `startDir` or the nearest parent directory. */
|
|
4
4
|
export declare function findConfigFile(startDir: string): string | undefined;
|
|
5
|
+
export interface LoadedConfig {
|
|
6
|
+
options: Partial<DocumentOptions>;
|
|
7
|
+
/** Problems that do not stop the conversion, such as misspelled option names. */
|
|
8
|
+
warnings: string[];
|
|
9
|
+
}
|
|
5
10
|
/** Read and validate a config file. Relative paths inside it resolve against its folder. */
|
|
6
11
|
export declare function loadConfig(file: string): Partial<DocumentOptions>;
|
|
12
|
+
/** Like `loadConfig`, also reporting unknown option names (likely typos). */
|
|
13
|
+
export declare function readConfig(file: string): LoadedConfig;
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { normalizeOptions, OptionsError } from "./options.js";
|
|
3
|
+
import { normalizeOptions, OptionsError, unknownOptions } from "./options.js";
|
|
4
4
|
export const CONFIG_FILE = "mdrender.config.json";
|
|
5
5
|
/** Find `mdrender.config.json` in `startDir` or the nearest parent directory. */
|
|
6
6
|
export function findConfigFile(startDir) {
|
|
@@ -17,6 +17,10 @@ export function findConfigFile(startDir) {
|
|
|
17
17
|
}
|
|
18
18
|
/** Read and validate a config file. Relative paths inside it resolve against its folder. */
|
|
19
19
|
export function loadConfig(file) {
|
|
20
|
+
return readConfig(file).options;
|
|
21
|
+
}
|
|
22
|
+
/** Like `loadConfig`, also reporting unknown option names (likely typos). */
|
|
23
|
+
export function readConfig(file) {
|
|
20
24
|
let raw;
|
|
21
25
|
try {
|
|
22
26
|
raw = JSON.parse(readFileSync(file, "utf8"));
|
|
@@ -27,6 +31,11 @@ export function loadConfig(file) {
|
|
|
27
31
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
28
32
|
throw new OptionsError(`${file}: the configuration must be a JSON object`);
|
|
29
33
|
}
|
|
30
|
-
|
|
34
|
+
const record = raw;
|
|
35
|
+
const relative = path.relative(process.cwd(), file);
|
|
36
|
+
const shown = relative && !relative.startsWith("..") && !path.isAbsolute(relative) ? relative : file;
|
|
37
|
+
const warnings = unknownOptions(record).map(({ key, suggestion }) => `${shown}: unknown option "${key}"` +
|
|
38
|
+
(suggestion ? ` (did you mean "${suggestion}"?)` : " (ignored)"));
|
|
39
|
+
return { options: normalizeOptions(record, file, path.dirname(file)), warnings };
|
|
31
40
|
}
|
|
32
41
|
//# sourceMappingURL=config.js.map
|
package/dist/convert.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Diagnostic } from "./diagnostics.js";
|
|
2
|
-
import { type DocumentOptions, type MermaidTheme } from "./options.js";
|
|
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
5
|
/** Settings shared by every file of a run (batch, watch or preview). */
|
|
@@ -53,6 +53,12 @@ export declare class BrowserPool {
|
|
|
53
53
|
get(): Promise<BrowserSession>;
|
|
54
54
|
close(): Promise<void>;
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Chrome draws the header and footer inside the top and bottom margins. Measured: below about
|
|
58
|
+
* 10 mm the header touches or overlaps the first line of text (and the footer the last line).
|
|
59
|
+
*/
|
|
60
|
+
export declare const MIN_HEADER_FOOTER_MARGIN = "10mm";
|
|
61
|
+
export declare function headerFooterSpaceWarnings(margin: Margins, hasHeader: boolean, hasFooter: boolean): Diagnostic[];
|
|
56
62
|
/** Mermaid theme: explicit choice, else dark diagrams for the dark page theme. */
|
|
57
63
|
export declare function effectiveMermaidTheme(options: DocumentOptions): MermaidTheme;
|
|
58
64
|
/**
|
package/dist/convert.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { findBrowser } from "./browser/detect.js";
|
|
5
|
-
import { findConfigFile,
|
|
5
|
+
import { findConfigFile, readConfig } from "./config.js";
|
|
6
6
|
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,
|
|
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 } from "./render/page.js";
|
|
15
|
-
import { buildHtmlDocument, builtinThemeCss } from "./render/template.js";
|
|
14
|
+
import { contentHeightPx, contentWidthPx, lengthToPx } from "./render/page.js";
|
|
15
|
+
import { buildHtmlDocument, builtinThemeCss, densityCss } from "./render/template.js";
|
|
16
|
+
import { writeFileAtomic } from "./write.js";
|
|
16
17
|
/** Starts one browser on first use and keeps it for later conversions. */
|
|
17
18
|
export class BrowserPool {
|
|
18
19
|
executable;
|
|
@@ -51,6 +52,40 @@ function readStylesheet(file) {
|
|
|
51
52
|
function themeStylesheets(theme) {
|
|
52
53
|
return theme === "light" || theme === "dark" ? builtinThemeCss(theme) : [readStylesheet(theme)];
|
|
53
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Chrome draws the header and footer inside the top and bottom margins. Measured: below about
|
|
57
|
+
* 10 mm the header touches or overlaps the first line of text (and the footer the last line).
|
|
58
|
+
*/
|
|
59
|
+
export const MIN_HEADER_FOOTER_MARGIN = "10mm";
|
|
60
|
+
export function headerFooterSpaceWarnings(margin, hasHeader, hasFooter) {
|
|
61
|
+
const minimum = lengthToPx(MIN_HEADER_FOOTER_MARGIN);
|
|
62
|
+
const warnings = [];
|
|
63
|
+
// Half a pixel of slack: "0.39in" and "10mm" are the same margin once Chrome rounds them.
|
|
64
|
+
if (hasHeader && lengthToPx(margin.top) < minimum - 0.5) {
|
|
65
|
+
warnings.push({
|
|
66
|
+
severity: "warning",
|
|
67
|
+
message: `the header may overlap the text: top margin is ${margin.top}, ` +
|
|
68
|
+
`at least ${MIN_HEADER_FOOTER_MARGIN} is needed (e.g. --margin-top 15mm)`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (hasFooter && lengthToPx(margin.bottom) < minimum - 0.5) {
|
|
72
|
+
warnings.push({
|
|
73
|
+
severity: "warning",
|
|
74
|
+
message: `the footer may overlap the text: bottom margin is ${margin.bottom}, ` +
|
|
75
|
+
`at least ${MIN_HEADER_FOOTER_MARGIN} is needed ` +
|
|
76
|
+
"(e.g. --margin-bottom 15mm, or --no-page-numbers for no footer)",
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return warnings;
|
|
80
|
+
}
|
|
81
|
+
/** Font size, if set, and the page's text height (used to fit images and diagrams). */
|
|
82
|
+
function layoutCss(options, margin) {
|
|
83
|
+
const height = contentHeightPx(options.pageSize, options.orientation === "landscape", margin);
|
|
84
|
+
const rules = [`:root { --page-content-height: ${height}px; }`];
|
|
85
|
+
if (options.fontSize)
|
|
86
|
+
rules.push(`html { font-size: ${options.fontSize}; }`);
|
|
87
|
+
return rules.join("\n");
|
|
88
|
+
}
|
|
54
89
|
/** Mermaid theme: explicit choice, else dark diagrams for the dark page theme. */
|
|
55
90
|
export function effectiveMermaidTheme(options) {
|
|
56
91
|
return options.mermaidTheme ?? (options.theme === "dark" ? "dark" : "neutral");
|
|
@@ -69,12 +104,16 @@ export async function renderDocument(inputPath, settings, browsers) {
|
|
|
69
104
|
: settings.config
|
|
70
105
|
? path.resolve(settings.config)
|
|
71
106
|
: findConfigFile(baseDir);
|
|
72
|
-
const
|
|
73
|
-
const
|
|
107
|
+
const config = configFile ? readConfig(configFile) : { options: {}, warnings: [] };
|
|
108
|
+
const options = mergeOptions(config.options, normalizeOptions(frontMatter.data, `${input} (front matter)`, baseDir), settings.overrides ?? {});
|
|
109
|
+
const margin = resolveMargins(options);
|
|
74
110
|
const themeCss = themeStylesheets(options.theme);
|
|
75
111
|
const extraCss = options.css.map(readStylesheet);
|
|
76
112
|
const md = renderMarkdown(frontMatter.body, { baseDir, lang: options.lang });
|
|
77
|
-
const diagnostics = [
|
|
113
|
+
const diagnostics = [
|
|
114
|
+
...config.warnings.map((message) => ({ severity: "warning", message })),
|
|
115
|
+
...md.diagnostics,
|
|
116
|
+
];
|
|
78
117
|
const title = options.title ?? md.title ?? path.parse(input).name;
|
|
79
118
|
// Table of contents: at the [[toc]] marker, or at the start with --toc.
|
|
80
119
|
let body = md.html;
|
|
@@ -88,9 +127,10 @@ export async function renderDocument(inputPath, settings, browsers) {
|
|
|
88
127
|
// Mermaid needs a real DOM to lay out diagrams, also for HTML output.
|
|
89
128
|
if (md.mermaid.length > 0) {
|
|
90
129
|
const session = await browsers.get();
|
|
91
|
-
const width = contentWidthPx(options.pageSize, options.landscape, margin);
|
|
130
|
+
const width = contentWidthPx(options.pageSize, options.orientation === "landscape", margin);
|
|
92
131
|
const theme = effectiveMermaidTheme(options);
|
|
93
|
-
const
|
|
132
|
+
const fontPx = fontSizePx(options.fontSize ?? DEFAULT_FONT_SIZE);
|
|
133
|
+
const results = await renderMermaid(session, md.mermaid, theme, width, fontPx);
|
|
94
134
|
const filled = fillMermaid(body, md.nonce, md.mermaid, results);
|
|
95
135
|
body = filled.html;
|
|
96
136
|
diagnostics.push(...filled.diagnostics);
|
|
@@ -105,6 +145,7 @@ export async function renderDocument(inputPath, settings, browsers) {
|
|
|
105
145
|
date: options.date,
|
|
106
146
|
showTitle: options.title !== undefined,
|
|
107
147
|
themeCss,
|
|
148
|
+
layoutCss: [...densityCss(options.density), layoutCss(options, margin)],
|
|
108
149
|
extraCss,
|
|
109
150
|
});
|
|
110
151
|
diagnostics.sort((a, b) => (a.line ?? 0) - (b.line ?? 0));
|
|
@@ -122,17 +163,16 @@ export async function convertFile(convert) {
|
|
|
122
163
|
try {
|
|
123
164
|
const doc = await renderDocument(convert.input, convert, browsers);
|
|
124
165
|
const { options } = doc;
|
|
125
|
-
await mkdir(path.dirname(output), { recursive: true });
|
|
126
166
|
if (convert.format === "pdf") {
|
|
127
|
-
const margin =
|
|
167
|
+
const margin = resolveMargins(options);
|
|
128
168
|
const values = { title: doc.title, author: options.author, date: options.date };
|
|
129
169
|
const footer = options.footer ?? (options.pageNumbers ? DEFAULT_FOOTER : undefined);
|
|
130
170
|
const hasHeaderFooter = options.header !== undefined || footer !== undefined;
|
|
171
|
+
doc.diagnostics.push(...headerFooterSpaceWarnings(margin, options.header !== undefined, footer !== undefined));
|
|
131
172
|
const session = await browsers.get();
|
|
132
|
-
const { blocked } = await session.printPdf(doc.html, {
|
|
133
|
-
output,
|
|
173
|
+
const { pdf, blocked } = await session.printPdf(doc.html, {
|
|
134
174
|
format: options.pageSize,
|
|
135
|
-
landscape: options.landscape,
|
|
175
|
+
landscape: options.orientation === "landscape",
|
|
136
176
|
margin,
|
|
137
177
|
headerTemplate: hasHeaderFooter
|
|
138
178
|
? headerFooterTemplate(options.header, values, margin)
|
|
@@ -147,9 +187,10 @@ export async function convertFile(convert) {
|
|
|
147
187
|
message: `resource not loaded: ${url}${hint}`,
|
|
148
188
|
});
|
|
149
189
|
}
|
|
190
|
+
await writeFileAtomic(output, pdf);
|
|
150
191
|
}
|
|
151
192
|
else {
|
|
152
|
-
await
|
|
193
|
+
await writeFileAtomic(output, doc.html);
|
|
153
194
|
}
|
|
154
195
|
return {
|
|
155
196
|
output,
|