@imontene/mdrender 2.3.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,13 @@
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
+
3
11
  ## 2.3.0 — 2026-10-16
4
12
 
5
13
  - **Manrope es la nueva tipografía por defecto** del texto, el encabezado, el pie y los
package/README.md CHANGED
@@ -153,6 +153,7 @@ equipo); `--port` fija el puerto y `--no-open` no abre el navegador.
153
153
  | `--footer <texto>` | Pie de página (ver abajo) | `{page} / {pages}` |
154
154
  | `--no-page-numbers` | Sin el pie con números de página | — |
155
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 |
156
157
  | `--density <densidad>` | `normal` o `compact` (menos espacio entre líneas y bloques) | `normal` |
157
158
  | `--font-size <tamaño>` | Tamaño del texto en `pt` o `px`, de 6 a 24 pt; p. ej. `10pt` | `10.5pt` |
158
159
  | `--theme <light\|dark\|css>` | `light`, `dark` o un `.css` que reemplaza el tema | `light` |
@@ -184,6 +185,14 @@ Ambas van incrustadas en el PDF, también en el encabezado, el pie y los diagram
184
185
  tiene cursivas (el navegador inclina la normal) ni algunos símbolos como `→`, `✓` o `≤`, que
185
186
  se toman de Inter.
186
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
+
187
196
  **Aprovechar la página**: `--density compact` reduce interlineado, espacios entre párrafos y
188
197
  títulos, y `--font-size 10pt` achica el texto (los diagramas Mermaid siguen el mismo tamaño).
189
198
  Juntos ahorran entre un 15 y un 25 % de páginas. Además, siempre:
package/dist/cli.js CHANGED
@@ -26,6 +26,7 @@ const DOCUMENT_FLAGS = [
26
26
  "footer",
27
27
  "pageNumbers",
28
28
  "font",
29
+ "justify",
29
30
  "density",
30
31
  "fontSize",
31
32
  "css",
@@ -36,6 +37,8 @@ const DOCUMENT_FLAGS = [
36
37
  "lang",
37
38
  "title",
38
39
  ];
40
+ /** Options set on the command line only through their --no-… form. */
41
+ const NEGATED_FLAGS = new Set(["pageNumbers", "mermaidAutofit", "justify"]);
39
42
  const toFlag = (key) => "--" + key.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase());
40
43
  export const defaultIo = {
41
44
  out: (text) => process.stdout.write(text),
@@ -59,6 +62,7 @@ function addDocumentOptions(command) {
59
62
  .option("--footer <text>", 'page footer (default: "{page} / {pages}")')
60
63
  .option("--no-page-numbers", "no default page-number footer")
61
64
  .option("--font <font>", "text typeface: manrope or inter (default: manrope)")
65
+ .option("--no-justify", "align paragraphs left in the PDF instead of justifying them")
62
66
  .option("--density <density>", "normal or compact (tighter spacing) (default: normal)")
63
67
  .option("--font-size <size>", "text size, e.g. 10pt or 11pt (default: 10.5pt)")
64
68
  .option("--theme <theme>", "light, dark, or a .css file replacing the theme (default: light)")
@@ -117,11 +121,7 @@ function cliOverrides(program) {
117
121
  if (program.getOptionValueSource(key) === "cli")
118
122
  raw[key] = program.getOptionValue(key);
119
123
  }
120
- return normalizeOptions(raw, "command line", process.cwd(), (key) => key === "pageNumbers"
121
- ? "--no-page-numbers"
122
- : key === "mermaidAutofit"
123
- ? "--no-mermaid-autofit"
124
- : toFlag(key));
124
+ return normalizeOptions(raw, "command line", process.cwd(), (key) => NEGATED_FLAGS.has(key) ? `--no-${toFlag(key).slice(2)}` : toFlag(key));
125
125
  }
126
126
  function settings(program, options) {
127
127
  return {
package/dist/convert.js CHANGED
@@ -13,7 +13,7 @@ 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
15
  import { FONT_FAMILIES, fontStack } from "./render/fonts.js";
16
- import { contentHeightPx, contentWidthPx, lengthToPx } from "./render/page.js";
16
+ import { contentHeightPx, contentWidthPx, lengthToPx, textWidthPx } from "./render/page.js";
17
17
  import { buildHtmlDocument, builtinThemeCss, densityCss } from "./render/template.js";
18
18
  import { writeFileAtomic } from "./write.js";
19
19
  /** Starts one browser on first use and keeps it for later conversions. */
@@ -80,6 +80,18 @@ export function headerFooterSpaceWarnings(margin, hasHeader, hasFooter) {
80
80
  }
81
81
  return warnings;
82
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
+ }`;
83
95
  /** Font size, if set, and the page's text height (used to fit images and diagrams). */
84
96
  function layoutCss(options, margin) {
85
97
  const height = contentHeightPx(options.pageSize, options.orientation === "landscape", margin);
@@ -93,6 +105,8 @@ function layoutCss(options, margin) {
93
105
  // heading is pushed to the next page, Chrome then repeats its text in the PDF bookmark.
94
106
  if (options.font === "manrope")
95
107
  rules.push("h1, h2, h3, h4, h5, h6 { line-height: 1.4; }");
108
+ if (options.justify)
109
+ rules.push(JUSTIFY_CSS);
96
110
  if (options.fontSize)
97
111
  rules.push(`html { font-size: ${options.fontSize}; }`);
98
112
  return rules.join("\n");
@@ -135,7 +149,12 @@ paged = false) {
135
149
  const margin = resolveMargins(options);
136
150
  const themeCss = themeStylesheets(options.theme);
137
151
  const extraCss = options.css.map(readStylesheet);
138
- 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
+ });
139
158
  const diagnostics = [
140
159
  ...config.warnings.map((message) => ({ severity: "warning", message })),
141
160
  ...md.diagnostics,
@@ -236,6 +255,9 @@ export async function convertFile(convert) {
236
255
  ? headerFooterTemplate(footer, values, margin, options.font)
237
256
  : undefined,
238
257
  allowRemote: convert.allowRemote,
258
+ softHyphensWidthPx: options.justify
259
+ ? textWidthPx(options.pageSize, options.orientation === "landscape", margin)
260
+ : undefined,
239
261
  });
240
262
  for (const url of blocked) {
241
263
  const hint = /^https?:/i.test(url) ? " (use --allow-remote to download it)" : "";
package/dist/doctor.js CHANGED
@@ -137,6 +137,7 @@ function settingsCheck(options) {
137
137
  detail: `${options.pageSize} ${options.orientation}, margins ${m.top} ${m.right} ${m.bottom} ` +
138
138
  `${m.left} (top right bottom left), ${options.font} ` +
139
139
  `${options.fontSize ?? DEFAULT_FONT_SIZE}, ${options.density} density, ` +
140
+ `${options.justify ? "justified" : "ragged"} paragraphs, ` +
140
141
  `language ${options.lang}, theme ` +
141
142
  path.basename(options.theme),
142
143
  };
@@ -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
@@ -43,6 +43,8 @@ export interface DocumentOptions {
43
43
  mermaidAutofit: boolean;
44
44
  /** Whether autofit may turn a flowchart to the other axis (LR ↔ TB) when it reads far better. */
45
45
  mermaidFlip: MermaidFlip;
46
+ /** Justify paragraphs in the PDF (body, quotes, alerts), breaking long words with hyphens. */
47
+ justify: boolean;
46
48
  /** Typeface of the text: Manrope or Inter (code is always JetBrains Mono). */
47
49
  font: TextFont;
48
50
  /** Vertical spacing: "compact" tightens line height, paragraph and heading spacing. */
package/dist/options.js CHANGED
@@ -19,6 +19,7 @@ export const DEFAULT_OPTIONS = {
19
19
  mermaidAutofit: true,
20
20
  mermaidFlip: "auto",
21
21
  font: "manrope",
22
+ justify: true,
22
23
  density: "normal",
23
24
  theme: "light",
24
25
  css: [],
@@ -118,6 +119,7 @@ const VALIDATORS = {
118
119
  mermaidAutofit: bool,
119
120
  mermaidFlip: oneOf(MERMAID_FLIPS),
120
121
  font: oneOf(TEXT_FONTS),
122
+ justify: bool,
121
123
  density: oneOf(DENSITIES),
122
124
  fontSize,
123
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,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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imontene/mdrender",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "Command-line app for Windows and Linux that renders Markdown — with LaTeX equations and Mermaid diagrams — to PDF and HTML set in Manrope or Inter",
5
5
  "keywords": [
6
6
  "markdown",
@@ -71,6 +71,7 @@
71
71
  "chokidar": "^5.0.0",
72
72
  "commander": "^15.0.0",
73
73
  "highlight.js": "^11.12.0",
74
+ "hyphen": "^1.14.1",
74
75
  "inter-ui": "^4.1.1",
75
76
  "katex": "^0.19.0",
76
77
  "markdown-it": "^15.0.2",
@@ -83,6 +84,7 @@
83
84
  },
84
85
  "devDependencies": {
85
86
  "@eslint/js": "^10.0.1",
87
+ "@types/hyphen": "^1.14.0",
86
88
  "@types/node": "^22.20.5",
87
89
  "@vitest/coverage-v8": "^5.0.3",
88
90
  "esbuild": "^0.28.2",