@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 +8 -0
- package/README.md +9 -0
- package/dist/cli.js +5 -5
- package/dist/convert.js +24 -2
- package/dist/doctor.js +1 -0
- package/dist/markdown/env.d.ts +2 -0
- package/dist/markdown/hyphenate.d.ts +14 -0
- package/dist/markdown/hyphenate.js +92 -0
- package/dist/markdown/index.d.ts +2 -0
- package/dist/markdown/index.js +3 -0
- package/dist/options.d.ts +2 -0
- package/dist/options.js +2 -0
- package/dist/render/browser.d.ts +13 -0
- package/dist/render/browser.js +50 -0
- package/dist/render/page.d.ts +6 -1
- package/dist/render/page.js +7 -3
- package/package.json +3 -1
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
|
|
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, {
|
|
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
|
};
|
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
|
@@ -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) => {
|
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/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) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imontene/mdrender",
|
|
3
|
-
"version": "2.
|
|
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",
|