@imontene/mdrender 2.3.0 → 2.5.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 +22 -0
- package/README.md +63 -37
- package/dist/assets/theme-antigravity-dark.css +37 -0
- package/dist/assets/theme-antigravity.css +239 -0
- package/dist/assets.js +2 -0
- package/dist/cli.js +9 -7
- package/dist/convert.js +37 -7
- package/dist/doctor.js +1 -0
- package/dist/markdown/env.d.ts +2 -0
- package/dist/markdown/hyphenate.d.ts +14 -0
- package/dist/markdown/hyphenate.js +92 -0
- package/dist/markdown/index.d.ts +2 -0
- package/dist/markdown/index.js +3 -0
- package/dist/options.d.ts +11 -3
- package/dist/options.js +7 -3
- package/dist/render/antigravity.d.ts +13 -0
- package/dist/render/antigravity.js +82 -0
- package/dist/render/browser.d.ts +13 -0
- package/dist/render/browser.js +50 -0
- package/dist/render/fonts.d.ts +2 -0
- package/dist/render/fonts.js +6 -0
- package/dist/render/header-footer.d.ts +2 -1
- package/dist/render/header-footer.js +9 -4
- package/dist/render/mermaid.d.ts +2 -0
- package/dist/render/mermaid.js +3 -3
- package/dist/render/page.d.ts +6 -1
- package/dist/render/page.js +7 -3
- package/dist/render/template.d.ts +3 -3
- package/dist/render/template.js +9 -2
- package/package.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Cambios
|
|
2
2
|
|
|
3
|
+
## 2.5.0 — 2026-10-22
|
|
4
|
+
|
|
5
|
+
- **Nuevo estilo por defecto: Anti-Gravity**, tomado del design system del mismo nombre y
|
|
6
|
+
llevado a páginas. Incluye:
|
|
7
|
+
- colores del sistema y títulos en Manrope 600 con interletrado negativo;
|
|
8
|
+
- etiquetas en JetBrains Mono, en mayúsculas: autor y fecha sobre el título, índice,
|
|
9
|
+
encabezado y pie;
|
|
10
|
+
- código, tablas, citas y alertas como superficies suaves;
|
|
11
|
+
- diagramas Mermaid con los mismos colores;
|
|
12
|
+
- un campo de luz tenue junto al título de la primera página.
|
|
13
|
+
- `--theme antigravity-dark` es su versión oscura. **Para conservar el aspecto anterior:**
|
|
14
|
+
`--theme light` (o `theme: light` en `mdrender.config.json`); `--theme dark` sigue
|
|
15
|
+
disponible.
|
|
16
|
+
|
|
17
|
+
## 2.4.0 — 2026-10-22
|
|
18
|
+
|
|
19
|
+
- **Párrafos justificados en el PDF**: los del texto, las citas y las alertas, con partición de
|
|
20
|
+
palabras según el idioma del documento (español, inglés, portugués, francés, alemán e
|
|
21
|
+
italiano). Listas, tablas, notas al pie, índice y diagramas siguen alineados a la izquierda,
|
|
22
|
+
y el HTML no cambia. `--no-justify` (o `justify: false`) lo desactiva. El texto del PDF solo
|
|
23
|
+
lleva el guion donde una palabra se corta: copiar y buscar siguen funcionando.
|
|
24
|
+
|
|
3
25
|
## 2.3.0 — 2026-10-16
|
|
4
26
|
|
|
5
27
|
- **Manrope es la nueva tipografía por defecto** del texto, el encabezado, el pie y los
|
package/README.md
CHANGED
|
@@ -23,7 +23,8 @@ Ejemplo: [`examples/ejemplo.md`](examples/ejemplo.md) → [`examples/ejemplo.pdf
|
|
|
23
23
|
tabla de contenidos y marcadores del PDF.
|
|
24
24
|
- 🗂️ **Lotes**: varios archivos, carpetas completas o patrones `docs/**/*.md`.
|
|
25
25
|
- 🔁 **`--watch`** y **vista previa en vivo** en el navegador.
|
|
26
|
-
- 🎨
|
|
26
|
+
- 🎨 Estilo **Anti-Gravity** por defecto (con variante oscura), los temas clásicos **claro** y
|
|
27
|
+
**oscuro**, o tu propio CSS.
|
|
27
28
|
- 🔌 **Sin conexión**: todo va empaquetado; por defecto no descarga nada de internet.
|
|
28
29
|
- 💻 **Windows 10/11 y Linux** (x64; arm64 en Linux), probado en ambos en cada cambio.
|
|
29
30
|
|
|
@@ -93,8 +94,9 @@ mdrender preview documento.md
|
|
|
93
94
|
# Más contenido por página
|
|
94
95
|
mdrender documento.md --density compact --font-size 10pt
|
|
95
96
|
|
|
96
|
-
#
|
|
97
|
-
mdrender documento.md --theme dark
|
|
97
|
+
# Variante oscura de Anti-Gravity, el estilo clásico, o un CSS propio que reemplaza el tema
|
|
98
|
+
mdrender documento.md --theme antigravity-dark
|
|
99
|
+
mdrender documento.md --theme light
|
|
98
100
|
mdrender documento.md --theme mi-tema.css
|
|
99
101
|
|
|
100
102
|
# Descargar un navegador si no tienes Chrome, Edge ni Chromium
|
|
@@ -135,40 +137,41 @@ equipo); `--port` fija el puerto y `--no-open` no abre el navegador.
|
|
|
135
137
|
|
|
136
138
|
### Opciones
|
|
137
139
|
|
|
138
|
-
| Opción | Descripción
|
|
139
|
-
| ------------------------------ |
|
|
140
|
-
| `-o, --output <ruta>` | Archivo de salida, o carpeta con varios archivos
|
|
141
|
-
| `-f, --format <pdf\|html>` | Formato de salida
|
|
142
|
-
| `--page-size <tamaño>` | `Letter`, `A4`, `Legal` (también `A3`, `A5`, `Tabloid`)
|
|
143
|
-
| `--margin <valor>` | Las cuatro márgenes como en CSS: `10mm`, `15mm 20mm`…
|
|
144
|
-
| `--margin-top <medida>` | Margen superior, p. ej. `15mm` (prevalece sobre `--margin`)
|
|
145
|
-
| `--margin-right <medida>` | Margen derecho
|
|
146
|
-
| `--margin-bottom <medida>` | Margen inferior
|
|
147
|
-
| `--margin-left <medida>` | Margen izquierdo
|
|
148
|
-
| `--orientation <orientación>` | `portrait` (vertical) o `landscape` (horizontal)
|
|
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
|
|
152
|
-
| `--header <texto>` | Encabezado de página (ver abajo)
|
|
153
|
-
| `--footer <texto>` | Pie de página (ver abajo)
|
|
154
|
-
| `--no-page-numbers` | Sin el pie con números de página
|
|
155
|
-
| `--font <manrope\|inter>` | Tipografía del texto (ver abajo)
|
|
156
|
-
| `--
|
|
157
|
-
| `--
|
|
158
|
-
| `--
|
|
159
|
-
| `--
|
|
160
|
-
| `--
|
|
161
|
-
| `--mermaid-
|
|
162
|
-
| `--
|
|
163
|
-
| `--
|
|
164
|
-
| `--
|
|
165
|
-
| `--
|
|
166
|
-
| `--
|
|
167
|
-
| `--
|
|
168
|
-
| `--
|
|
169
|
-
|
|
|
170
|
-
| `-
|
|
171
|
-
| `-
|
|
140
|
+
| Opción | Descripción | Por defecto |
|
|
141
|
+
| ------------------------------ | ------------------------------------------------------------------------------------------------ | --------------------- |
|
|
142
|
+
| `-o, --output <ruta>` | Archivo de salida, o carpeta con varios archivos | junto al `.md` |
|
|
143
|
+
| `-f, --format <pdf\|html>` | Formato de salida | `pdf` |
|
|
144
|
+
| `--page-size <tamaño>` | `Letter`, `A4`, `Legal` (también `A3`, `A5`, `Tabloid`) | `Letter` |
|
|
145
|
+
| `--margin <valor>` | Las cuatro márgenes como en CSS: `10mm`, `15mm 20mm`… | `10mm` |
|
|
146
|
+
| `--margin-top <medida>` | Margen superior, p. ej. `15mm` (prevalece sobre `--margin`) | `10mm` |
|
|
147
|
+
| `--margin-right <medida>` | Margen derecho | `10mm` |
|
|
148
|
+
| `--margin-bottom <medida>` | Margen inferior | `10mm` |
|
|
149
|
+
| `--margin-left <medida>` | Margen izquierdo | `10mm` |
|
|
150
|
+
| `--orientation <orientación>` | `portrait` (vertical) o `landscape` (horizontal) | `portrait` |
|
|
151
|
+
| `--landscape` | Atajo de `--orientation landscape` | — |
|
|
152
|
+
| `--toc` | Tabla de contenidos al inicio (o donde pongas `[[toc]]`) | — |
|
|
153
|
+
| `--toc-depth <n>` | Nivel de título más profundo en la tabla de contenidos | `3` |
|
|
154
|
+
| `--header <texto>` | Encabezado de página (ver abajo) | — |
|
|
155
|
+
| `--footer <texto>` | Pie de página (ver abajo) | `{page} / {pages}` |
|
|
156
|
+
| `--no-page-numbers` | Sin el pie con números de página | — |
|
|
157
|
+
| `--font <manrope\|inter>` | Tipografía del texto (ver abajo) | `manrope` |
|
|
158
|
+
| `--no-justify` | Párrafos alineados a la izquierda en el PDF (ver abajo) | justificado |
|
|
159
|
+
| `--density <densidad>` | `normal` o `compact` (menos espacio entre líneas y bloques) | `normal` |
|
|
160
|
+
| `--font-size <tamaño>` | Tamaño del texto en `pt` o `px`, de 6 a 24 pt; p. ej. `10pt` | `10.5pt` |
|
|
161
|
+
| `--theme <tema\|css>` | `antigravity`, `antigravity-dark`, `light`, `dark` o un `.css` que reemplaza el tema (ver abajo) | `antigravity` |
|
|
162
|
+
| `--css <archivo.css>` | CSS adicional (se puede repetir) | — |
|
|
163
|
+
| `--mermaid-theme <tema>` | `default`, `neutral`, `dark`, `forest`, `base` | el del tema de página |
|
|
164
|
+
| `--mermaid-flip <auto\|never>` | Si el autoajuste puede girar un diagrama de flujo de eje (ver abajo) | `auto` |
|
|
165
|
+
| `--no-mermaid-autofit` | Dibujar los diagramas de flujo tal como están escritos | — |
|
|
166
|
+
| `--lang <código>` | Idioma del documento; `en` para inglés (`pt`, `fr`, `de`…) | `es` |
|
|
167
|
+
| `--title <texto>` | Título del documento (muestra un bloque de título) | primer `# título` |
|
|
168
|
+
| `--config <archivo>` | Archivo de configuración | el más cercano |
|
|
169
|
+
| `--no-config` | Ignorar `mdrender.config.json` | — |
|
|
170
|
+
| `--browser <ruta>` | Ruta a Chrome/Edge/Chromium (o variable `MDRENDER_BROWSER`) | autodetección |
|
|
171
|
+
| `--allow-remote` | Permitir que el PDF descargue imágenes/CSS de internet | bloqueado |
|
|
172
|
+
| `-w, --watch` | Regenerar al guardar | — |
|
|
173
|
+
| `-q, --quiet` | Mostrar solo advertencias y errores | — |
|
|
174
|
+
| `-v, --verbose` | Mostrar también configuración, navegador y tiempos | — |
|
|
172
175
|
|
|
173
176
|
**Página**: por defecto hoja **Letter vertical con márgenes de 10 mm**. Las medidas aceptan
|
|
174
177
|
`mm`, `cm`, `in` o `px`. `--margin` fija las cuatro márgenes a la vez y `--margin-top`,
|
|
@@ -177,6 +180,21 @@ equipo); `--port` fija el puerto y `--no-open` no abre el navegador.
|
|
|
177
180
|
El encabezado y el pie se dibujan dentro de las márgenes superior e inferior: con menos de
|
|
178
181
|
10 mm pueden tocar el texto y mdrender lo advierte.
|
|
179
182
|
|
|
183
|
+
**Temas**: por defecto los documentos usan el estilo **Anti-Gravity**, tomado del design system
|
|
184
|
+
del mismo nombre y llevado de diapositivas a páginas:
|
|
185
|
+
|
|
186
|
+
- Manrope 600 en títulos con interletrado negativo, Manrope 400 en el texto.
|
|
187
|
+
- Etiquetas en JetBrains Mono, en mayúsculas: autor y fecha sobre el título, el índice, el
|
|
188
|
+
encabezado y el pie.
|
|
189
|
+
- Un único azul de acento para enlaces y etiquetas, superficies suaves sin bordes gruesos, y
|
|
190
|
+
alertas como tarjetas.
|
|
191
|
+
- Los diagramas Mermaid llevan los mismos colores.
|
|
192
|
+
- En la primera página, junto al título, un campo de luz muy tenue en azul, cian y violeta.
|
|
193
|
+
|
|
194
|
+
`--theme antigravity-dark` es su versión oscura. `--theme light` conserva el estilo de las
|
|
195
|
+
versiones anteriores y `--theme dark` su variante oscura. El design system pide texto alineado
|
|
196
|
+
a la izquierda: con este tema, usa `--no-justify` si quieres seguirlo al pie de la letra.
|
|
197
|
+
|
|
180
198
|
**Tipografía**: el texto usa [Manrope](https://fonts.google.com/specimen/Manrope);
|
|
181
199
|
`--font inter` (o `font: inter` en el front matter o en `mdrender.config.json`) vuelve a
|
|
182
200
|
[Inter](https://fonts.google.com/specimen/Inter), la tipografía de las versiones anteriores.
|
|
@@ -184,6 +202,14 @@ Ambas van incrustadas en el PDF, también en el encabezado, el pie y los diagram
|
|
|
184
202
|
tiene cursivas (el navegador inclina la normal) ni algunos símbolos como `→`, `✓` o `≤`, que
|
|
185
203
|
se toman de Inter.
|
|
186
204
|
|
|
205
|
+
**Párrafos justificados**: en el PDF, los párrafos del texto, de las citas y de las alertas
|
|
206
|
+
se justifican, y las palabras largas se parten con guion al final de la línea según el idioma
|
|
207
|
+
del documento (`lang`: español, inglés, portugués, francés, alemán o italiano; en otros idiomas
|
|
208
|
+
se justifica sin partir palabras). Listas, tablas, notas al pie, el índice y los diagramas
|
|
209
|
+
quedan alineados a la izquierda, igual que el HTML. El texto del PDF solo lleva el guion donde
|
|
210
|
+
la palabra se corta, así que copiar y buscar funcionan con las palabras completas.
|
|
211
|
+
`--no-justify` (o `justify: false`) vuelve a alinear a la izquierda.
|
|
212
|
+
|
|
187
213
|
**Aprovechar la página**: `--density compact` reduce interlineado, espacios entre párrafos y
|
|
188
214
|
títulos, y `--font-size 10pt` achica el texto (los diagramas Mermaid siguen el mismo tamaño).
|
|
189
215
|
Juntos ahorran entre un 15 y un 25 % de páginas. Además, siempre:
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/* mdrender Anti-Gravity dark stage: loaded after theme-antigravity.css, swaps its colours. */
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
--ag-canvas: #0b0e13;
|
|
5
|
+
--ag-surface-subtle: #10141b;
|
|
6
|
+
--ag-surface-raised: #141922;
|
|
7
|
+
--ag-ink: #eef1f6;
|
|
8
|
+
--ag-ink-secondary: #b6bdc9;
|
|
9
|
+
--ag-ink-muted: #8f98a7;
|
|
10
|
+
--ag-hairline: #232a35;
|
|
11
|
+
--ag-hairline-strong: #3a4352;
|
|
12
|
+
--ag-accent: #7ea4ff;
|
|
13
|
+
--ag-accent-soft: #16213b;
|
|
14
|
+
--ag-opacity-field: 0.22;
|
|
15
|
+
--ag-shadow-card: 0 1px 2px rgba(0, 0, 0, 0.4), 0 12px 32px rgba(0, 0, 0, 0.35);
|
|
16
|
+
|
|
17
|
+
--alert-warning: #e3b341;
|
|
18
|
+
--alert-caution: #ff8a8a;
|
|
19
|
+
--color-error: #ffa198;
|
|
20
|
+
--color-error-bg: #2d1416;
|
|
21
|
+
--color-error-border: #8e1519;
|
|
22
|
+
|
|
23
|
+
--code-title: #b9a6ff;
|
|
24
|
+
--code-constant: #b9a6ff;
|
|
25
|
+
--code-string: #6fd3ec;
|
|
26
|
+
--code-builtin: #6fd3ec;
|
|
27
|
+
--code-added: #aff5b4;
|
|
28
|
+
--code-added-bg: #0d2e19;
|
|
29
|
+
--code-removed: #ffdcd7;
|
|
30
|
+
--code-removed-bg: #4a0f16;
|
|
31
|
+
color-scheme: dark;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* Paint the whole sheet, margins included (the root background stops at the margins). */
|
|
35
|
+
@page {
|
|
36
|
+
background: #0b0e13;
|
|
37
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* mdrender Anti-Gravity theme (the default): loaded after theme.css. Colours, type and surfaces
|
|
3
|
+
* follow the Anti-Gravity design system (tokens.json), drawn for 1920 × 1080 slides and scaled
|
|
4
|
+
* here to a printed page. theme-antigravity-dark.css swaps the colours for its dark stage.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
:root {
|
|
8
|
+
/* Design system tokens */
|
|
9
|
+
--ag-canvas: #ffffff;
|
|
10
|
+
--ag-surface-subtle: #f6f8fb;
|
|
11
|
+
--ag-surface-raised: #ffffff;
|
|
12
|
+
--ag-ink: #0f1419;
|
|
13
|
+
--ag-ink-secondary: #454e5c;
|
|
14
|
+
--ag-ink-muted: #636c7a;
|
|
15
|
+
--ag-hairline: #e6e9ef;
|
|
16
|
+
--ag-hairline-strong: #c9d0db;
|
|
17
|
+
--ag-accent: #2456d3;
|
|
18
|
+
--ag-accent-soft: #ecf1fd;
|
|
19
|
+
--ag-field-blue: #4f7cff;
|
|
20
|
+
--ag-field-cyan: #22c3e6;
|
|
21
|
+
--ag-field-violet: #8c6cf0;
|
|
22
|
+
--ag-opacity-field: 0.16;
|
|
23
|
+
--ag-shadow-card: 0 1px 2px rgba(15, 20, 25, 0.04), 0 12px 32px rgba(15, 20, 25, 0.06);
|
|
24
|
+
|
|
25
|
+
/* The variables theme.css is written with */
|
|
26
|
+
--color-text: var(--ag-ink);
|
|
27
|
+
--color-muted: var(--ag-ink-muted);
|
|
28
|
+
--color-heading: var(--ag-ink);
|
|
29
|
+
--color-link: var(--ag-accent);
|
|
30
|
+
--color-border: var(--ag-hairline);
|
|
31
|
+
--color-subtle-bg: var(--ag-surface-subtle);
|
|
32
|
+
--color-accent: var(--ag-accent);
|
|
33
|
+
--color-bg: var(--ag-canvas);
|
|
34
|
+
--color-stripe: var(--ag-canvas);
|
|
35
|
+
/* Notes, tips and important points in the one accent. Warnings and cautions keep amber and
|
|
36
|
+
red: they carry risk, a deliberate exception to the system's single blue. */
|
|
37
|
+
--alert-note: var(--ag-accent);
|
|
38
|
+
--alert-tip: var(--ag-accent);
|
|
39
|
+
--alert-important: var(--ag-accent);
|
|
40
|
+
--alert-warning: #8a5d00;
|
|
41
|
+
--alert-caution: #c0262d;
|
|
42
|
+
|
|
43
|
+
/* Syntax highlighting: restrained, from the accent and the field hues (4.5:1 or better on
|
|
44
|
+
surface-subtle). */
|
|
45
|
+
--code-keyword: var(--ag-accent);
|
|
46
|
+
--code-title: #5b3fc4;
|
|
47
|
+
--code-constant: #5b3fc4;
|
|
48
|
+
--code-string: #0a6580;
|
|
49
|
+
--code-builtin: #0a6580;
|
|
50
|
+
--code-comment: var(--ag-ink-muted);
|
|
51
|
+
--code-tag: var(--ag-accent);
|
|
52
|
+
--code-text: var(--ag-ink);
|
|
53
|
+
--code-added: #116329;
|
|
54
|
+
--code-added-bg: #e6f6ea;
|
|
55
|
+
--code-removed: #a40e26;
|
|
56
|
+
--code-removed-bg: #fdecee;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* Headings: Manrope 600, negative tracking, separated by white space rather than rules. */
|
|
60
|
+
h1,
|
|
61
|
+
h2,
|
|
62
|
+
h3,
|
|
63
|
+
h4,
|
|
64
|
+
h5,
|
|
65
|
+
h6 {
|
|
66
|
+
font-weight: 600;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
h1 {
|
|
70
|
+
font-size: 2.1em;
|
|
71
|
+
letter-spacing: -0.025em;
|
|
72
|
+
padding-bottom: 0;
|
|
73
|
+
border-bottom: 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
h2 {
|
|
77
|
+
font-size: 1.55em;
|
|
78
|
+
letter-spacing: -0.02em;
|
|
79
|
+
padding-bottom: 0;
|
|
80
|
+
border-bottom: 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
h3 {
|
|
84
|
+
font-size: 1.15em;
|
|
85
|
+
letter-spacing: -0.01em;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
h4,
|
|
89
|
+
h5,
|
|
90
|
+
h6 {
|
|
91
|
+
font-size: 1em;
|
|
92
|
+
letter-spacing: 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
h6 {
|
|
96
|
+
color: var(--ag-ink-secondary);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* Title block as the system's TitleBlock: eyebrow (author · date), display title, lead. */
|
|
100
|
+
.doc-header {
|
|
101
|
+
display: flex;
|
|
102
|
+
flex-direction: column;
|
|
103
|
+
margin-bottom: 2.4em;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.doc-header .doc-meta {
|
|
107
|
+
order: -1;
|
|
108
|
+
margin: 0 0 1em;
|
|
109
|
+
padding-bottom: 0;
|
|
110
|
+
border-bottom: 0;
|
|
111
|
+
font-family: var(--font-code);
|
|
112
|
+
font-size: 0.78em;
|
|
113
|
+
font-weight: 500;
|
|
114
|
+
letter-spacing: 0.14em;
|
|
115
|
+
text-transform: uppercase;
|
|
116
|
+
color: var(--ag-accent);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.doc-header .doc-title {
|
|
120
|
+
margin: 0 0 0.35em;
|
|
121
|
+
font-size: 2.6em;
|
|
122
|
+
letter-spacing: -0.03em;
|
|
123
|
+
line-height: 1.12;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.doc-subtitle {
|
|
127
|
+
margin: 0;
|
|
128
|
+
font-size: 1.23em;
|
|
129
|
+
line-height: 1.4;
|
|
130
|
+
color: var(--ag-ink-secondary);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/*
|
|
134
|
+
* The gradient field: soft light in the top-right corner of the title block (so only on the
|
|
135
|
+
* first page, and only when there is one), never under text: the title block's text keeps to
|
|
136
|
+
* the left 64%, the system's text measure. Each light fades out inside the box, so no edge shows.
|
|
137
|
+
*/
|
|
138
|
+
.doc-header {
|
|
139
|
+
position: relative;
|
|
140
|
+
z-index: 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.doc-header > * {
|
|
144
|
+
max-width: 64%;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.doc-header::before {
|
|
148
|
+
content: "";
|
|
149
|
+
position: absolute;
|
|
150
|
+
z-index: -1;
|
|
151
|
+
top: 0;
|
|
152
|
+
right: 0;
|
|
153
|
+
bottom: 0;
|
|
154
|
+
width: 36%;
|
|
155
|
+
opacity: var(--ag-opacity-field);
|
|
156
|
+
pointer-events: none;
|
|
157
|
+
background:
|
|
158
|
+
radial-gradient(28% 38% at 58% 45%, var(--ag-field-blue), transparent),
|
|
159
|
+
radial-gradient(16% 26% at 78% 58%, var(--ag-field-cyan), transparent),
|
|
160
|
+
radial-gradient(25% 30% at 40% 32%, var(--ag-field-violet), transparent);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* Text */
|
|
164
|
+
li::marker {
|
|
165
|
+
color: var(--ag-ink-muted);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
blockquote {
|
|
169
|
+
padding: 0.8em 1.1em;
|
|
170
|
+
color: var(--ag-ink-secondary);
|
|
171
|
+
border-left: 0;
|
|
172
|
+
border-radius: 8px;
|
|
173
|
+
background: var(--ag-surface-subtle);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
hr {
|
|
177
|
+
background: var(--ag-hairline);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* Code */
|
|
181
|
+
code {
|
|
182
|
+
border-radius: 4px;
|
|
183
|
+
background: var(--ag-accent-soft);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
pre {
|
|
187
|
+
border: 1px solid var(--ag-hairline);
|
|
188
|
+
border-radius: 8px;
|
|
189
|
+
background: var(--ag-surface-subtle);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* Tables: header on surface-subtle, hairline rules, no stripes. */
|
|
193
|
+
th,
|
|
194
|
+
td {
|
|
195
|
+
border-color: var(--ag-hairline);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
th {
|
|
199
|
+
background: var(--ag-surface-subtle);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* Alerts as raised cards with a mono label; no left-border accents. */
|
|
203
|
+
.markdown-alert {
|
|
204
|
+
padding: 0.8em 1.1em;
|
|
205
|
+
color: var(--ag-ink);
|
|
206
|
+
border: 1px solid var(--ag-hairline);
|
|
207
|
+
border-radius: 12px;
|
|
208
|
+
background: var(--ag-surface-raised);
|
|
209
|
+
box-shadow: var(--ag-shadow-card);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.markdown-alert-title {
|
|
213
|
+
font-family: var(--font-code);
|
|
214
|
+
font-size: 0.78em;
|
|
215
|
+
font-weight: 500;
|
|
216
|
+
letter-spacing: 0.14em;
|
|
217
|
+
text-transform: uppercase;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* Table of contents as a quiet panel with a mono label. */
|
|
221
|
+
.toc {
|
|
222
|
+
border-color: var(--ag-hairline);
|
|
223
|
+
border-radius: 12px;
|
|
224
|
+
background: var(--ag-surface-subtle);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.toc-title {
|
|
228
|
+
font-family: var(--font-code);
|
|
229
|
+
font-size: 0.78em;
|
|
230
|
+
font-weight: 500;
|
|
231
|
+
letter-spacing: 0.14em;
|
|
232
|
+
text-transform: uppercase;
|
|
233
|
+
color: var(--ag-accent);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/* Footnotes */
|
|
237
|
+
.footnotes {
|
|
238
|
+
color: var(--ag-ink-muted);
|
|
239
|
+
}
|
package/dist/assets.js
CHANGED
|
@@ -94,6 +94,8 @@ export const MERMAID_JS = "mermaid/dist/mermaid.min.js";
|
|
|
94
94
|
export const THEMES = [
|
|
95
95
|
"mdrender/assets/theme.css",
|
|
96
96
|
"mdrender/assets/theme-dark.css",
|
|
97
|
+
"mdrender/assets/theme-antigravity.css",
|
|
98
|
+
"mdrender/assets/theme-antigravity-dark.css",
|
|
97
99
|
"mdrender/assets/density-compact.css",
|
|
98
100
|
];
|
|
99
101
|
export const PACKAGE_JSON = "mdrender/package.json";
|
package/dist/cli.js
CHANGED
|
@@ -26,6 +26,7 @@ const DOCUMENT_FLAGS = [
|
|
|
26
26
|
"footer",
|
|
27
27
|
"pageNumbers",
|
|
28
28
|
"font",
|
|
29
|
+
"justify",
|
|
29
30
|
"density",
|
|
30
31
|
"fontSize",
|
|
31
32
|
"css",
|
|
@@ -36,6 +37,8 @@ const DOCUMENT_FLAGS = [
|
|
|
36
37
|
"lang",
|
|
37
38
|
"title",
|
|
38
39
|
];
|
|
40
|
+
/** Options set on the command line only through their --no-… form. */
|
|
41
|
+
const NEGATED_FLAGS = new Set(["pageNumbers", "mermaidAutofit", "justify"]);
|
|
39
42
|
const toFlag = (key) => "--" + key.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase());
|
|
40
43
|
export const defaultIo = {
|
|
41
44
|
out: (text) => process.stdout.write(text),
|
|
@@ -59,11 +62,14 @@ function addDocumentOptions(command) {
|
|
|
59
62
|
.option("--footer <text>", 'page footer (default: "{page} / {pages}")')
|
|
60
63
|
.option("--no-page-numbers", "no default page-number footer")
|
|
61
64
|
.option("--font <font>", "text typeface: manrope or inter (default: manrope)")
|
|
65
|
+
.option("--no-justify", "align paragraphs left in the PDF instead of justifying them")
|
|
62
66
|
.option("--density <density>", "normal or compact (tighter spacing) (default: normal)")
|
|
63
67
|
.option("--font-size <size>", "text size, e.g. 10pt or 11pt (default: 10.5pt)")
|
|
64
|
-
.option("--theme <theme>", "light, dark, or a .css file replacing the theme
|
|
68
|
+
.option("--theme <theme>", "antigravity, antigravity-dark, light, dark, or a .css file replacing the theme " +
|
|
69
|
+
"(default: antigravity)")
|
|
65
70
|
.option("--css <file>", "extra stylesheet (repeatable)", collect)
|
|
66
|
-
.option("--mermaid-theme <theme>", `Mermaid theme: ${MERMAID_THEMES.join(", ")}
|
|
71
|
+
.option("--mermaid-theme <theme>", `Mermaid theme: ${MERMAID_THEMES.join(", ")} ` +
|
|
72
|
+
"(default: the page theme's — base in Anti-Gravity colours, neutral for light, dark for dark)")
|
|
67
73
|
.option("--no-mermaid-autofit", "draw flowcharts exactly as written (no layout search)")
|
|
68
74
|
.option("--mermaid-flip <mode>", "auto: turn a flowchart to the other axis if its text prints too small; never (default: auto)")
|
|
69
75
|
.option("--lang <tag>", "document language, e.g. es or en (default: es)")
|
|
@@ -117,11 +123,7 @@ function cliOverrides(program) {
|
|
|
117
123
|
if (program.getOptionValueSource(key) === "cli")
|
|
118
124
|
raw[key] = program.getOptionValue(key);
|
|
119
125
|
}
|
|
120
|
-
return normalizeOptions(raw, "command line", process.cwd(), (key) => key
|
|
121
|
-
? "--no-page-numbers"
|
|
122
|
-
: key === "mermaidAutofit"
|
|
123
|
-
? "--no-mermaid-autofit"
|
|
124
|
-
: toFlag(key));
|
|
126
|
+
return normalizeOptions(raw, "command line", process.cwd(), (key) => NEGATED_FLAGS.has(key) ? `--no-${toFlag(key).slice(2)}` : toFlag(key));
|
|
125
127
|
}
|
|
126
128
|
function settings(program, options) {
|
|
127
129
|
return {
|
package/dist/convert.js
CHANGED
|
@@ -7,13 +7,14 @@ 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 { DEFAULT_FONT_SIZE, fontSizePx, mergeOptions, normalizeOptions, OptionsError, resolveMargins, } from "./options.js";
|
|
10
|
+
import { BUILTIN_THEMES, DEFAULT_FONT_SIZE, fontSizePx, isDarkTheme, 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
14
|
import { SMALL_SCALE } from "./render/mermaid-fit.js";
|
|
15
|
+
import { headerFooterStyle, mermaidThemeVariables } from "./render/antigravity.js";
|
|
15
16
|
import { FONT_FAMILIES, fontStack } from "./render/fonts.js";
|
|
16
|
-
import { contentHeightPx, contentWidthPx, lengthToPx } from "./render/page.js";
|
|
17
|
+
import { contentHeightPx, contentWidthPx, lengthToPx, textWidthPx } from "./render/page.js";
|
|
17
18
|
import { buildHtmlDocument, builtinThemeCss, densityCss } from "./render/template.js";
|
|
18
19
|
import { writeFileAtomic } from "./write.js";
|
|
19
20
|
/** Starts one browser on first use and keeps it for later conversions. */
|
|
@@ -52,7 +53,8 @@ function readStylesheet(file) {
|
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
55
|
function themeStylesheets(theme) {
|
|
55
|
-
|
|
56
|
+
const builtin = BUILTIN_THEMES.find((name) => name === theme);
|
|
57
|
+
return builtin ? builtinThemeCss(builtin) : [readStylesheet(theme)];
|
|
56
58
|
}
|
|
57
59
|
/**
|
|
58
60
|
* Chrome draws the header and footer inside the top and bottom margins. Measured: below about
|
|
@@ -80,6 +82,18 @@ export function headerFooterSpaceWarnings(margin, hasHeader, hasFooter) {
|
|
|
80
82
|
}
|
|
81
83
|
return warnings;
|
|
82
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Justified paragraphs in the PDF: the body's, and those directly inside a quote or an alert
|
|
87
|
+
* (the same ones markdown/hyphenate.ts gives soft hyphens). Lists, tables, footnotes, the title
|
|
88
|
+
* block, the table of contents and diagram labels stay aligned left. Print only: on screen the
|
|
89
|
+
* HTML keeps ragged lines.
|
|
90
|
+
*/
|
|
91
|
+
const JUSTIFY_CSS = `@media print {
|
|
92
|
+
.markdown-body > p,
|
|
93
|
+
.markdown-body > blockquote > p:not(.markdown-alert-title) {
|
|
94
|
+
text-align: justify;
|
|
95
|
+
}
|
|
96
|
+
}`;
|
|
83
97
|
/** Font size, if set, and the page's text height (used to fit images and diagrams). */
|
|
84
98
|
function layoutCss(options, margin) {
|
|
85
99
|
const height = contentHeightPx(options.pageSize, options.orientation === "landscape", margin);
|
|
@@ -93,6 +107,8 @@ function layoutCss(options, margin) {
|
|
|
93
107
|
// heading is pushed to the next page, Chrome then repeats its text in the PDF bookmark.
|
|
94
108
|
if (options.font === "manrope")
|
|
95
109
|
rules.push("h1, h2, h3, h4, h5, h6 { line-height: 1.4; }");
|
|
110
|
+
if (options.justify)
|
|
111
|
+
rules.push(JUSTIFY_CSS);
|
|
96
112
|
if (options.fontSize)
|
|
97
113
|
rules.push(`html { font-size: ${options.fontSize}; }`);
|
|
98
114
|
return rules.join("\n");
|
|
@@ -112,7 +128,11 @@ export function describeLayout(layout) {
|
|
|
112
128
|
}
|
|
113
129
|
/** Mermaid theme: explicit choice, else dark diagrams for the dark page theme. */
|
|
114
130
|
export function effectiveMermaidTheme(options) {
|
|
115
|
-
|
|
131
|
+
if (options.mermaidTheme)
|
|
132
|
+
return options.mermaidTheme;
|
|
133
|
+
if (options.theme.startsWith("antigravity"))
|
|
134
|
+
return "base";
|
|
135
|
+
return isDarkTheme(options.theme) ? "dark" : "neutral";
|
|
116
136
|
}
|
|
117
137
|
/**
|
|
118
138
|
* Render a Markdown file to a complete HTML document. A browser is only started (through
|
|
@@ -135,7 +155,12 @@ paged = false) {
|
|
|
135
155
|
const margin = resolveMargins(options);
|
|
136
156
|
const themeCss = themeStylesheets(options.theme);
|
|
137
157
|
const extraCss = options.css.map(readStylesheet);
|
|
138
|
-
const md = renderMarkdown(frontMatter.body, {
|
|
158
|
+
const md = renderMarkdown(frontMatter.body, {
|
|
159
|
+
baseDir,
|
|
160
|
+
lang: options.lang,
|
|
161
|
+
// Soft hyphens only where paragraphs are justified: in the PDF.
|
|
162
|
+
hyphenate: paged && options.justify ? options.lang : undefined,
|
|
163
|
+
});
|
|
139
164
|
const diagnostics = [
|
|
140
165
|
...config.warnings.map((message) => ({ severity: "warning", message })),
|
|
141
166
|
...md.diagnostics,
|
|
@@ -169,6 +194,7 @@ paged = false) {
|
|
|
169
194
|
autofit: options.mermaidAutofit,
|
|
170
195
|
flip: options.mermaidFlip,
|
|
171
196
|
font: options.font,
|
|
197
|
+
themeVariables: theme === "base" ? mermaidThemeVariables(options.theme) : undefined,
|
|
172
198
|
});
|
|
173
199
|
const filled = fillMermaid(body, md.nonce, md.mermaid, results);
|
|
174
200
|
body = filled.html;
|
|
@@ -224,18 +250,22 @@ export async function convertFile(convert) {
|
|
|
224
250
|
const footer = options.footer ?? (options.pageNumbers ? DEFAULT_FOOTER : undefined);
|
|
225
251
|
const hasHeaderFooter = options.header !== undefined || footer !== undefined;
|
|
226
252
|
doc.diagnostics.push(...headerFooterSpaceWarnings(margin, options.header !== undefined, footer !== undefined));
|
|
253
|
+
const pageLook = headerFooterStyle(options.theme);
|
|
227
254
|
const session = await browsers.get();
|
|
228
255
|
const { pdf, blocked } = await session.printPdf(doc.html, {
|
|
229
256
|
format: options.pageSize,
|
|
230
257
|
landscape: options.orientation === "landscape",
|
|
231
258
|
margin,
|
|
232
259
|
headerTemplate: hasHeaderFooter
|
|
233
|
-
? headerFooterTemplate(options.header, values, margin, options.font)
|
|
260
|
+
? headerFooterTemplate(options.header, values, margin, options.font, pageLook)
|
|
234
261
|
: undefined,
|
|
235
262
|
footerTemplate: hasHeaderFooter
|
|
236
|
-
? headerFooterTemplate(footer, values, margin, options.font)
|
|
263
|
+
? headerFooterTemplate(footer, values, margin, options.font, pageLook)
|
|
237
264
|
: undefined,
|
|
238
265
|
allowRemote: convert.allowRemote,
|
|
266
|
+
softHyphensWidthPx: options.justify
|
|
267
|
+
? textWidthPx(options.pageSize, options.orientation === "landscape", margin)
|
|
268
|
+
: undefined,
|
|
239
269
|
});
|
|
240
270
|
for (const url of blocked) {
|
|
241
271
|
const hint = /^https?:/i.test(url) ? " (use --allow-remote to download it)" : "";
|
package/dist/doctor.js
CHANGED
|
@@ -137,6 +137,7 @@ function settingsCheck(options) {
|
|
|
137
137
|
detail: `${options.pageSize} ${options.orientation}, margins ${m.top} ${m.right} ${m.bottom} ` +
|
|
138
138
|
`${m.left} (top right bottom left), ${options.font} ` +
|
|
139
139
|
`${options.fontSize ?? DEFAULT_FONT_SIZE}, ${options.density} density, ` +
|
|
140
|
+
`${options.justify ? "justified" : "ragged"} paragraphs, ` +
|
|
140
141
|
`language ${options.lang}, theme ` +
|
|
141
142
|
path.basename(options.theme),
|
|
142
143
|
};
|
package/dist/markdown/env.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ export interface RenderEnv extends Env {
|
|
|
22
22
|
hasMath: boolean;
|
|
23
23
|
/** Document language, for translated labels (alert titles). */
|
|
24
24
|
lang: string;
|
|
25
|
+
/** Language whose hyphenation patterns break justified paragraphs (PDF only); see hyphenate.ts. */
|
|
26
|
+
hyphenate?: string;
|
|
25
27
|
/** Set when the document places its table of contents with a `[[toc]]` marker. */
|
|
26
28
|
hasTocMarker: boolean;
|
|
27
29
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { MarkdownIt } from "markdown-it";
|
|
2
|
+
/**
|
|
3
|
+
* Soft hyphens (U+00AD) for justified paragraphs in the PDF, so the browser can break long
|
|
4
|
+
* words at the end of a line instead of stretching the spaces. They are inserted here, from TeX
|
|
5
|
+
* hyphenation patterns, because CSS `hyphens: auto` depends on dictionaries that Chromium and
|
|
6
|
+
* Chrome Headless Shell do not ship. When printing, the soft hyphens that did not end up at the
|
|
7
|
+
* end of a line are removed again (render/browser.ts), so they stay out of the PDF's text.
|
|
8
|
+
*/
|
|
9
|
+
export declare const SOFT_HYPHEN = "\u00AD";
|
|
10
|
+
export declare const HYPHENATION_LANGUAGES: string[];
|
|
11
|
+
/** Hyphenation function for a BCP 47 language tag ("es", "es-CL", "en-GB"), if supported. */
|
|
12
|
+
export declare function hyphenatorFor(lang: string): ((text: string) => string) | undefined;
|
|
13
|
+
/** Insert soft hyphens in the paragraphs that are justified in the PDF (env.hyphenate). */
|
|
14
|
+
export declare function hyphenation(md: MarkdownIt): void;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import createHyphenator from "hyphen";
|
|
2
|
+
import de from "hyphen/patterns/de-1996.js";
|
|
3
|
+
import enGb from "hyphen/patterns/en-gb.js";
|
|
4
|
+
import enUs from "hyphen/patterns/en-us.js";
|
|
5
|
+
import es from "hyphen/patterns/es.js";
|
|
6
|
+
import fr from "hyphen/patterns/fr.js";
|
|
7
|
+
import it from "hyphen/patterns/it.js";
|
|
8
|
+
import pt from "hyphen/patterns/pt.js";
|
|
9
|
+
/**
|
|
10
|
+
* Soft hyphens (U+00AD) for justified paragraphs in the PDF, so the browser can break long
|
|
11
|
+
* words at the end of a line instead of stretching the spaces. They are inserted here, from TeX
|
|
12
|
+
* hyphenation patterns, because CSS `hyphens: auto` depends on dictionaries that Chromium and
|
|
13
|
+
* Chrome Headless Shell do not ship. When printing, the soft hyphens that did not end up at the
|
|
14
|
+
* end of a line are removed again (render/browser.ts), so they stay out of the PDF's text.
|
|
15
|
+
*/
|
|
16
|
+
export const SOFT_HYPHEN = "";
|
|
17
|
+
/** Hyphenation patterns by language: base language, or language-region where they differ. */
|
|
18
|
+
const PATTERNS = {
|
|
19
|
+
es,
|
|
20
|
+
en: enUs,
|
|
21
|
+
"en-gb": enGb,
|
|
22
|
+
pt,
|
|
23
|
+
fr,
|
|
24
|
+
de,
|
|
25
|
+
it,
|
|
26
|
+
};
|
|
27
|
+
export const HYPHENATION_LANGUAGES = Object.keys(PATTERNS);
|
|
28
|
+
const hyphenators = new Map();
|
|
29
|
+
/** Hyphenation function for a BCP 47 language tag ("es", "es-CL", "en-GB"), if supported. */
|
|
30
|
+
export function hyphenatorFor(lang) {
|
|
31
|
+
const tag = lang.trim().toLowerCase().replace(/_/g, "-");
|
|
32
|
+
const patterns = PATTERNS[tag.split("-").slice(0, 2).join("-")] ?? PATTERNS[tag.split("-")[0]];
|
|
33
|
+
if (!patterns)
|
|
34
|
+
return undefined;
|
|
35
|
+
let hyphenate = hyphenators.get(patterns);
|
|
36
|
+
if (!hyphenate) {
|
|
37
|
+
// The input is plain text from Markdown, not HTML (a literal "<" is just a character).
|
|
38
|
+
const sync = createHyphenator(patterns, { html: false });
|
|
39
|
+
hyphenate = (text) => sync(text);
|
|
40
|
+
hyphenators.set(patterns, hyphenate);
|
|
41
|
+
}
|
|
42
|
+
return hyphenate;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Whether a paragraph is justified: top-level, or directly inside a blockquote or alert (the
|
|
46
|
+
* same paragraphs as the print stylesheet in convert.ts). Lists, footnotes and tables are not.
|
|
47
|
+
*/
|
|
48
|
+
function justified(containers) {
|
|
49
|
+
return (containers.length === 0 || (containers.length === 1 && containers[0] === "blockquote_open"));
|
|
50
|
+
}
|
|
51
|
+
function hyphenateInline(inline, hyphenate) {
|
|
52
|
+
let inAutolink = 0;
|
|
53
|
+
for (const child of inline.children ?? []) {
|
|
54
|
+
if (child.type === "link_open" && (child.markup === "linkify" || child.markup === "autolink")) {
|
|
55
|
+
inAutolink++;
|
|
56
|
+
}
|
|
57
|
+
else if (child.type === "link_close" && inAutolink > 0) {
|
|
58
|
+
inAutolink--;
|
|
59
|
+
}
|
|
60
|
+
else if (child.type === "text" && inAutolink === 0) {
|
|
61
|
+
child.content = hyphenate(child.content);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Insert soft hyphens in the paragraphs that are justified in the PDF (env.hyphenate). */
|
|
66
|
+
export function hyphenation(md) {
|
|
67
|
+
md.core.ruler.push("hyphenation", (state) => {
|
|
68
|
+
const lang = state.env.hyphenate;
|
|
69
|
+
const hyphenate = lang === undefined ? undefined : hyphenatorFor(lang);
|
|
70
|
+
if (!hyphenate)
|
|
71
|
+
return;
|
|
72
|
+
const containers = [];
|
|
73
|
+
for (let i = 0; i < state.tokens.length; i++) {
|
|
74
|
+
const token = state.tokens[i];
|
|
75
|
+
if (token.type === "paragraph_open") {
|
|
76
|
+
const inline = state.tokens[i + 1];
|
|
77
|
+
if (justified(containers) && inline?.type === "inline")
|
|
78
|
+
hyphenateInline(inline, hyphenate);
|
|
79
|
+
}
|
|
80
|
+
else if (token.type === "paragraph_close") {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
else if (token.nesting === 1) {
|
|
84
|
+
containers.push(token.type);
|
|
85
|
+
}
|
|
86
|
+
else if (token.nesting === -1) {
|
|
87
|
+
containers.pop();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=hyphenate.js.map
|
package/dist/markdown/index.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ export interface MarkdownOptions {
|
|
|
22
22
|
baseDir?: string;
|
|
23
23
|
/** Document language for translated labels. Defaults to Spanish. */
|
|
24
24
|
lang?: string;
|
|
25
|
+
/** Insert soft hyphens in justified paragraphs, with this language's patterns. */
|
|
26
|
+
hyphenate?: string;
|
|
25
27
|
}
|
|
26
28
|
export declare function createMarkdown(): MarkdownIt;
|
|
27
29
|
export declare function renderMarkdown(source: string, options?: MarkdownOptions): MarkdownResult;
|
package/dist/markdown/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { alerts } from "./alerts.js";
|
|
|
7
7
|
import { codeBlocks } from "./code-blocks.js";
|
|
8
8
|
import { inlineChildLines } from "./env.js";
|
|
9
9
|
import { highlightCode } from "./highlight.js";
|
|
10
|
+
import { hyphenation } from "./hyphenate.js";
|
|
10
11
|
import { inlineLocalImages } from "./images.js";
|
|
11
12
|
import { keepWithHeading } from "./keep-with-heading.js";
|
|
12
13
|
import { math } from "./math.js";
|
|
@@ -32,6 +33,7 @@ export function createMarkdown() {
|
|
|
32
33
|
md.use(codeBlocks);
|
|
33
34
|
md.use(math);
|
|
34
35
|
md.use(mermaid);
|
|
36
|
+
md.use(hyphenation);
|
|
35
37
|
return md;
|
|
36
38
|
}
|
|
37
39
|
let shared;
|
|
@@ -46,6 +48,7 @@ export function renderMarkdown(source, options = {}) {
|
|
|
46
48
|
hasMath: false,
|
|
47
49
|
lang: options.lang ?? DEFAULT_LANG,
|
|
48
50
|
hasTocMarker: false,
|
|
51
|
+
hyphenate: options.hyphenate,
|
|
49
52
|
};
|
|
50
53
|
const tokens = md.parse(source, env);
|
|
51
54
|
let title;
|
package/dist/options.d.ts
CHANGED
|
@@ -13,7 +13,10 @@ export declare const MERMAID_THEMES: readonly ["default", "neutral", "dark", "fo
|
|
|
13
13
|
export type MermaidTheme = (typeof MERMAID_THEMES)[number];
|
|
14
14
|
export declare const MERMAID_FLIPS: readonly ["auto", "never"];
|
|
15
15
|
export type MermaidFlip = (typeof MERMAID_FLIPS)[number];
|
|
16
|
-
export declare const BUILTIN_THEMES: readonly ["light", "dark"];
|
|
16
|
+
export declare const BUILTIN_THEMES: readonly ["antigravity", "antigravity-dark", "light", "dark"];
|
|
17
|
+
export type BuiltinTheme = (typeof BUILTIN_THEMES)[number];
|
|
18
|
+
/** Built-in themes with a dark page. */
|
|
19
|
+
export declare const isDarkTheme: (theme: string) => boolean;
|
|
17
20
|
export declare const TEXT_FONTS: readonly ["manrope", "inter"];
|
|
18
21
|
export type TextFont = (typeof TEXT_FONTS)[number];
|
|
19
22
|
export declare const DENSITIES: readonly ["normal", "compact"];
|
|
@@ -37,19 +40,24 @@ export interface DocumentOptions {
|
|
|
37
40
|
/** Header/footer text; see render/header-footer.ts for placeholders and `|` columns. */
|
|
38
41
|
header?: string;
|
|
39
42
|
footer?: string;
|
|
40
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Mermaid theme; when unset, the page theme's: "base" coloured from the Anti-Gravity tokens,
|
|
45
|
+
* else "neutral" for light pages and "dark" for dark ones.
|
|
46
|
+
*/
|
|
41
47
|
mermaidTheme?: MermaidTheme;
|
|
42
48
|
/** Try several layouts of each flowchart and keep the one whose text prints largest. */
|
|
43
49
|
mermaidAutofit: boolean;
|
|
44
50
|
/** Whether autofit may turn a flowchart to the other axis (LR ↔ TB) when it reads far better. */
|
|
45
51
|
mermaidFlip: MermaidFlip;
|
|
52
|
+
/** Justify paragraphs in the PDF (body, quotes, alerts), breaking long words with hyphens. */
|
|
53
|
+
justify: boolean;
|
|
46
54
|
/** Typeface of the text: Manrope or Inter (code is always JetBrains Mono). */
|
|
47
55
|
font: TextFont;
|
|
48
56
|
/** Vertical spacing: "compact" tightens line height, paragraph and heading spacing. */
|
|
49
57
|
density: Density;
|
|
50
58
|
/** Base font size of the text, e.g. "10pt"; when unset, the theme's (10.5pt). */
|
|
51
59
|
fontSize?: string;
|
|
52
|
-
/**
|
|
60
|
+
/** A built-in theme (BUILTIN_THEMES), or the absolute path of a stylesheet replacing it. */
|
|
53
61
|
theme: string;
|
|
54
62
|
/** Extra stylesheets (absolute paths), applied after the built-in theme. */
|
|
55
63
|
css: string[];
|
package/dist/options.js
CHANGED
|
@@ -6,7 +6,9 @@ export const PAGE_SIZES = ["A3", "A4", "A5", "Letter", "Legal", "Tabloid"];
|
|
|
6
6
|
export const ORIENTATIONS = ["portrait", "landscape"];
|
|
7
7
|
export const MERMAID_THEMES = ["default", "neutral", "dark", "forest", "base"];
|
|
8
8
|
export const MERMAID_FLIPS = ["auto", "never"];
|
|
9
|
-
export const BUILTIN_THEMES = ["light", "dark"];
|
|
9
|
+
export const BUILTIN_THEMES = ["antigravity", "antigravity-dark", "light", "dark"];
|
|
10
|
+
/** Built-in themes with a dark page. */
|
|
11
|
+
export const isDarkTheme = (theme) => theme === "dark" || theme === "antigravity-dark";
|
|
10
12
|
export const TEXT_FONTS = ["manrope", "inter"];
|
|
11
13
|
export const DENSITIES = ["normal", "compact"];
|
|
12
14
|
export const DEFAULT_OPTIONS = {
|
|
@@ -19,8 +21,9 @@ export const DEFAULT_OPTIONS = {
|
|
|
19
21
|
mermaidAutofit: true,
|
|
20
22
|
mermaidFlip: "auto",
|
|
21
23
|
font: "manrope",
|
|
24
|
+
justify: true,
|
|
22
25
|
density: "normal",
|
|
23
|
-
theme: "
|
|
26
|
+
theme: "antigravity",
|
|
24
27
|
css: [],
|
|
25
28
|
lang: DEFAULT_LANG,
|
|
26
29
|
};
|
|
@@ -118,6 +121,7 @@ const VALIDATORS = {
|
|
|
118
121
|
mermaidAutofit: bool,
|
|
119
122
|
mermaidFlip: oneOf(MERMAID_FLIPS),
|
|
120
123
|
font: oneOf(TEXT_FONTS),
|
|
124
|
+
justify: bool,
|
|
121
125
|
density: oneOf(DENSITIES),
|
|
122
126
|
fontSize,
|
|
123
127
|
theme: (value, describe, baseDir) => {
|
|
@@ -127,7 +131,7 @@ const VALIDATORS = {
|
|
|
127
131
|
return builtin;
|
|
128
132
|
if (/\.css$/i.test(theme))
|
|
129
133
|
return path.resolve(baseDir, theme);
|
|
130
|
-
throw new OptionsError(`${describe} must be
|
|
134
|
+
throw new OptionsError(`${describe} must be ${BUILTIN_THEMES.join(", ")} or a .css file`);
|
|
131
135
|
},
|
|
132
136
|
css: (value, describe, baseDir) => {
|
|
133
137
|
const list = Array.isArray(value) ? value : [value];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const isAntigravityTheme: (theme: string) => boolean;
|
|
2
|
+
/**
|
|
3
|
+
* Mermaid "base" theme variables for an Anti-Gravity page: nodes on accent-soft outlined in
|
|
4
|
+
* accent, edges and arrows in ink-muted, groups on surface-subtle. Undefined for other themes.
|
|
5
|
+
*/
|
|
6
|
+
export declare function mermaidThemeVariables(theme: string): Record<string, string | boolean> | undefined;
|
|
7
|
+
export interface HeaderFooterStyle {
|
|
8
|
+
color: string;
|
|
9
|
+
/** Set in the mono family, uppercase and tracked, like the system's slide footers. */
|
|
10
|
+
mono: boolean;
|
|
11
|
+
}
|
|
12
|
+
/** How page headers and footers look for a page theme. */
|
|
13
|
+
export declare function headerFooterStyle(theme: string): HeaderFooterStyle;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { isDarkTheme } from "../options.js";
|
|
2
|
+
/**
|
|
3
|
+
* Anti-Gravity design system tokens needed outside the stylesheets (theme-antigravity.css):
|
|
4
|
+
* Mermaid draws its diagrams in a page of its own, and Chrome renders page headers and footers
|
|
5
|
+
* apart from the document, so neither sees the CSS variables.
|
|
6
|
+
*/
|
|
7
|
+
const TOKENS = {
|
|
8
|
+
light: {
|
|
9
|
+
canvas: "#ffffff",
|
|
10
|
+
surfaceSubtle: "#f6f8fb",
|
|
11
|
+
ink: "#0f1419",
|
|
12
|
+
inkSecondary: "#454e5c",
|
|
13
|
+
inkMuted: "#636c7a",
|
|
14
|
+
hairline: "#e6e9ef",
|
|
15
|
+
hairlineStrong: "#c9d0db",
|
|
16
|
+
accent: "#2456d3",
|
|
17
|
+
accentSoft: "#ecf1fd",
|
|
18
|
+
},
|
|
19
|
+
dark: {
|
|
20
|
+
canvas: "#0b0e13",
|
|
21
|
+
surfaceSubtle: "#10141b",
|
|
22
|
+
ink: "#eef1f6",
|
|
23
|
+
inkSecondary: "#b6bdc9",
|
|
24
|
+
inkMuted: "#8f98a7",
|
|
25
|
+
hairline: "#232a35",
|
|
26
|
+
hairlineStrong: "#3a4352",
|
|
27
|
+
accent: "#7ea4ff",
|
|
28
|
+
accentSoft: "#16213b",
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
export const isAntigravityTheme = (theme) => theme.startsWith("antigravity");
|
|
32
|
+
const tokensFor = (theme) => (isDarkTheme(theme) ? TOKENS.dark : TOKENS.light);
|
|
33
|
+
/**
|
|
34
|
+
* Mermaid "base" theme variables for an Anti-Gravity page: nodes on accent-soft outlined in
|
|
35
|
+
* accent, edges and arrows in ink-muted, groups on surface-subtle. Undefined for other themes.
|
|
36
|
+
*/
|
|
37
|
+
export function mermaidThemeVariables(theme) {
|
|
38
|
+
if (!isAntigravityTheme(theme))
|
|
39
|
+
return undefined;
|
|
40
|
+
const t = tokensFor(theme);
|
|
41
|
+
return {
|
|
42
|
+
darkMode: isDarkTheme(theme),
|
|
43
|
+
background: t.canvas,
|
|
44
|
+
primaryColor: t.accentSoft,
|
|
45
|
+
primaryBorderColor: t.accent,
|
|
46
|
+
primaryTextColor: t.ink,
|
|
47
|
+
secondaryColor: t.surfaceSubtle,
|
|
48
|
+
secondaryBorderColor: t.hairlineStrong,
|
|
49
|
+
secondaryTextColor: t.ink,
|
|
50
|
+
tertiaryColor: t.canvas,
|
|
51
|
+
tertiaryBorderColor: t.hairlineStrong,
|
|
52
|
+
tertiaryTextColor: t.ink,
|
|
53
|
+
mainBkg: t.accentSoft,
|
|
54
|
+
nodeBorder: t.accent,
|
|
55
|
+
nodeTextColor: t.ink,
|
|
56
|
+
lineColor: t.inkMuted,
|
|
57
|
+
textColor: t.ink,
|
|
58
|
+
titleColor: t.ink,
|
|
59
|
+
clusterBkg: t.surfaceSubtle,
|
|
60
|
+
clusterBorder: t.hairlineStrong,
|
|
61
|
+
edgeLabelBackground: t.canvas,
|
|
62
|
+
noteBkgColor: t.surfaceSubtle,
|
|
63
|
+
noteBorderColor: t.hairlineStrong,
|
|
64
|
+
noteTextColor: t.ink,
|
|
65
|
+
actorBkg: t.accentSoft,
|
|
66
|
+
actorBorder: t.accent,
|
|
67
|
+
actorTextColor: t.ink,
|
|
68
|
+
actorLineColor: t.hairlineStrong,
|
|
69
|
+
signalColor: t.inkMuted,
|
|
70
|
+
signalTextColor: t.ink,
|
|
71
|
+
labelBoxBkgColor: t.surfaceSubtle,
|
|
72
|
+
labelBoxBorderColor: t.hairlineStrong,
|
|
73
|
+
labelTextColor: t.ink,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/** How page headers and footers look for a page theme. */
|
|
77
|
+
export function headerFooterStyle(theme) {
|
|
78
|
+
if (isAntigravityTheme(theme))
|
|
79
|
+
return { color: tokensFor(theme).inkMuted, mono: true };
|
|
80
|
+
return { color: "#59636e", mono: false };
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=antigravity.js.map
|
package/dist/render/browser.d.ts
CHANGED
|
@@ -21,7 +21,20 @@ export interface PrintOptions {
|
|
|
21
21
|
footerTemplate?: string;
|
|
22
22
|
/** Let the page load http(s) resources (remote images, stylesheets). Off by default. */
|
|
23
23
|
allowRemote?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Width of the text column in CSS px. When set, the soft hyphens that do not fall at the end of
|
|
26
|
+
* a line are removed before printing (see SOFT_HYPHEN_CLEANUP).
|
|
27
|
+
*/
|
|
28
|
+
softHyphensWidthPx?: number;
|
|
24
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Keep only the soft hyphens (U+00AD) where a line actually breaks. Chrome copies every soft
|
|
32
|
+
* hyphen into the PDF's text, which splits words for copy, search and text extraction; only the
|
|
33
|
+
* ones that print as "-" belong there. The page is laid out at the printed column width with
|
|
34
|
+
* print styles; Chrome breaks lines greedily, so removing break points it did not use leaves
|
|
35
|
+
* every line as it was. Runs as our own code in the print page, whose scripts are disabled.
|
|
36
|
+
*/
|
|
37
|
+
export declare const SOFT_HYPHEN_CLEANUP = "(width) => {\n const root = document.documentElement;\n root.style.width = width + \"px\";\n try {\n const range = document.createRange();\n // The character right after a used soft hyphen also reports the \"-\" drawn at the end of the\n // previous line: take its last rectangle, and the first one for the character before.\n const top = (node, i, last) => {\n range.setStart(node, i);\n range.setEnd(node, i + 1);\n const rects = [...range.getClientRects()].filter((r) => r.width > 0);\n const rect = last ? rects[rects.length - 1] : rects[0];\n return rect ? rect.top : NaN;\n };\n const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);\n const nodes = [];\n for (let node = walker.nextNode(); node; node = walker.nextNode()) {\n if (node.data.includes(\"\u00AD\")) nodes.push(node);\n }\n let kept = 0;\n for (const node of nodes) {\n const text = node.data;\n let result = \"\";\n for (let i = 0; i < text.length; i++) {\n if (text[i] !== \"\u00AD\") {\n result += text[i];\n } else if (i > 0 && i < text.length - 1 && top(node, i + 1, true) > top(node, i - 1) + 1) {\n result += text[i];\n kept++;\n }\n }\n node.data = result;\n }\n return kept;\n } finally {\n root.style.width = \"\";\n }\n}";
|
|
25
38
|
/** One headless browser shared by every step of a conversion (Mermaid, then PDF). */
|
|
26
39
|
export declare class BrowserSession {
|
|
27
40
|
private readonly browser;
|
package/dist/render/browser.js
CHANGED
|
@@ -22,6 +22,51 @@ export function shouldRetryWithoutSandbox(error, args, platform = process.platfo
|
|
|
22
22
|
!args.includes("--no-sandbox") &&
|
|
23
23
|
/no usable sandbox/i.test(String(error?.message ?? error)));
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Keep only the soft hyphens (U+00AD) where a line actually breaks. Chrome copies every soft
|
|
27
|
+
* hyphen into the PDF's text, which splits words for copy, search and text extraction; only the
|
|
28
|
+
* ones that print as "-" belong there. The page is laid out at the printed column width with
|
|
29
|
+
* print styles; Chrome breaks lines greedily, so removing break points it did not use leaves
|
|
30
|
+
* every line as it was. Runs as our own code in the print page, whose scripts are disabled.
|
|
31
|
+
*/
|
|
32
|
+
export const SOFT_HYPHEN_CLEANUP = `(width) => {
|
|
33
|
+
const root = document.documentElement;
|
|
34
|
+
root.style.width = width + "px";
|
|
35
|
+
try {
|
|
36
|
+
const range = document.createRange();
|
|
37
|
+
// The character right after a used soft hyphen also reports the "-" drawn at the end of the
|
|
38
|
+
// previous line: take its last rectangle, and the first one for the character before.
|
|
39
|
+
const top = (node, i, last) => {
|
|
40
|
+
range.setStart(node, i);
|
|
41
|
+
range.setEnd(node, i + 1);
|
|
42
|
+
const rects = [...range.getClientRects()].filter((r) => r.width > 0);
|
|
43
|
+
const rect = last ? rects[rects.length - 1] : rects[0];
|
|
44
|
+
return rect ? rect.top : NaN;
|
|
45
|
+
};
|
|
46
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
|
47
|
+
const nodes = [];
|
|
48
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
49
|
+
if (node.data.includes("\u00AD")) nodes.push(node);
|
|
50
|
+
}
|
|
51
|
+
let kept = 0;
|
|
52
|
+
for (const node of nodes) {
|
|
53
|
+
const text = node.data;
|
|
54
|
+
let result = "";
|
|
55
|
+
for (let i = 0; i < text.length; i++) {
|
|
56
|
+
if (text[i] !== "\u00AD") {
|
|
57
|
+
result += text[i];
|
|
58
|
+
} else if (i > 0 && i < text.length - 1 && top(node, i + 1, true) > top(node, i - 1) + 1) {
|
|
59
|
+
result += text[i];
|
|
60
|
+
kept++;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
node.data = result;
|
|
64
|
+
}
|
|
65
|
+
return kept;
|
|
66
|
+
} finally {
|
|
67
|
+
root.style.width = "";
|
|
68
|
+
}
|
|
69
|
+
}`;
|
|
25
70
|
/**
|
|
26
71
|
* Only allow requests the document needs: inline data, plus http(s) when `allowRemote`.
|
|
27
72
|
* file: URLs are always refused, so HTML in a Markdown file cannot pull local files into
|
|
@@ -105,6 +150,11 @@ export class BrowserSession {
|
|
|
105
150
|
await page.setContent(html, { waitUntil: "load" });
|
|
106
151
|
// Make sure every embedded font has loaded before printing.
|
|
107
152
|
await page.evaluate("document.fonts.ready.then(() => undefined)");
|
|
153
|
+
if (options.softHyphensWidthPx !== undefined) {
|
|
154
|
+
await page.emulateMediaType("print");
|
|
155
|
+
await page.setViewport({ width: Math.ceil(options.softHyphensWidthPx), height: 1000 });
|
|
156
|
+
await page.evaluate(`(${SOFT_HYPHEN_CLEANUP})(${options.softHyphensWidthPx})`);
|
|
157
|
+
}
|
|
108
158
|
const pdf = await page.pdf({
|
|
109
159
|
format: options.format ?? "A4",
|
|
110
160
|
landscape: options.landscape ?? false,
|
package/dist/render/fonts.d.ts
CHANGED
|
@@ -20,5 +20,7 @@ export declare function fontStack(font: TextFont): string;
|
|
|
20
20
|
export declare function inlineFontStylesheet(cssKey: string): string;
|
|
21
21
|
/** @font-face rules for every font a document uses, with the font files embedded. */
|
|
22
22
|
export declare function fontFaceCss(font?: TextFont): string;
|
|
23
|
+
/** JetBrains Mono Regular alone, for page headers/footers set in the mono family. */
|
|
24
|
+
export declare function monoRegularFontFace(): string;
|
|
23
25
|
/** The text font's regular weight alone, for page headers/footers (rendered apart from the page). */
|
|
24
26
|
export declare function regularFontFace(font?: TextFont): string;
|
package/dist/render/fonts.js
CHANGED
|
@@ -64,6 +64,12 @@ export function fontFaceCss(font = "manrope") {
|
|
|
64
64
|
}
|
|
65
65
|
return css;
|
|
66
66
|
}
|
|
67
|
+
let monoRegular;
|
|
68
|
+
/** JetBrains Mono Regular alone, for page headers/footers set in the mono family. */
|
|
69
|
+
export function monoRegularFontFace() {
|
|
70
|
+
monoRegular ??= inlineFontStylesheet(MONO_STYLESHEETS[0]);
|
|
71
|
+
return monoRegular;
|
|
72
|
+
}
|
|
67
73
|
const regularFaces = new Map();
|
|
68
74
|
/** The text font's regular weight alone, for page headers/footers (rendered apart from the page). */
|
|
69
75
|
export function regularFontFace(font = "manrope") {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TextFont } from "../options.js";
|
|
2
|
+
import type { HeaderFooterStyle } from "./antigravity.js";
|
|
2
3
|
/**
|
|
3
4
|
* Header/footer text with placeholders, in up to three columns separated by `|`:
|
|
4
5
|
*
|
|
@@ -19,5 +20,5 @@ export interface HeaderFooterValues {
|
|
|
19
20
|
export declare function headerFooterTemplate(spec: string | undefined, values: HeaderFooterValues, horizontalMargin: {
|
|
20
21
|
left: string;
|
|
21
22
|
right: string;
|
|
22
|
-
}, font?: TextFont): string;
|
|
23
|
+
}, font?: TextFont, style?: HeaderFooterStyle): string;
|
|
23
24
|
export declare const DEFAULT_FOOTER = "{page} / {pages}";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { escapeHtml } from "../html.js";
|
|
2
|
-
import { fontStack, regularFontFace } from "./fonts.js";
|
|
2
|
+
import { fontStack, monoRegularFontFace, regularFontFace } from "./fonts.js";
|
|
3
3
|
const CHROME_CLASSES = { page: "pageNumber", pages: "totalPages" };
|
|
4
4
|
function renderText(text, values) {
|
|
5
5
|
return escapeHtml(text.trim()).replace(/\{(\w+)\}/g, (match, name) => {
|
|
@@ -23,13 +23,18 @@ function columns(spec) {
|
|
|
23
23
|
* Chrome header/footer template. Chrome renders it apart from the page, so it carries its own
|
|
24
24
|
* styles and its own copy of the text font.
|
|
25
25
|
*/
|
|
26
|
-
export function headerFooterTemplate(spec, values, horizontalMargin, font = "manrope") {
|
|
26
|
+
export function headerFooterTemplate(spec, values, horizontalMargin, font = "manrope", style = { color: "#59636e", mono: false }) {
|
|
27
27
|
if (!spec)
|
|
28
28
|
return "<span></span>";
|
|
29
29
|
const [left, center, right] = columns(spec).map((c) => renderText(c, values));
|
|
30
|
-
|
|
30
|
+
// The font list sits in a style="…" attribute: single quotes only.
|
|
31
|
+
const look = style.mono
|
|
32
|
+
? `font-family: 'JetBrains Mono', ui-monospace, monospace; font-size: 7pt; ` +
|
|
33
|
+
`letter-spacing: 0.14em; text-transform: uppercase;`
|
|
34
|
+
: `font-family: ${fontStack(font).replaceAll('"', "'")}; font-size: 8pt;`;
|
|
35
|
+
return `<style>${style.mono ? monoRegularFontFace() : regularFontFace(font)}</style>
|
|
31
36
|
<div style="box-sizing: border-box; width: 100%; display: flex; padding: 0 ${horizontalMargin.right} 0 ${horizontalMargin.left};
|
|
32
|
-
|
|
37
|
+
${look} color: ${style.color};">
|
|
33
38
|
<span style="flex: 1; text-align: left;">${left}</span>
|
|
34
39
|
<span style="flex: 1; text-align: center;">${center}</span>
|
|
35
40
|
<span style="flex: 1; text-align: right;">${right}</span>
|
package/dist/render/mermaid.d.ts
CHANGED
|
@@ -39,6 +39,8 @@ export interface MermaidRenderOptions {
|
|
|
39
39
|
flip?: MermaidFlip;
|
|
40
40
|
/** Text font of the document, used for diagram labels too. */
|
|
41
41
|
font?: TextFont;
|
|
42
|
+
/** Extra Mermaid theme variables (the Anti-Gravity colours for the "base" theme). */
|
|
43
|
+
themeVariables?: Record<string, string | boolean>;
|
|
42
44
|
}
|
|
43
45
|
/**
|
|
44
46
|
* Room kept for a heading printed together with the diagram below it (see keep-with-heading in
|
package/dist/render/mermaid.js
CHANGED
|
@@ -14,7 +14,7 @@ const DEFAULT_FONT_PX = 14;
|
|
|
14
14
|
*/
|
|
15
15
|
export const HEADING_ALLOWANCE_EM = 5;
|
|
16
16
|
let mermaidJs;
|
|
17
|
-
function mermaidConfig(theme, font, fontPx, wrap) {
|
|
17
|
+
function mermaidConfig(theme, font, fontPx, wrap, themeVariables = {}) {
|
|
18
18
|
const fontFamily = fontStack(font);
|
|
19
19
|
return {
|
|
20
20
|
startOnLoad: false,
|
|
@@ -23,7 +23,7 @@ function mermaidConfig(theme, font, fontPx, wrap) {
|
|
|
23
23
|
fontFamily,
|
|
24
24
|
// Sequence diagrams take their text sizes from here, the others from themeVariables.
|
|
25
25
|
fontSize: fontPx,
|
|
26
|
-
themeVariables: { fontFamily, fontSize: `${fontPx}px` },
|
|
26
|
+
themeVariables: { ...themeVariables, fontFamily, fontSize: `${fontPx}px` },
|
|
27
27
|
htmlLabels: true,
|
|
28
28
|
// Actors only at the top of sequence diagrams: repeating them below costs height.
|
|
29
29
|
sequence: { mirrorActors: false },
|
|
@@ -61,7 +61,7 @@ body { margin: 0; width: ${widthPx}px; font-family: ${fontStack(font)}; }</style
|
|
|
61
61
|
catch (error) {
|
|
62
62
|
return { error: String(error?.message ?? error) };
|
|
63
63
|
}
|
|
64
|
-
}, source, id, mermaidConfig(theme, font, fontPx, wrap));
|
|
64
|
+
}, source, id, mermaidConfig(theme, font, fontPx, wrap, options.themeVariables));
|
|
65
65
|
const results = [];
|
|
66
66
|
for (const [i, block] of blocks.entries()) {
|
|
67
67
|
const id = `mermaid-${i + 1}`;
|
package/dist/render/page.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { PageSize } from "../options.js";
|
|
2
2
|
export declare function lengthToPx(length: string): number;
|
|
3
|
-
/**
|
|
3
|
+
/** Exact width of the text column in CSS pixels, as Chrome prints it. */
|
|
4
|
+
export declare function textWidthPx(pageSize: PageSize, landscape: boolean, margin: {
|
|
5
|
+
left: string;
|
|
6
|
+
right: string;
|
|
7
|
+
}): number;
|
|
8
|
+
/** Width of the text column in whole CSS pixels, so diagrams are laid out at their printed size. */
|
|
4
9
|
export declare function contentWidthPx(pageSize: PageSize, landscape: boolean, margin: {
|
|
5
10
|
left: string;
|
|
6
11
|
right: string;
|
package/dist/render/page.js
CHANGED
|
@@ -14,11 +14,15 @@ export function lengthToPx(length) {
|
|
|
14
14
|
throw new Error(`invalid length: ${length}`);
|
|
15
15
|
return Number(match[1]) * PX_PER_UNIT[match[2]];
|
|
16
16
|
}
|
|
17
|
-
/**
|
|
18
|
-
export function
|
|
17
|
+
/** Exact width of the text column in CSS pixels, as Chrome prints it. */
|
|
18
|
+
export function textWidthPx(pageSize, landscape, margin) {
|
|
19
19
|
const [width, height] = PAPER_MM[pageSize];
|
|
20
20
|
const paperPx = ((landscape ? height : width) * 96) / 25.4;
|
|
21
|
-
return
|
|
21
|
+
return paperPx - lengthToPx(margin.left) - lengthToPx(margin.right);
|
|
22
|
+
}
|
|
23
|
+
/** Width of the text column in whole CSS pixels, so diagrams are laid out at their printed size. */
|
|
24
|
+
export function contentWidthPx(pageSize, landscape, margin) {
|
|
25
|
+
return Math.round(textWidthPx(pageSize, landscape, margin));
|
|
22
26
|
}
|
|
23
27
|
/** Height of the text area in CSS pixels: the paper minus the top and bottom margins. */
|
|
24
28
|
export function contentHeightPx(pageSize, landscape, margin) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { escapeHtml } from "../html.js";
|
|
2
|
-
import type { TextFont } from "../options.js";
|
|
2
|
+
import type { BuiltinTheme, TextFont } from "../options.js";
|
|
3
3
|
export { escapeHtml };
|
|
4
|
-
/** Stylesheets of a built-in theme
|
|
5
|
-
export declare function builtinThemeCss(theme:
|
|
4
|
+
/** Stylesheets of a built-in theme. */
|
|
5
|
+
export declare function builtinThemeCss(theme: BuiltinTheme): string[];
|
|
6
6
|
/** Stylesheet for a density other than "normal", applied after the theme. */
|
|
7
7
|
export declare function densityCss(density: "normal" | "compact"): string[];
|
|
8
8
|
export interface DocumentParts {
|
package/dist/render/template.js
CHANGED
|
@@ -13,9 +13,16 @@ function asset(name) {
|
|
|
13
13
|
}
|
|
14
14
|
return css;
|
|
15
15
|
}
|
|
16
|
-
/** Stylesheets of
|
|
16
|
+
/** Stylesheets of each built-in theme, applied in order. */
|
|
17
|
+
const THEME_FILES = {
|
|
18
|
+
antigravity: ["theme.css", "theme-antigravity.css"],
|
|
19
|
+
"antigravity-dark": ["theme.css", "theme-antigravity.css", "theme-antigravity-dark.css"],
|
|
20
|
+
light: ["theme.css"],
|
|
21
|
+
dark: ["theme.css", "theme-dark.css"],
|
|
22
|
+
};
|
|
23
|
+
/** Stylesheets of a built-in theme. */
|
|
17
24
|
export function builtinThemeCss(theme) {
|
|
18
|
-
return
|
|
25
|
+
return THEME_FILES[theme].map(asset);
|
|
19
26
|
}
|
|
20
27
|
/** Stylesheet for a density other than "normal", applied after the theme. */
|
|
21
28
|
export function densityCss(density) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imontene/mdrender",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "Command-line app for Windows and Linux that renders Markdown — with LaTeX equations and Mermaid diagrams — to PDF and HTML set in Manrope or Inter",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|
|
@@ -71,6 +71,7 @@
|
|
|
71
71
|
"chokidar": "^5.0.0",
|
|
72
72
|
"commander": "^15.0.0",
|
|
73
73
|
"highlight.js": "^11.12.0",
|
|
74
|
+
"hyphen": "^1.14.1",
|
|
74
75
|
"inter-ui": "^4.1.1",
|
|
75
76
|
"katex": "^0.19.0",
|
|
76
77
|
"markdown-it": "^15.0.2",
|
|
@@ -83,6 +84,7 @@
|
|
|
83
84
|
},
|
|
84
85
|
"devDependencies": {
|
|
85
86
|
"@eslint/js": "^10.0.1",
|
|
87
|
+
"@types/hyphen": "^1.14.0",
|
|
86
88
|
"@types/node": "^22.20.5",
|
|
87
89
|
"@vitest/coverage-v8": "^5.0.3",
|
|
88
90
|
"esbuild": "^0.28.2",
|