@imontene/mdrender 2.7.0 → 2.8.1

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,25 @@
1
1
  # Cambios
2
2
 
3
+ ## 2.8.1 — 2026-10-29
4
+
5
+ - Tema `academic_basic`: márgenes laterales de 8 mm (antes 16 mm), con más ancho para las dos
6
+ columnas. Los márgenes superior e inferior siguen en 18 mm.
7
+
8
+ ## 2.8.0 — 2026-10-28
9
+
10
+ - **Tema académico** `--theme academic_basic` (o `theme: academic_basic`), con el aspecto de
11
+ un artículo de revista científica:
12
+ - texto en STIX Two Text (incrustada en el PDF) y títulos, leyendas y tablas en Inter;
13
+ - en el PDF, dos columnas justificadas con sangría de párrafo; el bloque de título, las
14
+ tablas, los diagramas y las figuras anchas ocupan las dos;
15
+ - imágenes solas como figuras numeradas («Fig. 1.») con el texto alternativo como leyenda,
16
+ y `#wide` al final de la ruta para que ocupen las dos columnas;
17
+ - tablas con tres líneas, sin rejilla;
18
+ - márgenes de 18 × 16 mm, texto de 9,5 pt e Inter por defecto.
19
+ - **Bloque de título para artículos**, con cualquier tema: `kicker` (etiqueta sobre el título),
20
+ `affiliations` (numeradas), `abstract` y `keywords` en el front matter, y `^1,2^` en
21
+ `author` para las afiliaciones en superíndice.
22
+
3
23
  ## 2.7.0 — 2026-10-23
4
24
 
5
25
  - `--watermark-field` pone ahora dos campos de luz en cada página: arriba a la izquierda (antes
package/README.md CHANGED
@@ -24,7 +24,7 @@ Ejemplo: [`examples/ejemplo.md`](examples/ejemplo.md) → [`examples/ejemplo.pdf
24
24
  - 🗂️ **Lotes**: varios archivos, carpetas completas o patrones `docs/**/*.md`.
25
25
  - 🔁 **`--watch`** y **vista previa en vivo** en el navegador.
26
26
  - 🎨 Estilo **Anti-Gravity** por defecto (con variante oscura), los temas clásicos **claro** y
27
- **oscuro**, o tu propio CSS.
27
+ **oscuro**, el tema **académico** a dos columnas (`academic_basic`), o tu propio CSS.
28
28
  - 🔌 **Sin conexión**: todo va empaquetado; por defecto no descarga nada de internet.
29
29
  - 💻 **Windows 10/11 y Linux** (x64; arm64 en Linux), probado en ambos en cada cambio.
30
30
 
@@ -160,7 +160,7 @@ equipo); `--port` fija el puerto y `--no-open` no abre el navegador.
160
160
  | `--watermark-field` | Campos de luz con el degradado arriba a la izquierda y abajo a la derecha de cada página del PDF | — |
161
161
  | `--density <densidad>` | `normal` o `compact` (menos espacio entre líneas y bloques) | `normal` |
162
162
  | `--font-size <tamaño>` | Tamaño del texto en `pt` o `px`, de 6 a 24 pt; p. ej. `10pt` | `10.5pt` |
163
- | `--theme <tema\|css>` | `antigravity`, `antigravity-dark`, `light`, `dark` o un `.css` que reemplaza el tema (ver abajo) | `antigravity` |
163
+ | `--theme <tema\|css>` | `antigravity`, `antigravity-dark`, `light`, `dark`, `academic_basic` o un `.css` (ver abajo) | `antigravity` |
164
164
  | `--css <archivo.css>` | CSS adicional (se puede repetir) | — |
165
165
  | `--mermaid-theme <tema>` | `default`, `neutral`, `dark`, `forest`, `base` | el del tema de página |
166
166
  | `--mermaid-flip <auto\|never>` | Si el autoajuste puede girar un diagrama de flujo de eje (ver abajo) | `auto` |
@@ -197,6 +197,28 @@ del mismo nombre y llevado de diapositivas a páginas:
197
197
  versiones anteriores y `--theme dark` su variante oscura. El design system pide texto alineado
198
198
  a la izquierda: con este tema, usa `--no-justify` si quieres seguirlo al pie de la letra.
199
199
 
200
+ **Tema académico** (`--theme academic_basic`): el aspecto de un artículo de revista
201
+ científica. Ejemplo: [`examples/articulo-academico.md`](examples/articulo-academico.md) →
202
+ [`examples/articulo-academico.pdf`](examples/articulo-academico.pdf).
203
+
204
+ - Texto en [STIX Two Text](https://www.stixfonts.org), la serifa de las fuentes STIX hechas
205
+ para publicaciones científicas; títulos, leyendas, tablas y etiquetas en Inter.
206
+ - En el PDF, **dos columnas** justificadas, con sangría en lugar de espacio entre párrafos. El
207
+ bloque de título, las tablas, los diagramas Mermaid y las figuras anchas ocupan las dos.
208
+ - Bloque de título con etiqueta sobre una línea de color (`kicker`), autores con afiliaciones
209
+ numeradas (`author` y `affiliations`), resumen en negrita (`abstract`) y palabras clave
210
+ (`keywords`); ver [Front matter](#front-matter-y-configuración).
211
+ - Cada imagen sola en su párrafo es una **figura numerada**: el texto alternativo es la
212
+ leyenda, que se imprime como «**Fig. 1.** …». Termina la ruta en `#wide` para que ocupe las
213
+ dos columnas: `![Leyenda.](grafico.png#wide)`.
214
+ - Tablas con tres líneas (arriba, bajo el encabezado y abajo), sin rejilla.
215
+ - Por defecto: márgenes de 18 mm arriba y abajo y 8 mm a los lados, texto de 9,5 pt y
216
+ `font: inter`; cualquier opción los cambia.
217
+
218
+ En el HTML se ve a una sola columna. Chrome no ubica las figuras arriba o abajo de la página
219
+ como en LaTeX: quedan donde aparecen en el texto, y las que ocupan las dos columnas pueden
220
+ dejar algo de espacio libre antes de ellas.
221
+
200
222
  **Marca de agua**: dos marcas con el degradado azul → cian → violeta del design system
201
223
  Anti-Gravity, cada una por separado y solo en el PDF:
202
224
 
@@ -275,6 +297,28 @@ header: "{title} | | {date}"
275
297
  Con `title` en el front matter se muestra un bloque de título (título, subtítulo, autor y
276
298
  fecha). Las rutas de `css` y `theme` son relativas al archivo donde aparecen.
277
299
 
300
+ Para artículos, el bloque de título admite además estos campos (con cualquier tema; el tema
301
+ académico es el que les da forma de revista):
302
+
303
+ ```markdown
304
+ ---
305
+ theme: academic_basic
306
+ kicker: Artículo de investigación
307
+ title: Cinética de conversión catalítica a baja temperatura
308
+ author: Ana Pérez^1^, Luis Gómez^1,2^
309
+ affiliations:
310
+ - Departamento de Química, Universidad de Ejemplo.
311
+ - Instituto de Materiales Avanzados.
312
+ abstract: >
313
+ Medimos la conversión de un sustrato modelo con dos catalizadores…
314
+ keywords: [catálisis, cinética]
315
+ ---
316
+ ```
317
+
318
+ `^1,2^` en `author` marca las afiliaciones en superíndice; el encabezado y el pie
319
+ (`{author}`) y los metadatos del PDF muestran los nombres sin esas marcas. Los párrafos del
320
+ resumen se separan con una línea en blanco.
321
+
278
322
  ## Ecuaciones y diagramas
279
323
 
280
324
  | Sintaxis | Resultado |
@@ -369,6 +413,6 @@ cambios: [CHANGELOG.md](CHANGELOG.md).
369
413
 
370
414
  ## Licencia
371
415
 
372
- [MIT](LICENSE). Manrope, Inter y JetBrains Mono se distribuyen bajo la
416
+ [MIT](LICENSE). Manrope, Inter, JetBrains Mono y STIX Two Text se distribuyen bajo la
373
417
  [SIL Open Font License 1.1](https://openfontlicense.org); los ejecutables incluyen
374
418
  `THIRD-PARTY-NOTICES.txt` con las licencias de todos los componentes.
@@ -0,0 +1,378 @@
1
+ /*
2
+ * mdrender academic theme (academic_basic): loaded after theme.css. A research-journal page:
3
+ * STIX Two Text for the text, Inter for titles, captions and labels, two justified columns in
4
+ * the PDF with the title block, wide figures, tables and diagrams across both, and tables with
5
+ * three rules only. On screen (HTML) the same type in a single column.
6
+ */
7
+
8
+ :root {
9
+ --font-serif: "STIX Two Text", "Inter", Georgia, serif;
10
+ --ac-ink: #141414;
11
+ --ac-ink-secondary: #3d3d3d;
12
+ --ac-ink-muted: #5c5c5c;
13
+ --ac-rule: #141414;
14
+ --ac-hairline: #c4c4c4;
15
+ --ac-surface: #f6f6f4;
16
+ --ac-accent: #9b1b1f;
17
+ --ac-link: #1d4e89;
18
+ --ac-column-gap: 6.5mm;
19
+
20
+ /* The variables theme.css is written with */
21
+ --color-text: var(--ac-ink);
22
+ --color-muted: var(--ac-ink-muted);
23
+ --color-heading: var(--ac-ink);
24
+ --color-link: var(--ac-link);
25
+ --color-border: var(--ac-hairline);
26
+ --color-subtle-bg: var(--ac-surface);
27
+ --color-accent: var(--ac-accent);
28
+ --color-bg: #ffffff;
29
+ --color-stripe: #ffffff;
30
+ --alert-note: var(--ac-ink-secondary);
31
+ --alert-tip: var(--ac-ink-secondary);
32
+ --alert-important: var(--ac-accent);
33
+ --alert-warning: #8a5d00;
34
+ --alert-caution: var(--ac-accent);
35
+ }
36
+
37
+ body {
38
+ font-family: var(--font-serif);
39
+ font-feature-settings: "kern", "liga";
40
+ line-height: 1.38;
41
+ counter-reset: figure;
42
+ }
43
+
44
+ .markdown-body {
45
+ max-width: 720px;
46
+ }
47
+
48
+ /* Titles, captions and labels: the sans (--font-text, Inter by default). */
49
+ h1,
50
+ h2,
51
+ h3,
52
+ .doc-kicker,
53
+ .doc-title,
54
+ .doc-meta,
55
+ .doc-abstract-title,
56
+ .doc-keywords,
57
+ .figure figcaption,
58
+ table,
59
+ .toc,
60
+ .markdown-alert-title {
61
+ font-family: var(--font-text);
62
+ }
63
+
64
+ /* Headings: small and bold, set apart by space rather than rules. */
65
+ h1,
66
+ h2,
67
+ h3,
68
+ h4,
69
+ h5,
70
+ h6 {
71
+ letter-spacing: 0;
72
+ line-height: 1.25;
73
+ padding-bottom: 0;
74
+ border-bottom: 0;
75
+ }
76
+
77
+ h1 {
78
+ margin: 0 0 0.6em;
79
+ font-size: 2.1em;
80
+ font-weight: 700;
81
+ letter-spacing: -0.015em;
82
+ line-height: 1.12;
83
+ }
84
+
85
+ h2 {
86
+ margin: 1.3em 0 0.35em;
87
+ font-size: 1.05em;
88
+ font-weight: 700;
89
+ }
90
+
91
+ h3 {
92
+ margin: 1.1em 0 0.3em;
93
+ font-size: 1em;
94
+ font-weight: 600;
95
+ font-style: italic;
96
+ }
97
+
98
+ h4,
99
+ h5,
100
+ h6 {
101
+ margin: 1em 0 0.2em;
102
+ font-size: 1em;
103
+ font-weight: 400;
104
+ font-style: italic;
105
+ }
106
+
107
+ /* Paragraphs: no space between them, the first line of the next one indented. */
108
+ .markdown-body > p {
109
+ margin: 0;
110
+ }
111
+
112
+ .markdown-body > p + p {
113
+ text-indent: 1.2em;
114
+ }
115
+
116
+ ul,
117
+ ol,
118
+ blockquote,
119
+ pre,
120
+ table,
121
+ dl,
122
+ .math-display {
123
+ margin: 0.6em 0;
124
+ }
125
+
126
+ ul,
127
+ ol {
128
+ padding-left: 1.4em;
129
+ }
130
+
131
+ li + li {
132
+ margin-top: 0.15em;
133
+ }
134
+
135
+ strong {
136
+ font-weight: 700;
137
+ }
138
+
139
+ a {
140
+ color: var(--ac-link);
141
+ }
142
+
143
+ hr {
144
+ margin: 1.2em 0;
145
+ background: var(--ac-hairline);
146
+ }
147
+
148
+ blockquote {
149
+ padding: 0 0 0 0.9em;
150
+ color: var(--ac-ink-secondary);
151
+ border-left: 1.5px solid var(--ac-hairline);
152
+ }
153
+
154
+ /* Title block: kicker over a rule in the accent, title, authors, affiliations, abstract. */
155
+ .doc-header {
156
+ margin-bottom: 1.4em;
157
+ padding-bottom: 1em;
158
+ border-bottom: 0.75pt solid var(--ac-rule);
159
+ }
160
+
161
+ .doc-kicker {
162
+ margin: 0 0 0.9em;
163
+ padding-top: 0.45em;
164
+ font-size: 0.78em;
165
+ font-weight: 700;
166
+ letter-spacing: 0.12em;
167
+ border-top: 2.5pt solid var(--ac-accent);
168
+ }
169
+
170
+ .doc-header .doc-title {
171
+ margin: 0 0 0.3em;
172
+ font-size: 2.3em;
173
+ }
174
+
175
+ .doc-subtitle {
176
+ margin: 0 0 0.7em;
177
+ font-size: 1.2em;
178
+ font-style: italic;
179
+ line-height: 1.3;
180
+ color: var(--ac-ink-secondary);
181
+ }
182
+
183
+ .doc-meta {
184
+ margin: 0.7em 0 0;
185
+ padding-bottom: 0;
186
+ font-size: 1.02em;
187
+ font-weight: 600;
188
+ color: var(--ac-ink);
189
+ border-bottom: 0;
190
+ }
191
+
192
+ .doc-meta sup,
193
+ .doc-affiliations sup {
194
+ font-weight: 400;
195
+ font-size: 0.7em;
196
+ }
197
+
198
+ .doc-affiliations {
199
+ margin-top: 0.5em;
200
+ font-size: 0.82em;
201
+ line-height: 1.35;
202
+ color: var(--ac-ink-secondary);
203
+ }
204
+
205
+ .doc-abstract {
206
+ margin-top: 1.1em;
207
+ }
208
+
209
+ .doc-abstract-title {
210
+ margin-bottom: 0.35em;
211
+ font-size: 0.75em;
212
+ font-weight: 700;
213
+ letter-spacing: 0.12em;
214
+ text-transform: uppercase;
215
+ color: var(--ac-accent);
216
+ }
217
+
218
+ .doc-abstract p:not(.doc-abstract-title) {
219
+ margin: 0;
220
+ font-size: 1.08em;
221
+ font-weight: 700;
222
+ line-height: 1.38;
223
+ }
224
+
225
+ .doc-abstract p:not(.doc-abstract-title) + p {
226
+ margin-top: 0.4em;
227
+ }
228
+
229
+ .doc-keywords {
230
+ margin-top: 0.8em;
231
+ font-size: 0.8em;
232
+ color: var(--ac-ink-secondary);
233
+ }
234
+
235
+ .doc-keywords-label {
236
+ font-weight: 700;
237
+ }
238
+
239
+ /* Figures: "Fig. 1." in bold, the caption in the sans, aligned left. */
240
+ .figure {
241
+ counter-increment: figure;
242
+ margin: 0.9em 0 1.1em;
243
+ }
244
+
245
+ .figure img {
246
+ width: auto;
247
+ max-width: 100%;
248
+ }
249
+
250
+ .figure figcaption {
251
+ margin-top: 0.55em;
252
+ font-size: 0.8em;
253
+ line-height: 1.35;
254
+ text-align: left;
255
+ color: var(--ac-ink);
256
+ }
257
+
258
+ .figure figcaption::before {
259
+ content: "Fig. " counter(figure) ". ";
260
+ font-weight: 700;
261
+ }
262
+
263
+ /* Tables: three rules (top, under the header, bottom), no grid and no stripes. */
264
+ table {
265
+ width: 100%;
266
+ font-size: 0.82em;
267
+ line-height: 1.35;
268
+ border-top: 1.2pt solid var(--ac-rule);
269
+ border-bottom: 1.2pt solid var(--ac-rule);
270
+ }
271
+
272
+ th,
273
+ td {
274
+ padding: 0.35em 0.6em;
275
+ border: 0;
276
+ text-align: left;
277
+ }
278
+
279
+ th {
280
+ font-weight: 700;
281
+ background: none;
282
+ border-bottom: 0.6pt solid var(--ac-rule);
283
+ }
284
+
285
+ tbody tr:nth-child(2n) {
286
+ background: none;
287
+ }
288
+
289
+ /* Code: quiet, no boxes around inline code. */
290
+ code {
291
+ padding: 0;
292
+ font-size: 0.84em;
293
+ background: none;
294
+ }
295
+
296
+ pre {
297
+ font-size: 0.78em;
298
+ border: 0;
299
+ border-left: 1.5px solid var(--ac-hairline);
300
+ border-radius: 0;
301
+ background: var(--ac-surface);
302
+ }
303
+
304
+ /* Alerts: a rule and a small label, no cards. */
305
+ .markdown-alert {
306
+ margin: 0.8em 0;
307
+ padding: 0.1em 0 0.1em 0.9em;
308
+ border-left-width: 1.5px;
309
+ }
310
+
311
+ .markdown-alert-title {
312
+ font-size: 0.75em;
313
+ font-weight: 700;
314
+ letter-spacing: 0.1em;
315
+ text-transform: uppercase;
316
+ }
317
+
318
+ .markdown-alert > p {
319
+ margin: 0 0 0.4em;
320
+ }
321
+
322
+ .toc {
323
+ padding: 0.8em 1em;
324
+ border: 0;
325
+ border-top: 0.75pt solid var(--ac-rule);
326
+ border-bottom: 0.75pt solid var(--ac-rule);
327
+ border-radius: 0;
328
+ background: none;
329
+ font-size: 0.9em;
330
+ }
331
+
332
+ /* Footnotes and references: small, after a short rule. */
333
+ hr.footnotes-sep {
334
+ width: 30%;
335
+ margin: 1.6em 0 0.6em;
336
+ background: var(--ac-rule);
337
+ }
338
+
339
+ .footnotes {
340
+ font-size: 0.82em;
341
+ line-height: 1.35;
342
+ color: var(--ac-ink-secondary);
343
+ }
344
+
345
+ .footnotes-list {
346
+ padding-left: 1.6em;
347
+ }
348
+
349
+ .katex {
350
+ font-size: 1.05em;
351
+ }
352
+
353
+ /* The PDF: two columns. Spanning elements break the flow and balance the columns above them. */
354
+ @media print {
355
+ .markdown-body {
356
+ columns: 2;
357
+ column-gap: var(--ac-column-gap);
358
+ }
359
+
360
+ .doc-header,
361
+ .markdown-body > h1,
362
+ .toc,
363
+ table,
364
+ .mermaid-diagram,
365
+ .figure-wide,
366
+ .keep-with-heading:has(.mermaid-diagram, .figure-wide) {
367
+ column-span: all;
368
+ }
369
+
370
+ /* Text-wide items keep some air from the columns around them. */
371
+ .markdown-body > h1,
372
+ .markdown-body > table,
373
+ .markdown-body > .mermaid-diagram,
374
+ .markdown-body > .figure-wide {
375
+ margin-top: 0.8em;
376
+ margin-bottom: 1.1em;
377
+ }
378
+ }
@@ -112,6 +112,14 @@ h6 {
112
112
  color: var(--ag-accent);
113
113
  }
114
114
 
115
+ .doc-header .doc-kicker {
116
+ order: -2;
117
+ font-family: var(--font-code);
118
+ font-size: 0.78em;
119
+ font-weight: 400;
120
+ letter-spacing: 0.14em;
121
+ }
122
+
115
123
  .doc-header .doc-title {
116
124
  margin: 0 0 0.35em;
117
125
  font-size: 2.6em;
@@ -494,6 +494,69 @@ div.math-error {
494
494
  border-bottom: 1px solid var(--color-border);
495
495
  }
496
496
 
497
+ .doc-kicker {
498
+ margin: 0 0 0.5em;
499
+ font-size: 0.8em;
500
+ font-weight: 600;
501
+ letter-spacing: 0.08em;
502
+ text-transform: uppercase;
503
+ color: var(--color-accent);
504
+ }
505
+
506
+ .doc-affiliations {
507
+ list-style: none;
508
+ margin: 0.6em 0 0;
509
+ padding: 0;
510
+ font-size: 0.85em;
511
+ color: var(--color-muted);
512
+ }
513
+
514
+ .doc-affiliations sup {
515
+ margin-right: 0.2em;
516
+ }
517
+
518
+ .doc-abstract {
519
+ margin: 1.2em 0 0;
520
+ }
521
+
522
+ .doc-abstract-title {
523
+ margin: 0 0 0.3em;
524
+ font-weight: 600;
525
+ color: var(--color-heading);
526
+ }
527
+
528
+ .doc-abstract p:not(.doc-abstract-title) {
529
+ margin: 0 0 0.6em;
530
+ }
531
+
532
+ .doc-keywords {
533
+ margin: 0.6em 0 0;
534
+ font-size: 0.9em;
535
+ color: var(--color-muted);
536
+ }
537
+
538
+ .doc-keywords-label {
539
+ font-weight: 600;
540
+ }
541
+
542
+ /* Figures (academic theme: a lone image with its alt text as caption) */
543
+ .figure {
544
+ margin: 1.2em 0;
545
+ text-align: center;
546
+ break-inside: avoid;
547
+ }
548
+
549
+ .figure img {
550
+ display: block;
551
+ margin: 0 auto;
552
+ }
553
+
554
+ .figure figcaption {
555
+ margin-top: 0.5em;
556
+ font-size: 0.9em;
557
+ color: var(--color-muted);
558
+ }
559
+
497
560
  /* Table of contents */
498
561
  .toc {
499
562
  margin: 0 0 2em;
package/dist/assets.d.ts CHANGED
@@ -34,6 +34,8 @@ export declare const INTER_FACES: readonly [{
34
34
  /** Manrope faces in use (Fontsource, split into unicode-range subsets); it has no italics. */
35
35
  export declare const MANROPE_STYLESHEETS: string[];
36
36
  export declare const MONO_STYLESHEETS: string[];
37
+ /** STIX Two Text, the serif text of the academic theme: regular and bold, each with its italic. */
38
+ export declare const SERIF_STYLESHEETS: string[];
37
39
  export declare const KATEX_CSS = "katex/dist/katex.min.css";
38
40
  export declare const MERMAID_JS = "mermaid/dist/mermaid.min.js";
39
41
  export declare const THEMES: string[];
package/dist/assets.js CHANGED
@@ -89,6 +89,13 @@ export const MONO_STYLESHEETS = [
89
89
  "@fontsource/jetbrains-mono/400-italic.css",
90
90
  "@fontsource/jetbrains-mono/700.css",
91
91
  ];
92
+ /** STIX Two Text, the serif text of the academic theme: regular and bold, each with its italic. */
93
+ export const SERIF_STYLESHEETS = [
94
+ "@fontsource/stix-two-text/400.css",
95
+ "@fontsource/stix-two-text/400-italic.css",
96
+ "@fontsource/stix-two-text/700.css",
97
+ "@fontsource/stix-two-text/700-italic.css",
98
+ ];
92
99
  export const KATEX_CSS = "katex/dist/katex.min.css";
93
100
  export const MERMAID_JS = "mermaid/dist/mermaid.min.js";
94
101
  export const THEMES = [
@@ -96,6 +103,7 @@ export const THEMES = [
96
103
  "mdrender/assets/theme-dark.css",
97
104
  "mdrender/assets/theme-antigravity.css",
98
105
  "mdrender/assets/theme-antigravity-dark.css",
106
+ "mdrender/assets/theme-academic-basic.css",
99
107
  "mdrender/assets/density-compact.css",
100
108
  ];
101
109
  export const PACKAGE_JSON = "mdrender/package.json";
@@ -111,7 +119,12 @@ export function assetManifest() {
111
119
  const keys = new Set([PACKAGE_JSON, ...THEMES, MERMAID_JS]);
112
120
  for (const { file } of INTER_FACES)
113
121
  keys.add(`${INTER_DIR}/${file}`);
114
- for (const cssKey of [...MANROPE_STYLESHEETS, ...MONO_STYLESHEETS, KATEX_CSS]) {
122
+ for (const cssKey of [
123
+ ...MANROPE_STYLESHEETS,
124
+ ...MONO_STYLESHEETS,
125
+ ...SERIF_STYLESHEETS,
126
+ KATEX_CSS,
127
+ ]) {
115
128
  keys.add(cssKey);
116
129
  for (const font of woff2References(cssKey, readTextAsset(cssKey)))
117
130
  keys.add(font);
package/dist/cli.js CHANGED
@@ -69,8 +69,8 @@ function addDocumentOptions(command) {
69
69
  .option("--watermark-field", "soft fields of light in the top-left and bottom-right corners of every PDF page")
70
70
  .option("--density <density>", "normal or compact (tighter spacing) (default: normal)")
71
71
  .option("--font-size <size>", "text size, e.g. 10pt or 11pt (default: 10.5pt)")
72
- .option("--theme <theme>", "antigravity, antigravity-dark, light, dark, or a .css file replacing the theme " +
73
- "(default: antigravity)")
72
+ .option("--theme <theme>", "antigravity, antigravity-dark, light, dark, academic_basic, or a .css file replacing " +
73
+ "the theme (default: antigravity)")
74
74
  .option("--css <file>", "extra stylesheet (repeatable)", collect)
75
75
  .option("--mermaid-theme <theme>", `Mermaid theme: ${MERMAID_THEMES.join(", ")} ` +
76
76
  "(default: the page theme's — base in Anti-Gravity colours, neutral for light, dark for dark)")
package/dist/convert.js CHANGED
@@ -17,7 +17,7 @@ import { fieldImage } from "./render/field.js";
17
17
  import { FONT_FAMILIES, fontStack } from "./render/fonts.js";
18
18
  import { WATERMARK_CSS, watermarkHtml } from "./render/watermark.js";
19
19
  import { contentHeightPx, contentWidthPx, lengthToPx, textWidthPx } from "./render/page.js";
20
- import { buildHtmlDocument, builtinThemeCss, densityCss } from "./render/template.js";
20
+ import { buildHtmlDocument, builtinThemeCss, densityCss, plainAuthor } from "./render/template.js";
21
21
  import { writeFileAtomic } from "./write.js";
22
22
  /** Starts one browser on first use and keeps it for later conversions. */
23
23
  export class BrowserPool {
@@ -176,6 +176,7 @@ paged = false) {
176
176
  lang: options.lang,
177
177
  // Soft hyphens only where paragraphs are justified: in the PDF.
178
178
  hyphenate: paged && options.justify ? options.lang : undefined,
179
+ figures: options.theme === "academic_basic",
179
180
  });
180
181
  const diagnostics = [
181
182
  ...config.warnings.map((message) => ({ severity: "warning", message })),
@@ -240,6 +241,10 @@ paged = false) {
240
241
  subtitle: options.subtitle,
241
242
  author: options.author,
242
243
  date: options.date,
244
+ kicker: options.kicker,
245
+ affiliations: options.affiliations,
246
+ abstract: options.abstract,
247
+ keywords: options.keywords,
243
248
  showTitle: options.title !== undefined,
244
249
  themeCss,
245
250
  font: options.font,
@@ -268,7 +273,8 @@ export async function convertFile(convert) {
268
273
  const { options } = doc;
269
274
  if (convert.format === "pdf") {
270
275
  const margin = resolveMargins(options);
271
- const values = { title: doc.title, author: options.author, date: options.date };
276
+ const author = options.author === undefined ? undefined : plainAuthor(options.author);
277
+ const values = { title: doc.title, author, date: options.date };
272
278
  const footer = options.footer ?? (options.pageNumbers ? DEFAULT_FOOTER : undefined);
273
279
  const hasHeaderFooter = options.header !== undefined || footer !== undefined;
274
280
  doc.diagnostics.push(...headerFooterSpaceWarnings(margin, options.header !== undefined, footer !== undefined));
package/dist/i18n.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  export type AlertType = "note" | "tip" | "important" | "warning" | "caution";
2
2
  export interface Strings {
3
3
  toc: string;
4
+ /** Title-block labels (front matter `abstract` and `keywords`). */
5
+ abstract: string;
6
+ keywords: string;
4
7
  alerts: Record<AlertType, string>;
5
8
  }
6
9
  /** Default document language: Spanish. English and others are available with `lang`. */
package/dist/i18n.js CHANGED
@@ -1,6 +1,8 @@
1
1
  const STRINGS = {
2
2
  en: {
3
3
  toc: "Contents",
4
+ abstract: "Abstract",
5
+ keywords: "Keywords",
4
6
  alerts: {
5
7
  note: "Note",
6
8
  tip: "Tip",
@@ -11,6 +13,8 @@ const STRINGS = {
11
13
  },
12
14
  es: {
13
15
  toc: "Contenido",
16
+ abstract: "Resumen",
17
+ keywords: "Palabras clave",
14
18
  alerts: {
15
19
  note: "Nota",
16
20
  tip: "Consejo",
@@ -21,6 +25,8 @@ const STRINGS = {
21
25
  },
22
26
  pt: {
23
27
  toc: "Sumário",
28
+ abstract: "Resumo",
29
+ keywords: "Palavras-chave",
24
30
  alerts: {
25
31
  note: "Nota",
26
32
  tip: "Dica",
@@ -31,6 +37,8 @@ const STRINGS = {
31
37
  },
32
38
  fr: {
33
39
  toc: "Sommaire",
40
+ abstract: "Résumé",
41
+ keywords: "Mots-clés",
34
42
  alerts: {
35
43
  note: "Remarque",
36
44
  tip: "Astuce",
@@ -41,6 +49,8 @@ const STRINGS = {
41
49
  },
42
50
  de: {
43
51
  toc: "Inhalt",
52
+ abstract: "Zusammenfassung",
53
+ keywords: "Schlüsselwörter",
44
54
  alerts: {
45
55
  note: "Hinweis",
46
56
  tip: "Tipp",
@@ -24,6 +24,8 @@ export interface RenderEnv extends Env {
24
24
  lang: string;
25
25
  /** Language whose hyphenation patterns break justified paragraphs (PDF only); see hyphenate.ts. */
26
26
  hyphenate?: string;
27
+ /** Render lone images as numbered figures captioned with their alt text (see figures.ts). */
28
+ figures?: boolean;
27
29
  /** Set when the document places its table of contents with a `[[toc]]` marker. */
28
30
  hasTocMarker: boolean;
29
31
  }
@@ -0,0 +1,7 @@
1
+ import type { MarkdownIt } from "markdown-it";
2
+ /**
3
+ * With `env.figures` (the academic theme), a lone image becomes a numbered figure: its alt text
4
+ * is the caption, printed as "Fig. 1. …" by the theme. A `#wide` fragment on the image makes it
5
+ * span both columns.
6
+ */
7
+ export declare function figures(md: MarkdownIt): void;
@@ -0,0 +1,53 @@
1
+ /** Fragment that sets a figure across the full page width: `![…](plot.png#wide)`. */
2
+ const WIDE = /#wide$/i;
3
+ /** The image of a paragraph holding nothing but one image (`![caption](img.png)`). */
4
+ function loneImage(tokens, i) {
5
+ if (tokens[i]?.type !== "paragraph_open" || tokens[i + 2]?.type !== "paragraph_close") {
6
+ return undefined;
7
+ }
8
+ const children = tokens[i + 1]?.children?.filter((child) => !(child.type === "text" && child.content.trim() === ""));
9
+ return children?.length === 1 && children[0].type === "image" ? children[0] : undefined;
10
+ }
11
+ /**
12
+ * With `env.figures` (the academic theme), a lone image becomes a numbered figure: its alt text
13
+ * is the caption, printed as "Fig. 1. …" by the theme. A `#wide` fragment on the image makes it
14
+ * span both columns.
15
+ */
16
+ export function figures(md) {
17
+ md.core.ruler.push("figures", (state) => {
18
+ if (!state.env.figures)
19
+ return;
20
+ const tokens = state.tokens;
21
+ for (let i = 0; i < tokens.length; i++) {
22
+ const image = loneImage(tokens, i);
23
+ if (!image)
24
+ continue;
25
+ const src = String(image.attrGet("src") ?? "");
26
+ const wide = WIDE.test(src);
27
+ if (wide)
28
+ image.attrSet("src", src.replace(WIDE, ""));
29
+ tokens[i].meta = { ...tokens[i].meta, figure: { wide } };
30
+ tokens[i + 2].meta = { ...tokens[i + 2].meta, figure: { image } };
31
+ i += 2;
32
+ }
33
+ });
34
+ const open = md.renderer.rules.paragraph_open;
35
+ const close = md.renderer.rules.paragraph_close;
36
+ md.renderer.rules.paragraph_open = (tokens, idx, options, env, self) => {
37
+ const figure = tokens[idx].meta?.figure;
38
+ if (figure)
39
+ return `<figure class="figure${figure.wide ? " figure-wide" : ""}">\n`;
40
+ return open ? open(tokens, idx, options, env, self) : self.renderToken(tokens, idx, options);
41
+ };
42
+ md.renderer.rules.paragraph_close = (tokens, idx, options, env, self) => {
43
+ const figure = tokens[idx].meta?.figure;
44
+ if (!figure) {
45
+ return close
46
+ ? close(tokens, idx, options, env, self)
47
+ : self.renderToken(tokens, idx, options);
48
+ }
49
+ const caption = self.renderInline(figure.image.children ?? [], options, env).trim();
50
+ return `${caption ? `\n<figcaption>${caption}</figcaption>` : ""}\n</figure>\n`;
51
+ };
52
+ }
53
+ //# sourceMappingURL=figures.js.map
@@ -24,6 +24,8 @@ export interface MarkdownOptions {
24
24
  lang?: string;
25
25
  /** Insert soft hyphens in justified paragraphs, with this language's patterns. */
26
26
  hyphenate?: string;
27
+ /** Render lone images as numbered figures, captioned with their alt text. */
28
+ figures?: boolean;
27
29
  }
28
30
  export declare function createMarkdown(): MarkdownIt;
29
31
  export declare function renderMarkdown(source: string, options?: MarkdownOptions): MarkdownResult;
@@ -6,6 +6,7 @@ import { DEFAULT_LANG } from "../i18n.js";
6
6
  import { alerts } from "./alerts.js";
7
7
  import { codeBlocks } from "./code-blocks.js";
8
8
  import { inlineChildLines } from "./env.js";
9
+ import { figures } from "./figures.js";
9
10
  import { highlightCode } from "./highlight.js";
10
11
  import { hyphenation } from "./hyphenate.js";
11
12
  import { inlineLocalImages } from "./images.js";
@@ -28,6 +29,8 @@ export function createMarkdown() {
28
29
  md.use(tocMarker);
29
30
  md.use(keepWithHeading);
30
31
  md.use(inlineChildLines);
32
+ // Before images are embedded: it takes the #wide mark off their src.
33
+ md.use(figures);
31
34
  md.use(inlineLocalImages);
32
35
  // Before math and mermaid, which hand their non-code fences on to this renderer.
33
36
  md.use(codeBlocks);
@@ -49,6 +52,7 @@ export function renderMarkdown(source, options = {}) {
49
52
  lang: options.lang ?? DEFAULT_LANG,
50
53
  hasTocMarker: false,
51
54
  hyphenate: options.hyphenate,
55
+ figures: options.figures,
52
56
  };
53
57
  const tokens = md.parse(source, env);
54
58
  let title;
package/dist/options.d.ts CHANGED
@@ -13,7 +13,7 @@ export declare const MERMAID_THEMES: readonly ["default", "neutral", "dark", "fo
13
13
  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
- export declare const BUILTIN_THEMES: readonly ["antigravity", "antigravity-dark", "light", "dark"];
16
+ export declare const BUILTIN_THEMES: readonly ["antigravity", "antigravity-dark", "light", "dark", "academic_basic"];
17
17
  export type BuiltinTheme = (typeof BUILTIN_THEMES)[number];
18
18
  /** Built-in themes with a dark page. */
19
19
  export declare const isDarkTheme: (theme: string) => boolean;
@@ -69,10 +69,23 @@ export interface DocumentOptions {
69
69
  lang: string;
70
70
  title?: string;
71
71
  subtitle?: string;
72
+ /** Authors; `^1,2^` marks affiliation numbers as superscripts. */
72
73
  author?: string;
73
74
  date?: string;
75
+ /** Label above the title, e.g. "Research article". */
76
+ kicker?: string;
77
+ /** Affiliations, numbered 1, 2… under the authors. */
78
+ affiliations?: string[];
79
+ /** Summary shown under the title block (paragraphs separated by blank lines). */
80
+ abstract?: string;
81
+ keywords?: string[];
74
82
  }
75
83
  export declare const DEFAULT_OPTIONS: DocumentOptions;
84
+ /**
85
+ * Defaults of a built-in theme, applied under every other layer: the academic theme sets its
86
+ * two columns in smaller type on wider margins, with Inter for titles, captions and labels.
87
+ */
88
+ export declare const THEME_DEFAULTS: Partial<Record<BuiltinTheme, Partial<DocumentOptions>>>;
76
89
  export declare class OptionsError extends MdRenderError {
77
90
  constructor(message: string);
78
91
  }
@@ -95,6 +108,7 @@ label?: (key: string) => string): Partial<DocumentOptions>;
95
108
  * Merge option layers, later ones winning. CSS lists accumulate instead of replacing. A layer
96
109
  * that sets the `margin` shorthand also replaces single-side margins from earlier layers, so
97
110
  * `--margin 15mm` on the command line is not partly undone by `marginLeft` in a config file.
111
+ * The defaults of the chosen built-in theme (THEME_DEFAULTS) go under every layer.
98
112
  */
99
113
  export declare function mergeOptions(...layers: Partial<DocumentOptions>[]): DocumentOptions;
100
114
  /** The four page margins: the `margin` shorthand, with single-side options on top. */
package/dist/options.js CHANGED
@@ -6,7 +6,13 @@ export const PAGE_SIZES = ["A3", "A4", "A5", "Letter", "Legal", "Tabloid"];
6
6
  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
- export const BUILTIN_THEMES = ["antigravity", "antigravity-dark", "light", "dark"];
9
+ export const BUILTIN_THEMES = [
10
+ "antigravity",
11
+ "antigravity-dark",
12
+ "light",
13
+ "dark",
14
+ "academic_basic",
15
+ ];
10
16
  /** Built-in themes with a dark page. */
11
17
  export const isDarkTheme = (theme) => theme === "dark" || theme === "antigravity-dark";
12
18
  export const TEXT_FONTS = ["manrope", "inter"];
@@ -28,6 +34,13 @@ export const DEFAULT_OPTIONS = {
28
34
  css: [],
29
35
  lang: DEFAULT_LANG,
30
36
  };
37
+ /**
38
+ * Defaults of a built-in theme, applied under every other layer: the academic theme sets its
39
+ * two columns in smaller type on wider margins, with Inter for titles, captions and labels.
40
+ */
41
+ export const THEME_DEFAULTS = {
42
+ academic_basic: { margin: "18mm 8mm", fontSize: "9.5pt", font: "inter" },
43
+ };
31
44
  export class OptionsError extends MdRenderError {
32
45
  constructor(message) {
33
46
  super(message, ExitCode.Usage);
@@ -63,6 +76,11 @@ const oneOf = (choices) => (value, describe) => {
63
76
  return match;
64
77
  throw new OptionsError(`${describe} must be one of: ${choices.join(", ")}`);
65
78
  };
79
+ /** Text, or a list of texts (`keywords: [a, b]`). */
80
+ const textList = (value, describe, baseDir) => {
81
+ const list = Array.isArray(value) ? value : [value];
82
+ return list.map((item) => text(item, describe, baseDir));
83
+ };
66
84
  const length = (value, describe) => {
67
85
  const raw = text(value, describe, "");
68
86
  const trimmed = raw.trim();
@@ -147,6 +165,10 @@ const VALIDATORS = {
147
165
  subtitle: text,
148
166
  author: text,
149
167
  date: text,
168
+ kicker: text,
169
+ affiliations: textList,
170
+ abstract: text,
171
+ keywords: textList,
150
172
  };
151
173
  /**
152
174
  * Validate options from a config file, front matter or the command line.
@@ -180,10 +202,13 @@ const SIDES = ["marginTop", "marginRight", "marginBottom", "marginLeft"];
180
202
  * Merge option layers, later ones winning. CSS lists accumulate instead of replacing. A layer
181
203
  * that sets the `margin` shorthand also replaces single-side margins from earlier layers, so
182
204
  * `--margin 15mm` on the command line is not partly undone by `marginLeft` in a config file.
205
+ * The defaults of the chosen built-in theme (THEME_DEFAULTS) go under every layer.
183
206
  */
184
207
  export function mergeOptions(...layers) {
208
+ const theme = layers.reduce((t, layer) => layer.theme ?? t, DEFAULT_OPTIONS.theme);
209
+ const themeDefaults = THEME_DEFAULTS[theme];
185
210
  const merged = { ...DEFAULT_OPTIONS, css: [] };
186
- for (const layer of layers) {
211
+ for (const layer of themeDefaults ? [themeDefaults, ...layers] : layers) {
187
212
  const { css, ...rest } = layer;
188
213
  if (rest.margin !== undefined)
189
214
  for (const side of SIDES)
@@ -77,6 +77,8 @@ export function mermaidThemeVariables(theme) {
77
77
  export function headerFooterStyle(theme) {
78
78
  if (isAntigravityTheme(theme))
79
79
  return { color: tokensFor(theme).inkMuted, mono: true };
80
+ if (theme === "academic_basic")
81
+ return { color: "#4a4a4a", mono: false };
80
82
  return { color: "#59636e", mono: false };
81
83
  }
82
84
  //# sourceMappingURL=antigravity.js.map
@@ -8,6 +8,8 @@ import type { TextFont } from "../options.js";
8
8
  * slants it) nor arrows and math symbols such as → ✓ ≤: those fall back to Inter, which is
9
9
  * embedded too in its regular and bold weights.
10
10
  * Code: JetBrains Mono from Fontsource (split into unicode-range subsets).
11
+ * Serif (academic theme only): STIX Two Text from Fontsource, the text face of the STIX fonts
12
+ * made for scientific publishing.
11
13
  */
12
14
  export declare const FONT_FAMILIES: Record<TextFont, string>;
13
15
  /** CSS font-family list for the text: the chosen font, Inter for missing glyphs, the system's. */
@@ -20,6 +22,8 @@ export declare function fontStack(font: TextFont): string;
20
22
  export declare function inlineFontStylesheet(cssKey: string): string;
21
23
  /** @font-face rules for every font a document uses, with the font files embedded. */
22
24
  export declare function fontFaceCss(font?: TextFont): string;
25
+ /** @font-face rules for STIX Two Text, the academic theme's serif, with the files embedded. */
26
+ export declare function serifFontFaceCss(): string;
23
27
  /** JetBrains Mono Regular alone, for page headers/footers set in the mono family. */
24
28
  export declare function monoRegularFontFace(): string;
25
29
  /** The text font's regular weight alone, for page headers/footers (rendered apart from the page). */
@@ -1,4 +1,4 @@
1
- import { INTER_DIR, INTER_FACES, MANROPE_STYLESHEETS, MONO_STYLESHEETS, readAsset, readTextAsset, woff2References, } from "../assets.js";
1
+ import { INTER_DIR, INTER_FACES, MANROPE_STYLESHEETS, MONO_STYLESHEETS, readAsset, SERIF_STYLESHEETS, 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.
@@ -8,6 +8,8 @@ import { INTER_DIR, INTER_FACES, MANROPE_STYLESHEETS, MONO_STYLESHEETS, readAsse
8
8
  * slants it) nor arrows and math symbols such as → ✓ ≤: those fall back to Inter, which is
9
9
  * embedded too in its regular and bold weights.
10
10
  * Code: JetBrains Mono from Fontsource (split into unicode-range subsets).
11
+ * Serif (academic theme only): STIX Two Text from Fontsource, the text face of the STIX fonts
12
+ * made for scientific publishing.
11
13
  */
12
14
  export const FONT_FAMILIES = { manrope: "Manrope", inter: "Inter" };
13
15
  /** CSS font-family list for the text: the chosen font, Inter for missing glyphs, the system's. */
@@ -64,6 +66,12 @@ export function fontFaceCss(font = "manrope") {
64
66
  }
65
67
  return css;
66
68
  }
69
+ let serif;
70
+ /** @font-face rules for STIX Two Text, the academic theme's serif, with the files embedded. */
71
+ export function serifFontFaceCss() {
72
+ serif ??= SERIF_STYLESHEETS.map(inlineFontStylesheet).join("\n");
73
+ return serif;
74
+ }
67
75
  let monoRegular;
68
76
  /** JetBrains Mono Regular alone, for page headers/footers set in the mono family. */
69
77
  export function monoRegularFontFace() {
@@ -1,7 +1,7 @@
1
1
  import { escapeHtml } from "../html.js";
2
2
  import type { BuiltinTheme, TextFont } from "../options.js";
3
3
  export { escapeHtml };
4
- /** Stylesheets of a built-in theme. */
4
+ /** Stylesheets of a built-in theme, with the fonts it adds to the text and code ones. */
5
5
  export declare function builtinThemeCss(theme: BuiltinTheme): string[];
6
6
  /** Stylesheet for a density other than "normal", applied after the theme. */
7
7
  export declare function densityCss(density: "normal" | "compact"): string[];
@@ -13,8 +13,13 @@ export interface DocumentParts {
13
13
  lang?: string;
14
14
  /** Front matter metadata. With `showTitle`, rendered as a title block before the body. */
15
15
  subtitle?: string;
16
+ /** Authors; `^1,2^` marks affiliation numbers (superscripts in the title block). */
16
17
  author?: string;
17
18
  date?: string;
19
+ kicker?: string;
20
+ affiliations?: string[];
21
+ abstract?: string;
22
+ keywords?: string[];
18
23
  showTitle?: boolean;
19
24
  /** Theme stylesheet contents. Defaults to the built-in light theme. */
20
25
  themeCss?: string[];
@@ -27,5 +32,7 @@ export interface DocumentParts {
27
32
  /** Extra stylesheet contents, applied after the theme and layout. */
28
33
  extraCss?: string[];
29
34
  }
35
+ /** The authors without their `^1,2^` affiliation marks, for metadata and page headers. */
36
+ export declare const plainAuthor: (author: string) => string;
30
37
  /** Wrap a rendered Markdown fragment in a complete, self-contained HTML document. */
31
38
  export declare function buildHtmlDocument(parts: DocumentParts): string;
@@ -1,6 +1,7 @@
1
1
  import { readTextAsset } from "../assets.js";
2
2
  import { escapeHtml } from "../html.js";
3
- import { fontFaceCss } from "./fonts.js";
3
+ import { DEFAULT_LANG, strings } from "../i18n.js";
4
+ import { fontFaceCss, serifFontFaceCss } from "./fonts.js";
4
5
  import { katexCss } from "./katex.js";
5
6
  import { version } from "../version.js";
6
7
  export { escapeHtml };
@@ -19,22 +20,53 @@ const THEME_FILES = {
19
20
  "antigravity-dark": ["theme.css", "theme-antigravity.css", "theme-antigravity-dark.css"],
20
21
  light: ["theme.css"],
21
22
  dark: ["theme.css", "theme-dark.css"],
23
+ academic_basic: ["theme.css", "theme-academic-basic.css"],
22
24
  };
23
- /** Stylesheets of a built-in theme. */
25
+ /** Stylesheets of a built-in theme, with the fonts it adds to the text and code ones. */
24
26
  export function builtinThemeCss(theme) {
25
- return THEME_FILES[theme].map(asset);
27
+ const css = THEME_FILES[theme].map(asset);
28
+ return theme === "academic_basic" ? [serifFontFaceCss(), ...css] : css;
26
29
  }
27
30
  /** Stylesheet for a density other than "normal", applied after the theme. */
28
31
  export function densityCss(density) {
29
32
  return density === "compact" ? [asset("density-compact.css")] : [];
30
33
  }
31
- function titleBlock({ title, subtitle, author, date }) {
32
- const meta = [author, date].filter(Boolean).map((part) => escapeHtml(part));
34
+ /** The authors without their `^1,2^` affiliation marks, for metadata and page headers. */
35
+ export const plainAuthor = (author) => author.replace(/\^[^^\s][^^]*\^/g, "").trim();
36
+ /** Escaped text with `^…^` turned into superscripts. */
37
+ const withSuperscripts = (value) => escapeHtml(value).replace(/\^([^^\s][^^]*)\^/g, "<sup>$1</sup>");
38
+ /** Paragraphs of a text, separated by blank lines. */
39
+ const paragraphs = (value) => value
40
+ .split(/\n\s*\n/)
41
+ .map((p) => p.trim())
42
+ .filter(Boolean)
43
+ .map((p) => `<p>${escapeHtml(p)}</p>`);
44
+ function titleBlock(parts) {
45
+ const { title, subtitle, author, date, kicker, affiliations = [], abstract, keywords = [], } = parts;
46
+ const labels = strings(parts.lang ?? DEFAULT_LANG);
47
+ const meta = [author ? withSuperscripts(author) : undefined, date ? escapeHtml(date) : undefined];
48
+ const metaText = meta.filter(Boolean).join(" · ");
33
49
  return [
34
50
  `<header class="doc-header">`,
51
+ kicker ? `<p class="doc-kicker">${escapeHtml(kicker)}</p>` : "",
35
52
  `<h1 class="doc-title">${escapeHtml(title)}</h1>`,
36
53
  subtitle ? `<p class="doc-subtitle">${escapeHtml(subtitle)}</p>` : "",
37
- meta.length ? `<p class="doc-meta">${meta.join(" · ")}</p>` : "",
54
+ metaText ? `<p class="doc-meta">${metaText}</p>` : "",
55
+ affiliations.length
56
+ ? `<ol class="doc-affiliations">` +
57
+ affiliations.map((a, i) => `<li><sup>${i + 1}</sup>${escapeHtml(a)}</li>`).join("") +
58
+ `</ol>`
59
+ : "",
60
+ abstract
61
+ ? `<section class="doc-abstract">\n<p class="doc-abstract-title">${labels.abstract}</p>\n` +
62
+ paragraphs(abstract).join("\n") +
63
+ `\n</section>`
64
+ : "",
65
+ keywords.length
66
+ ? `<p class="doc-keywords"><span class="doc-keywords-label">${labels.keywords}:</span> ` +
67
+ keywords.map(escapeHtml).join(", ") +
68
+ `</p>`
69
+ : "",
38
70
  `</header>`,
39
71
  ]
40
72
  .filter(Boolean)
@@ -57,7 +89,7 @@ export function buildHtmlDocument(parts) {
57
89
  <meta charset="utf-8">
58
90
  <meta name="viewport" content="width=device-width, initial-scale=1">
59
91
  <meta name="generator" content="mdrender ${escapeHtml(version)}">
60
- ${author ? `<meta name="author" content="${escapeHtml(author)}">\n` : ""}<title>${escapeHtml(title)}</title>
92
+ ${author ? `<meta name="author" content="${escapeHtml(plainAuthor(author))}">\n` : ""}<title>${escapeHtml(title)}</title>
61
93
  ${styles.map((css) => `<style>\n${css}\n</style>`).join("\n")}
62
94
  </head>
63
95
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imontene/mdrender",
3
- "version": "2.7.0",
3
+ "version": "2.8.1",
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",
@@ -67,6 +67,7 @@
67
67
  "dependencies": {
68
68
  "@fontsource/jetbrains-mono": "^5.3.0",
69
69
  "@fontsource/manrope": "^5.3.0",
70
+ "@fontsource/stix-two-text": "^5.3.0",
70
71
  "@puppeteer/browsers": "^3.2.3",
71
72
  "chokidar": "^5.0.0",
72
73
  "commander": "^15.0.0",