@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 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
- - 🔤 **Inter** para el texto y **JetBrains Mono** para el código, incrustadas en el PDF.
18
+ - 🔤 **Manrope** para el texto (o **Inter** con `--font inter`) y **JetBrains Mono** para el
19
+ código, incrustadas en el PDF.
19
20
  - 🧩 **GFM**: tablas, listas de tareas, tachado, notas al pie, alertas `> [!NOTE]`,
20
21
  resaltado de código para ~35 lenguajes.
21
22
  - 🖨️ Tamaño de página, márgenes, orientación, encabezado/pie con números de página,
@@ -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.
@@ -1,7 +1,8 @@
1
- /* mdrender light theme — Inter for text, JetBrains Mono for code. */
1
+ /* mdrender light theme — Manrope (or Inter) for text, JetBrains Mono for code. */
2
2
 
3
3
  :root {
4
- --font-text: "Inter", system-ui, sans-serif;
4
+ /* mdrender sets both after the theme for the chosen --font (render/fonts.ts). */
5
+ --font-text: "Manrope", "Inter", system-ui, sans-serif;
5
6
  /* Inter after the mono font covers symbols (arrows, math) missing from the code font. */
6
7
  --font-code: "JetBrains Mono", "Inter", ui-monospace, monospace;
7
8
  --color-text: #1f2328;
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) using the Inter typeface.")
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 === "pageNumbers"
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 { contentHeightPx, contentWidthPx, lengthToPx } from "./render/page.js";
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 rules = [`:root { --page-content-height: ${height}px; }`];
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, { baseDir, lang: options.lang });
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.density} density, ` +
139
- `${options.fontSize ?? DEFAULT_FONT_SIZE} text, language ${options.lang}, theme ` +
138
+ `${m.left} (top right bottom left), ${options.font} ` +
139
+ `${options.fontSize ?? DEFAULT_FONT_SIZE}, ${options.density} density, ` +
140
+ `${options.justify ? "justified" : "ragged"} paragraphs, ` +
141
+ `language ${options.lang}, theme ` +
140
142
  path.basename(options.theme),
141
143
  };
142
144
  }
@@ -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
@@ -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;
@@ -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) => {
@@ -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;
@@ -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,
@@ -1,10 +1,24 @@
1
- /** Inter Regular alone, for page headers/footers (Chrome renders them apart from the page). */
2
- export declare function interRegularFontFace(): string;
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 bundled font, with the font files embedded. */
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;
@@ -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: Inter 4 (the typeface served by Google Fonts) from the upstream release, full glyph set.
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
- let cached;
42
- /** @font-face rules for every bundled font, with the font files embedded. */
43
- export function fontFaceCss() {
44
- cached ??= [...INTER_FACES.map(interFace), ...MONO_STYLESHEETS.map(inlineFontStylesheet)].join("\n");
45
- return cached;
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 Inter.
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 { interRegularFontFace } from "./fonts.js";
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 Inter.
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>${interRegularFontFace()}</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: Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; font-size: 8pt; color: #59636e;">
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>
@@ -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
@@ -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: "Inter, sans-serif",
23
+ fontFamily,
23
24
  // Sequence diagrams take their text sizes from here, the others from themeVariables.
24
25
  fontSize: fontPx,
25
- themeVariables: { fontFamily: "Inter, sans-serif", fontSize: `${fontPx}px` },
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: "Inter", sans-serif; }</style>
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 Inter`)));
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}`;
@@ -1,6 +1,11 @@
1
1
  import type { PageSize } from "../options.js";
2
2
  export declare function lengthToPx(length: string): number;
3
- /** Width of the text column in CSS pixels, so diagrams are laid out at their printed size. */
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;
@@ -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
- /** Width of the text column in CSS pixels, so diagrams are laid out at their printed size. */
18
- export function contentWidthPx(pageSize, landscape, margin) {
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 Math.round(paperPx - lengthToPx(margin.left) - lengthToPx(margin.right));
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. */
@@ -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.2.0",
4
- "description": "Command-line app for Windows and Linux that renders Markdown — with LaTeX equations and Mermaid diagrams — to PDF and HTML using the Inter typeface",
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",