@imontene/mdrender 2.2.0 → 2.4.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 +15 -0
- package/README.md +21 -3
- package/dist/assets/theme.css +3 -2
- package/dist/assets.d.ts +2 -0
- package/dist/assets.js +7 -1
- package/dist/cli.js +8 -6
- package/dist/convert.js +41 -5
- package/dist/doctor.js +4 -2
- 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 +6 -0
- package/dist/options.js +5 -0
- package/dist/render/browser.d.ts +13 -0
- package/dist/render/browser.js +50 -0
- package/dist/render/fonts.d.ts +18 -4
- package/dist/render/fonts.js +46 -13
- package/dist/render/header-footer.d.ts +3 -2
- package/dist/render/header-footer.js +5 -5
- package/dist/render/mermaid.d.ts +3 -1
- package/dist/render/mermaid.js +13 -11
- package/dist/render/page.d.ts +6 -1
- package/dist/render/page.js +7 -3
- package/dist/render/template.d.ts +3 -0
- package/dist/render/template.js +1 -1
- package/package.json +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Cambios
|
|
2
2
|
|
|
3
|
+
## 2.4.0 — 2026-10-22
|
|
4
|
+
|
|
5
|
+
- **Párrafos justificados en el PDF**: los del texto, las citas y las alertas, con partición de
|
|
6
|
+
palabras según el idioma del documento (español, inglés, portugués, francés, alemán e
|
|
7
|
+
italiano). Listas, tablas, notas al pie, índice y diagramas siguen alineados a la izquierda,
|
|
8
|
+
y el HTML no cambia. `--no-justify` (o `justify: false`) lo desactiva. El texto del PDF solo
|
|
9
|
+
lleva el guion donde una palabra se corta: copiar y buscar siguen funcionando.
|
|
10
|
+
|
|
11
|
+
## 2.3.0 — 2026-10-16
|
|
12
|
+
|
|
13
|
+
- **Manrope es la nueva tipografía por defecto** del texto, el encabezado, el pie y los
|
|
14
|
+
diagramas Mermaid. `--font inter` (o `font: inter`) conserva Inter, la de versiones
|
|
15
|
+
anteriores. Manrope no tiene cursivas (el navegador inclina la normal) y toma de Inter los
|
|
16
|
+
símbolos que le faltan (`→`, `✓`, `≤`…).
|
|
17
|
+
|
|
3
18
|
## 2.2.0 — 2026-10-15
|
|
4
19
|
|
|
5
20
|
- **Autoajuste de diagramas de flujo Mermaid** (`flowchart`/`graph`): cada diagrama se dibuja
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Convierte archivos Markdown (`.md`) en PDF y HTML desde la línea de comandos, en Windows y
|
|
4
4
|
Linux**, con ecuaciones LaTeX, diagramas Mermaid y la tipografía
|
|
5
|
-
[Inter](https://fonts.google.com/specimen/Inter).
|
|
5
|
+
[Manrope](https://fonts.google.com/specimen/Manrope) (o [Inter](https://fonts.google.com/specimen/Inter)).
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
mdrender informe.md # → informe.pdf
|
|
@@ -15,7 +15,8 @@ Ejemplo: [`examples/ejemplo.md`](examples/ejemplo.md) → [`examples/ejemplo.pdf
|
|
|
15
15
|
- 📄 **Markdown → PDF o HTML autocontenido** (fuentes, imágenes y diagramas incluidos).
|
|
16
16
|
- ➗ **Ecuaciones** `$…$` y `$$…$$` con KaTeX, dibujadas al convertir (sin JavaScript en la salida).
|
|
17
17
|
- 📊 **Diagramas Mermaid**: flujo, secuencia, clases, estados, ER, Gantt, torta, mapa mental…
|
|
18
|
-
- 🔤 **
|
|
18
|
+
- 🔤 **Manrope** para el texto (o **Inter** con `--font inter`) y **JetBrains Mono** para el
|
|
19
|
+
código, incrustadas en el PDF.
|
|
19
20
|
- 🧩 **GFM**: tablas, listas de tareas, tachado, notas al pie, alertas `> [!NOTE]`,
|
|
20
21
|
resaltado de código para ~35 lenguajes.
|
|
21
22
|
- 🖨️ Tamaño de página, márgenes, orientación, encabezado/pie con números de página,
|
|
@@ -151,6 +152,8 @@ equipo); `--port` fija el puerto y `--no-open` no abre el navegador.
|
|
|
151
152
|
| `--header <texto>` | Encabezado de página (ver abajo) | — |
|
|
152
153
|
| `--footer <texto>` | Pie de página (ver abajo) | `{page} / {pages}` |
|
|
153
154
|
| `--no-page-numbers` | Sin el pie con números de página | — |
|
|
155
|
+
| `--font <manrope\|inter>` | Tipografía del texto (ver abajo) | `manrope` |
|
|
156
|
+
| `--no-justify` | Párrafos alineados a la izquierda en el PDF (ver abajo) | justificado |
|
|
154
157
|
| `--density <densidad>` | `normal` o `compact` (menos espacio entre líneas y bloques) | `normal` |
|
|
155
158
|
| `--font-size <tamaño>` | Tamaño del texto en `pt` o `px`, de 6 a 24 pt; p. ej. `10pt` | `10.5pt` |
|
|
156
159
|
| `--theme <light\|dark\|css>` | `light`, `dark` o un `.css` que reemplaza el tema | `light` |
|
|
@@ -175,6 +178,21 @@ equipo); `--port` fija el puerto y `--no-open` no abre el navegador.
|
|
|
175
178
|
El encabezado y el pie se dibujan dentro de las márgenes superior e inferior: con menos de
|
|
176
179
|
10 mm pueden tocar el texto y mdrender lo advierte.
|
|
177
180
|
|
|
181
|
+
**Tipografía**: el texto usa [Manrope](https://fonts.google.com/specimen/Manrope);
|
|
182
|
+
`--font inter` (o `font: inter` en el front matter o en `mdrender.config.json`) vuelve a
|
|
183
|
+
[Inter](https://fonts.google.com/specimen/Inter), la tipografía de las versiones anteriores.
|
|
184
|
+
Ambas van incrustadas en el PDF, también en el encabezado, el pie y los diagramas. Manrope no
|
|
185
|
+
tiene cursivas (el navegador inclina la normal) ni algunos símbolos como `→`, `✓` o `≤`, que
|
|
186
|
+
se toman de Inter.
|
|
187
|
+
|
|
188
|
+
**Párrafos justificados**: en el PDF, los párrafos del texto, de las citas y de las alertas
|
|
189
|
+
se justifican, y las palabras largas se parten con guion al final de la línea según el idioma
|
|
190
|
+
del documento (`lang`: español, inglés, portugués, francés, alemán o italiano; en otros idiomas
|
|
191
|
+
se justifica sin partir palabras). Listas, tablas, notas al pie, el índice y los diagramas
|
|
192
|
+
quedan alineados a la izquierda, igual que el HTML. El texto del PDF solo lleva el guion donde
|
|
193
|
+
la palabra se corta, así que copiar y buscar funcionan con las palabras completas.
|
|
194
|
+
`--no-justify` (o `justify: false`) vuelve a alinear a la izquierda.
|
|
195
|
+
|
|
178
196
|
**Aprovechar la página**: `--density compact` reduce interlineado, espacios entre párrafos y
|
|
179
197
|
títulos, y `--font-size 10pt` achica el texto (los diagramas Mermaid siguen el mismo tamaño).
|
|
180
198
|
Juntos ahorran entre un 15 y un 25 % de páginas. Además, siempre:
|
|
@@ -318,6 +336,6 @@ cambios: [CHANGELOG.md](CHANGELOG.md).
|
|
|
318
336
|
|
|
319
337
|
## Licencia
|
|
320
338
|
|
|
321
|
-
[MIT](LICENSE). Inter y JetBrains Mono se distribuyen bajo la
|
|
339
|
+
[MIT](LICENSE). Manrope, Inter y JetBrains Mono se distribuyen bajo la
|
|
322
340
|
[SIL Open Font License 1.1](https://openfontlicense.org); los ejecutables incluyen
|
|
323
341
|
`THIRD-PARTY-NOTICES.txt` con las licencias de todos los componentes.
|
package/dist/assets/theme.css
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
/* mdrender light theme — Inter for text, JetBrains Mono for code. */
|
|
1
|
+
/* mdrender light theme — Manrope (or Inter) for text, JetBrains Mono for code. */
|
|
2
2
|
|
|
3
3
|
:root {
|
|
4
|
-
--font
|
|
4
|
+
/* mdrender sets both after the theme for the chosen --font (render/fonts.ts). */
|
|
5
|
+
--font-text: "Manrope", "Inter", system-ui, sans-serif;
|
|
5
6
|
/* Inter after the mono font covers symbols (arrows, math) missing from the code font. */
|
|
6
7
|
--font-code: "JetBrains Mono", "Inter", ui-monospace, monospace;
|
|
7
8
|
--color-text: #1f2328;
|
package/dist/assets.d.ts
CHANGED
|
@@ -31,6 +31,8 @@ export declare const INTER_FACES: readonly [{
|
|
|
31
31
|
readonly style: "italic";
|
|
32
32
|
readonly file: "Inter-BoldItalic.woff2";
|
|
33
33
|
}];
|
|
34
|
+
/** Manrope faces in use (Fontsource, split into unicode-range subsets); it has no italics. */
|
|
35
|
+
export declare const MANROPE_STYLESHEETS: string[];
|
|
34
36
|
export declare const MONO_STYLESHEETS: string[];
|
|
35
37
|
export declare const KATEX_CSS = "katex/dist/katex.min.css";
|
|
36
38
|
export declare const MERMAID_JS = "mermaid/dist/mermaid.min.js";
|
package/dist/assets.js
CHANGED
|
@@ -78,6 +78,12 @@ export const INTER_FACES = [
|
|
|
78
78
|
{ weight: 700, style: "normal", file: "Inter-Bold.woff2" },
|
|
79
79
|
{ weight: 700, style: "italic", file: "Inter-BoldItalic.woff2" },
|
|
80
80
|
];
|
|
81
|
+
/** Manrope faces in use (Fontsource, split into unicode-range subsets); it has no italics. */
|
|
82
|
+
export const MANROPE_STYLESHEETS = [
|
|
83
|
+
"@fontsource/manrope/400.css",
|
|
84
|
+
"@fontsource/manrope/600.css",
|
|
85
|
+
"@fontsource/manrope/700.css",
|
|
86
|
+
];
|
|
81
87
|
export const MONO_STYLESHEETS = [
|
|
82
88
|
"@fontsource/jetbrains-mono/400.css",
|
|
83
89
|
"@fontsource/jetbrains-mono/400-italic.css",
|
|
@@ -103,7 +109,7 @@ export function assetManifest() {
|
|
|
103
109
|
const keys = new Set([PACKAGE_JSON, ...THEMES, MERMAID_JS]);
|
|
104
110
|
for (const { file } of INTER_FACES)
|
|
105
111
|
keys.add(`${INTER_DIR}/${file}`);
|
|
106
|
-
for (const cssKey of [...MONO_STYLESHEETS, KATEX_CSS]) {
|
|
112
|
+
for (const cssKey of [...MANROPE_STYLESHEETS, ...MONO_STYLESHEETS, KATEX_CSS]) {
|
|
107
113
|
keys.add(cssKey);
|
|
108
114
|
for (const font of woff2References(cssKey, readTextAsset(cssKey)))
|
|
109
115
|
keys.add(font);
|
package/dist/cli.js
CHANGED
|
@@ -25,6 +25,8 @@ const DOCUMENT_FLAGS = [
|
|
|
25
25
|
"header",
|
|
26
26
|
"footer",
|
|
27
27
|
"pageNumbers",
|
|
28
|
+
"font",
|
|
29
|
+
"justify",
|
|
28
30
|
"density",
|
|
29
31
|
"fontSize",
|
|
30
32
|
"css",
|
|
@@ -35,6 +37,8 @@ const DOCUMENT_FLAGS = [
|
|
|
35
37
|
"lang",
|
|
36
38
|
"title",
|
|
37
39
|
];
|
|
40
|
+
/** Options set on the command line only through their --no-… form. */
|
|
41
|
+
const NEGATED_FLAGS = new Set(["pageNumbers", "mermaidAutofit", "justify"]);
|
|
38
42
|
const toFlag = (key) => "--" + key.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase());
|
|
39
43
|
export const defaultIo = {
|
|
40
44
|
out: (text) => process.stdout.write(text),
|
|
@@ -57,6 +61,8 @@ function addDocumentOptions(command) {
|
|
|
57
61
|
.option("--header <text>", 'page header, e.g. "{title} | | {date}"')
|
|
58
62
|
.option("--footer <text>", 'page footer (default: "{page} / {pages}")')
|
|
59
63
|
.option("--no-page-numbers", "no default page-number footer")
|
|
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")
|
|
60
66
|
.option("--density <density>", "normal or compact (tighter spacing) (default: normal)")
|
|
61
67
|
.option("--font-size <size>", "text size, e.g. 10pt or 11pt (default: 10.5pt)")
|
|
62
68
|
.option("--theme <theme>", "light, dark, or a .css file replacing the theme (default: light)")
|
|
@@ -77,7 +83,7 @@ function configure(command, io) {
|
|
|
77
83
|
function buildRenderProgram(io) {
|
|
78
84
|
const program = new Command()
|
|
79
85
|
.name("mdrender")
|
|
80
|
-
.description("Render Markdown files to PDF (or self-contained HTML)
|
|
86
|
+
.description("Render Markdown files to PDF (or self-contained HTML) set in Manrope (or Inter).")
|
|
81
87
|
.version(version, "-V, --version")
|
|
82
88
|
.argument("<inputs...>", "Markdown files, folders or glob patterns (e.g. docs/**/*.md)")
|
|
83
89
|
.option("-o, --output <path>", "output file, or folder for several inputs")
|
|
@@ -115,11 +121,7 @@ function cliOverrides(program) {
|
|
|
115
121
|
if (program.getOptionValueSource(key) === "cli")
|
|
116
122
|
raw[key] = program.getOptionValue(key);
|
|
117
123
|
}
|
|
118
|
-
return normalizeOptions(raw, "command line", process.cwd(), (key) => key
|
|
119
|
-
? "--no-page-numbers"
|
|
120
|
-
: key === "mermaidAutofit"
|
|
121
|
-
? "--no-mermaid-autofit"
|
|
122
|
-
: toFlag(key));
|
|
124
|
+
return normalizeOptions(raw, "command line", process.cwd(), (key) => NEGATED_FLAGS.has(key) ? `--no-${toFlag(key).slice(2)}` : toFlag(key));
|
|
123
125
|
}
|
|
124
126
|
function settings(program, options) {
|
|
125
127
|
return {
|
package/dist/convert.js
CHANGED
|
@@ -12,7 +12,8 @@ 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 {
|
|
15
|
+
import { FONT_FAMILIES, fontStack } from "./render/fonts.js";
|
|
16
|
+
import { contentHeightPx, contentWidthPx, lengthToPx, textWidthPx } from "./render/page.js";
|
|
16
17
|
import { buildHtmlDocument, builtinThemeCss, densityCss } from "./render/template.js";
|
|
17
18
|
import { writeFileAtomic } from "./write.js";
|
|
18
19
|
/** Starts one browser on first use and keeps it for later conversions. */
|
|
@@ -79,10 +80,33 @@ export function headerFooterSpaceWarnings(margin, hasHeader, hasFooter) {
|
|
|
79
80
|
}
|
|
80
81
|
return warnings;
|
|
81
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Justified paragraphs in the PDF: the body's, and those directly inside a quote or an alert
|
|
85
|
+
* (the same ones markdown/hyphenate.ts gives soft hyphens). Lists, tables, footnotes, the title
|
|
86
|
+
* block, the table of contents and diagram labels stay aligned left. Print only: on screen the
|
|
87
|
+
* HTML keeps ragged lines.
|
|
88
|
+
*/
|
|
89
|
+
const JUSTIFY_CSS = `@media print {
|
|
90
|
+
.markdown-body > p,
|
|
91
|
+
.markdown-body > blockquote > p:not(.markdown-alert-title) {
|
|
92
|
+
text-align: justify;
|
|
93
|
+
}
|
|
94
|
+
}`;
|
|
82
95
|
/** Font size, if set, and the page's text height (used to fit images and diagrams). */
|
|
83
96
|
function layoutCss(options, margin) {
|
|
84
97
|
const height = contentHeightPx(options.pageSize, options.orientation === "landscape", margin);
|
|
85
|
-
const
|
|
98
|
+
const family = FONT_FAMILIES[options.font];
|
|
99
|
+
const rules = [
|
|
100
|
+
`:root { --page-content-height: ${height}px; }`,
|
|
101
|
+
`:root { --font-text: ${fontStack(options.font)}; ` +
|
|
102
|
+
`--font-code: "JetBrains Mono", "${family}", "Inter", ui-monospace, monospace; }`,
|
|
103
|
+
];
|
|
104
|
+
// Manrope's glyphs are taller than the headings' 1.25 line height (about 1.37 em). When a
|
|
105
|
+
// heading is pushed to the next page, Chrome then repeats its text in the PDF bookmark.
|
|
106
|
+
if (options.font === "manrope")
|
|
107
|
+
rules.push("h1, h2, h3, h4, h5, h6 { line-height: 1.4; }");
|
|
108
|
+
if (options.justify)
|
|
109
|
+
rules.push(JUSTIFY_CSS);
|
|
86
110
|
if (options.fontSize)
|
|
87
111
|
rules.push(`html { font-size: ${options.fontSize}; }`);
|
|
88
112
|
return rules.join("\n");
|
|
@@ -125,7 +149,12 @@ paged = false) {
|
|
|
125
149
|
const margin = resolveMargins(options);
|
|
126
150
|
const themeCss = themeStylesheets(options.theme);
|
|
127
151
|
const extraCss = options.css.map(readStylesheet);
|
|
128
|
-
const md = renderMarkdown(frontMatter.body, {
|
|
152
|
+
const md = renderMarkdown(frontMatter.body, {
|
|
153
|
+
baseDir,
|
|
154
|
+
lang: options.lang,
|
|
155
|
+
// Soft hyphens only where paragraphs are justified: in the PDF.
|
|
156
|
+
hyphenate: paged && options.justify ? options.lang : undefined,
|
|
157
|
+
});
|
|
129
158
|
const diagnostics = [
|
|
130
159
|
...config.warnings.map((message) => ({ severity: "warning", message })),
|
|
131
160
|
...md.diagnostics,
|
|
@@ -158,6 +187,7 @@ paged = false) {
|
|
|
158
187
|
: Infinity,
|
|
159
188
|
autofit: options.mermaidAutofit,
|
|
160
189
|
flip: options.mermaidFlip,
|
|
190
|
+
font: options.font,
|
|
161
191
|
});
|
|
162
192
|
const filled = fillMermaid(body, md.nonce, md.mermaid, results);
|
|
163
193
|
body = filled.html;
|
|
@@ -188,6 +218,7 @@ paged = false) {
|
|
|
188
218
|
date: options.date,
|
|
189
219
|
showTitle: options.title !== undefined,
|
|
190
220
|
themeCss,
|
|
221
|
+
font: options.font,
|
|
191
222
|
layoutCss: [...densityCss(options.density), layoutCss(options, margin)],
|
|
192
223
|
extraCss,
|
|
193
224
|
});
|
|
@@ -218,10 +249,15 @@ export async function convertFile(convert) {
|
|
|
218
249
|
landscape: options.orientation === "landscape",
|
|
219
250
|
margin,
|
|
220
251
|
headerTemplate: hasHeaderFooter
|
|
221
|
-
? headerFooterTemplate(options.header, values, margin)
|
|
252
|
+
? headerFooterTemplate(options.header, values, margin, options.font)
|
|
253
|
+
: undefined,
|
|
254
|
+
footerTemplate: hasHeaderFooter
|
|
255
|
+
? headerFooterTemplate(footer, values, margin, options.font)
|
|
222
256
|
: undefined,
|
|
223
|
-
footerTemplate: hasHeaderFooter ? headerFooterTemplate(footer, values, margin) : undefined,
|
|
224
257
|
allowRemote: convert.allowRemote,
|
|
258
|
+
softHyphensWidthPx: options.justify
|
|
259
|
+
? textWidthPx(options.pageSize, options.orientation === "landscape", margin)
|
|
260
|
+
: undefined,
|
|
225
261
|
});
|
|
226
262
|
for (const url of blocked) {
|
|
227
263
|
const hint = /^https?:/i.test(url) ? " (use --allow-remote to download it)" : "";
|
package/dist/doctor.js
CHANGED
|
@@ -135,8 +135,10 @@ function settingsCheck(options) {
|
|
|
135
135
|
name: "Settings",
|
|
136
136
|
status: "info",
|
|
137
137
|
detail: `${options.pageSize} ${options.orientation}, margins ${m.top} ${m.right} ${m.bottom} ` +
|
|
138
|
-
`${m.left} (top right bottom left), ${options.
|
|
139
|
-
`${options.fontSize ?? DEFAULT_FONT_SIZE}
|
|
138
|
+
`${m.left} (top right bottom left), ${options.font} ` +
|
|
139
|
+
`${options.fontSize ?? DEFAULT_FONT_SIZE}, ${options.density} density, ` +
|
|
140
|
+
`${options.justify ? "justified" : "ragged"} paragraphs, ` +
|
|
141
|
+
`language ${options.lang}, theme ` +
|
|
140
142
|
path.basename(options.theme),
|
|
141
143
|
};
|
|
142
144
|
}
|
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
|
@@ -14,6 +14,8 @@ 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
16
|
export declare const BUILTIN_THEMES: readonly ["light", "dark"];
|
|
17
|
+
export declare const TEXT_FONTS: readonly ["manrope", "inter"];
|
|
18
|
+
export type TextFont = (typeof TEXT_FONTS)[number];
|
|
17
19
|
export declare const DENSITIES: readonly ["normal", "compact"];
|
|
18
20
|
export type Density = (typeof DENSITIES)[number];
|
|
19
21
|
/** Rendering options, merged from defaults < config file < front matter < command line. */
|
|
@@ -41,6 +43,10 @@ export interface DocumentOptions {
|
|
|
41
43
|
mermaidAutofit: boolean;
|
|
42
44
|
/** Whether autofit may turn a flowchart to the other axis (LR ↔ TB) when it reads far better. */
|
|
43
45
|
mermaidFlip: MermaidFlip;
|
|
46
|
+
/** Justify paragraphs in the PDF (body, quotes, alerts), breaking long words with hyphens. */
|
|
47
|
+
justify: boolean;
|
|
48
|
+
/** Typeface of the text: Manrope or Inter (code is always JetBrains Mono). */
|
|
49
|
+
font: TextFont;
|
|
44
50
|
/** Vertical spacing: "compact" tightens line height, paragraph and heading spacing. */
|
|
45
51
|
density: Density;
|
|
46
52
|
/** Base font size of the text, e.g. "10pt"; when unset, the theme's (10.5pt). */
|
package/dist/options.js
CHANGED
|
@@ -7,6 +7,7 @@ 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
9
|
export const BUILTIN_THEMES = ["light", "dark"];
|
|
10
|
+
export const TEXT_FONTS = ["manrope", "inter"];
|
|
10
11
|
export const DENSITIES = ["normal", "compact"];
|
|
11
12
|
export const DEFAULT_OPTIONS = {
|
|
12
13
|
pageSize: "Letter",
|
|
@@ -17,6 +18,8 @@ export const DEFAULT_OPTIONS = {
|
|
|
17
18
|
pageNumbers: true,
|
|
18
19
|
mermaidAutofit: true,
|
|
19
20
|
mermaidFlip: "auto",
|
|
21
|
+
font: "manrope",
|
|
22
|
+
justify: true,
|
|
20
23
|
density: "normal",
|
|
21
24
|
theme: "light",
|
|
22
25
|
css: [],
|
|
@@ -115,6 +118,8 @@ const VALIDATORS = {
|
|
|
115
118
|
mermaidTheme: oneOf(MERMAID_THEMES),
|
|
116
119
|
mermaidAutofit: bool,
|
|
117
120
|
mermaidFlip: oneOf(MERMAID_FLIPS),
|
|
121
|
+
font: oneOf(TEXT_FONTS),
|
|
122
|
+
justify: bool,
|
|
118
123
|
density: oneOf(DENSITIES),
|
|
119
124
|
fontSize,
|
|
120
125
|
theme: (value, describe, baseDir) => {
|
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
|
@@ -1,10 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import type { TextFont } from "../options.js";
|
|
2
|
+
/**
|
|
3
|
+
* Static (non-variable) font files are used on purpose: Chromium embeds variable fonts in PDFs
|
|
4
|
+
* as Type 3 fonts, while static fonts are embedded as proper TrueType fonts.
|
|
5
|
+
*
|
|
6
|
+
* Text: Manrope (default) from Fontsource, or Inter 4 from the upstream release (full glyph
|
|
7
|
+
* set). Manrope covers Latin, Greek, Cyrillic and Vietnamese but has no italics (the browser
|
|
8
|
+
* slants it) nor arrows and math symbols such as → ✓ ≤: those fall back to Inter, which is
|
|
9
|
+
* embedded too in its regular and bold weights.
|
|
10
|
+
* Code: JetBrains Mono from Fontsource (split into unicode-range subsets).
|
|
11
|
+
*/
|
|
12
|
+
export declare const FONT_FAMILIES: Record<TextFont, string>;
|
|
13
|
+
/** CSS font-family list for the text: the chosen font, Inter for missing glyphs, the system's. */
|
|
14
|
+
export declare function fontStack(font: TextFont): string;
|
|
3
15
|
/**
|
|
4
16
|
* Inline a Fontsource stylesheet: the woff2 source becomes a data URI (the legacy woff fallback is
|
|
5
17
|
* dropped) so the document needs no network or file access. `font-display: block` makes the
|
|
6
18
|
* browser wait for the real font instead of printing with a fallback.
|
|
7
19
|
*/
|
|
8
20
|
export declare function inlineFontStylesheet(cssKey: string): string;
|
|
9
|
-
/** @font-face rules for every
|
|
10
|
-
export declare function fontFaceCss(): string;
|
|
21
|
+
/** @font-face rules for every font a document uses, with the font files embedded. */
|
|
22
|
+
export declare function fontFaceCss(font?: TextFont): string;
|
|
23
|
+
/** The text font's regular weight alone, for page headers/footers (rendered apart from the page). */
|
|
24
|
+
export declare function regularFontFace(font?: TextFont): string;
|
package/dist/render/fonts.js
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
|
-
import { INTER_DIR, INTER_FACES, MONO_STYLESHEETS, readAsset, readTextAsset, woff2References, } from "../assets.js";
|
|
1
|
+
import { INTER_DIR, INTER_FACES, MANROPE_STYLESHEETS, MONO_STYLESHEETS, readAsset, readTextAsset, woff2References, } from "../assets.js";
|
|
2
2
|
/**
|
|
3
3
|
* Static (non-variable) font files are used on purpose: Chromium embeds variable fonts in PDFs
|
|
4
4
|
* as Type 3 fonts, while static fonts are embedded as proper TrueType fonts.
|
|
5
5
|
*
|
|
6
|
-
* Text:
|
|
6
|
+
* Text: Manrope (default) from Fontsource, or Inter 4 from the upstream release (full glyph
|
|
7
|
+
* set). Manrope covers Latin, Greek, Cyrillic and Vietnamese but has no italics (the browser
|
|
8
|
+
* slants it) nor arrows and math symbols such as → ✓ ≤: those fall back to Inter, which is
|
|
9
|
+
* embedded too in its regular and bold weights.
|
|
7
10
|
* Code: JetBrains Mono from Fontsource (split into unicode-range subsets).
|
|
8
11
|
*/
|
|
12
|
+
export const FONT_FAMILIES = { manrope: "Manrope", inter: "Inter" };
|
|
13
|
+
/** CSS font-family list for the text: the chosen font, Inter for missing glyphs, the system's. */
|
|
14
|
+
export function fontStack(font) {
|
|
15
|
+
return font === "inter"
|
|
16
|
+
? `"Inter", system-ui, sans-serif`
|
|
17
|
+
: `"Manrope", "Inter", system-ui, sans-serif`;
|
|
18
|
+
}
|
|
9
19
|
function woff2DataUri(key) {
|
|
10
20
|
return `data:font/woff2;base64,${readAsset(key).toString("base64")}`;
|
|
11
21
|
}
|
|
@@ -18,12 +28,6 @@ function interFace({ weight, style, file }) {
|
|
|
18
28
|
src: url(${woff2DataUri(`${INTER_DIR}/${file}`)}) format("woff2");
|
|
19
29
|
}`;
|
|
20
30
|
}
|
|
21
|
-
let regularFace;
|
|
22
|
-
/** Inter Regular alone, for page headers/footers (Chrome renders them apart from the page). */
|
|
23
|
-
export function interRegularFontFace() {
|
|
24
|
-
regularFace ??= interFace(INTER_FACES[0]);
|
|
25
|
-
return regularFace;
|
|
26
|
-
}
|
|
27
31
|
/**
|
|
28
32
|
* Inline a Fontsource stylesheet: the woff2 source becomes a data URI (the legacy woff fallback is
|
|
29
33
|
* dropped) so the document needs no network or file access. `font-display: block` makes the
|
|
@@ -38,10 +42,39 @@ export function inlineFontStylesheet(cssKey) {
|
|
|
38
42
|
.replace(/,\s*url\([^)]+\.woff\)\s*format\(['"]woff['"]\)/g, "")
|
|
39
43
|
.replace(/url\((['"]?)([^)'"]+\.woff2)\1\)/g, () => `url(${woff2DataUri(fonts[index++])})`);
|
|
40
44
|
}
|
|
41
|
-
|
|
42
|
-
/** @font-face rules for
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
const interUpright = (weight) => INTER_FACES.find((face) => face.weight === weight && face.style === "normal");
|
|
46
|
+
/** @font-face rules for the text font and its fallback, with the font files embedded. */
|
|
47
|
+
function textFaces(font) {
|
|
48
|
+
if (font === "inter")
|
|
49
|
+
return INTER_FACES.map(interFace);
|
|
50
|
+
return [
|
|
51
|
+
...MANROPE_STYLESHEETS.map(inlineFontStylesheet),
|
|
52
|
+
// Fallback for glyphs Manrope lacks; 600 text takes the nearest heavier face, 700.
|
|
53
|
+
interFace(interUpright(400)),
|
|
54
|
+
interFace(interUpright(700)),
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
const cached = new Map();
|
|
58
|
+
/** @font-face rules for every font a document uses, with the font files embedded. */
|
|
59
|
+
export function fontFaceCss(font = "manrope") {
|
|
60
|
+
let css = cached.get(font);
|
|
61
|
+
if (css === undefined) {
|
|
62
|
+
css = [...textFaces(font), ...MONO_STYLESHEETS.map(inlineFontStylesheet)].join("\n");
|
|
63
|
+
cached.set(font, css);
|
|
64
|
+
}
|
|
65
|
+
return css;
|
|
66
|
+
}
|
|
67
|
+
const regularFaces = new Map();
|
|
68
|
+
/** The text font's regular weight alone, for page headers/footers (rendered apart from the page). */
|
|
69
|
+
export function regularFontFace(font = "manrope") {
|
|
70
|
+
let css = regularFaces.get(font);
|
|
71
|
+
if (css === undefined) {
|
|
72
|
+
css =
|
|
73
|
+
font === "inter"
|
|
74
|
+
? interFace(interUpright(400))
|
|
75
|
+
: [inlineFontStylesheet(MANROPE_STYLESHEETS[0]), interFace(interUpright(400))].join("\n");
|
|
76
|
+
regularFaces.set(font, css);
|
|
77
|
+
}
|
|
78
|
+
return css;
|
|
46
79
|
}
|
|
47
80
|
//# sourceMappingURL=fonts.js.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TextFont } from "../options.js";
|
|
1
2
|
/**
|
|
2
3
|
* Header/footer text with placeholders, in up to three columns separated by `|`:
|
|
3
4
|
*
|
|
@@ -13,10 +14,10 @@ export interface HeaderFooterValues {
|
|
|
13
14
|
}
|
|
14
15
|
/**
|
|
15
16
|
* Chrome header/footer template. Chrome renders it apart from the page, so it carries its own
|
|
16
|
-
* styles and its own copy of
|
|
17
|
+
* styles and its own copy of the text font.
|
|
17
18
|
*/
|
|
18
19
|
export declare function headerFooterTemplate(spec: string | undefined, values: HeaderFooterValues, horizontalMargin: {
|
|
19
20
|
left: string;
|
|
20
21
|
right: string;
|
|
21
|
-
}): string;
|
|
22
|
+
}, font?: TextFont): string;
|
|
22
23
|
export declare const DEFAULT_FOOTER = "{page} / {pages}";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { escapeHtml } from "../html.js";
|
|
2
|
-
import {
|
|
2
|
+
import { fontStack, 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) => {
|
|
@@ -21,15 +21,15 @@ function columns(spec) {
|
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
23
|
* Chrome header/footer template. Chrome renders it apart from the page, so it carries its own
|
|
24
|
-
* styles and its own copy of
|
|
24
|
+
* styles and its own copy of the text font.
|
|
25
25
|
*/
|
|
26
|
-
export function headerFooterTemplate(spec, values, horizontalMargin) {
|
|
26
|
+
export function headerFooterTemplate(spec, values, horizontalMargin, font = "manrope") {
|
|
27
27
|
if (!spec)
|
|
28
28
|
return "<span></span>";
|
|
29
29
|
const [left, center, right] = columns(spec).map((c) => renderText(c, values));
|
|
30
|
-
return `<style>${
|
|
30
|
+
return `<style>${regularFontFace(font)}</style>
|
|
31
31
|
<div style="box-sizing: border-box; width: 100%; display: flex; padding: 0 ${horizontalMargin.right} 0 ${horizontalMargin.left};
|
|
32
|
-
font-family:
|
|
32
|
+
font-family: ${fontStack(font).replaceAll('"', "'")}; font-size: 8pt; color: #59636e;">
|
|
33
33
|
<span style="flex: 1; text-align: left;">${left}</span>
|
|
34
34
|
<span style="flex: 1; text-align: center;">${center}</span>
|
|
35
35
|
<span style="flex: 1; text-align: right;">${right}</span>
|
package/dist/render/mermaid.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Diagnostic } from "../diagnostics.js";
|
|
2
2
|
import type { MermaidBlock } from "../markdown/env.js";
|
|
3
|
-
import type { MermaidFlip, MermaidTheme } from "../options.js";
|
|
3
|
+
import type { MermaidFlip, MermaidTheme, TextFont } from "../options.js";
|
|
4
4
|
import type { BrowserSession } from "./browser.js";
|
|
5
5
|
export { MERMAID_THEMES, type MermaidTheme } from "../options.js";
|
|
6
6
|
/** How autofit laid out a flowchart (see render/mermaid-fit.ts). */
|
|
@@ -37,6 +37,8 @@ export interface MermaidRenderOptions {
|
|
|
37
37
|
/** Search label wrapping widths (and, with `flip`, the other axis) for each flowchart. */
|
|
38
38
|
autofit?: boolean;
|
|
39
39
|
flip?: MermaidFlip;
|
|
40
|
+
/** Text font of the document, used for diagram labels too. */
|
|
41
|
+
font?: TextFont;
|
|
40
42
|
}
|
|
41
43
|
/**
|
|
42
44
|
* Room kept for a heading printed together with the diagram below it (see keep-with-heading in
|
package/dist/render/mermaid.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MERMAID_JS, readTextAsset } from "../assets.js";
|
|
2
2
|
import { mermaidPlaceholder } from "../markdown/mermaid.js";
|
|
3
|
-
import { fontFaceCss } from "./fonts.js";
|
|
3
|
+
import { FONT_FAMILIES, fontFaceCss, fontStack } from "./fonts.js";
|
|
4
4
|
import { escapeHtml } from "../html.js";
|
|
5
5
|
import { authorConfigured, candidate, chooseCandidate, flipWorthTrying, flippedDirection, flowchartHeader, withDirection, WRAPPING_WIDTHS, wrapsExhausted, } from "./mermaid-fit.js";
|
|
6
6
|
export { MERMAID_THEMES } from "../options.js";
|
|
@@ -14,15 +14,16 @@ const DEFAULT_FONT_PX = 14;
|
|
|
14
14
|
*/
|
|
15
15
|
export const HEADING_ALLOWANCE_EM = 5;
|
|
16
16
|
let mermaidJs;
|
|
17
|
-
function mermaidConfig(theme, fontPx, wrap) {
|
|
17
|
+
function mermaidConfig(theme, font, fontPx, wrap) {
|
|
18
|
+
const fontFamily = fontStack(font);
|
|
18
19
|
return {
|
|
19
20
|
startOnLoad: false,
|
|
20
21
|
securityLevel: "strict",
|
|
21
22
|
theme,
|
|
22
|
-
fontFamily
|
|
23
|
+
fontFamily,
|
|
23
24
|
// Sequence diagrams take their text sizes from here, the others from themeVariables.
|
|
24
25
|
fontSize: fontPx,
|
|
25
|
-
themeVariables: { fontFamily
|
|
26
|
+
themeVariables: { fontFamily, fontSize: `${fontPx}px` },
|
|
26
27
|
htmlLabels: true,
|
|
27
28
|
// Actors only at the top of sequence diagrams: repeating them below costs height.
|
|
28
29
|
sequence: { mirrorActors: false },
|
|
@@ -35,20 +36,21 @@ function mermaidConfig(theme, fontPx, wrap) {
|
|
|
35
36
|
* flowcharts are rendered in several layouts and the one whose text prints largest is kept.
|
|
36
37
|
*/
|
|
37
38
|
export async function renderMermaid(session, blocks, options = {}) {
|
|
38
|
-
const { theme = "neutral", widthPx = DEFAULT_WIDTH_PX, fontPx = DEFAULT_FONT_PX, pageHeightPx = Infinity, autofit = true, flip = "auto", } = options;
|
|
39
|
+
const { theme = "neutral", widthPx = DEFAULT_WIDTH_PX, fontPx = DEFAULT_FONT_PX, pageHeightPx = Infinity, autofit = true, flip = "auto", font = "manrope", } = options;
|
|
40
|
+
const family = FONT_FAMILIES[font];
|
|
39
41
|
mermaidJs ??= readTextAsset(MERMAID_JS);
|
|
40
42
|
const page = await session.newPage();
|
|
41
43
|
try {
|
|
42
|
-
await page.setContent(`<!doctype html><html><head><meta charset="utf-8"><style>${fontFaceCss()}
|
|
43
|
-
body { margin: 0; width: ${widthPx}px; font-family:
|
|
44
|
+
await page.setContent(`<!doctype html><html><head><meta charset="utf-8"><style>${fontFaceCss(font)}
|
|
45
|
+
body { margin: 0; width: ${widthPx}px; font-family: ${fontStack(font)}; }</style>
|
|
44
46
|
</head><body></body></html>`);
|
|
45
47
|
await page.evaluate(mermaidJs);
|
|
46
|
-
await page.evaluate(async () => {
|
|
48
|
+
await page.evaluate(async (family) => {
|
|
47
49
|
// Runs in the browser. The project is compiled without DOM types, hence PageGlobals.
|
|
48
50
|
const { document } = globalThis;
|
|
49
51
|
// Load the font weights Mermaid uses before it measures any text.
|
|
50
|
-
await Promise.all(["400", "600", "700"].map((w) => document.fonts.load(`${w} 16px
|
|
51
|
-
});
|
|
52
|
+
await Promise.all(["400", "600", "700"].map((w) => document.fonts.load(`${w} 16px "${family}"`)));
|
|
53
|
+
}, family);
|
|
52
54
|
const render = (source, id, wrap) => page.evaluate(async (source, id, config) => {
|
|
53
55
|
const { mermaid } = globalThis;
|
|
54
56
|
// Each render sets the whole configuration, so one layout never leaks into the next.
|
|
@@ -59,7 +61,7 @@ body { margin: 0; width: ${widthPx}px; font-family: "Inter", sans-serif; }</styl
|
|
|
59
61
|
catch (error) {
|
|
60
62
|
return { error: String(error?.message ?? error) };
|
|
61
63
|
}
|
|
62
|
-
}, source, id, mermaidConfig(theme, fontPx, wrap));
|
|
64
|
+
}, source, id, mermaidConfig(theme, font, fontPx, wrap));
|
|
63
65
|
const results = [];
|
|
64
66
|
for (const [i, block] of blocks.entries()) {
|
|
65
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,4 +1,5 @@
|
|
|
1
1
|
import { escapeHtml } from "../html.js";
|
|
2
|
+
import type { TextFont } from "../options.js";
|
|
2
3
|
export { escapeHtml };
|
|
3
4
|
/** Stylesheets of a built-in theme ("light" or "dark"). */
|
|
4
5
|
export declare function builtinThemeCss(theme: "light" | "dark"): string[];
|
|
@@ -17,6 +18,8 @@ export interface DocumentParts {
|
|
|
17
18
|
showTitle?: boolean;
|
|
18
19
|
/** Theme stylesheet contents. Defaults to the built-in light theme. */
|
|
19
20
|
themeCss?: string[];
|
|
21
|
+
/** Text font to embed. Defaults to Manrope. */
|
|
22
|
+
font?: TextFont;
|
|
20
23
|
/** Layout stylesheets (density, font size, page height), applied after the theme. */
|
|
21
24
|
layoutCss?: string[];
|
|
22
25
|
/** Extra stylesheet contents, applied after the theme and layout. */
|
package/dist/render/template.js
CHANGED
|
@@ -38,7 +38,7 @@ export function buildHtmlDocument(parts) {
|
|
|
38
38
|
const { title, body, math = false, lang, author, layoutCss = [], extraCss = [] } = parts;
|
|
39
39
|
const themeCss = parts.themeCss ?? builtinThemeCss("light");
|
|
40
40
|
const styles = [
|
|
41
|
-
fontFaceCss(),
|
|
41
|
+
fontFaceCss(parts.font),
|
|
42
42
|
...(math ? [katexCss()] : []),
|
|
43
43
|
...themeCss,
|
|
44
44
|
...layoutCss,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imontene/mdrender",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Command-line app for Windows and Linux that renders Markdown — with LaTeX equations and Mermaid diagrams — to PDF and HTML
|
|
3
|
+
"version": "2.4.0",
|
|
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",
|
|
7
7
|
"pdf",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"latex",
|
|
12
12
|
"cli",
|
|
13
13
|
"inter",
|
|
14
|
+
"manrope",
|
|
14
15
|
"converter"
|
|
15
16
|
],
|
|
16
17
|
"homepage": "https://github.com/imontene/MD_reder#readme",
|
|
@@ -65,10 +66,12 @@
|
|
|
65
66
|
},
|
|
66
67
|
"dependencies": {
|
|
67
68
|
"@fontsource/jetbrains-mono": "^5.3.0",
|
|
69
|
+
"@fontsource/manrope": "^5.3.0",
|
|
68
70
|
"@puppeteer/browsers": "^3.2.3",
|
|
69
71
|
"chokidar": "^5.0.0",
|
|
70
72
|
"commander": "^15.0.0",
|
|
71
73
|
"highlight.js": "^11.12.0",
|
|
74
|
+
"hyphen": "^1.14.1",
|
|
72
75
|
"inter-ui": "^4.1.1",
|
|
73
76
|
"katex": "^0.19.0",
|
|
74
77
|
"markdown-it": "^15.0.2",
|
|
@@ -81,6 +84,7 @@
|
|
|
81
84
|
},
|
|
82
85
|
"devDependencies": {
|
|
83
86
|
"@eslint/js": "^10.0.1",
|
|
87
|
+
"@types/hyphen": "^1.14.0",
|
|
84
88
|
"@types/node": "^22.20.5",
|
|
85
89
|
"@vitest/coverage-v8": "^5.0.3",
|
|
86
90
|
"esbuild": "^0.28.2",
|