@imontene/mdrender 1.0.0 → 2.0.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 +24 -0
- package/README.md +49 -29
- package/dist/bin.js +6 -0
- package/dist/cli.js +14 -3
- package/dist/config.d.ts +7 -0
- package/dist/config.js +11 -2
- package/dist/convert.d.ts +7 -1
- package/dist/convert.js +45 -14
- package/dist/inputs.js +15 -2
- package/dist/options.d.ts +34 -8
- package/dist/options.js +82 -5
- package/dist/render/browser.d.ts +2 -1
- package/dist/render/browser.js +4 -4
- package/dist/write.d.ts +9 -0
- package/dist/write.js +41 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Cambios
|
|
2
2
|
|
|
3
|
+
## 2.0.0 — 2026-10-13
|
|
4
|
+
|
|
5
|
+
- Márgenes por lado: `--margin-top`, `--margin-right`, `--margin-bottom`, `--margin-left`
|
|
6
|
+
(también `marginTop`… en front matter y `mdrender.config.json`), por encima de `--margin`.
|
|
7
|
+
- Orientación con `--orientation portrait|landscape` (`--landscape` sigue funcionando).
|
|
8
|
+
- **Cambian los valores por defecto**: hoja **Letter** (antes A4) y márgenes de **10 mm**
|
|
9
|
+
(antes 20 mm). Para el formato anterior: `--page-size A4 --margin 20mm`, o ponerlo en
|
|
10
|
+
`mdrender.config.json`.
|
|
11
|
+
- El proceso de publicación falla al inicio, con un error claro, si falta `NPM_TOKEN`.
|
|
12
|
+
- Nunca sobrescribe el `.md` de entrada: `mdrender doc.md -o doc.md` termina con un error.
|
|
13
|
+
- Avisa de opciones desconocidas en `mdrender.config.json` y sugiere la correcta
|
|
14
|
+
(`"margn"` → `"margin"`).
|
|
15
|
+
- Escritura segura: el PDF/HTML se escribe en un archivo temporal y se renombra al final; un
|
|
16
|
+
fallo no deja archivos a medias y conserva la versión anterior. Mensaje claro si el archivo
|
|
17
|
+
está abierto en otro programa.
|
|
18
|
+
- Avisa si un encabezado o pie no cabe (margen superior o inferior menor de 10 mm).
|
|
19
|
+
|
|
20
|
+
## 1.0.1 — 2026-10-12
|
|
21
|
+
|
|
22
|
+
- Corrige un error EPIPE sin capturar cuando la salida se corta con una tubería
|
|
23
|
+
(por ejemplo `mdrender --help | head`); ahora el comando termina normalmente.
|
|
24
|
+
- El flujo de publicación se puede lanzar a mano con la versión
|
|
25
|
+
(Actions → Release → Run workflow).
|
|
26
|
+
|
|
3
27
|
## 1.0.0 — 2026-10-04
|
|
4
28
|
|
|
5
29
|
Primera versión estable.
|
package/README.md
CHANGED
|
@@ -73,7 +73,7 @@ npm link # deja disponible el comando `mdrender`
|
|
|
73
73
|
mdrender documento.md
|
|
74
74
|
|
|
75
75
|
# Elegir salida, tamaño de página y tabla de contenidos
|
|
76
|
-
mdrender documento.md -o salida/informe.pdf --page-size
|
|
76
|
+
mdrender documento.md -o salida/informe.pdf --page-size A4 --toc
|
|
77
77
|
|
|
78
78
|
# HTML autocontenido
|
|
79
79
|
mdrender documento.md --format html
|
|
@@ -109,30 +109,42 @@ equipo); `--port` fija el puerto y `--no-open` no abre el navegador.
|
|
|
109
109
|
|
|
110
110
|
### Opciones
|
|
111
111
|
|
|
112
|
-
| Opción
|
|
113
|
-
|
|
|
114
|
-
| `-o, --output <ruta>`
|
|
115
|
-
| `-f, --format <pdf\|html>`
|
|
116
|
-
| `--page-size <tamaño>`
|
|
117
|
-
| `--margin <valor>`
|
|
118
|
-
| `--
|
|
119
|
-
| `--
|
|
120
|
-
| `--
|
|
121
|
-
| `--
|
|
122
|
-
| `--
|
|
123
|
-
| `--
|
|
124
|
-
| `--
|
|
125
|
-
| `--
|
|
126
|
-
| `--
|
|
127
|
-
| `--
|
|
128
|
-
| `--
|
|
129
|
-
| `--
|
|
130
|
-
| `--
|
|
131
|
-
| `--
|
|
132
|
-
| `--
|
|
133
|
-
|
|
|
134
|
-
|
|
|
135
|
-
|
|
|
112
|
+
| Opción | Descripción | Por defecto |
|
|
113
|
+
| ----------------------------- | ----------------------------------------------------------- | ------------------ |
|
|
114
|
+
| `-o, --output <ruta>` | Archivo de salida, o carpeta con varios archivos | junto al `.md` |
|
|
115
|
+
| `-f, --format <pdf\|html>` | Formato de salida | `pdf` |
|
|
116
|
+
| `--page-size <tamaño>` | `Letter`, `A4`, `Legal` (también `A3`, `A5`, `Tabloid`) | `Letter` |
|
|
117
|
+
| `--margin <valor>` | Las cuatro márgenes como en CSS: `10mm`, `15mm 20mm`… | `10mm` |
|
|
118
|
+
| `--margin-top <medida>` | Margen superior, p. ej. `15mm` (prevalece sobre `--margin`) | `10mm` |
|
|
119
|
+
| `--margin-right <medida>` | Margen derecho | `10mm` |
|
|
120
|
+
| `--margin-bottom <medida>` | Margen inferior | `10mm` |
|
|
121
|
+
| `--margin-left <medida>` | Margen izquierdo | `10mm` |
|
|
122
|
+
| `--orientation <orientación>` | `portrait` (vertical) o `landscape` (horizontal) | `portrait` |
|
|
123
|
+
| `--landscape` | Atajo de `--orientation landscape` | — |
|
|
124
|
+
| `--toc` | Tabla de contenidos al inicio (o donde pongas `[[toc]]`) | — |
|
|
125
|
+
| `--toc-depth <n>` | Nivel de título más profundo en la tabla de contenidos | `3` |
|
|
126
|
+
| `--header <texto>` | Encabezado de página (ver abajo) | — |
|
|
127
|
+
| `--footer <texto>` | Pie de página (ver abajo) | `{page} / {pages}` |
|
|
128
|
+
| `--no-page-numbers` | Sin el pie con números de página | — |
|
|
129
|
+
| `--theme <light\|dark\|css>` | `light`, `dark` o un `.css` que reemplaza el tema | `light` |
|
|
130
|
+
| `--css <archivo.css>` | CSS adicional (se puede repetir) | — |
|
|
131
|
+
| `--mermaid-theme <tema>` | `default`, `neutral`, `dark`, `forest`, `base` | `neutral` / `dark` |
|
|
132
|
+
| `--lang <código>` | Idioma del documento; `en` para inglés (`pt`, `fr`, `de`…) | `es` |
|
|
133
|
+
| `--title <texto>` | Título del documento (muestra un bloque de título) | primer `# título` |
|
|
134
|
+
| `--config <archivo>` | Archivo de configuración | el más cercano |
|
|
135
|
+
| `--no-config` | Ignorar `mdrender.config.json` | — |
|
|
136
|
+
| `--browser <ruta>` | Ruta a Chrome/Edge/Chromium (o variable `MDRENDER_BROWSER`) | autodetección |
|
|
137
|
+
| `--allow-remote` | Permitir que el PDF descargue imágenes/CSS de internet | bloqueado |
|
|
138
|
+
| `-w, --watch` | Regenerar al guardar | — |
|
|
139
|
+
| `-q, --quiet` | Mostrar solo advertencias y errores | — |
|
|
140
|
+
| `-v, --verbose` | Mostrar también configuración, navegador y tiempos | — |
|
|
141
|
+
|
|
142
|
+
**Página**: por defecto hoja **Letter vertical con márgenes de 10 mm**. Las medidas aceptan
|
|
143
|
+
`mm`, `cm`, `in` o `px`. `--margin` fija las cuatro márgenes a la vez y `--margin-top`,
|
|
144
|
+
`--margin-right`, `--margin-bottom` y `--margin-left` ajustan una sola, por encima de
|
|
145
|
+
`--margin`. Ejemplo: `--page-size A4 --orientation landscape --margin 15mm --margin-left 25mm`.
|
|
146
|
+
El encabezado y el pie se dibujan dentro de las márgenes superior e inferior: con menos de
|
|
147
|
+
10 mm pueden tocar el texto y mdrender lo advierte.
|
|
136
148
|
|
|
137
149
|
**Encabezado y pie**: texto con marcadores `{page}`, `{pages}`, `{title}`, `{author}` y
|
|
138
150
|
`{date}`; `|` separa columnas izquierda, centro y derecha. Ejemplos:
|
|
@@ -151,7 +163,9 @@ subtitle: Resultados del trimestre
|
|
|
151
163
|
author: Ana Pérez
|
|
152
164
|
date: 2026-10-04
|
|
153
165
|
toc: true
|
|
154
|
-
page-size:
|
|
166
|
+
page-size: A4
|
|
167
|
+
orientation: landscape
|
|
168
|
+
margin-left: 25mm
|
|
155
169
|
header: "{title} | | {date}"
|
|
156
170
|
---
|
|
157
171
|
```
|
|
@@ -159,8 +173,9 @@ header: "{title} | | {date}"
|
|
|
159
173
|
```json
|
|
160
174
|
{
|
|
161
175
|
"lang": "es",
|
|
162
|
-
"pageSize": "
|
|
163
|
-
"margin": "
|
|
176
|
+
"pageSize": "Letter",
|
|
177
|
+
"margin": "15mm",
|
|
178
|
+
"marginLeft": "25mm",
|
|
164
179
|
"css": "estilos/empresa.css"
|
|
165
180
|
}
|
|
166
181
|
```
|
|
@@ -199,6 +214,10 @@ fecha). Las rutas de `css` y `theme` son relativas al archivo donde aparecen.
|
|
|
199
214
|
como imágenes referenciadas desde el `.md`.
|
|
200
215
|
- Mermaid corre en una página aparte, sin contenido del usuario, con `securityLevel: "strict"`.
|
|
201
216
|
- La vista previa solo escucha en `127.0.0.1`.
|
|
217
|
+
- Nunca sobrescribe un `.md` de entrada, y escribe cada PDF/HTML en un archivo temporal que
|
|
218
|
+
renombra al final: si algo falla, el archivo anterior queda intacto.
|
|
219
|
+
- Las opciones mal escritas en `mdrender.config.json` generan una advertencia con la opción
|
|
220
|
+
correcta sugerida.
|
|
202
221
|
- En Ubuntu 23.10+ el navegador descargado con `mdrender setup` no puede usar el sandbox de
|
|
203
222
|
Chrome (restricción de AppArmor); mdrender lo inicia entonces sin sandbox, lo que es seguro
|
|
204
223
|
aquí porque las páginas no ejecutan JavaScript del documento ni acceden a la red
|
|
@@ -231,7 +250,8 @@ El CI (GitHub Actions) prueba en **Windows y Linux** con Node 22 y 24, construye
|
|
|
231
250
|
ejecutable y valida `mdrender setup`. Para publicar una versión, sube una etiqueta `v*` o
|
|
232
251
|
lanza a mano **Actions → Release → Run workflow** con la versión (debe coincidir con
|
|
233
252
|
`package.json`): construye los ejecutables, crea la etiqueta y el GitHub Release con sus sumas
|
|
234
|
-
SHA-256, y publica en npm
|
|
253
|
+
SHA-256, y publica en npm. Requiere el secreto de repositorio `NPM_TOKEN`; si falta, la
|
|
254
|
+
publicación se detiene al inicio, antes de publicar nada.
|
|
235
255
|
|
|
236
256
|
Plan completo y decisiones de diseño: [docs/PLAN_MAESTRO.md](docs/PLAN_MAESTRO.md) ·
|
|
237
257
|
cambios: [CHANGELOG.md](CHANGELOG.md).
|
package/dist/bin.js
CHANGED
|
@@ -4,6 +4,12 @@ import { defaultIo, main } from "./cli.js";
|
|
|
4
4
|
const controller = new AbortController();
|
|
5
5
|
process.once("SIGINT", () => controller.abort());
|
|
6
6
|
process.once("SIGTERM", () => controller.abort());
|
|
7
|
+
// A reader that stops early (`mdrender --help | head`) closes the pipe: drop the rest of the
|
|
8
|
+
// output instead of crashing with an unhandled EPIPE.
|
|
9
|
+
process.stdout.on("error", (error) => {
|
|
10
|
+
if (error.code !== "EPIPE")
|
|
11
|
+
throw error;
|
|
12
|
+
});
|
|
7
13
|
main(process.argv.slice(2), defaultIo, { signal: controller.signal }).then((code) => {
|
|
8
14
|
process.exitCode = code;
|
|
9
15
|
// Leftover handles (e.g. a keep-alive proxy connection after `setup`) must not keep the CLI
|
package/dist/cli.js
CHANGED
|
@@ -12,7 +12,13 @@ import { watchAndConvert } from "./watch.js";
|
|
|
12
12
|
const DOCUMENT_FLAGS = [
|
|
13
13
|
"pageSize",
|
|
14
14
|
"margin",
|
|
15
|
+
"marginTop",
|
|
16
|
+
"marginRight",
|
|
17
|
+
"marginBottom",
|
|
18
|
+
"marginLeft",
|
|
19
|
+
// --landscape before --orientation so an explicit --orientation wins.
|
|
15
20
|
"landscape",
|
|
21
|
+
"orientation",
|
|
16
22
|
"toc",
|
|
17
23
|
"tocDepth",
|
|
18
24
|
"header",
|
|
@@ -33,9 +39,14 @@ const collect = (value, previous = []) => [...previous, value];
|
|
|
33
39
|
/** Options shared by `mdrender` (render) and `mdrender preview`. */
|
|
34
40
|
function addDocumentOptions(command) {
|
|
35
41
|
return command
|
|
36
|
-
.option("--page-size <size>", `paper size: ${PAGE_SIZES.join(", ")} (default:
|
|
37
|
-
.option("--
|
|
38
|
-
.option("--landscape", "
|
|
42
|
+
.option("--page-size <size>", `paper size: ${PAGE_SIZES.join(", ")} (default: Letter)`)
|
|
43
|
+
.option("--orientation <orientation>", "portrait or landscape (default: portrait)")
|
|
44
|
+
.option("--landscape", "same as --orientation landscape")
|
|
45
|
+
.option("--margin <margin>", 'all page margins, CSS-style: "10mm", "15mm 20mm"… (default: 10mm)')
|
|
46
|
+
.option("--margin-top <length>", "top margin, e.g. 15mm (overrides --margin)")
|
|
47
|
+
.option("--margin-right <length>", "right margin")
|
|
48
|
+
.option("--margin-bottom <length>", "bottom margin")
|
|
49
|
+
.option("--margin-left <length>", "left margin")
|
|
39
50
|
.option("--toc", "insert a table of contents (or place it with [[toc]])")
|
|
40
51
|
.option("--toc-depth <n>", "deepest heading level in the table of contents (default: 3)")
|
|
41
52
|
.option("--header <text>", 'page header, e.g. "{title} | | {date}"')
|
package/dist/config.d.ts
CHANGED
|
@@ -2,5 +2,12 @@ import { type DocumentOptions } from "./options.js";
|
|
|
2
2
|
export declare const CONFIG_FILE = "mdrender.config.json";
|
|
3
3
|
/** Find `mdrender.config.json` in `startDir` or the nearest parent directory. */
|
|
4
4
|
export declare function findConfigFile(startDir: string): string | undefined;
|
|
5
|
+
export interface LoadedConfig {
|
|
6
|
+
options: Partial<DocumentOptions>;
|
|
7
|
+
/** Problems that do not stop the conversion, such as misspelled option names. */
|
|
8
|
+
warnings: string[];
|
|
9
|
+
}
|
|
5
10
|
/** Read and validate a config file. Relative paths inside it resolve against its folder. */
|
|
6
11
|
export declare function loadConfig(file: string): Partial<DocumentOptions>;
|
|
12
|
+
/** Like `loadConfig`, also reporting unknown option names (likely typos). */
|
|
13
|
+
export declare function readConfig(file: string): LoadedConfig;
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { normalizeOptions, OptionsError } from "./options.js";
|
|
3
|
+
import { normalizeOptions, OptionsError, unknownOptions } from "./options.js";
|
|
4
4
|
export const CONFIG_FILE = "mdrender.config.json";
|
|
5
5
|
/** Find `mdrender.config.json` in `startDir` or the nearest parent directory. */
|
|
6
6
|
export function findConfigFile(startDir) {
|
|
@@ -17,6 +17,10 @@ export function findConfigFile(startDir) {
|
|
|
17
17
|
}
|
|
18
18
|
/** Read and validate a config file. Relative paths inside it resolve against its folder. */
|
|
19
19
|
export function loadConfig(file) {
|
|
20
|
+
return readConfig(file).options;
|
|
21
|
+
}
|
|
22
|
+
/** Like `loadConfig`, also reporting unknown option names (likely typos). */
|
|
23
|
+
export function readConfig(file) {
|
|
20
24
|
let raw;
|
|
21
25
|
try {
|
|
22
26
|
raw = JSON.parse(readFileSync(file, "utf8"));
|
|
@@ -27,6 +31,11 @@ export function loadConfig(file) {
|
|
|
27
31
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
28
32
|
throw new OptionsError(`${file}: the configuration must be a JSON object`);
|
|
29
33
|
}
|
|
30
|
-
|
|
34
|
+
const record = raw;
|
|
35
|
+
const relative = path.relative(process.cwd(), file);
|
|
36
|
+
const shown = relative && !relative.startsWith("..") && !path.isAbsolute(relative) ? relative : file;
|
|
37
|
+
const warnings = unknownOptions(record).map(({ key, suggestion }) => `${shown}: unknown option "${key}"` +
|
|
38
|
+
(suggestion ? ` (did you mean "${suggestion}"?)` : " (ignored)"));
|
|
39
|
+
return { options: normalizeOptions(record, file, path.dirname(file)), warnings };
|
|
31
40
|
}
|
|
32
41
|
//# sourceMappingURL=config.js.map
|
package/dist/convert.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Diagnostic } from "./diagnostics.js";
|
|
2
|
-
import { type DocumentOptions, type MermaidTheme } from "./options.js";
|
|
2
|
+
import { type DocumentOptions, type Margins, type MermaidTheme } from "./options.js";
|
|
3
3
|
import type { OutputFormat } from "./paths.js";
|
|
4
4
|
import { BrowserSession } from "./render/browser.js";
|
|
5
5
|
/** Settings shared by every file of a run (batch, watch or preview). */
|
|
@@ -53,6 +53,12 @@ export declare class BrowserPool {
|
|
|
53
53
|
get(): Promise<BrowserSession>;
|
|
54
54
|
close(): Promise<void>;
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Chrome draws the header and footer inside the top and bottom margins. Measured: below about
|
|
58
|
+
* 10 mm the header touches or overlaps the first line of text (and the footer the last line).
|
|
59
|
+
*/
|
|
60
|
+
export declare const MIN_HEADER_FOOTER_MARGIN = "10mm";
|
|
61
|
+
export declare function headerFooterSpaceWarnings(margin: Margins, hasHeader: boolean, hasFooter: boolean): Diagnostic[];
|
|
56
62
|
/** Mermaid theme: explicit choice, else dark diagrams for the dark page theme. */
|
|
57
63
|
export declare function effectiveMermaidTheme(options: DocumentOptions): MermaidTheme;
|
|
58
64
|
/**
|
package/dist/convert.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { findBrowser } from "./browser/detect.js";
|
|
5
|
-
import { findConfigFile,
|
|
5
|
+
import { findConfigFile, readConfig } from "./config.js";
|
|
6
6
|
import { extractFrontMatter } from "./frontmatter.js";
|
|
7
7
|
import { strings } from "./i18n.js";
|
|
8
8
|
import { renderMarkdown } from "./markdown/index.js";
|
|
9
9
|
import { buildToc, tocPlaceholder } from "./markdown/toc.js";
|
|
10
|
-
import { mergeOptions, normalizeOptions, OptionsError,
|
|
10
|
+
import { mergeOptions, normalizeOptions, OptionsError, resolveMargins, } from "./options.js";
|
|
11
11
|
import { BrowserSession } from "./render/browser.js";
|
|
12
12
|
import { DEFAULT_FOOTER, headerFooterTemplate } from "./render/header-footer.js";
|
|
13
13
|
import { fillMermaid, renderMermaid } from "./render/mermaid.js";
|
|
14
|
-
import { contentWidthPx } from "./render/page.js";
|
|
14
|
+
import { contentWidthPx, lengthToPx } from "./render/page.js";
|
|
15
15
|
import { buildHtmlDocument, builtinThemeCss } from "./render/template.js";
|
|
16
|
+
import { writeFileAtomic } from "./write.js";
|
|
16
17
|
/** Starts one browser on first use and keeps it for later conversions. */
|
|
17
18
|
export class BrowserPool {
|
|
18
19
|
executable;
|
|
@@ -51,6 +52,32 @@ function readStylesheet(file) {
|
|
|
51
52
|
function themeStylesheets(theme) {
|
|
52
53
|
return theme === "light" || theme === "dark" ? builtinThemeCss(theme) : [readStylesheet(theme)];
|
|
53
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Chrome draws the header and footer inside the top and bottom margins. Measured: below about
|
|
57
|
+
* 10 mm the header touches or overlaps the first line of text (and the footer the last line).
|
|
58
|
+
*/
|
|
59
|
+
export const MIN_HEADER_FOOTER_MARGIN = "10mm";
|
|
60
|
+
export function headerFooterSpaceWarnings(margin, hasHeader, hasFooter) {
|
|
61
|
+
const minimum = lengthToPx(MIN_HEADER_FOOTER_MARGIN);
|
|
62
|
+
const warnings = [];
|
|
63
|
+
// Half a pixel of slack: "0.39in" and "10mm" are the same margin once Chrome rounds them.
|
|
64
|
+
if (hasHeader && lengthToPx(margin.top) < minimum - 0.5) {
|
|
65
|
+
warnings.push({
|
|
66
|
+
severity: "warning",
|
|
67
|
+
message: `the header may overlap the text: top margin is ${margin.top}, ` +
|
|
68
|
+
`at least ${MIN_HEADER_FOOTER_MARGIN} is needed (e.g. --margin-top 15mm)`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (hasFooter && lengthToPx(margin.bottom) < minimum - 0.5) {
|
|
72
|
+
warnings.push({
|
|
73
|
+
severity: "warning",
|
|
74
|
+
message: `the footer may overlap the text: bottom margin is ${margin.bottom}, ` +
|
|
75
|
+
`at least ${MIN_HEADER_FOOTER_MARGIN} is needed ` +
|
|
76
|
+
"(e.g. --margin-bottom 15mm, or --no-page-numbers for no footer)",
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return warnings;
|
|
80
|
+
}
|
|
54
81
|
/** Mermaid theme: explicit choice, else dark diagrams for the dark page theme. */
|
|
55
82
|
export function effectiveMermaidTheme(options) {
|
|
56
83
|
return options.mermaidTheme ?? (options.theme === "dark" ? "dark" : "neutral");
|
|
@@ -69,12 +96,16 @@ export async function renderDocument(inputPath, settings, browsers) {
|
|
|
69
96
|
: settings.config
|
|
70
97
|
? path.resolve(settings.config)
|
|
71
98
|
: findConfigFile(baseDir);
|
|
72
|
-
const
|
|
73
|
-
const
|
|
99
|
+
const config = configFile ? readConfig(configFile) : { options: {}, warnings: [] };
|
|
100
|
+
const options = mergeOptions(config.options, normalizeOptions(frontMatter.data, `${input} (front matter)`, baseDir), settings.overrides ?? {});
|
|
101
|
+
const margin = resolveMargins(options);
|
|
74
102
|
const themeCss = themeStylesheets(options.theme);
|
|
75
103
|
const extraCss = options.css.map(readStylesheet);
|
|
76
104
|
const md = renderMarkdown(frontMatter.body, { baseDir, lang: options.lang });
|
|
77
|
-
const diagnostics = [
|
|
105
|
+
const diagnostics = [
|
|
106
|
+
...config.warnings.map((message) => ({ severity: "warning", message })),
|
|
107
|
+
...md.diagnostics,
|
|
108
|
+
];
|
|
78
109
|
const title = options.title ?? md.title ?? path.parse(input).name;
|
|
79
110
|
// Table of contents: at the [[toc]] marker, or at the start with --toc.
|
|
80
111
|
let body = md.html;
|
|
@@ -88,7 +119,7 @@ export async function renderDocument(inputPath, settings, browsers) {
|
|
|
88
119
|
// Mermaid needs a real DOM to lay out diagrams, also for HTML output.
|
|
89
120
|
if (md.mermaid.length > 0) {
|
|
90
121
|
const session = await browsers.get();
|
|
91
|
-
const width = contentWidthPx(options.pageSize, options.landscape, margin);
|
|
122
|
+
const width = contentWidthPx(options.pageSize, options.orientation === "landscape", margin);
|
|
92
123
|
const theme = effectiveMermaidTheme(options);
|
|
93
124
|
const results = await renderMermaid(session, md.mermaid, theme, width);
|
|
94
125
|
const filled = fillMermaid(body, md.nonce, md.mermaid, results);
|
|
@@ -122,17 +153,16 @@ export async function convertFile(convert) {
|
|
|
122
153
|
try {
|
|
123
154
|
const doc = await renderDocument(convert.input, convert, browsers);
|
|
124
155
|
const { options } = doc;
|
|
125
|
-
await mkdir(path.dirname(output), { recursive: true });
|
|
126
156
|
if (convert.format === "pdf") {
|
|
127
|
-
const margin =
|
|
157
|
+
const margin = resolveMargins(options);
|
|
128
158
|
const values = { title: doc.title, author: options.author, date: options.date };
|
|
129
159
|
const footer = options.footer ?? (options.pageNumbers ? DEFAULT_FOOTER : undefined);
|
|
130
160
|
const hasHeaderFooter = options.header !== undefined || footer !== undefined;
|
|
161
|
+
doc.diagnostics.push(...headerFooterSpaceWarnings(margin, options.header !== undefined, footer !== undefined));
|
|
131
162
|
const session = await browsers.get();
|
|
132
|
-
const { blocked } = await session.printPdf(doc.html, {
|
|
133
|
-
output,
|
|
163
|
+
const { pdf, blocked } = await session.printPdf(doc.html, {
|
|
134
164
|
format: options.pageSize,
|
|
135
|
-
landscape: options.landscape,
|
|
165
|
+
landscape: options.orientation === "landscape",
|
|
136
166
|
margin,
|
|
137
167
|
headerTemplate: hasHeaderFooter
|
|
138
168
|
? headerFooterTemplate(options.header, values, margin)
|
|
@@ -147,9 +177,10 @@ export async function convertFile(convert) {
|
|
|
147
177
|
message: `resource not loaded: ${url}${hint}`,
|
|
148
178
|
});
|
|
149
179
|
}
|
|
180
|
+
await writeFileAtomic(output, pdf);
|
|
150
181
|
}
|
|
151
182
|
else {
|
|
152
|
-
await
|
|
183
|
+
await writeFileAtomic(output, doc.html);
|
|
153
184
|
}
|
|
154
185
|
return {
|
|
155
186
|
output,
|
package/dist/inputs.js
CHANGED
|
@@ -95,7 +95,7 @@ export function planOutputs(inputs, format, output, cwd = process.cwd()) {
|
|
|
95
95
|
if (several) {
|
|
96
96
|
throw new OptionsError(`with several input files, --output must be a folder: ${output}`);
|
|
97
97
|
}
|
|
98
|
-
return [{ input: inputs[0].file, output: outDir }];
|
|
98
|
+
return checkJobs([{ input: inputs[0].file, output: outDir }], inputs);
|
|
99
99
|
}
|
|
100
100
|
const jobs = inputs.map(({ file, root }) => {
|
|
101
101
|
const name = path.parse(file).name + ext;
|
|
@@ -104,9 +104,22 @@ export function planOutputs(inputs, format, output, cwd = process.cwd()) {
|
|
|
104
104
|
const relativeDir = root ? path.relative(root, path.dirname(file)) : "";
|
|
105
105
|
return { input: file, output: path.join(outDir, relativeDir, name) };
|
|
106
106
|
});
|
|
107
|
+
return checkJobs(jobs, inputs);
|
|
108
|
+
}
|
|
109
|
+
/** Path comparison key: Windows paths are case-insensitive. */
|
|
110
|
+
const pathKey = (file) => process.platform === "win32" ? path.resolve(file).toLowerCase() : path.resolve(file);
|
|
111
|
+
/**
|
|
112
|
+
* Refuse plans that would destroy data: an output that is one of the input Markdown files
|
|
113
|
+
* (`mdrender doc.md -o doc.md`), or two inputs written to the same output.
|
|
114
|
+
*/
|
|
115
|
+
function checkJobs(jobs, inputs) {
|
|
116
|
+
const sources = new Set(inputs.map(({ file }) => pathKey(file)));
|
|
107
117
|
const targets = new Map();
|
|
108
118
|
for (const job of jobs) {
|
|
109
|
-
const key =
|
|
119
|
+
const key = pathKey(job.output);
|
|
120
|
+
if (sources.has(key)) {
|
|
121
|
+
throw new OptionsError(`refusing to overwrite the input file ${job.output}; choose another --output`);
|
|
122
|
+
}
|
|
110
123
|
const other = targets.get(key);
|
|
111
124
|
if (other) {
|
|
112
125
|
throw new OptionsError(`${other} and ${job.input} would both be written to ${job.output}`);
|
package/dist/options.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { MdRenderError } from "./errors.js";
|
|
2
2
|
export declare const PAGE_SIZES: readonly ["A3", "A4", "A5", "Letter", "Legal", "Tabloid"];
|
|
3
3
|
export type PageSize = (typeof PAGE_SIZES)[number];
|
|
4
|
+
export declare const ORIENTATIONS: readonly ["portrait", "landscape"];
|
|
5
|
+
export type Orientation = (typeof ORIENTATIONS)[number];
|
|
6
|
+
export type Margins = {
|
|
7
|
+
top: string;
|
|
8
|
+
right: string;
|
|
9
|
+
bottom: string;
|
|
10
|
+
left: string;
|
|
11
|
+
};
|
|
4
12
|
export declare const MERMAID_THEMES: readonly ["default", "neutral", "dark", "forest", "base"];
|
|
5
13
|
export type MermaidTheme = (typeof MERMAID_THEMES)[number];
|
|
6
14
|
export declare const BUILTIN_THEMES: readonly ["light", "dark"];
|
|
@@ -9,7 +17,12 @@ export interface DocumentOptions {
|
|
|
9
17
|
pageSize: PageSize;
|
|
10
18
|
/** CSS margin shorthand: 1 to 4 lengths in mm, cm, in or px. */
|
|
11
19
|
margin: string;
|
|
12
|
-
|
|
20
|
+
/** Single sides, overriding the shorthand (see `resolveMargins`). */
|
|
21
|
+
marginTop?: string;
|
|
22
|
+
marginRight?: string;
|
|
23
|
+
marginBottom?: string;
|
|
24
|
+
marginLeft?: string;
|
|
25
|
+
orientation: Orientation;
|
|
13
26
|
toc: boolean;
|
|
14
27
|
/** Deepest heading level listed in the table of contents. */
|
|
15
28
|
tocDepth: number;
|
|
@@ -36,12 +49,7 @@ export declare class OptionsError extends MdRenderError {
|
|
|
36
49
|
constructor(message: string);
|
|
37
50
|
}
|
|
38
51
|
/** Parse a CSS margin shorthand into the four sides, or throw. */
|
|
39
|
-
export declare function parseMargin(margin: string):
|
|
40
|
-
top: string;
|
|
41
|
-
right: string;
|
|
42
|
-
bottom: string;
|
|
43
|
-
left: string;
|
|
44
|
-
};
|
|
52
|
+
export declare function parseMargin(margin: string): Margins;
|
|
45
53
|
/**
|
|
46
54
|
* Validate options from a config file, front matter or the command line.
|
|
47
55
|
*
|
|
@@ -51,5 +59,23 @@ export declare function parseMargin(margin: string): {
|
|
|
51
59
|
export declare function normalizeOptions(raw: Record<string, unknown>, source: string, baseDir: string,
|
|
52
60
|
/** How to name a key in error messages (e.g. as a command-line flag). */
|
|
53
61
|
label?: (key: string) => string): Partial<DocumentOptions>;
|
|
54
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* Merge option layers, later ones winning. CSS lists accumulate instead of replacing. A layer
|
|
64
|
+
* that sets the `margin` shorthand also replaces single-side margins from earlier layers, so
|
|
65
|
+
* `--margin 15mm` on the command line is not partly undone by `marginLeft` in a config file.
|
|
66
|
+
*/
|
|
55
67
|
export declare function mergeOptions(...layers: Partial<DocumentOptions>[]): DocumentOptions;
|
|
68
|
+
/** The four page margins: the `margin` shorthand, with single-side options on top. */
|
|
69
|
+
export declare function resolveMargins(options: DocumentOptions): Margins;
|
|
70
|
+
/** Every option name accepted in a config file, in camelCase. */
|
|
71
|
+
export declare const OPTION_KEYS: readonly string[];
|
|
72
|
+
export interface UnknownOption {
|
|
73
|
+
key: string;
|
|
74
|
+
/** Closest known option, written in the same style (camelCase or kebab-case) as `key`. */
|
|
75
|
+
suggestion?: string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Keys that are not options, with the closest known option when one is near: catches typos
|
|
79
|
+
* such as "pagesize" or "margn" that would otherwise be ignored silently.
|
|
80
|
+
*/
|
|
81
|
+
export declare function unknownOptions(raw: Record<string, unknown>): UnknownOption[];
|
package/dist/options.js
CHANGED
|
@@ -3,12 +3,13 @@ import { ExitCode } from "./exit-codes.js";
|
|
|
3
3
|
import { MdRenderError } from "./errors.js";
|
|
4
4
|
import { DEFAULT_LANG } from "./i18n.js";
|
|
5
5
|
export const PAGE_SIZES = ["A3", "A4", "A5", "Letter", "Legal", "Tabloid"];
|
|
6
|
+
export const ORIENTATIONS = ["portrait", "landscape"];
|
|
6
7
|
export const MERMAID_THEMES = ["default", "neutral", "dark", "forest", "base"];
|
|
7
8
|
export const BUILTIN_THEMES = ["light", "dark"];
|
|
8
9
|
export const DEFAULT_OPTIONS = {
|
|
9
|
-
pageSize: "
|
|
10
|
-
margin: "
|
|
11
|
-
|
|
10
|
+
pageSize: "Letter",
|
|
11
|
+
margin: "10mm",
|
|
12
|
+
orientation: "portrait",
|
|
12
13
|
toc: false,
|
|
13
14
|
tocDepth: 3,
|
|
14
15
|
pageNumbers: true,
|
|
@@ -51,6 +52,13 @@ const oneOf = (choices) => (value, describe) => {
|
|
|
51
52
|
return match;
|
|
52
53
|
throw new OptionsError(`${describe} must be one of: ${choices.join(", ")}`);
|
|
53
54
|
};
|
|
55
|
+
const length = (value, describe) => {
|
|
56
|
+
const raw = text(value, describe, "");
|
|
57
|
+
const trimmed = raw.trim();
|
|
58
|
+
if (LENGTH.test(trimmed))
|
|
59
|
+
return trimmed;
|
|
60
|
+
throw new OptionsError(`${describe} must be a length in mm, cm, in or px, e.g. "10mm"`);
|
|
61
|
+
};
|
|
54
62
|
const VALIDATORS = {
|
|
55
63
|
pageSize: oneOf(PAGE_SIZES),
|
|
56
64
|
margin: (value, describe) => {
|
|
@@ -63,7 +71,11 @@ const VALIDATORS = {
|
|
|
63
71
|
}
|
|
64
72
|
return margin;
|
|
65
73
|
},
|
|
66
|
-
|
|
74
|
+
marginTop: length,
|
|
75
|
+
marginRight: length,
|
|
76
|
+
marginBottom: length,
|
|
77
|
+
marginLeft: length,
|
|
78
|
+
orientation: oneOf(ORIENTATIONS),
|
|
67
79
|
toc: bool,
|
|
68
80
|
tocDepth: (value, describe) => {
|
|
69
81
|
const depth = Number(value);
|
|
@@ -109,6 +121,12 @@ label = (key) => `"${key}"`) {
|
|
|
109
121
|
for (const [key, value] of Object.entries(raw)) {
|
|
110
122
|
if (value === undefined || value === null)
|
|
111
123
|
continue;
|
|
124
|
+
// `landscape: true` is kept as a shorthand for `orientation: landscape`.
|
|
125
|
+
if (toCamel(key) === "landscape") {
|
|
126
|
+
const landscape = bool(value, `${source}: ${label(key)}`, baseDir);
|
|
127
|
+
result.orientation = landscape ? "landscape" : "portrait";
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
112
130
|
const name = toCamel(key);
|
|
113
131
|
const validate = VALIDATORS[name];
|
|
114
132
|
if (!validate)
|
|
@@ -117,15 +135,74 @@ label = (key) => `"${key}"`) {
|
|
|
117
135
|
}
|
|
118
136
|
return result;
|
|
119
137
|
}
|
|
120
|
-
|
|
138
|
+
const SIDES = ["marginTop", "marginRight", "marginBottom", "marginLeft"];
|
|
139
|
+
/**
|
|
140
|
+
* Merge option layers, later ones winning. CSS lists accumulate instead of replacing. A layer
|
|
141
|
+
* that sets the `margin` shorthand also replaces single-side margins from earlier layers, so
|
|
142
|
+
* `--margin 15mm` on the command line is not partly undone by `marginLeft` in a config file.
|
|
143
|
+
*/
|
|
121
144
|
export function mergeOptions(...layers) {
|
|
122
145
|
const merged = { ...DEFAULT_OPTIONS, css: [] };
|
|
123
146
|
for (const layer of layers) {
|
|
124
147
|
const { css, ...rest } = layer;
|
|
148
|
+
if (rest.margin !== undefined)
|
|
149
|
+
for (const side of SIDES)
|
|
150
|
+
delete merged[side];
|
|
125
151
|
Object.assign(merged, rest);
|
|
126
152
|
if (css)
|
|
127
153
|
merged.css.push(...css);
|
|
128
154
|
}
|
|
129
155
|
return merged;
|
|
130
156
|
}
|
|
157
|
+
/** The four page margins: the `margin` shorthand, with single-side options on top. */
|
|
158
|
+
export function resolveMargins(options) {
|
|
159
|
+
const base = parseMargin(options.margin);
|
|
160
|
+
return {
|
|
161
|
+
top: options.marginTop ?? base.top,
|
|
162
|
+
right: options.marginRight ?? base.right,
|
|
163
|
+
bottom: options.marginBottom ?? base.bottom,
|
|
164
|
+
left: options.marginLeft ?? base.left,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/** Edit distance between two strings (insertions, deletions, substitutions). */
|
|
168
|
+
function editDistance(a, b) {
|
|
169
|
+
let previous = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
170
|
+
for (let i = 1; i <= a.length; i++) {
|
|
171
|
+
const current = [i];
|
|
172
|
+
for (let j = 1; j <= b.length; j++) {
|
|
173
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
174
|
+
current[j] = Math.min(previous[j] + 1, current[j - 1] + 1, previous[j - 1] + cost);
|
|
175
|
+
}
|
|
176
|
+
previous = current;
|
|
177
|
+
}
|
|
178
|
+
return previous[b.length];
|
|
179
|
+
}
|
|
180
|
+
const toKebab = (key) => key.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase());
|
|
181
|
+
/** Every option name accepted in a config file, in camelCase. */
|
|
182
|
+
export const OPTION_KEYS = [...Object.keys(VALIDATORS), "landscape"];
|
|
183
|
+
/**
|
|
184
|
+
* Keys that are not options, with the closest known option when one is near: catches typos
|
|
185
|
+
* such as "pagesize" or "margn" that would otherwise be ignored silently.
|
|
186
|
+
*/
|
|
187
|
+
export function unknownOptions(raw) {
|
|
188
|
+
const unknown = [];
|
|
189
|
+
for (const key of Object.keys(raw)) {
|
|
190
|
+
if (OPTION_KEYS.includes(toCamel(key)))
|
|
191
|
+
continue;
|
|
192
|
+
const plain = key.toLowerCase().replace(/[-_]/g, "");
|
|
193
|
+
let best;
|
|
194
|
+
let bestDistance = Infinity;
|
|
195
|
+
for (const known of OPTION_KEYS) {
|
|
196
|
+
const distance = editDistance(plain, known.toLowerCase());
|
|
197
|
+
if (distance < bestDistance) {
|
|
198
|
+
best = known;
|
|
199
|
+
bestDistance = distance;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const close = best !== undefined && bestDistance <= Math.max(1, Math.floor(plain.length / 4));
|
|
203
|
+
const suggestion = close ? (key.includes("-") ? toKebab(best) : best) : undefined;
|
|
204
|
+
unknown.push({ key, suggestion });
|
|
205
|
+
}
|
|
206
|
+
return unknown;
|
|
207
|
+
}
|
|
131
208
|
//# sourceMappingURL=options.js.map
|
package/dist/render/browser.d.ts
CHANGED
|
@@ -8,7 +8,6 @@ import { type Page, type PDFOptions } from "puppeteer-core";
|
|
|
8
8
|
*/
|
|
9
9
|
export declare function shouldRetryWithoutSandbox(error: unknown, args: string[], platform?: NodeJS.Platform): boolean;
|
|
10
10
|
export interface PrintOptions {
|
|
11
|
-
output: string;
|
|
12
11
|
format?: PDFOptions["format"];
|
|
13
12
|
landscape?: boolean;
|
|
14
13
|
margin?: {
|
|
@@ -41,7 +40,9 @@ export declare class BrowserSession {
|
|
|
41
40
|
* rendered by then (math by KaTeX, diagrams by Mermaid), and any <script> in the Markdown
|
|
42
41
|
* must not run.
|
|
43
42
|
*/
|
|
43
|
+
/** Returns the PDF bytes (the caller writes them) and the URLs that were blocked. */
|
|
44
44
|
printPdf(html: string, options: PrintOptions): Promise<{
|
|
45
|
+
pdf: Uint8Array;
|
|
45
46
|
blocked: string[];
|
|
46
47
|
}>;
|
|
47
48
|
close(): Promise<void>;
|
package/dist/render/browser.js
CHANGED
|
@@ -93,8 +93,9 @@ export class BrowserSession {
|
|
|
93
93
|
* rendered by then (math by KaTeX, diagrams by Mermaid), and any <script> in the Markdown
|
|
94
94
|
* must not run.
|
|
95
95
|
*/
|
|
96
|
+
/** Returns the PDF bytes (the caller writes them) and the URLs that were blocked. */
|
|
96
97
|
async printPdf(html, options) {
|
|
97
|
-
const margin = options.margin ?? { top: "
|
|
98
|
+
const margin = options.margin ?? { top: "10mm", right: "10mm", bottom: "10mm", left: "10mm" };
|
|
98
99
|
const headerFooter = options.headerTemplate !== undefined || options.footerTemplate !== undefined;
|
|
99
100
|
const page = await this.browser.newPage();
|
|
100
101
|
page.setDefaultTimeout(this.timeout);
|
|
@@ -104,8 +105,7 @@ export class BrowserSession {
|
|
|
104
105
|
await page.setContent(html, { waitUntil: "load" });
|
|
105
106
|
// Make sure every embedded font has loaded before printing.
|
|
106
107
|
await page.evaluate("document.fonts.ready.then(() => undefined)");
|
|
107
|
-
await page.pdf({
|
|
108
|
-
path: options.output,
|
|
108
|
+
const pdf = await page.pdf({
|
|
109
109
|
format: options.format ?? "A4",
|
|
110
110
|
landscape: options.landscape ?? false,
|
|
111
111
|
margin,
|
|
@@ -118,7 +118,7 @@ export class BrowserSession {
|
|
|
118
118
|
tagged: true,
|
|
119
119
|
timeout: this.timeout,
|
|
120
120
|
});
|
|
121
|
-
return { blocked: [...new Set(blocked)] };
|
|
121
|
+
return { pdf, blocked: [...new Set(blocked)] };
|
|
122
122
|
}
|
|
123
123
|
catch (error) {
|
|
124
124
|
throw new RenderError(`failed to render PDF: ${error.message}`);
|
package/dist/write.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write a file all at once: the content goes to a hidden temporary file in the same folder,
|
|
3
|
+
* which then replaces the target. An interrupted or failed conversion never leaves a truncated
|
|
4
|
+
* file and keeps the previous version.
|
|
5
|
+
*
|
|
6
|
+
* On Windows a file open in another program (a PDF viewer) cannot be replaced; short-lived locks
|
|
7
|
+
* (antivirus, indexing) are retried before reporting it.
|
|
8
|
+
*/
|
|
9
|
+
export declare function writeFileAtomic(file: string, data: string | Uint8Array): Promise<void>;
|
package/dist/write.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { RenderError } from "./errors.js";
|
|
5
|
+
const BUSY = new Set(["EPERM", "EBUSY", "EACCES"]);
|
|
6
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
7
|
+
/**
|
|
8
|
+
* Write a file all at once: the content goes to a hidden temporary file in the same folder,
|
|
9
|
+
* which then replaces the target. An interrupted or failed conversion never leaves a truncated
|
|
10
|
+
* file and keeps the previous version.
|
|
11
|
+
*
|
|
12
|
+
* On Windows a file open in another program (a PDF viewer) cannot be replaced; short-lived locks
|
|
13
|
+
* (antivirus, indexing) are retried before reporting it.
|
|
14
|
+
*/
|
|
15
|
+
export async function writeFileAtomic(file, data) {
|
|
16
|
+
const dir = path.dirname(file);
|
|
17
|
+
await mkdir(dir, { recursive: true });
|
|
18
|
+
const temp = path.join(dir, `.${path.basename(file)}.${randomBytes(6).toString("hex")}.tmp`);
|
|
19
|
+
try {
|
|
20
|
+
await writeFile(temp, data);
|
|
21
|
+
for (let attempt = 1;; attempt++) {
|
|
22
|
+
try {
|
|
23
|
+
await rename(temp, file);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
const code = error.code ?? "";
|
|
28
|
+
if (!BUSY.has(code) || attempt >= 5)
|
|
29
|
+
throw error;
|
|
30
|
+
await sleep(100 * attempt);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
await rm(temp, { force: true });
|
|
36
|
+
const code = error.code ?? "";
|
|
37
|
+
const hint = BUSY.has(code) ? " (is it open in another program, such as a PDF viewer?)" : "";
|
|
38
|
+
throw new RenderError(`cannot write ${file}: ${error.message}${hint}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=write.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imontene/mdrender",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
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",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|