mrmd-editor 0.7.1 → 0.8.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.
Files changed (58) hide show
  1. package/package.json +3 -1
  2. package/src/commands.js +112 -4
  3. package/src/comment-syntax.js +364 -39
  4. package/src/config/handlers.js +1 -2
  5. package/src/config/schema.js +46 -4
  6. package/src/document-template.js +2236 -0
  7. package/src/frontmatter-updater.js +204 -74
  8. package/src/grammar.js +758 -0
  9. package/src/index.js +1074 -55
  10. package/src/keymap.js +11 -2
  11. package/src/markdown/block-decorations.js +108 -5
  12. package/src/markdown/facets.js +37 -0
  13. package/src/markdown/html-inline.js +9 -5
  14. package/src/markdown/index.js +13 -3
  15. package/src/markdown/inline-commands.js +256 -0
  16. package/src/markdown/inline-model.js +578 -0
  17. package/src/markdown/inline-state.js +103 -0
  18. package/src/markdown/renderer.js +219 -12
  19. package/src/markdown/styles.js +290 -3
  20. package/src/markdown/widgets/alert-title.js +10 -8
  21. package/src/markdown/widgets/frontmatter.js +0 -6
  22. package/src/markdown/widgets/index.js +1 -0
  23. package/src/markdown/widgets/list-marker.js +29 -0
  24. package/src/markdown/wysiwyg.js +1158 -0
  25. package/src/mrp-types.js +2 -0
  26. package/src/output-widget.js +532 -18
  27. package/src/page-view-pagination.js +127 -0
  28. package/src/runtime-lsp.js +1757 -150
  29. package/src/section-controls/commands.js +617 -0
  30. package/src/section-controls/index.js +63 -0
  31. package/src/section-controls/plugin.js +165 -0
  32. package/src/section-controls/widgets.js +936 -0
  33. package/src/shell/ai-menu.js +11 -0
  34. package/src/shell/components/context-panel.js +572 -0
  35. package/src/shell/components/status-bar.js +10 -2
  36. package/src/shell/layouts/studio.js +206 -14
  37. package/src/shell/orchestrator-client.js +69 -0
  38. package/src/spellcheck.js +166 -0
  39. package/src/tables/README.md +97 -0
  40. package/src/tables/commands/insert-linked-table.js +122 -0
  41. package/src/tables/commands/open-table-workspace.js +43 -0
  42. package/src/tables/index.js +24 -0
  43. package/src/tables/jobs/client.js +158 -0
  44. package/src/tables/parsing/anchors.js +82 -0
  45. package/src/tables/parsing/linked-table-blocks.js +61 -0
  46. package/src/tables/state/linked-table-state.js +68 -0
  47. package/src/tables/widgets/linked-table-source-banner.js +77 -0
  48. package/src/tables/widgets/linked-table-widget.js +256 -0
  49. package/src/tables/workspace/controller.js +616 -0
  50. package/src/term-pty-client.js +51 -2
  51. package/src/term-widget.js +43 -3
  52. package/src/widgets/theme-utils.js +24 -16
  53. package/src/widgets/theme.js +1015 -1
  54. package/src/runtime-codelens/detector.js +0 -279
  55. package/src/runtime-codelens/index.js +0 -76
  56. package/src/runtime-codelens/plugin.js +0 -142
  57. package/src/runtime-codelens/styles.js +0 -184
  58. package/src/runtime-codelens/widgets.js +0 -216
@@ -0,0 +1,2236 @@
1
+ /**
2
+ * Document template system.
3
+ *
4
+ * Semantic, reusable document styling separate from the app/editor chrome theme.
5
+ * Phase 1 focuses on editor preview + reusable presets.
6
+ *
7
+ * @module document-template
8
+ */
9
+
10
+ import { EditorView, ViewPlugin } from '@codemirror/view';
11
+ import { syntaxHighlighting } from '@codemirror/language';
12
+ import { tags as lezerTags, tagHighlighter } from '@lezer/highlight';
13
+
14
+ export const defaultDocumentTemplate = {
15
+ name: 'Default',
16
+ version: 5,
17
+ editor: {
18
+ applyDocumentStyles: true,
19
+ },
20
+ page: {
21
+ background: '',
22
+ maxWidth: '',
23
+ // Print / export properties (not used in editor preview)
24
+ paperSize: '', // 'letter' | 'a4' | 'a5' | 'legal' | ''
25
+ marginTop: '', // e.g. '1in', '2.54cm'
26
+ marginBottom: '',
27
+ marginLeft: '',
28
+ marginRight: '',
29
+ },
30
+ body: {
31
+ fontFamily: '',
32
+ fontSize: '',
33
+ lineHeight: '',
34
+ color: '',
35
+ textTransform: '', // '' | 'uppercase' | 'lowercase' | 'capitalize'
36
+ paragraphSpacing: '', // e.g. '1em', '12pt' — maps to CSS margin-bottom on p, LaTeX \parskip
37
+ },
38
+ // --- Inline mark styles (bold, italic, underline, strikethrough) ---
39
+ // Base defaults apply everywhere. Per-block overrides cascade:
40
+ // inlineMarks.bold → heading.bold → heading.h1.bold
41
+ inlineMarks: {
42
+ bold: {
43
+ color: '',
44
+ fontFamily: '',
45
+ fontWeight: '',
46
+ relativeSize: '', // e.g. '1.05' → 1.05em
47
+ textTransform: '',
48
+ },
49
+ italic: {
50
+ color: '',
51
+ fontFamily: '',
52
+ fontStyle: '',
53
+ relativeSize: '',
54
+ textTransform: '',
55
+ },
56
+ underline: {
57
+ color: '',
58
+ lineColor: '',
59
+ lineStyle: '', // '' | 'solid' | 'dashed' | 'dotted' | 'wavy'
60
+ textTransform: '',
61
+ },
62
+ strikethrough: {
63
+ color: '',
64
+ lineColor: '',
65
+ textTransform: '',
66
+ },
67
+ },
68
+ heading: {
69
+ color: '',
70
+ fontFamily: '',
71
+ textTransform: '',
72
+ // Per-heading sizes plus optional inline-mark overrides
73
+ h1: {
74
+ fontFamily: '', fontSize: '', fontWeight: '', textTransform: '',
75
+ bold: { color: '', fontFamily: '', fontWeight: '', relativeSize: '', textTransform: '' },
76
+ italic: { color: '', fontFamily: '', fontStyle: '', relativeSize: '', textTransform: '' },
77
+ underline: { color: '', lineColor: '', lineStyle: '', textTransform: '' },
78
+ strikethrough: { color: '', lineColor: '', textTransform: '' },
79
+ },
80
+ h2: {
81
+ fontFamily: '', fontSize: '', fontWeight: '', textTransform: '',
82
+ bold: { color: '', fontFamily: '', fontWeight: '', relativeSize: '', textTransform: '' },
83
+ italic: { color: '', fontFamily: '', fontStyle: '', relativeSize: '', textTransform: '' },
84
+ underline: { color: '', lineColor: '', lineStyle: '', textTransform: '' },
85
+ strikethrough: { color: '', lineColor: '', textTransform: '' },
86
+ },
87
+ h3: {
88
+ fontFamily: '', fontSize: '', fontWeight: '', textTransform: '',
89
+ bold: { color: '', fontFamily: '', fontWeight: '', relativeSize: '', textTransform: '' },
90
+ italic: { color: '', fontFamily: '', fontStyle: '', relativeSize: '', textTransform: '' },
91
+ underline: { color: '', lineColor: '', lineStyle: '', textTransform: '' },
92
+ strikethrough: { color: '', lineColor: '', textTransform: '' },
93
+ },
94
+ h4: {
95
+ fontFamily: '', fontSize: '', fontWeight: '', textTransform: '',
96
+ bold: { color: '', fontFamily: '', fontWeight: '', relativeSize: '', textTransform: '' },
97
+ italic: { color: '', fontFamily: '', fontStyle: '', relativeSize: '', textTransform: '' },
98
+ underline: { color: '', lineColor: '', lineStyle: '', textTransform: '' },
99
+ strikethrough: { color: '', lineColor: '', textTransform: '' },
100
+ },
101
+ h5: {
102
+ fontFamily: '', fontSize: '', fontWeight: '', textTransform: '',
103
+ bold: { color: '', fontFamily: '', fontWeight: '', relativeSize: '', textTransform: '' },
104
+ italic: { color: '', fontFamily: '', fontStyle: '', relativeSize: '', textTransform: '' },
105
+ underline: { color: '', lineColor: '', lineStyle: '', textTransform: '' },
106
+ strikethrough: { color: '', lineColor: '', textTransform: '' },
107
+ },
108
+ h6: {
109
+ fontFamily: '', fontSize: '', fontWeight: '', textTransform: '',
110
+ bold: { color: '', fontFamily: '', fontWeight: '', relativeSize: '', textTransform: '' },
111
+ italic: { color: '', fontFamily: '', fontStyle: '', relativeSize: '', textTransform: '' },
112
+ underline: { color: '', lineColor: '', lineStyle: '', textTransform: '' },
113
+ strikethrough: { color: '', lineColor: '', textTransform: '' },
114
+ },
115
+ // Heading-wide mark overrides (applied to all h1-h6 unless a per-level override exists)
116
+ bold: { color: '', fontFamily: '', fontWeight: '', relativeSize: '', textTransform: '' },
117
+ italic: { color: '', fontFamily: '', fontStyle: '', relativeSize: '', textTransform: '' },
118
+ underline: { color: '', lineColor: '', lineStyle: '', textTransform: '' },
119
+ strikethrough: { color: '', lineColor: '', textTransform: '' },
120
+ numbering: '', // '' | 'none' | 'all' | 'h2+' — hint for Pandoc --number-sections
121
+ },
122
+ blockquote: {
123
+ borderLeftColor: '',
124
+ background: '',
125
+ color: '',
126
+ fontFamily: '',
127
+ fontStyle: '', // '' | 'italic' | 'normal'
128
+ textTransform: '',
129
+ // Blockquote-specific mark overrides
130
+ bold: { color: '', fontFamily: '', fontWeight: '', relativeSize: '', textTransform: '' },
131
+ italic: { color: '', fontFamily: '', fontStyle: '', relativeSize: '', textTransform: '' },
132
+ underline: { color: '', lineColor: '', lineStyle: '', textTransform: '' },
133
+ strikethrough: { color: '', lineColor: '', textTransform: '' },
134
+ },
135
+ code: {
136
+ inline: {
137
+ fontFamily: '',
138
+ background: '',
139
+ color: '',
140
+ },
141
+ block: {
142
+ fontFamily: '',
143
+ fontSize: '',
144
+ background: '',
145
+ color: '',
146
+ lineNumbers: '', // '' | 'true' | 'false' — hint for Pandoc --listings / code line numbers
147
+ lineHeight: '', // e.g. '1.5'
148
+ borderRadius: '', // e.g. '6px'
149
+ borderColor: '', // e.g. '#333'
150
+ padding: '', // e.g. '1em'
151
+ },
152
+ // --- Cell chrome: header bar, run button, output area ---
153
+ cell: {
154
+ headerBackground: '', // code cell header bar background
155
+ headerColor: '', // code cell header bar text color
156
+ headerBorderColor: '', // border below header
157
+ outputBackground: '', // output area background
158
+ outputColor: '', // output area text color
159
+ outputBorderColor: '', // border above output area
160
+ outputFontFamily: '', // output area font family
161
+ outputFontSize: '', // output area font size (e.g. '0.85em')
162
+ outputLineHeight: '', // output area line height
163
+ },
164
+ // --- Syntax highlighting tokens ---
165
+ // Base token colors apply to all code blocks/languages.
166
+ // Per-language overrides cascade: code.highlight.keyword → code.highlight.languages.python.keyword
167
+ //
168
+ // Each token property is a color string (e.g. '#569cd6').
169
+ // Additional style properties (fontWeight, fontStyle) are supported for some tokens.
170
+ highlight: {
171
+ // Semantic token colors (base — all languages)
172
+ keyword: '', // for, if, while, return, import, class, function, etc.
173
+ controlKeyword: '', // if, else, for, while, try, catch — control flow subset
174
+ string: '', // "hello", 'world', `template`
175
+ number: '', // 42, 3.14, 0xFF
176
+ comment: '', // // line comment, /* block comment */
177
+ function: '', // function names: print(), len(), my_func()
178
+ variable: '', // variable names
179
+ type: '', // type/class names: int, String, MyClass
180
+ operator: '', // +, -, *, =, ==, !=, &&
181
+ punctuation: '', // (), {}, [], ;, :, .
182
+ property: '', // object.property, dict.key
183
+ constant: '', // true, false, null, None, nil
184
+ regexp: '', // /pattern/flags
185
+ escape: '', // \n, \t, unicode escapes
186
+ tag: '', // HTML/XML tag names
187
+ attribute: '', // HTML/XML attribute names
188
+ attributeValue: '', // HTML/XML attribute values
189
+ meta: '', // preprocessor, decorators, annotations
190
+ inserted: '', // diff: added lines
191
+ deleted: '', // diff: removed lines
192
+ changed: '', // diff: changed lines
193
+
194
+ // Style modifiers for specific tokens (beyond just color)
195
+ keywordStyle: '', // '' | 'bold' | 'italic' | 'bold italic'
196
+ commentStyle: '', // '' | 'italic' | 'bold' | 'bold italic'
197
+ functionStyle: '', // '' | 'bold' | 'italic'
198
+ typeStyle: '', // '' | 'bold' | 'italic'
199
+
200
+ // Per-language overrides
201
+ // Each language key contains the same token properties as above.
202
+ // Only non-empty values override the base.
203
+ languages: {
204
+ python: { keyword: '', controlKeyword: '', string: '', number: '', comment: '', function: '', variable: '', type: '', operator: '', punctuation: '', property: '', constant: '', meta: '', keywordStyle: '', commentStyle: '', functionStyle: '', typeStyle: '' },
205
+ r: { keyword: '', controlKeyword: '', string: '', number: '', comment: '', function: '', variable: '', type: '', operator: '', punctuation: '', property: '', constant: '', meta: '', keywordStyle: '', commentStyle: '', functionStyle: '', typeStyle: '' },
206
+ julia: { keyword: '', controlKeyword: '', string: '', number: '', comment: '', function: '', variable: '', type: '', operator: '', punctuation: '', property: '', constant: '', meta: '', keywordStyle: '', commentStyle: '', functionStyle: '', typeStyle: '' },
207
+ javascript: { keyword: '', controlKeyword: '', string: '', number: '', comment: '', function: '', variable: '', type: '', operator: '', punctuation: '', property: '', constant: '', meta: '', keywordStyle: '', commentStyle: '', functionStyle: '', typeStyle: '' },
208
+ typescript: { keyword: '', controlKeyword: '', string: '', number: '', comment: '', function: '', variable: '', type: '', operator: '', punctuation: '', property: '', constant: '', meta: '', keywordStyle: '', commentStyle: '', functionStyle: '', typeStyle: '' },
209
+ html: { keyword: '', string: '', comment: '', tag: '', attribute: '', attributeValue: '', punctuation: '', meta: '', commentStyle: '' },
210
+ css: { keyword: '', string: '', number: '', comment: '', property: '', punctuation: '', constant: '', meta: '', commentStyle: '' },
211
+ sql: { keyword: '', string: '', number: '', comment: '', function: '', operator: '', punctuation: '', constant: '', keywordStyle: '', commentStyle: '' },
212
+ rust: { keyword: '', controlKeyword: '', string: '', number: '', comment: '', function: '', variable: '', type: '', operator: '', punctuation: '', property: '', constant: '', meta: '', keywordStyle: '', commentStyle: '', functionStyle: '', typeStyle: '' },
213
+ go: { keyword: '', controlKeyword: '', string: '', number: '', comment: '', function: '', variable: '', type: '', operator: '', punctuation: '', property: '', constant: '', meta: '', keywordStyle: '', commentStyle: '', functionStyle: '', typeStyle: '' },
214
+ java: { keyword: '', controlKeyword: '', string: '', number: '', comment: '', function: '', variable: '', type: '', operator: '', punctuation: '', property: '', constant: '', meta: '', keywordStyle: '', commentStyle: '', functionStyle: '', typeStyle: '' },
215
+ cpp: { keyword: '', controlKeyword: '', string: '', number: '', comment: '', function: '', variable: '', type: '', operator: '', punctuation: '', property: '', constant: '', meta: '', keywordStyle: '', commentStyle: '', functionStyle: '', typeStyle: '' },
216
+ shell: { keyword: '', string: '', number: '', comment: '', variable: '', operator: '', punctuation: '', constant: '', commentStyle: '' },
217
+ yaml: { keyword: '', string: '', number: '', comment: '', property: '', punctuation: '', constant: '', commentStyle: '' },
218
+ json: { keyword: '', string: '', number: '', property: '', punctuation: '', constant: '' },
219
+ },
220
+ },
221
+ },
222
+ link: {
223
+ color: '',
224
+ underline: '',
225
+ },
226
+ table: {
227
+ borderColor: '',
228
+ headerBackground: '',
229
+ headerColor: '', // header text color
230
+ headerFontWeight: '', // '' | '400' | '600' | '700' | '800'
231
+ fontFamily: '', // table body font (falls back to body font)
232
+ fontSize: '', // e.g. '0.9em' — relative to body
233
+ color: '', // table body text color
234
+ cellPadding: '', // e.g. '8px 12px'
235
+ stripedRows: '', // '' | 'even' | 'odd' — alternating row background
236
+ stripedColor: '', // background color for striped rows
237
+ },
238
+ math: {
239
+ color: '', // math text color
240
+ fontSize: '', // e.g. '1.1em' — relative size for math
241
+ displayBackground: '', // background behind display math blocks
242
+ displayPadding: '', // padding for display math blocks
243
+ displayBorderRadius: '', // border radius for display math blocks
244
+ },
245
+ hr: {
246
+ color: '',
247
+ style: '', // '' | 'solid' | 'dashed' | 'dotted'
248
+ thickness: '', // e.g. '1px', '2px'
249
+ },
250
+ list: {
251
+ bulletStyle: '', // '' | 'disc' | 'circle' | 'square' | 'dash'
252
+ numberStyle: '', // '' | 'decimal' | 'lower-alpha' | 'lower-roman' | 'upper-alpha' | 'upper-roman'
253
+ },
254
+ image: {
255
+ captionFontSize: '', // e.g. '0.9em'
256
+ captionColor: '',
257
+ borderRadius: '', // e.g. '4px'
258
+ maxWidth: '', // e.g. '100%', '80%'
259
+ },
260
+ toc: {
261
+ enabled: '', // '' | 'true' | 'false' — whether export includes TOC
262
+ depth: '', // '' | '2' | '3' | '4' — heading depth for TOC
263
+ },
264
+ };
265
+
266
+ export const documentTemplatePresets = [
267
+ defaultDocumentTemplate,
268
+ {
269
+ name: 'Manuscript',
270
+ version: 1,
271
+ page: { background: '#fffefc', maxWidth: '760px' },
272
+ body: {
273
+ fontFamily: 'Charter, Georgia, serif',
274
+ fontSize: '17px',
275
+ lineHeight: '1.8',
276
+ color: '#232323',
277
+ },
278
+ heading: {
279
+ color: '#121212',
280
+ h1: { fontSize: '2.3em', fontWeight: '800' },
281
+ h2: { fontSize: '1.75em', fontWeight: '700' },
282
+ h3: { fontSize: '1.35em', fontWeight: '700' },
283
+ },
284
+ blockquote: {
285
+ borderLeftColor: '#7c3aed',
286
+ background: '#f6f1ff',
287
+ color: '#4b5563',
288
+ },
289
+ code: {
290
+ inline: {
291
+ fontFamily: 'JetBrains Mono, monospace',
292
+ background: '#f3f4f6',
293
+ color: '#9d174d',
294
+ },
295
+ block: {
296
+ fontFamily: 'JetBrains Mono, monospace',
297
+ fontSize: '0.84em',
298
+ background: '#111827',
299
+ color: '#e5e7eb',
300
+ },
301
+ },
302
+ link: { color: '#1d4ed8', underline: true },
303
+ table: { borderColor: '#d1d5db', headerBackground: '#f9fafb' },
304
+ },
305
+ {
306
+ name: 'Report',
307
+ version: 1,
308
+ page: { background: '#ffffff', maxWidth: '900px' },
309
+ body: {
310
+ fontFamily: 'Inter, system-ui, sans-serif',
311
+ fontSize: '15px',
312
+ lineHeight: '1.7',
313
+ color: '#1f2937',
314
+ },
315
+ heading: {
316
+ color: '#0f172a',
317
+ h1: { fontSize: '2.1em', fontWeight: '800' },
318
+ h2: { fontSize: '1.6em', fontWeight: '700' },
319
+ h3: { fontSize: '1.25em', fontWeight: '700' },
320
+ },
321
+ blockquote: {
322
+ borderLeftColor: '#2563eb',
323
+ background: '#eff6ff',
324
+ color: '#334155',
325
+ },
326
+ code: {
327
+ inline: {
328
+ fontFamily: 'JetBrains Mono, monospace',
329
+ background: '#eef2ff',
330
+ color: '#4338ca',
331
+ },
332
+ block: {
333
+ fontFamily: 'JetBrains Mono, monospace',
334
+ fontSize: '0.82em',
335
+ background: '#0f172a',
336
+ color: '#e2e8f0',
337
+ },
338
+ },
339
+ link: { color: '#2563eb', underline: true },
340
+ table: { borderColor: '#cbd5e1', headerBackground: '#f8fafc' },
341
+ },
342
+ {
343
+ name: 'Notebook',
344
+ version: 1,
345
+ page: { background: '', maxWidth: '840px' },
346
+ body: {
347
+ fontFamily: 'Alegreya, Georgia, serif',
348
+ fontSize: '18px',
349
+ lineHeight: '1.75',
350
+ color: '',
351
+ },
352
+ heading: {
353
+ color: '',
354
+ h1: { fontSize: '2.4em', fontWeight: '800' },
355
+ h2: { fontSize: '1.8em', fontWeight: '700' },
356
+ h3: { fontSize: '1.4em', fontWeight: '700' },
357
+ },
358
+ blockquote: {
359
+ borderLeftColor: '#f59e0b',
360
+ background: '#fffbeb',
361
+ color: '#5b4636',
362
+ },
363
+ code: {
364
+ inline: {
365
+ fontFamily: 'SF Mono, monospace',
366
+ background: '#f3f4f6',
367
+ color: '#b45309',
368
+ },
369
+ block: {
370
+ fontFamily: 'SF Mono, monospace',
371
+ fontSize: '0.8em',
372
+ background: '#f8fafc',
373
+ color: '#111827',
374
+ },
375
+ },
376
+ link: { color: '#0ea5e9', underline: true },
377
+ table: { borderColor: '#e5e7eb', headerBackground: '#fafaf9' },
378
+ },
379
+ // ── Academic ──────────────────────────────────────────────
380
+ // Formal serif layout for papers, theses, and research notes.
381
+ // Tight body, generous heading hierarchy, muted palette.
382
+ {
383
+ name: 'Academic',
384
+ version: 1,
385
+ page: { background: '#ffffff', maxWidth: '780px', paperSize: 'letter', marginTop: '1in', marginBottom: '1in', marginLeft: '1.25in', marginRight: '1.25in' },
386
+ body: {
387
+ fontFamily: 'Palatino, "Palatino Linotype", "Book Antiqua", Georgia, serif',
388
+ fontSize: '16px',
389
+ lineHeight: '1.65',
390
+ color: '#1a1a1a',
391
+ paragraphSpacing: '0.6em',
392
+ },
393
+ heading: {
394
+ color: '#1a1a1a',
395
+ numbering: 'all',
396
+ h1: { fontSize: '1.9em', fontWeight: '700' },
397
+ h2: { fontSize: '1.5em', fontWeight: '700' },
398
+ h3: { fontSize: '1.2em', fontWeight: '700' },
399
+ h4: { fontSize: '1.05em', fontWeight: '700' },
400
+ },
401
+ blockquote: {
402
+ borderLeftColor: '#6b7280',
403
+ background: '#f9fafb',
404
+ color: '#374151',
405
+ fontStyle: 'italic',
406
+ },
407
+ code: {
408
+ inline: {
409
+ fontFamily: 'Inconsolata, "Source Code Pro", monospace',
410
+ background: '#f3f4f6',
411
+ color: '#6d28d9',
412
+ },
413
+ block: {
414
+ fontFamily: 'Inconsolata, "Source Code Pro", monospace',
415
+ fontSize: '0.85em',
416
+ background: '#f8f9fa',
417
+ color: '#1f2937',
418
+ lineNumbers: 'true',
419
+ },
420
+ highlight: {
421
+ keyword: '#7c3aed',
422
+ controlKeyword: '#be185d',
423
+ string: '#059669',
424
+ number: '#d97706',
425
+ comment: '#6b7280',
426
+ function: '#1e40af',
427
+ variable: '#1f2937',
428
+ type: '#0d9488',
429
+ operator: '#374151',
430
+ punctuation: '#6b7280',
431
+ property: '#1e40af',
432
+ constant: '#7c3aed',
433
+ commentStyle: 'italic',
434
+ typeStyle: 'italic',
435
+ },
436
+ },
437
+ link: { color: '#1e40af', underline: true },
438
+ table: { borderColor: '#9ca3af', headerBackground: '#f3f4f6' },
439
+ hr: { color: '#d1d5db', style: 'solid', thickness: '1px' },
440
+ toc: { enabled: 'true', depth: '3' },
441
+ },
442
+ // ── Dark Prose ───────────────────────────────────────────
443
+ // Warm dark background with soft amber text — easy on the eyes
444
+ // for long reading sessions. Earthy blockquotes, muted code.
445
+ {
446
+ name: 'Dark Prose',
447
+ version: 1,
448
+ page: { background: '#1c1917', maxWidth: '760px' },
449
+ body: {
450
+ fontFamily: '"Libre Baskerville", Georgia, serif',
451
+ fontSize: '17px',
452
+ lineHeight: '1.85',
453
+ color: '#d6d3d1',
454
+ },
455
+ heading: {
456
+ color: '#fafaf9',
457
+ h1: { fontSize: '2.2em', fontWeight: '700' },
458
+ h2: { fontSize: '1.65em', fontWeight: '700' },
459
+ h3: { fontSize: '1.3em', fontWeight: '600' },
460
+ },
461
+ blockquote: {
462
+ borderLeftColor: '#a16207',
463
+ background: '#292524',
464
+ color: '#a8a29e',
465
+ fontStyle: 'italic',
466
+ },
467
+ code: {
468
+ inline: {
469
+ fontFamily: 'JetBrains Mono, monospace',
470
+ background: '#292524',
471
+ color: '#fbbf24',
472
+ },
473
+ block: {
474
+ fontFamily: 'JetBrains Mono, monospace',
475
+ fontSize: '0.84em',
476
+ background: '#0c0a09',
477
+ color: '#e7e5e4',
478
+ },
479
+ highlight: {
480
+ keyword: '#fbbf24',
481
+ controlKeyword: '#f59e0b',
482
+ string: '#a3e635',
483
+ number: '#e879f9',
484
+ comment: '#78716c',
485
+ function: '#38bdf8',
486
+ variable: '#e7e5e4',
487
+ type: '#34d399',
488
+ operator: '#a8a29e',
489
+ punctuation: '#a8a29e',
490
+ property: '#67e8f9',
491
+ constant: '#fbbf24',
492
+ commentStyle: 'italic',
493
+ },
494
+ },
495
+ link: { color: '#f59e0b', underline: false },
496
+ table: { borderColor: '#44403c', headerBackground: '#292524' },
497
+ hr: { color: '#44403c', style: 'solid', thickness: '1px' },
498
+ image: { borderRadius: '4px' },
499
+ },
500
+ // ── Magazine ─────────────────────────────────────────────
501
+ // Bold sans-serif, compact body, strong color accents.
502
+ // Inspired by digital publication and editorial layouts.
503
+ {
504
+ name: 'Magazine',
505
+ version: 1,
506
+ page: { background: '#ffffff', maxWidth: '880px' },
507
+ body: {
508
+ fontFamily: '"Source Sans Pro", "Open Sans", system-ui, sans-serif',
509
+ fontSize: '15px',
510
+ lineHeight: '1.65',
511
+ color: '#292524',
512
+ paragraphSpacing: '0.8em',
513
+ },
514
+ heading: {
515
+ color: '#0f172a',
516
+ h1: { fontSize: '2.8em', fontWeight: '900' },
517
+ h2: { fontSize: '1.9em', fontWeight: '800' },
518
+ h3: { fontSize: '1.35em', fontWeight: '700' },
519
+ },
520
+ blockquote: {
521
+ borderLeftColor: '#e11d48',
522
+ background: '#fff1f2',
523
+ color: '#4c0519',
524
+ },
525
+ code: {
526
+ inline: {
527
+ fontFamily: 'SF Mono, "Fira Code", monospace',
528
+ background: '#f1f5f9',
529
+ color: '#0f766e',
530
+ },
531
+ block: {
532
+ fontFamily: 'SF Mono, "Fira Code", monospace',
533
+ fontSize: '0.82em',
534
+ background: '#0f172a',
535
+ color: '#e2e8f0',
536
+ },
537
+ },
538
+ link: { color: '#e11d48', underline: false },
539
+ table: { borderColor: '#e2e8f0', headerBackground: '#f8fafc' },
540
+ hr: { color: '#e11d48', style: 'solid', thickness: '2px' },
541
+ image: { borderRadius: '6px', maxWidth: '100%' },
542
+ list: { bulletStyle: 'square' },
543
+ },
544
+ // ── Warm Novel ───────────────────────────────────────────
545
+ // Cream paper, generous margins, Garamond-style type.
546
+ // Optimized for fiction, essays, and immersive long-form reading.
547
+ {
548
+ name: 'Warm Novel',
549
+ version: 1,
550
+ page: { background: '#fdf6e3', maxWidth: '680px', paperSize: 'a5' },
551
+ body: {
552
+ fontFamily: '"EB Garamond", Garamond, "Times New Roman", serif',
553
+ fontSize: '19px',
554
+ lineHeight: '1.9',
555
+ color: '#3c3226',
556
+ paragraphSpacing: '0.5em',
557
+ },
558
+ heading: {
559
+ color: '#2c1810',
560
+ h1: { fontSize: '2.4em', fontWeight: '600' },
561
+ h2: { fontSize: '1.7em', fontWeight: '600' },
562
+ h3: { fontSize: '1.3em', fontWeight: '600' },
563
+ },
564
+ blockquote: {
565
+ borderLeftColor: '#92400e',
566
+ background: '#fef3c7',
567
+ color: '#78350f',
568
+ fontStyle: 'italic',
569
+ },
570
+ code: {
571
+ inline: {
572
+ fontFamily: '"Courier Prime", "Courier New", monospace',
573
+ background: '#fef9ef',
574
+ color: '#92400e',
575
+ },
576
+ block: {
577
+ fontFamily: '"Courier Prime", "Courier New", monospace',
578
+ fontSize: '0.82em',
579
+ background: '#fffbeb',
580
+ color: '#422006',
581
+ },
582
+ },
583
+ link: { color: '#b45309', underline: true },
584
+ table: { borderColor: '#d6cbb5', headerBackground: '#fef9ef' },
585
+ hr: { color: '#d6cbb5', style: 'solid', thickness: '1px' },
586
+ },
587
+ // ── Swiss Minimal ────────────────────────────────────────
588
+ // Ultra-clean Helvetica/system sans, lots of whitespace,
589
+ // no decorative accents — lets the content breathe.
590
+ {
591
+ name: 'Swiss Minimal',
592
+ version: 1,
593
+ page: { background: '#ffffff', maxWidth: '720px' },
594
+ body: {
595
+ fontFamily: '"Helvetica Neue", Helvetica, Arial, system-ui, sans-serif',
596
+ fontSize: '15px',
597
+ lineHeight: '1.7',
598
+ color: '#111111',
599
+ paragraphSpacing: '1em',
600
+ },
601
+ heading: {
602
+ color: '#000000',
603
+ h1: { fontSize: '2.6em', fontWeight: '300' },
604
+ h2: { fontSize: '1.8em', fontWeight: '400' },
605
+ h3: { fontSize: '1.2em', fontWeight: '600' },
606
+ },
607
+ blockquote: {
608
+ borderLeftColor: '#111111',
609
+ background: '',
610
+ color: '#555555',
611
+ },
612
+ code: {
613
+ inline: {
614
+ fontFamily: 'SF Mono, "Cascadia Code", monospace',
615
+ background: '#f5f5f5',
616
+ color: '#333333',
617
+ },
618
+ block: {
619
+ fontFamily: 'SF Mono, "Cascadia Code", monospace',
620
+ fontSize: '0.84em',
621
+ background: '#fafafa',
622
+ color: '#222222',
623
+ },
624
+ },
625
+ link: { color: '#111111', underline: true },
626
+ table: { borderColor: '#e5e5e5', headerBackground: '#fafafa' },
627
+ hr: { color: '#e5e5e5', style: 'solid', thickness: '1px' },
628
+ list: { bulletStyle: 'dash' },
629
+ },
630
+ ];
631
+
632
+ function clone(value) {
633
+ return JSON.parse(JSON.stringify(value));
634
+ }
635
+
636
+ function mergeDeep(base, override) {
637
+ if (!override || typeof override !== 'object' || Array.isArray(override)) return clone(base);
638
+ const result = Array.isArray(base) ? [...base] : { ...base };
639
+ for (const [key, value] of Object.entries(override)) {
640
+ if (value && typeof value === 'object' && !Array.isArray(value) && result[key] && typeof result[key] === 'object' && !Array.isArray(result[key])) {
641
+ result[key] = mergeDeep(result[key], value);
642
+ } else {
643
+ result[key] = value;
644
+ }
645
+ }
646
+ return result;
647
+ }
648
+
649
+ export function normalizeDocumentTemplate(template = {}) {
650
+ const merged = mergeDeep(defaultDocumentTemplate, template || {});
651
+ if (!merged.name) merged.name = 'Untitled Template';
652
+ if (!merged.version) merged.version = 1;
653
+ return merged;
654
+ }
655
+
656
+ export function cloneDocumentTemplate(template) {
657
+ return clone(normalizeDocumentTemplate(template));
658
+ }
659
+
660
+ export function findDocumentTemplatePreset(name) {
661
+ return documentTemplatePresets.find((t) => t.name === name) || null;
662
+ }
663
+
664
+ const HEADING_MARK_CONTEXT_SELECTORS = ['.cm-md-h1', '.cm-md-h2', '.cm-md-h3', '.cm-md-h4', '.cm-md-h5', '.cm-md-h6'];
665
+
666
+ /**
667
+ * Build CSS rule objects for inline mark overrides (bold, italic, underline, strikethrough).
668
+ * `parentSel` can be empty, a selector string, or an array of selector strings.
669
+ *
670
+ * Important detail for CodeMirror decorations:
671
+ * overlapping mark decorations often end up on the SAME span, not only in a
672
+ * parent/child DOM relationship. So for contextual mark styling we emit both:
673
+ *
674
+ * .cm-md-h1.cm-md-bold
675
+ * .cm-md-h1 .cm-md-bold
676
+ *
677
+ * Cascading order:
678
+ * inlineMarks.bold → heading.bold → heading.h1.bold
679
+ * More-specific selectors win via normal CSS specificity.
680
+ */
681
+ function inlineMarkCSS(scopeFn, source, parentSel) {
682
+ if (!source) return {};
683
+ const rules = {};
684
+ const parents = Array.isArray(parentSel)
685
+ ? parentSel.filter(Boolean)
686
+ : parentSel
687
+ ? String(parentSel).split(',').map((s) => s.trim()).filter(Boolean)
688
+ : [];
689
+
690
+ const mk = (cls) => {
691
+ const leaf = `.${cls}`;
692
+ if (!parents.length) return scopeFn(leaf);
693
+ return parents
694
+ .flatMap((parent) => [scopeFn(`${parent}${leaf}`), scopeFn(`${parent} ${leaf}`)])
695
+ .join(', ');
696
+ };
697
+
698
+ // Color precedence for inline marks is handled by the override extension.
699
+ // This structural theme keeps only non-color typography + decoration rules.
700
+
701
+ // --- Bold ---
702
+ const b = source.bold;
703
+ if (b) {
704
+ const decls = {};
705
+ if (b.fontFamily) decls.fontFamily = `${b.fontFamily} !important`;
706
+ if (b.fontWeight) decls.fontWeight = `${b.fontWeight} !important`;
707
+ if (b.relativeSize) decls.fontSize = `${b.relativeSize}em !important`;
708
+ if (b.textTransform) decls.textTransform = `${b.textTransform} !important`;
709
+ if (Object.keys(decls).length) rules[mk('cm-md-bold')] = decls;
710
+ }
711
+
712
+ // --- Italic ---
713
+ const it = source.italic;
714
+ if (it) {
715
+ const decls = {};
716
+ if (it.fontFamily) decls.fontFamily = `${it.fontFamily} !important`;
717
+ if (it.fontStyle) decls.fontStyle = `${it.fontStyle} !important`;
718
+ if (it.relativeSize) decls.fontSize = `${it.relativeSize}em !important`;
719
+ if (it.textTransform) decls.textTransform = `${it.textTransform} !important`;
720
+ if (Object.keys(decls).length) rules[mk('cm-md-italic')] = decls;
721
+ }
722
+
723
+ // --- Underline ---
724
+ const ul = source.underline;
725
+ if (ul) {
726
+ const decls = {};
727
+ if (ul.lineColor) decls.textDecorationColor = `${ul.lineColor} !important`;
728
+ if (ul.lineStyle) decls.textDecorationStyle = `${ul.lineStyle} !important`;
729
+ if (ul.textTransform) decls.textTransform = `${ul.textTransform} !important`;
730
+ if (Object.keys(decls).length) rules[mk('cm-md-underline')] = decls;
731
+ }
732
+
733
+ // --- Strikethrough ---
734
+ const st = source.strikethrough;
735
+ if (st) {
736
+ const decls = {};
737
+ if (st.lineColor) decls.textDecorationColor = `${st.lineColor} !important`;
738
+ if (st.textTransform) decls.textTransform = `${st.textTransform} !important`;
739
+ if (Object.keys(decls).length) rules[mk('cm-md-strikethrough')] = decls;
740
+ }
741
+
742
+ return rules;
743
+ }
744
+
745
+ export function compileDocumentTemplateCSS(template, scope = '&') {
746
+ const t = normalizeDocumentTemplate(template);
747
+ const bodyFont = t.body.fontFamily || '';
748
+ const bodySize = t.body.fontSize || '';
749
+ const bodyLineHeight = t.body.lineHeight || '';
750
+ const applyDocumentStyles = t.editor?.applyDocumentStyles !== false;
751
+
752
+ if (!applyDocumentStyles) {
753
+ return {};
754
+ }
755
+
756
+ const s = (sel) => `${scope} ${sel}`;
757
+
758
+ return {
759
+ // --- Page-level background covers the whole editor ---
760
+ [scope]: {
761
+ ...(t.page.background ? { backgroundColor: t.page.background } : {}),
762
+ // Neutralize theme influence inside the document surface when document
763
+ // styling is explicitly enabled. App chrome theme stays outside this scope.
764
+ '--editor-background': t.page.background || '#ffffff',
765
+ '--editor-foreground': t.body.color || '#222222',
766
+ '--md-heading-color': t.heading?.color || (t.body.color || '#111827'),
767
+ '--md-link-color': t.link?.color || '#1d4ed8',
768
+ '--md-link-decoration': t.link?.underline === false ? 'none' : 'underline',
769
+ '--md-code-background': t.code?.inline?.background || '#f3f4f6',
770
+ '--md-code-color': t.code?.inline?.color || '#a31515',
771
+ '--md-blockquote-color': t.blockquote?.color || (t.body.color || '#4b5563'),
772
+ '--md-marker-color': '#9ca3af',
773
+ '--md-list-marker-color': '#6b7280',
774
+ '--md-hr-color': t.hr?.color || '#d1d5db',
775
+ '--widget-text': t.body.color || '#222222',
776
+ '--widget-text-muted': '#6b7280',
777
+ '--widget-text-accent': t.link?.color || '#1d4ed8',
778
+ '--widget-border': t.table?.borderColor || '#d1d5db',
779
+ '--widget-surface': t.code?.block?.background || '#f8fafc',
780
+ '--widget-surface-inset': '#f3f4f6',
781
+ },
782
+ [`${scope} .cm-scroller`]: {
783
+ ...(t.page.background ? { backgroundColor: t.page.background } : {}),
784
+ ...(t.page.maxWidth ? { ['--document-max-width']: t.page.maxWidth } : {}),
785
+ ...(bodyFont ? { fontFamily: bodyFont } : {}),
786
+ ...(bodySize ? { fontSize: bodySize } : {}),
787
+ ...(bodyLineHeight ? { lineHeight: bodyLineHeight } : {}),
788
+ },
789
+ [`${scope} .cm-content`]: {
790
+ ...(t.page.maxWidth ? { maxWidth: t.page.maxWidth } : {}),
791
+ },
792
+ // --- Body text ---
793
+ [`${scope} .cm-line`]: {
794
+ ...(bodyFont ? { fontFamily: bodyFont } : {}),
795
+ ...(bodySize ? { fontSize: bodySize } : {}),
796
+ ...(bodyLineHeight ? { lineHeight: bodyLineHeight } : {}),
797
+ ...(t.body.textTransform ? { textTransform: t.body.textTransform } : {}),
798
+ },
799
+ // --- Paragraph spacing (applied as margin between paragraphs via empty lines) ---
800
+ ...(t.body.paragraphSpacing ? {
801
+ [`${scope} .cm-line:empty + .cm-line:not(:empty)`]: {
802
+ marginTop: t.body.paragraphSpacing,
803
+ },
804
+ } : {}),
805
+ // --- Headings h1-h6 ---
806
+ [s('.cm-md-h1') + ', ' + s('.cm-frontmatter-title-input')]: {
807
+ ...(t.heading?.h1?.fontSize ? { fontSize: t.heading.h1.fontSize } : {}),
808
+ ...(t.heading?.h1?.fontWeight ? { fontWeight: t.heading.h1.fontWeight } : {}),
809
+ ...((t.heading?.h1?.fontFamily || t.heading?.fontFamily || bodyFont) ? { fontFamily: t.heading?.h1?.fontFamily || t.heading?.fontFamily || bodyFont } : {}),
810
+ ...((t.heading?.h1?.textTransform || t.heading?.textTransform || t.body?.textTransform) ? { textTransform: t.heading?.h1?.textTransform || t.heading?.textTransform || t.body?.textTransform } : {}),
811
+ },
812
+ [s('.cm-md-h2')]: {
813
+ ...(t.heading?.h2?.fontSize ? { fontSize: t.heading.h2.fontSize } : {}),
814
+ ...(t.heading?.h2?.fontWeight ? { fontWeight: t.heading.h2.fontWeight } : {}),
815
+ ...((t.heading?.h2?.fontFamily || t.heading?.fontFamily || bodyFont) ? { fontFamily: t.heading?.h2?.fontFamily || t.heading?.fontFamily || bodyFont } : {}),
816
+ ...((t.heading?.h2?.textTransform || t.heading?.textTransform || t.body?.textTransform) ? { textTransform: t.heading?.h2?.textTransform || t.heading?.textTransform || t.body?.textTransform } : {}),
817
+ },
818
+ [s('.cm-md-h3')]: {
819
+ ...(t.heading?.h3?.fontSize ? { fontSize: t.heading.h3.fontSize } : {}),
820
+ ...(t.heading?.h3?.fontWeight ? { fontWeight: t.heading.h3.fontWeight } : {}),
821
+ ...((t.heading?.h3?.fontFamily || t.heading?.fontFamily || bodyFont) ? { fontFamily: t.heading?.h3?.fontFamily || t.heading?.fontFamily || bodyFont } : {}),
822
+ ...((t.heading?.h3?.textTransform || t.heading?.textTransform || t.body?.textTransform) ? { textTransform: t.heading?.h3?.textTransform || t.heading?.textTransform || t.body?.textTransform } : {}),
823
+ },
824
+ [s('.cm-md-h4')]: {
825
+ ...(t.heading?.h4?.fontSize ? { fontSize: t.heading.h4.fontSize } : {}),
826
+ ...(t.heading?.h4?.fontWeight ? { fontWeight: t.heading.h4.fontWeight } : {}),
827
+ ...((t.heading?.h4?.fontFamily || t.heading?.fontFamily || bodyFont) ? { fontFamily: t.heading?.h4?.fontFamily || t.heading?.fontFamily || bodyFont } : {}),
828
+ ...((t.heading?.h4?.textTransform || t.heading?.textTransform || t.body?.textTransform) ? { textTransform: t.heading?.h4?.textTransform || t.heading?.textTransform || t.body?.textTransform } : {}),
829
+ },
830
+ [s('.cm-md-h5')]: {
831
+ ...(t.heading?.h5?.fontSize ? { fontSize: t.heading.h5.fontSize } : {}),
832
+ ...(t.heading?.h5?.fontWeight ? { fontWeight: t.heading.h5.fontWeight } : {}),
833
+ ...((t.heading?.h5?.fontFamily || t.heading?.fontFamily || bodyFont) ? { fontFamily: t.heading?.h5?.fontFamily || t.heading?.fontFamily || bodyFont } : {}),
834
+ ...((t.heading?.h5?.textTransform || t.heading?.textTransform || t.body?.textTransform) ? { textTransform: t.heading?.h5?.textTransform || t.heading?.textTransform || t.body?.textTransform } : {}),
835
+ },
836
+ [s('.cm-md-h6')]: {
837
+ ...(t.heading?.h6?.fontSize ? { fontSize: t.heading.h6.fontSize } : {}),
838
+ ...(t.heading?.h6?.fontWeight ? { fontWeight: t.heading.h6.fontWeight } : {}),
839
+ ...((t.heading?.h6?.fontFamily || t.heading?.fontFamily || bodyFont) ? { fontFamily: t.heading?.h6?.fontFamily || t.heading?.fontFamily || bodyFont } : {}),
840
+ ...((t.heading?.h6?.textTransform || t.heading?.textTransform || t.body?.textTransform) ? { textTransform: t.heading?.h6?.textTransform || t.heading?.textTransform || t.body?.textTransform } : {}),
841
+ },
842
+ [[
843
+ s('.cm-frontmatter-subtitle'),
844
+ s('.cm-frontmatter-author'),
845
+ s('.cm-frontmatter-author-affiliation'),
846
+ s('.cm-frontmatter-date'),
847
+ s('.cm-frontmatter-abstract'),
848
+ s('.cm-frontmatter-keyword'),
849
+ ].join(', ')]: {
850
+ ...(bodyFont ? { fontFamily: bodyFont } : {}),
851
+ ...(t.body?.textTransform ? { textTransform: t.body.textTransform } : {}),
852
+ },
853
+ // --- Blockquotes ---
854
+ [s('.cm-md-blockquote-line')]: {
855
+ ...(t.blockquote?.borderLeftColor ? { borderLeftColor: t.blockquote.borderLeftColor } : {}),
856
+ ...(t.blockquote?.background ? { backgroundColor: t.blockquote.background } : {}),
857
+ ...(t.blockquote?.fontFamily ? { fontFamily: t.blockquote.fontFamily } : bodyFont ? { fontFamily: bodyFont } : {}),
858
+ ...(t.blockquote?.fontStyle ? { fontStyle: t.blockquote.fontStyle } : {}),
859
+ ...((t.blockquote?.textTransform || t.body?.textTransform) ? { textTransform: t.blockquote?.textTransform || t.body?.textTransform } : {}),
860
+ },
861
+ // --- Inline marks: base styles ---
862
+ ...inlineMarkCSS(s, t.inlineMarks, ''),
863
+ // --- Inline marks: heading-wide overrides (all h1-h6) ---
864
+ ...inlineMarkCSS(s, t.heading, HEADING_MARK_CONTEXT_SELECTORS),
865
+ // --- Inline marks: per-heading-level overrides (h1-h6) ---
866
+ ...inlineMarkCSS(s, t.heading?.h1, '.cm-md-h1'),
867
+ ...inlineMarkCSS(s, t.heading?.h2, '.cm-md-h2'),
868
+ ...inlineMarkCSS(s, t.heading?.h3, '.cm-md-h3'),
869
+ ...inlineMarkCSS(s, t.heading?.h4, '.cm-md-h4'),
870
+ ...inlineMarkCSS(s, t.heading?.h5, '.cm-md-h5'),
871
+ ...inlineMarkCSS(s, t.heading?.h6, '.cm-md-h6'),
872
+ // --- Inline marks: blockquote overrides ---
873
+ ...inlineMarkCSS(s, t.blockquote, '.cm-md-blockquote-line'),
874
+ // --- Inline code ---
875
+ [s('.cm-md-inline-code')]: {
876
+ ...(t.code?.inline?.fontFamily ? { fontFamily: t.code.inline.fontFamily } : {}),
877
+ ...(t.code?.inline?.background ? { backgroundColor: t.code.inline.background } : {}),
878
+ },
879
+ // --- Code blocks (source) ---
880
+ [s('.cm-codeblock-line') + ', ' + s('.cm-codeblock-fence') + ', ' + s('.cm-wysiwyg-code-fence-line')]: {
881
+ ...(t.code?.block?.fontFamily ? { fontFamily: t.code.block.fontFamily } : {}),
882
+ ...(t.code?.block?.fontSize ? { fontSize: t.code.block.fontSize } : {}),
883
+ ...(t.code?.block?.background ? { backgroundColor: t.code.block.background } : {}),
884
+ },
885
+ [s('.cm-wysiwyg-code-fence-widget') + ', ' + s('.cm-wysiwyg-code-header')]: {
886
+ ...(t.code?.block?.background ? { backgroundColor: t.code.block.background } : {}),
887
+ },
888
+ // --- Output widgets: inherit page background so they don't punch holes ---
889
+ ...(t.page.background ? {
890
+ [s('.cm-output-widget') + ', ' + s('.cm-html-output-widget') + ', ' + s('.cm-css-output-widget') + ', ' + s('.cm-scroll-output-widget') + ', ' + s('.cm-json-output-widget')]: {
891
+ background: `color-mix(in srgb, ${t.page.background} 85%, black)`,
892
+ },
893
+ } : {}),
894
+ // --- Links ---
895
+ [s('.cm-md-link-text') + ', ' + s('.cm-external-link') + ', ' + s('.cm-file-link') + ', ' + s('.cm-wiki-link')]: {
896
+ ...(t.link?.underline === false ? { textDecoration: 'none' } : {}),
897
+ },
898
+ // --- Tables ---
899
+ [s('.cm-table-widget table') + ', ' + s('.cm-table-widget th') + ', ' + s('.cm-table-widget td')]: {
900
+ ...(t.table?.borderColor ? { borderColor: t.table.borderColor } : {}),
901
+ },
902
+ [s('.cm-table-widget table')]: {
903
+ ...(t.table?.fontFamily ? { fontFamily: t.table.fontFamily } : bodyFont ? { fontFamily: bodyFont } : {}),
904
+ ...(t.table?.fontSize ? { fontSize: t.table.fontSize } : {}),
905
+ },
906
+ [s('.cm-table-widget th')]: {
907
+ ...(t.table?.headerBackground ? { backgroundColor: t.table.headerBackground } : {}),
908
+ ...(t.table?.headerColor ? { color: t.table.headerColor } : {}),
909
+ ...(t.table?.headerFontWeight ? { fontWeight: t.table.headerFontWeight } : {}),
910
+ },
911
+ [s('.cm-table-widget td')]: {
912
+ ...(t.table?.color ? { color: t.table.color } : {}),
913
+ ...(t.table?.cellPadding ? { padding: t.table.cellPadding } : {}),
914
+ },
915
+ // Striped rows
916
+ ...(t.table?.stripedRows && t.table?.stripedColor ? {
917
+ [s(`.cm-table-widget tbody tr:nth-child(${t.table.stripedRows}) td`)]: {
918
+ backgroundColor: t.table.stripedColor,
919
+ },
920
+ } : {}),
921
+ // --- Math ---
922
+ // KaTeX renders its own elements inside .cm-math-* containers.
923
+ // We must target .katex and internal spans to override KaTeX's own color.
924
+ ...(t.math?.color || t.math?.fontSize ? {
925
+ [s('.cm-math-inline') + ', ' + s('.cm-math-display')]: {
926
+ ...(t.math.color ? { color: t.math.color } : {}),
927
+ ...(t.math.fontSize ? { fontSize: t.math.fontSize } : {}),
928
+ },
929
+ // KaTeX internal elements need explicit override
930
+ [s('.cm-math-inline .katex') + ', ' + s('.cm-math-display .katex')]: {
931
+ ...(t.math.color ? { color: `${t.math.color} !important` } : {}),
932
+ ...(t.math.fontSize ? { fontSize: t.math.fontSize } : {}),
933
+ },
934
+ } : {}),
935
+ ...(t.math?.displayBackground || t.math?.displayPadding || t.math?.displayBorderRadius ? {
936
+ [s('.cm-math-display')]: {
937
+ ...(t.math.displayBackground ? { backgroundColor: t.math.displayBackground } : {}),
938
+ ...(t.math.displayPadding ? { padding: t.math.displayPadding } : {}),
939
+ ...(t.math.displayBorderRadius ? { borderRadius: t.math.displayBorderRadius } : {}),
940
+ },
941
+ } : {}),
942
+ // --- Horizontal rules ---
943
+ ...(t.hr?.color || t.hr?.thickness ? {
944
+ [s('.cm-md-hr-line::after')]: {
945
+ ...(t.hr.color ? { background: t.hr.color } : {}),
946
+ ...(t.hr.thickness ? { height: t.hr.thickness } : {}),
947
+ },
948
+ } : {}),
949
+ // --- List markers ---
950
+ ...(t.list?.bulletStyle ? {
951
+ [s('.cm-md-list-bullet')]: {
952
+ content: ({ disc: '"•"', circle: '"○"', square: '"■"', dash: '"—"' })[t.list.bulletStyle] || undefined,
953
+ },
954
+ } : {}),
955
+ // --- Neutralize syntax-theme token colors inside ALL code surfaces ---
956
+ // ALWAYS neutralize ͼN (CodeMirror HighlightStyle) classes so the
957
+ // app theme's syntax colors don't leak through. Document-template-owned
958
+ // token colors are applied via deterministic .cm-dt-* classes stamped by
959
+ // our tagHighlighter extension; those rules use higher specificity and
960
+ // !important in the override <style> element, so they win here.
961
+ // When no highlight tokens are set, code blocks simply inherit
962
+ // code.block.color (or body.color) — clean, theme-neutral look.
963
+ [s('.cm-codeblock-line span[class^="ͼ"], .cm-codeblock-line span[class*=" ͼ"], .cm-codeblock-line span[class*="ͼ"], .cm-codeblock-fence span[class^="ͼ"], .cm-codeblock-fence span[class*=" ͼ"], .cm-codeblock-fence span[class*="ͼ"], .cm-wysiwyg-code-fence-line span[class^="ͼ"], .cm-wysiwyg-code-fence-line span[class*=" ͼ"], .cm-wysiwyg-code-fence-line span[class*="ͼ"], .cm-md-inline-code span[class^="ͼ"], .cm-md-inline-code span[class*=" ͼ"], .cm-md-inline-code span[class*="ͼ"]')]: {
964
+ color: 'inherit !important',
965
+ backgroundColor: 'transparent !important',
966
+ fontStyle: 'inherit !important',
967
+ fontWeight: 'inherit !important',
968
+ textDecorationColor: 'inherit !important',
969
+ },
970
+ // --- Code block additional properties ---
971
+ ...(t.code?.block?.lineHeight ? {
972
+ [s('.cm-codeblock-line')]: {
973
+ ...((rules) => rules)({}),
974
+ lineHeight: t.code.block.lineHeight,
975
+ },
976
+ } : {}),
977
+ ...(t.code?.block?.borderRadius ? {
978
+ [s('.cm-codeblock-fence:first-child, .cm-codeblock-line:first-child')]: {
979
+ borderTopLeftRadius: t.code.block.borderRadius,
980
+ borderTopRightRadius: t.code.block.borderRadius,
981
+ },
982
+ [s('.cm-codeblock-fence:last-child, .cm-codeblock-line:last-child')]: {
983
+ borderBottomLeftRadius: t.code.block.borderRadius,
984
+ borderBottomRightRadius: t.code.block.borderRadius,
985
+ },
986
+ } : {}),
987
+ ...(t.code?.block?.borderColor ? {
988
+ [s('.cm-codeblock-fence') + ', ' + s('.cm-codeblock-line')]: {
989
+ borderLeft: `1px solid ${t.code.block.borderColor}`,
990
+ borderRight: `1px solid ${t.code.block.borderColor}`,
991
+ },
992
+ [s('.cm-codeblock-fence:first-of-type')]: {
993
+ borderTop: `1px solid ${t.code.block.borderColor}`,
994
+ },
995
+ [s('.cm-codeblock-fence:last-of-type')]: {
996
+ borderBottom: `1px solid ${t.code.block.borderColor}`,
997
+ },
998
+ } : {}),
999
+ ...(t.code?.block?.padding ? {
1000
+ [s('.cm-codeblock-line')]: {
1001
+ paddingLeft: t.code.block.padding,
1002
+ paddingRight: t.code.block.padding,
1003
+ },
1004
+ } : {}),
1005
+ // --- Code cell chrome ---
1006
+ ...(t.code?.cell?.headerBackground || t.code?.cell?.headerColor || t.code?.cell?.headerBorderColor ? {
1007
+ [s('.cm-wysiwyg-code-header')]: {
1008
+ ...(t.code.cell.headerBackground ? { backgroundColor: `${t.code.cell.headerBackground} !important` } : {}),
1009
+ ...(t.code.cell.headerColor ? { color: `${t.code.cell.headerColor} !important` } : {}),
1010
+ ...(t.code.cell.headerBorderColor ? { borderBottomColor: `${t.code.cell.headerBorderColor} !important` } : {}),
1011
+ },
1012
+ } : {}),
1013
+ ...(t.code?.cell?.outputBackground || t.code?.cell?.outputColor || t.code?.cell?.outputBorderColor ||
1014
+ t.code?.cell?.outputFontFamily || t.code?.cell?.outputFontSize || t.code?.cell?.outputLineHeight ? {
1015
+ [s('.cm-output-widget') + ', ' + s('.cm-html-output-widget') + ', ' + s('.cm-css-output-widget') + ', ' + s('.cm-scroll-output-widget') + ', ' + s('.cm-json-output-widget')]: {
1016
+ ...(t.code.cell.outputBackground ? { backgroundColor: `${t.code.cell.outputBackground} !important` } : {}),
1017
+ ...(t.code.cell.outputColor ? { color: `${t.code.cell.outputColor} !important` } : {}),
1018
+ ...(t.code.cell.outputBorderColor ? { borderTopColor: `${t.code.cell.outputBorderColor} !important` } : {}),
1019
+ ...(t.code.cell.outputFontFamily ? { fontFamily: `${t.code.cell.outputFontFamily} !important` } : {}),
1020
+ ...(t.code.cell.outputFontSize ? { fontSize: `${t.code.cell.outputFontSize} !important` } : {}),
1021
+ ...(t.code.cell.outputLineHeight ? { lineHeight: `${t.code.cell.outputLineHeight} !important` } : {}),
1022
+ },
1023
+ } : {}),
1024
+ // Output content (pre blocks inside output widgets)
1025
+ ...(t.code?.cell?.outputFontFamily || t.code?.cell?.outputFontSize ? {
1026
+ [s('.cm-output-content') + ', ' + s('.cm-scroll-output-content')]: {
1027
+ ...(t.code.cell.outputFontFamily ? { fontFamily: `${t.code.cell.outputFontFamily} !important` } : {}),
1028
+ ...(t.code.cell.outputFontSize ? { fontSize: `${t.code.cell.outputFontSize} !important` } : {}),
1029
+ },
1030
+ } : {}),
1031
+ // Scroll output header bar
1032
+ ...(t.code?.cell?.headerBackground || t.code?.cell?.headerColor ? {
1033
+ [s('.cm-scroll-output-header')]: {
1034
+ ...(t.code.cell.headerBackground ? { backgroundColor: `${t.code.cell.headerBackground} !important` } : {}),
1035
+ ...(t.code.cell.headerColor ? { color: `${t.code.cell.headerColor} !important` } : {}),
1036
+ },
1037
+ } : {}),
1038
+ // --- Images ---
1039
+ [s('.cm-image-block-img')]: {
1040
+ ...(t.image?.borderRadius ? { borderRadius: t.image.borderRadius } : {}),
1041
+ ...(t.image?.maxWidth ? { maxWidth: t.image.maxWidth } : {}),
1042
+ },
1043
+ [s('.cm-image-caption')]: {
1044
+ ...(bodyFont ? { fontFamily: bodyFont } : {}),
1045
+ ...(t.image?.captionFontSize ? { fontSize: t.image.captionFontSize } : {}),
1046
+ ...(t.image?.captionColor ? { color: t.image.captionColor } : t.body.color ? { color: t.body.color } : {}),
1047
+ opacity: '0.8',
1048
+ },
1049
+ // --- Page break markers ---
1050
+ [s('.cm-pagebreak-line')]: {
1051
+ position: 'relative',
1052
+ height: '3em',
1053
+ lineHeight: '3em',
1054
+ textAlign: 'center',
1055
+ color: 'transparent',
1056
+ userSelect: 'none',
1057
+ },
1058
+ [s('.cm-pagebreak-line::after')]: {
1059
+ content: '"— page break —"',
1060
+ position: 'absolute',
1061
+ left: '0',
1062
+ right: '0',
1063
+ top: '50%',
1064
+ transform: 'translateY(-50%)',
1065
+ color: 'var(--text-secondary, #999)',
1066
+ fontSize: '11px',
1067
+ letterSpacing: '0.1em',
1068
+ textTransform: 'uppercase',
1069
+ borderTop: '2px dashed var(--border, #ddd)',
1070
+ paddingTop: '8px',
1071
+ },
1072
+ };
1073
+ }
1074
+
1075
+ function cssDecls(obj = {}) {
1076
+ return Object.entries(obj)
1077
+ .filter(([, value]) => value !== undefined && value !== null && value !== '')
1078
+ .map(([key, value]) => ` ${key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)}: ${value};`)
1079
+ .join('\n');
1080
+ }
1081
+
1082
+ export function serializeDocumentTemplateToCss(template, scope = '.markdown-body') {
1083
+ const t = normalizeDocumentTemplate(template);
1084
+ const bodyFont = t.body.fontFamily || undefined;
1085
+ const bodySize = t.body.fontSize || undefined;
1086
+ const bodyLineHeight = t.body.lineHeight || undefined;
1087
+
1088
+ return [
1089
+ `${scope} {\n${cssDecls({
1090
+ backgroundColor: t.page.background || undefined,
1091
+ color: t.body.color || undefined,
1092
+ fontFamily: bodyFont,
1093
+ fontSize: bodySize,
1094
+ lineHeight: bodyLineHeight,
1095
+ textTransform: t.body?.textTransform || undefined,
1096
+ maxWidth: t.page.maxWidth || undefined,
1097
+ })}\n}`,
1098
+ `${scope} h1 {\n${cssDecls({
1099
+ color: t.heading?.color || undefined,
1100
+ fontFamily: t.heading?.h1?.fontFamily || t.heading?.fontFamily || bodyFont,
1101
+ fontSize: t.heading?.h1?.fontSize || undefined,
1102
+ fontWeight: t.heading?.h1?.fontWeight || undefined,
1103
+ textTransform: t.heading?.h1?.textTransform || t.heading?.textTransform || t.body?.textTransform || undefined,
1104
+ })}\n}`,
1105
+ `${scope} h2 {\n${cssDecls({
1106
+ color: t.heading?.color || undefined,
1107
+ fontFamily: t.heading?.h2?.fontFamily || t.heading?.fontFamily || bodyFont,
1108
+ fontSize: t.heading?.h2?.fontSize || undefined,
1109
+ fontWeight: t.heading?.h2?.fontWeight || undefined,
1110
+ textTransform: t.heading?.h2?.textTransform || t.heading?.textTransform || t.body?.textTransform || undefined,
1111
+ })}\n}`,
1112
+ `${scope} h3 {\n${cssDecls({
1113
+ color: t.heading?.color || undefined,
1114
+ fontFamily: t.heading?.h3?.fontFamily || t.heading?.fontFamily || bodyFont,
1115
+ fontSize: t.heading?.h3?.fontSize || undefined,
1116
+ fontWeight: t.heading?.h3?.fontWeight || undefined,
1117
+ textTransform: t.heading?.h3?.textTransform || t.heading?.textTransform || t.body?.textTransform || undefined,
1118
+ })}\n}`,
1119
+ `${scope} blockquote {\n${cssDecls({
1120
+ borderLeftColor: t.blockquote?.borderLeftColor || undefined,
1121
+ backgroundColor: t.blockquote?.background || undefined,
1122
+ color: t.blockquote?.color || undefined,
1123
+ fontFamily: t.blockquote?.fontFamily || bodyFont,
1124
+ fontStyle: t.blockquote?.fontStyle || undefined,
1125
+ textTransform: t.blockquote?.textTransform || t.body?.textTransform || undefined,
1126
+ })}\n}`,
1127
+ `${scope} code {\n${cssDecls({
1128
+ fontFamily: t.code?.inline?.fontFamily || undefined,
1129
+ backgroundColor: t.code?.inline?.background || undefined,
1130
+ color: t.code?.inline?.color || undefined,
1131
+ })}\n}`,
1132
+ `${scope} pre, ${scope} pre code {\n${cssDecls({
1133
+ fontFamily: t.code?.block?.fontFamily || undefined,
1134
+ fontSize: t.code?.block?.fontSize || undefined,
1135
+ backgroundColor: t.code?.block?.background || undefined,
1136
+ color: t.code?.block?.color || undefined,
1137
+ })}\n}`,
1138
+ `${scope} a {\n${cssDecls({
1139
+ color: t.link?.color || undefined,
1140
+ textDecoration: t.link?.underline === false ? 'none' : undefined,
1141
+ })}\n}`,
1142
+ `${scope} table {\n${cssDecls({
1143
+ borderCollapse: 'collapse',
1144
+ width: '100%',
1145
+ fontFamily: t.table?.fontFamily || bodyFont || undefined,
1146
+ fontSize: t.table?.fontSize || undefined,
1147
+ })}\n}`,
1148
+ `${scope} table, ${scope} th, ${scope} td {\n${cssDecls({ borderColor: t.table?.borderColor || undefined })}\n}`,
1149
+ `${scope} th {\n${cssDecls({
1150
+ backgroundColor: t.table?.headerBackground || undefined,
1151
+ color: t.table?.headerColor || undefined,
1152
+ fontWeight: t.table?.headerFontWeight || undefined,
1153
+ })}\n}`,
1154
+ `${scope} td {\n${cssDecls({
1155
+ color: t.table?.color || undefined,
1156
+ padding: t.table?.cellPadding || undefined,
1157
+ })}\n}`,
1158
+ ...(t.table?.stripedRows && t.table?.stripedColor ? [
1159
+ `${scope} tbody tr:nth-child(${t.table.stripedRows}) td {\n${cssDecls({
1160
+ backgroundColor: t.table.stripedColor,
1161
+ })}\n}`,
1162
+ ] : []),
1163
+ ...(t.math?.color || t.math?.fontSize ? [
1164
+ `${scope} .math, ${scope} .MathJax, ${scope} .katex {\n${cssDecls({
1165
+ color: t.math?.color || undefined,
1166
+ fontSize: t.math?.fontSize || undefined,
1167
+ })}\n}`,
1168
+ ] : []),
1169
+ // Syntax highlighting token classes for code blocks (used by highlight.js / Pandoc)
1170
+ ...(() => {
1171
+ const hl = t.code?.highlight;
1172
+ if (!hl) return [];
1173
+ // Map our token names to common highlight.js / Pandoc CSS classes
1174
+ const hlMap = {
1175
+ keyword: '.hljs-keyword, .kw',
1176
+ controlKeyword: '.hljs-keyword.hljs-control',
1177
+ string: '.hljs-string, .st',
1178
+ number: '.hljs-number, .fl, .dv',
1179
+ comment: '.hljs-comment, .co',
1180
+ function: '.hljs-function, .hljs-title.function_, .fu',
1181
+ variable: '.hljs-variable, .va',
1182
+ type: '.hljs-type, .hljs-title.class_, .dt',
1183
+ operator: '.hljs-operator, .op',
1184
+ punctuation: '.hljs-punctuation',
1185
+ property: '.hljs-property, .hljs-attr',
1186
+ constant: '.hljs-literal, .hljs-built_in, .cn',
1187
+ regexp: '.hljs-regexp, .ss',
1188
+ escape: '.hljs-char.escape_, .sc',
1189
+ tag: '.hljs-tag, .hljs-name',
1190
+ attribute: '.hljs-attr',
1191
+ meta: '.hljs-meta, .an',
1192
+ };
1193
+ const rules = [];
1194
+ for (const [token, sel] of Object.entries(hlMap)) {
1195
+ if (hl[token]) {
1196
+ const selectors = sel.split(',').map((s) => `${scope} pre ${s.trim()}`).join(', ');
1197
+ const decls = { color: hl[token] || undefined };
1198
+ if (token === 'keyword' && hl.keywordStyle) Object.assign(decls, parseStyleModifier(hl.keywordStyle));
1199
+ if (token === 'comment' && hl.commentStyle) Object.assign(decls, parseStyleModifier(hl.commentStyle));
1200
+ if (token === 'function' && hl.functionStyle) Object.assign(decls, parseStyleModifier(hl.functionStyle));
1201
+ if (token === 'type' && hl.typeStyle) Object.assign(decls, parseStyleModifier(hl.typeStyle));
1202
+ rules.push(`${selectors} {\n${cssDecls(decls)}\n}`);
1203
+ }
1204
+ }
1205
+ return rules;
1206
+ })(),
1207
+ ].join('\n\n');
1208
+ }
1209
+
1210
+ // ---------------------------------------------------------------------------
1211
+ // Font mapping: CSS font-family → system font names for Pandoc (xelatex/lualatex)
1212
+ // and LaTeX package names for pdflatex fallback.
1213
+ // ---------------------------------------------------------------------------
1214
+
1215
+ const FONT_MAP = [
1216
+ // CSS family pattern → { system: <OTF/TTF name for xelatex>, latex: <package for pdflatex>, category }
1217
+ { css: /charter/i, system: 'XCharter', latex: 'XCharter', category: 'serif' },
1218
+ { css: /georgia/i, system: 'Georgia', latex: 'mathpazo', category: 'serif' },
1219
+ { css: /times\s*new\s*roman/i, system: 'Times New Roman', latex: 'mathptmx', category: 'serif' },
1220
+ { css: /palatino/i, system: 'Palatino Linotype', latex: 'mathpazo', category: 'serif' },
1221
+ { css: /garamond/i, system: 'EB Garamond', latex: 'ebgaramond', category: 'serif' },
1222
+ { css: /alegreya/i, system: 'Alegreya', latex: 'Alegreya', category: 'serif' },
1223
+ { css: /crimson/i, system: 'Crimson Pro', latex: 'CrimsonPro', category: 'serif' },
1224
+ { css: /libre\s*baskerville/i, system: 'Libre Baskerville', latex: 'LibreBaskerville', category: 'serif' },
1225
+ { css: /source\s*serif/i, system: 'Source Serif Pro', latex: 'sourceserifpro', category: 'serif' },
1226
+ { css: /inter/i, system: 'Inter', latex: 'Inter', category: 'sans' },
1227
+ { css: /helvetica/i, system: 'Helvetica Neue', latex: 'helvet', category: 'sans' },
1228
+ { css: /arial/i, system: 'Arial', latex: 'helvet', category: 'sans' },
1229
+ { css: /open\s*sans/i, system: 'Open Sans', latex: 'opensans', category: 'sans' },
1230
+ { css: /lato/i, system: 'Lato', latex: 'lato', category: 'sans' },
1231
+ { css: /roboto/i, system: 'Roboto', latex: 'roboto', category: 'sans' },
1232
+ { css: /source\s*sans/i, system: 'Source Sans Pro', latex: 'sourcesanspro', category: 'sans' },
1233
+ { css: /jetbrains\s*mono/i, system: 'JetBrains Mono', latex: '', category: 'mono' },
1234
+ { css: /sf\s*mono/i, system: 'SF Mono', latex: '', category: 'mono' },
1235
+ { css: /cascadia\s*code/i, system: 'Cascadia Code', latex: '', category: 'mono' },
1236
+ { css: /fira\s*code/i, system: 'Fira Code', latex: '', category: 'mono' },
1237
+ { css: /source\s*code\s*pro/i, system: 'Source Code Pro', latex: 'sourcecodepro', category: 'mono' },
1238
+ { css: /inconsolata/i, system: 'Inconsolata', latex: 'inconsolata', category: 'mono' },
1239
+ { css: /courier\s*new/i, system: 'Courier New', latex: 'courier', category: 'mono' },
1240
+ // Generic fallbacks
1241
+ { css: /\bserif\b/i, system: '', latex: '', category: 'serif' },
1242
+ { css: /\bsans-serif\b/i, system: '', latex: '', category: 'sans' },
1243
+ { css: /\bsans\b/i, system: '', latex: '', category: 'sans' },
1244
+ { css: /\bmonospace\b/i, system: '', latex: '', category: 'mono' },
1245
+ { css: /\bmono\b/i, system: '', latex: '', category: 'mono' },
1246
+ { css: /system-ui/i, system: '', latex: '', category: 'sans' },
1247
+ ];
1248
+
1249
+ /**
1250
+ * Look up a CSS font-family string in the font map.
1251
+ * Returns { system, latex, category } for the best match, or null.
1252
+ */
1253
+ export function resolveFontForExport(cssFontFamily) {
1254
+ if (!cssFontFamily) return null;
1255
+ for (const entry of FONT_MAP) {
1256
+ if (entry.css.test(cssFontFamily)) return { system: entry.system, latex: entry.latex, category: entry.category };
1257
+ }
1258
+ // If it's a single unquoted name, assume it's a system font name
1259
+ const trimmed = cssFontFamily.replace(/["']/g, '').split(',')[0].trim();
1260
+ if (trimmed && !/\b(serif|sans|mono|system|inherit)\b/i.test(trimmed)) {
1261
+ return { system: trimmed, latex: '', category: 'serif' };
1262
+ }
1263
+ return null;
1264
+ }
1265
+
1266
+ // ---------------------------------------------------------------------------
1267
+ // CSS size → pt conversion (best-effort for LaTeX)
1268
+ // ---------------------------------------------------------------------------
1269
+
1270
+ function cssSizeToPt(size) {
1271
+ if (!size) return '';
1272
+ const num = parseFloat(size);
1273
+ if (isNaN(num)) return '';
1274
+ if (/pt$/i.test(size)) return `${num}pt`;
1275
+ if (/px$/i.test(size)) return `${Math.round(num * 0.75)}pt`;
1276
+ if (/em$/i.test(size)) return `${Math.round(num * 12)}pt`; // rough: 1em ≈ 12pt
1277
+ if (/rem$/i.test(size)) return `${Math.round(num * 12)}pt`;
1278
+ // bare number → assume px
1279
+ return `${Math.round(num * 0.75)}pt`;
1280
+ }
1281
+
1282
+ function hexToRgbNormalized(hex) {
1283
+ if (!hex || !/^#[0-9a-fA-F]{6}$/.test(hex)) return null;
1284
+ const r = parseInt(hex.slice(1, 3), 16) / 255;
1285
+ const g = parseInt(hex.slice(3, 5), 16) / 255;
1286
+ const b = parseInt(hex.slice(5, 7), 16) / 255;
1287
+ return { r, g, b };
1288
+ }
1289
+
1290
+ // ---------------------------------------------------------------------------
1291
+ // Pandoc YAML metadata serializer
1292
+ //
1293
+ // Generates YAML frontmatter variables that Pandoc understands for PDF (via
1294
+ // LaTeX), HTML, and Word output. The recommended Pandoc engine is xelatex
1295
+ // or lualatex so that system fonts (mainfont, monofont) work. For pdflatex,
1296
+ // a header-includes fallback with \usepackage is also emitted.
1297
+ // ---------------------------------------------------------------------------
1298
+
1299
+ /**
1300
+ * Serialize a document template to a Pandoc-compatible YAML metadata object.
1301
+ *
1302
+ * The returned object can be merged into a document's frontmatter or written
1303
+ * as a standalone defaults file. Values that are empty/unset are omitted so
1304
+ * Pandoc uses its own defaults.
1305
+ *
1306
+ * @param {object} template
1307
+ * @returns {object} Plain JS object suitable for YAML serialization
1308
+ */
1309
+ export function serializeDocumentTemplateToPandocMeta(template) {
1310
+ const t = normalizeDocumentTemplate(template);
1311
+ const meta = {};
1312
+
1313
+ // --- PDF engine recommendation -------------------------------------------
1314
+ // We always recommend xelatex so system font names work via mainfont/monofont.
1315
+ // The caller can override this.
1316
+ meta['pdf-engine'] = 'xelatex';
1317
+
1318
+ // --- Fonts ---------------------------------------------------------------
1319
+ const bodyFontInfo = resolveFontForExport(t.body.fontFamily);
1320
+ const monoFontInfo = resolveFontForExport(t.code?.block?.fontFamily || t.code?.inline?.fontFamily);
1321
+
1322
+ if (bodyFontInfo?.system) meta.mainfont = bodyFontInfo.system;
1323
+ if (monoFontInfo?.system) meta.monofont = monoFontInfo.system;
1324
+
1325
+ // If body font is sans, also set sansfont and documentclass hint
1326
+ if (bodyFontInfo?.category === 'sans') {
1327
+ if (bodyFontInfo.system) meta.sansfont = bodyFontInfo.system;
1328
+ }
1329
+
1330
+ // --- Font size -----------------------------------------------------------
1331
+ const bodyPt = cssSizeToPt(t.body.fontSize);
1332
+ if (bodyPt) meta.fontsize = bodyPt;
1333
+
1334
+ // --- Line height / stretch -----------------------------------------------
1335
+ const lh = parseFloat(t.body.lineHeight);
1336
+ if (!isNaN(lh) && lh > 0) meta.linestretch = String(lh);
1337
+
1338
+ // --- Geometry (margins, paper) -------------------------------------------
1339
+ const geoParts = [];
1340
+ const paper = t.page.paperSize;
1341
+ if (paper === 'a4') geoParts.push('a4paper');
1342
+ else if (paper === 'a5') geoParts.push('a5paper');
1343
+ else if (paper === 'legal') geoParts.push('legalpaper');
1344
+ else if (paper === 'letter') geoParts.push('letterpaper');
1345
+
1346
+ if (t.page.marginTop) geoParts.push(`top=${t.page.marginTop}`);
1347
+ if (t.page.marginBottom) geoParts.push(`bottom=${t.page.marginBottom}`);
1348
+ if (t.page.marginLeft) geoParts.push(`left=${t.page.marginLeft}`);
1349
+ if (t.page.marginRight) geoParts.push(`right=${t.page.marginRight}`);
1350
+
1351
+ if (geoParts.length) meta.geometry = geoParts.join(', ');
1352
+
1353
+ // --- Link color ----------------------------------------------------------
1354
+ if (t.link.color) {
1355
+ meta.colorlinks = true;
1356
+ // Pandoc uses LaTeX xcolor names or HTML hex
1357
+ meta.linkcolor = t.link.color;
1358
+ meta.urlcolor = t.link.color;
1359
+ meta.citecolor = t.link.color;
1360
+ }
1361
+
1362
+ // --- TOC -----------------------------------------------------------------
1363
+ if (t.toc?.enabled === 'true' || t.toc?.enabled === true) {
1364
+ meta.toc = true;
1365
+ const depth = parseInt(t.toc.depth, 10);
1366
+ if (!isNaN(depth) && depth > 0) meta['toc-depth'] = depth;
1367
+ }
1368
+
1369
+ // --- Heading numbering ---------------------------------------------------
1370
+ if (t.heading.numbering === 'all' || t.heading.numbering === 'h2+') {
1371
+ meta['number-sections'] = true;
1372
+ } else if (t.heading.numbering === 'none') {
1373
+ meta['number-sections'] = false;
1374
+ }
1375
+
1376
+ // --- Code highlighting ---------------------------------------------------
1377
+ if (t.code?.block?.lineNumbers === 'true' || t.code?.block?.lineNumbers === true) {
1378
+ meta['code-line-numbers'] = true;
1379
+ }
1380
+
1381
+ // --- Background page color (rare, but supported) -------------------------
1382
+ // Pandoc doesn't natively handle page background; this goes in header-includes.
1383
+
1384
+ // Clean empty
1385
+ for (const key of Object.keys(meta)) {
1386
+ if (meta[key] === '' || meta[key] === undefined || meta[key] === null) delete meta[key];
1387
+ }
1388
+
1389
+ return meta;
1390
+ }
1391
+
1392
+ /**
1393
+ * Serialize the Pandoc metadata to a YAML string.
1394
+ * @param {object} template
1395
+ * @returns {string}
1396
+ */
1397
+ export function serializeDocumentTemplateToPandocYaml(template) {
1398
+ const meta = serializeDocumentTemplateToPandocMeta(template);
1399
+ const lines = [];
1400
+ for (const [key, value] of Object.entries(meta)) {
1401
+ if (typeof value === 'boolean') {
1402
+ lines.push(`${key}: ${value}`);
1403
+ } else if (typeof value === 'number') {
1404
+ lines.push(`${key}: ${value}`);
1405
+ } else {
1406
+ // Quote strings that contain special YAML chars
1407
+ const str = String(value);
1408
+ const needsQuote = /[:#\[\]{}&*!|>'"%@`]/.test(str) || str.includes(', ');
1409
+ lines.push(`${key}: ${needsQuote ? `"${str.replace(/"/g, '\\"')}"` : str}`);
1410
+ }
1411
+ }
1412
+ return lines.join('\n');
1413
+ }
1414
+
1415
+ // ---------------------------------------------------------------------------
1416
+ // LaTeX preamble serializer
1417
+ //
1418
+ // Generates \usepackage / \definecolor / \titleformat commands for fine-grained
1419
+ // PDF styling that goes beyond what Pandoc YAML variables support.
1420
+ // This is meant to be included via Pandoc's header-includes or as a .tex file.
1421
+ // ---------------------------------------------------------------------------
1422
+
1423
+ /**
1424
+ * Serialize a document template to a LaTeX preamble string.
1425
+ *
1426
+ * Covers: colors (heading, blockquote, link, code), font weight overrides,
1427
+ * paragraph spacing, blockquote styling, code block appearance, HR, lists.
1428
+ *
1429
+ * @param {object} template
1430
+ * @returns {string} LaTeX preamble commands
1431
+ */
1432
+ export function serializeDocumentTemplateToLatexPreamble(template) {
1433
+ const t = normalizeDocumentTemplate(template);
1434
+ const lines = [];
1435
+
1436
+ lines.push('% Generated by mrmd document template system');
1437
+ lines.push('% Include via: header-includes in Pandoc YAML or \\input{style.tex}');
1438
+ lines.push('');
1439
+
1440
+ // --- Required packages ---------------------------------------------------
1441
+ lines.push('\\usepackage{xcolor}');
1442
+ lines.push('\\usepackage{hyperref}');
1443
+
1444
+ // --- Define colors from template -----------------------------------------
1445
+ const defineColor = (name, hex) => {
1446
+ const rgb = hexToRgbNormalized(hex);
1447
+ if (!rgb) return;
1448
+ lines.push(`\\definecolor{${name}}{rgb}{${rgb.r.toFixed(3)}, ${rgb.g.toFixed(3)}, ${rgb.b.toFixed(3)}}`);
1449
+ };
1450
+
1451
+ if (t.body.color) defineColor('mrmd-body', t.body.color);
1452
+ if (t.heading.color) defineColor('mrmd-heading', t.heading.color);
1453
+ if (t.link.color) defineColor('mrmd-link', t.link.color);
1454
+ if (t.blockquote.color) defineColor('mrmd-blockquote-text', t.blockquote.color);
1455
+ if (t.blockquote.borderLeftColor) defineColor('mrmd-blockquote-accent', t.blockquote.borderLeftColor);
1456
+ if (t.blockquote.background) defineColor('mrmd-blockquote-bg', t.blockquote.background);
1457
+ if (t.code?.inline?.color) defineColor('mrmd-code-inline', t.code.inline.color);
1458
+ if (t.code?.inline?.background) defineColor('mrmd-code-inline-bg', t.code.inline.background);
1459
+ if (t.code?.block?.color) defineColor('mrmd-code-block', t.code.block.color);
1460
+ if (t.code?.block?.background) defineColor('mrmd-code-block-bg', t.code.block.background);
1461
+ if (t.table?.borderColor) defineColor('mrmd-table-border', t.table.borderColor);
1462
+ if (t.table?.headerBackground) defineColor('mrmd-table-header-bg', t.table.headerBackground);
1463
+ if (t.hr?.color) defineColor('mrmd-hr', t.hr.color);
1464
+
1465
+ lines.push('');
1466
+
1467
+ // --- Body text color -----------------------------------------------------
1468
+ if (t.body.color) {
1469
+ lines.push('\\color{mrmd-body}');
1470
+ }
1471
+
1472
+ // --- Paragraph spacing ---------------------------------------------------
1473
+ if (t.body.paragraphSpacing) {
1474
+ const pt = cssSizeToPt(t.body.paragraphSpacing);
1475
+ if (pt) {
1476
+ lines.push(`\\setlength{\\parskip}{${pt}}`);
1477
+ lines.push('\\setlength{\\parindent}{0pt}');
1478
+ }
1479
+ }
1480
+
1481
+ // --- Link styling --------------------------------------------------------
1482
+ if (t.link.color) {
1483
+ lines.push('\\hypersetup{');
1484
+ lines.push(' colorlinks=true,');
1485
+ lines.push(' linkcolor=mrmd-link,');
1486
+ lines.push(' urlcolor=mrmd-link,');
1487
+ lines.push(' citecolor=mrmd-link,');
1488
+ lines.push('}');
1489
+ }
1490
+
1491
+ // --- Heading colors (requires titlesec) -----------------------------------
1492
+ if (t.heading.color) {
1493
+ lines.push('');
1494
+ lines.push('\\usepackage{titlesec}');
1495
+ for (const level of ['section', 'subsection', 'subsubsection', 'paragraph', 'subparagraph']) {
1496
+ lines.push(`\\titleformat{\\${level}}{\\normalfont\\bfseries\\color{mrmd-heading}}{\\the${level}}{1em}{}`);
1497
+ }
1498
+ }
1499
+
1500
+ // --- Blockquote styling (tcolorbox or mdframed) --------------------------
1501
+ if (t.blockquote.borderLeftColor || t.blockquote.background || t.blockquote.color) {
1502
+ lines.push('');
1503
+ lines.push('% Blockquote styling via mdframed');
1504
+ lines.push('\\usepackage{mdframed}');
1505
+ const opts = [
1506
+ 'skipabove=\\topsep',
1507
+ 'skipbelow=\\topsep',
1508
+ t.blockquote.borderLeftColor ? 'leftline=true, linewidth=3pt, linecolor=mrmd-blockquote-accent' : 'leftline=true, linewidth=3pt',
1509
+ t.blockquote.background ? 'backgroundcolor=mrmd-blockquote-bg' : '',
1510
+ t.blockquote.color ? 'fontcolor=mrmd-blockquote-text' : '',
1511
+ 'rightline=false, topline=false, bottomline=false',
1512
+ 'innerleftmargin=10pt, innerrightmargin=10pt, innertopmargin=8pt, innerbottommargin=8pt',
1513
+ ].filter(Boolean).join(',\n ');
1514
+ lines.push(`\\newmdenv[${opts}]{mrmdblockquote}`);
1515
+ lines.push('% To use: wrap blockquotes in \\begin{mrmdblockquote}...\\end{mrmdblockquote}');
1516
+ lines.push('% Pandoc does this automatically if you add a Lua filter.');
1517
+ }
1518
+
1519
+ // --- Inline code background ----------------------------------------------
1520
+ if (t.code?.inline?.background || t.code?.inline?.color) {
1521
+ lines.push('');
1522
+ lines.push('% Inline code styling');
1523
+ lines.push('\\usepackage{soul}');
1524
+ if (t.code.inline.background) {
1525
+ lines.push('\\sethlcolor{mrmd-code-inline-bg}');
1526
+ }
1527
+ lines.push('% Apply with a custom Pandoc Lua filter on Code inlines');
1528
+ }
1529
+
1530
+ // --- Code block background -----------------------------------------------
1531
+ if (t.code?.block?.background || t.code?.block?.color) {
1532
+ lines.push('');
1533
+ lines.push('% Code block styling');
1534
+ lines.push('\\usepackage{fancyvrb}');
1535
+ lines.push('\\DefineVerbatimEnvironment{Highlighting}{Verbatim}{');
1536
+ const vOpts = [];
1537
+ if (t.code.block.color) vOpts.push('formatcom=\\color{mrmd-code-block}');
1538
+ vOpts.push('commandchars=\\\\\\{\\}');
1539
+ lines.push(' ' + vOpts.join(', '));
1540
+ lines.push('}');
1541
+ if (t.code.block.background) {
1542
+ lines.push('\\usepackage{framed}');
1543
+ lines.push('\\definecolor{shadecolor}{named}{mrmd-code-block-bg}');
1544
+ }
1545
+ }
1546
+
1547
+ // --- Syntax highlighting token colors (Pandoc/LaTeX) ----------------------
1548
+ const hl = t.code?.highlight;
1549
+ if (hl) {
1550
+ const tokenLatexMap = {
1551
+ keyword: 'KeywordTok',
1552
+ string: 'StringTok',
1553
+ number: 'DecValTok',
1554
+ comment: 'CommentTok',
1555
+ function: 'FunctionTok',
1556
+ variable: 'VariableTok',
1557
+ type: 'DataTypeTok',
1558
+ operator: 'OperatorTok',
1559
+ constant: 'ConstantTok',
1560
+ regexp: 'SpecialStringTok',
1561
+ escape: 'SpecialCharTok',
1562
+ meta: 'AnnotationTok',
1563
+ };
1564
+ lines.push('');
1565
+ lines.push('% Syntax highlighting token colors');
1566
+ for (const [token, latexCmd] of Object.entries(tokenLatexMap)) {
1567
+ if (hl[token]) {
1568
+ const colorName = `mrmd-tok-${token}`;
1569
+ defineColor(colorName, hl[token]);
1570
+ lines.push(`\\newcommand{\\${latexCmd}}[1]{\\textcolor{${colorName}}{#1}}`);
1571
+ }
1572
+ }
1573
+ }
1574
+
1575
+ // --- Page background color -----------------------------------------------
1576
+ if (t.page.background && t.page.background !== '#ffffff' && t.page.background !== '#fff') {
1577
+ lines.push('');
1578
+ defineColor('mrmd-page-bg', t.page.background);
1579
+ lines.push('\\usepackage{pagecolor}');
1580
+ lines.push('\\pagecolor{mrmd-page-bg}');
1581
+ }
1582
+
1583
+ // --- HR styling ----------------------------------------------------------
1584
+ if (t.hr?.color || t.hr?.thickness) {
1585
+ lines.push('');
1586
+ lines.push('% Horizontal rule styling');
1587
+ const thickness = cssSizeToPt(t.hr.thickness) || '0.4pt';
1588
+ if (t.hr.color) {
1589
+ lines.push(`\\renewcommand{\\rule}[2]{\\textcolor{mrmd-hr}{\\vrule width #1 height #2}}`);
1590
+ }
1591
+ lines.push(`% Default HR thickness: ${thickness}`);
1592
+ }
1593
+
1594
+ // --- List styling --------------------------------------------------------
1595
+ if (t.list?.bulletStyle || t.list?.numberStyle) {
1596
+ lines.push('');
1597
+ lines.push('\\usepackage{enumitem}');
1598
+ if (t.list.bulletStyle) {
1599
+ const bulletMap = {
1600
+ disc: '$\\bullet$',
1601
+ circle: '$\\circ$',
1602
+ square: '$\\blacksquare$',
1603
+ dash: '--',
1604
+ };
1605
+ const bullet = bulletMap[t.list.bulletStyle] || '$\\bullet$';
1606
+ lines.push(`\\setlist[itemize]{label=${bullet}}`);
1607
+ }
1608
+ if (t.list.numberStyle) {
1609
+ const numMap = {
1610
+ decimal: '\\arabic*.',
1611
+ 'lower-alpha': '\\alph*.',
1612
+ 'lower-roman': '\\roman*.',
1613
+ 'upper-alpha': '\\Alph*.',
1614
+ 'upper-roman': '\\Roman*.',
1615
+ };
1616
+ const numFmt = numMap[t.list.numberStyle] || '\\arabic*.';
1617
+ lines.push(`\\setlist[enumerate]{label=${numFmt}}`);
1618
+ }
1619
+ }
1620
+
1621
+ return lines.join('\n');
1622
+ }
1623
+
1624
+ // ---------------------------------------------------------------------------
1625
+ // HTML wrapper serializer
1626
+ //
1627
+ // Generates a standalone HTML page that wraps markdown-rendered HTML with the
1628
+ // template's CSS. This is what users would use if they want to self-host or
1629
+ // render their MRMD docs on the web with the same styling.
1630
+ // ---------------------------------------------------------------------------
1631
+
1632
+ /**
1633
+ * Generate a standalone HTML wrapper with the template CSS embedded.
1634
+ *
1635
+ * @param {object} template
1636
+ * @param {object} [options]
1637
+ * @param {string} [options.title] - HTML <title>
1638
+ * @param {string} [options.bodyHtml] - Pre-rendered HTML body (if available)
1639
+ * @param {string} [options.scope] - CSS scope class (default: 'markdown-body')
1640
+ * @returns {string} Complete HTML document string
1641
+ */
1642
+ export function serializeDocumentTemplateToHtml(template, options = {}) {
1643
+ const scope = options.scope || 'markdown-body';
1644
+ const css = serializeDocumentTemplateToCss(template, `.${scope}`);
1645
+ const t = normalizeDocumentTemplate(template);
1646
+ const title = options.title || 'Document';
1647
+ const bodyHtml = options.bodyHtml || '';
1648
+
1649
+ // Collect Google Fonts URLs for web font loading
1650
+ const fontUrls = [];
1651
+ const addGoogleFont = (family) => {
1652
+ if (!family) return;
1653
+ // Extract first font name from CSS font stack
1654
+ const name = family.replace(/["']/g, '').split(',')[0].trim();
1655
+ if (!name || /^(serif|sans-serif|monospace|system-ui|inherit|Georgia|Times New Roman|Arial|Helvetica|Courier New)$/i.test(name)) return;
1656
+ const encoded = name.replace(/\s+/g, '+');
1657
+ fontUrls.push(`https://fonts.googleapis.com/css2?family=${encoded}:ital,wght@0,400;0,700;0,800;1,400&display=swap`);
1658
+ };
1659
+ addGoogleFont(t.body.fontFamily);
1660
+ addGoogleFont(t.code?.inline?.fontFamily);
1661
+ addGoogleFont(t.code?.block?.fontFamily);
1662
+
1663
+ const fontLinks = [...new Set(fontUrls)].map((url) => ` <link rel="stylesheet" href="${url}">`).join('\n');
1664
+
1665
+ return `<!DOCTYPE html>
1666
+ <html lang="en">
1667
+ <head>
1668
+ <meta charset="utf-8">
1669
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1670
+ <title>${title.replace(/</g, '&lt;')}</title>
1671
+ ${fontLinks}
1672
+ <style>
1673
+ /* Reset */
1674
+ *, *::before, *::after { box-sizing: border-box; }
1675
+ body {
1676
+ margin: 0;
1677
+ padding: 0;
1678
+ display: flex;
1679
+ justify-content: center;
1680
+ background: ${t.page.background || '#ffffff'};
1681
+ }
1682
+ .${scope} {
1683
+ width: 100%;
1684
+ max-width: ${t.page.maxWidth || '800px'};
1685
+ padding: 2rem 1.5rem;
1686
+ margin: 0 auto;
1687
+ }
1688
+ /* Template styles */
1689
+ ${css.split('\n').map((l) => ' ' + l).join('\n')}
1690
+ /* Base prose defaults (only if template doesn't override) */
1691
+ .${scope} img { max-width: ${t.image?.maxWidth || '100%'}; height: auto; ${t.image?.borderRadius ? `border-radius: ${t.image.borderRadius};` : ''} }
1692
+ .${scope} table { border-collapse: collapse; width: 100%; }
1693
+ .${scope} th, .${scope} td { padding: 8px 12px; text-align: left; }
1694
+ .${scope} hr { border: none; ${t.hr?.color ? `border-top-color: ${t.hr.color};` : ''} border-top-style: ${t.hr?.style || 'solid'}; border-top-width: ${t.hr?.thickness || '1px'}; }
1695
+ .${scope} pre { padding: 1em; border-radius: 6px; overflow-x: auto; }
1696
+ .${scope} code { padding: 2px 5px; border-radius: 3px; }
1697
+ .${scope} pre code { padding: 0; background: none; }
1698
+ .${scope} blockquote { margin: 1em 0; padding: 0.5em 1em; border-left-width: 4px; border-left-style: solid; }
1699
+ </style>
1700
+ </head>
1701
+ <body>
1702
+ <article class="${scope}">
1703
+ ${bodyHtml}
1704
+ </article>
1705
+ </body>
1706
+ </html>`;
1707
+ }
1708
+
1709
+ // ---------------------------------------------------------------------------
1710
+ // Word (docx) export hints
1711
+ //
1712
+ // Word styling through Pandoc works best via --reference-doc. We can't
1713
+ // generate a .docx from JS alone, but we can output the mapping between
1714
+ // template properties and Word style names that a reference doc should use.
1715
+ // ---------------------------------------------------------------------------
1716
+
1717
+ /**
1718
+ * Return a mapping object describing how the template properties correspond
1719
+ * to Word style names. This can be used to:
1720
+ * 1. Guide users on which Word styles to customize in a reference .docx
1721
+ * 2. Drive automated reference doc generation (via a server-side tool)
1722
+ *
1723
+ * @param {object} template
1724
+ * @returns {object}
1725
+ */
1726
+ export function serializeDocumentTemplateToWordStyleMap(template) {
1727
+ const t = normalizeDocumentTemplate(template);
1728
+ const map = {
1729
+ _comment: 'Map of mrmd template properties to Word style names for --reference-doc',
1730
+ body: {
1731
+ wordStyle: 'Body Text',
1732
+ fontFamily: t.body.fontFamily || '(default)',
1733
+ fontSize: t.body.fontSize || '(default)',
1734
+ lineHeight: t.body.lineHeight || '(default)',
1735
+ color: t.body.color || '(default)',
1736
+ },
1737
+ headings: [
1738
+ { level: 1, wordStyle: 'Heading 1', fontSize: t.heading?.h1?.fontSize || '(default)', fontWeight: t.heading?.h1?.fontWeight || '(default)', color: t.heading?.color || '(default)' },
1739
+ { level: 2, wordStyle: 'Heading 2', fontSize: t.heading?.h2?.fontSize || '(default)', fontWeight: t.heading?.h2?.fontWeight || '(default)', color: t.heading?.color || '(default)' },
1740
+ { level: 3, wordStyle: 'Heading 3', fontSize: t.heading?.h3?.fontSize || '(default)', fontWeight: t.heading?.h3?.fontWeight || '(default)', color: t.heading?.color || '(default)' },
1741
+ { level: 4, wordStyle: 'Heading 4', fontSize: t.heading?.h4?.fontSize || '(default)', fontWeight: t.heading?.h4?.fontWeight || '(default)', color: t.heading?.color || '(default)' },
1742
+ { level: 5, wordStyle: 'Heading 5', fontSize: t.heading?.h5?.fontSize || '(default)', fontWeight: t.heading?.h5?.fontWeight || '(default)', color: t.heading?.color || '(default)' },
1743
+ { level: 6, wordStyle: 'Heading 6', fontSize: t.heading?.h6?.fontSize || '(default)', fontWeight: t.heading?.h6?.fontWeight || '(default)', color: t.heading?.color || '(default)' },
1744
+ ],
1745
+ blockquote: { wordStyle: 'Block Text', color: t.blockquote?.color || '(default)' },
1746
+ codeInline: { wordStyle: 'Verbatim Char', fontFamily: t.code?.inline?.fontFamily || '(default)' },
1747
+ codeBlock: { wordStyle: 'Source Code', fontFamily: t.code?.block?.fontFamily || '(default)', fontSize: t.code?.block?.fontSize || '(default)' },
1748
+ link: { wordStyle: 'Hyperlink', color: t.link?.color || '(default)' },
1749
+ table: { wordStyle: 'Table', borderColor: t.table?.borderColor || '(default)', headerBackground: t.table?.headerBackground || '(default)' },
1750
+ };
1751
+ return map;
1752
+ }
1753
+
1754
+ // ---------------------------------------------------------------------------
1755
+ // Pandoc command-line builder
1756
+ //
1757
+ // Generates the recommended Pandoc invocation for a given template + format.
1758
+ // ---------------------------------------------------------------------------
1759
+
1760
+ /**
1761
+ * Generate a Pandoc CLI command string for exporting a document.
1762
+ *
1763
+ * @param {object} template
1764
+ * @param {object} options
1765
+ * @param {'pdf'|'html'|'docx'|'latex'} options.format
1766
+ * @param {string} options.input - Input file path
1767
+ * @param {string} [options.output] - Output file path
1768
+ * @param {string} [options.referenceDoc] - Path to Word reference doc
1769
+ * @param {string} [options.preambleFile] - Path to LaTeX preamble .tex file
1770
+ * @returns {string}
1771
+ */
1772
+ export function buildPandocCommand(template, options = {}) {
1773
+ const meta = serializeDocumentTemplateToPandocMeta(template);
1774
+ const format = options.format || 'pdf';
1775
+ const input = options.input || 'document.md';
1776
+ const output = options.output || input.replace(/\.md$/, `.${format}`);
1777
+
1778
+ const args = ['pandoc', input, '-o', output];
1779
+
1780
+ // Format-specific settings
1781
+ if (format === 'pdf') {
1782
+ args.push('--pdf-engine=xelatex');
1783
+ if (options.preambleFile) {
1784
+ args.push(`-H ${options.preambleFile}`);
1785
+ }
1786
+ } else if (format === 'html') {
1787
+ args.push('--standalone');
1788
+ // CSS can be passed with --css
1789
+ } else if (format === 'docx') {
1790
+ if (options.referenceDoc) {
1791
+ args.push(`--reference-doc=${options.referenceDoc}`);
1792
+ }
1793
+ }
1794
+
1795
+ // Add metadata variables
1796
+ for (const [key, value] of Object.entries(meta)) {
1797
+ if (key === 'pdf-engine') continue; // already handled
1798
+ if (typeof value === 'boolean') {
1799
+ if (value) args.push(`-V ${key}`);
1800
+ } else {
1801
+ args.push(`-V ${key}="${String(value).replace(/"/g, '\\"')}"`);
1802
+ }
1803
+ }
1804
+
1805
+ if (meta.toc) args.push('--toc');
1806
+ if (meta['number-sections']) args.push('--number-sections');
1807
+
1808
+ return args.join(' \\\n ');
1809
+ }
1810
+
1811
+ let documentTemplateOverrideId = 0;
1812
+
1813
+ function pushInlineMarkColorRules(rules, scopeSelector, source, parentSel = '') {
1814
+ if (!source) return;
1815
+ const parents = Array.isArray(parentSel)
1816
+ ? parentSel.filter(Boolean)
1817
+ : parentSel
1818
+ ? String(parentSel).split(',').map((s) => s.trim()).filter(Boolean)
1819
+ : [];
1820
+
1821
+ const selectorsFor = (cls) => {
1822
+ const leaf = `.${cls}`;
1823
+ if (!parents.length) return [`${scopeSelector} ${leaf}`];
1824
+ return parents.flatMap((parent) => [
1825
+ `${scopeSelector} ${parent}${leaf}`,
1826
+ `${scopeSelector} ${parent} ${leaf}`,
1827
+ ]);
1828
+ };
1829
+
1830
+ const pushColor = (cls, value) => {
1831
+ if (!value) return;
1832
+ rules.push(`${selectorsFor(cls).join(', ')} { color: ${value} !important; }`);
1833
+ };
1834
+
1835
+ pushColor('cm-md-bold', source.bold?.color);
1836
+ pushColor('cm-md-italic', source.italic?.color);
1837
+ pushColor('cm-md-underline', source.underline?.color);
1838
+ pushColor('cm-md-strikethrough', source.strikethrough?.color);
1839
+ }
1840
+
1841
+ function buildDocumentTemplateOverrideCSS(template, scopeSelector) {
1842
+ const t = normalizeDocumentTemplate(template);
1843
+ if (t.editor?.applyDocumentStyles === false) return '';
1844
+
1845
+ const rules = [];
1846
+ const rule = (selectors, prop, value) => {
1847
+ if (!value) return;
1848
+ const parts = (Array.isArray(selectors) ? selectors : String(selectors).split(','))
1849
+ .map((s) => String(s).trim())
1850
+ .filter(Boolean);
1851
+ if (!parts.length) return;
1852
+ rules.push(`${parts.map((s) => `${scopeSelector} ${s}`).join(', ')} { ${prop}: ${value} !important; }`);
1853
+ };
1854
+
1855
+ // --- Heading color ---
1856
+ if (t.heading?.color) {
1857
+ for (let i = 1; i <= 6; i++) {
1858
+ rule(`.cm-md-h${i}`, 'color', t.heading.color);
1859
+ }
1860
+ rule('.cm-frontmatter-title-input', 'color', t.heading.color);
1861
+ }
1862
+
1863
+ // --- Per-heading color ---
1864
+ for (let i = 1; i <= 6; i++) {
1865
+ const h = t.heading?.[`h${i}`];
1866
+ if (h?.color) rule(`.cm-md-h${i}`, 'color', h.color);
1867
+ }
1868
+
1869
+ // --- Body color baseline ---
1870
+ if (t.body?.color) {
1871
+ rule(['.cm-line', '.cm-content'], 'color', t.body.color);
1872
+ }
1873
+
1874
+ // --- Inline-mark color precedence ---
1875
+ pushInlineMarkColorRules(rules, scopeSelector, t.inlineMarks, '');
1876
+ pushInlineMarkColorRules(rules, scopeSelector, t.heading, HEADING_MARK_CONTEXT_SELECTORS);
1877
+ for (let i = 1; i <= 6; i++) {
1878
+ pushInlineMarkColorRules(rules, scopeSelector, t.heading?.[`h${i}`], `.cm-md-h${i}`);
1879
+ }
1880
+
1881
+ // --- Blockquote inline marks + text ---
1882
+ pushInlineMarkColorRules(rules, scopeSelector, t.blockquote, '.cm-md-blockquote-line');
1883
+ if (t.blockquote?.color) {
1884
+ rule('.cm-md-blockquote-line', 'color', t.blockquote.color);
1885
+ }
1886
+
1887
+ // --- Links ---
1888
+ if (t.link?.color) {
1889
+ rule(['.cm-md-link-text', '.cm-external-link', '.cm-file-link', '.cm-wiki-link'], 'color', t.link.color);
1890
+ }
1891
+
1892
+ // --- Inline code ---
1893
+ if (t.code?.inline?.color) rule('.cm-md-inline-code', 'color', t.code.inline.color);
1894
+ if (t.code?.inline?.background) rule('.cm-md-inline-code', 'background-color', t.code.inline.background);
1895
+
1896
+ // --- Code blocks ---
1897
+ if (t.code?.block?.color) {
1898
+ rule([
1899
+ '.cm-codeblock-line',
1900
+ '.cm-codeblock-fence',
1901
+ '.cm-wysiwyg-code-fence-line',
1902
+ '.cm-wysiwyg-code-fence-widget',
1903
+ '.cm-wysiwyg-code-header',
1904
+ ], 'color', t.code.block.color);
1905
+ }
1906
+ if (t.code?.block?.background) {
1907
+ rule([
1908
+ '.cm-codeblock-line',
1909
+ '.cm-codeblock-fence',
1910
+ '.cm-wysiwyg-code-fence-line',
1911
+ '.cm-wysiwyg-code-fence-widget',
1912
+ '.cm-wysiwyg-code-header',
1913
+ ], 'background-color', t.code.block.background);
1914
+ }
1915
+
1916
+ // --- Frontmatter secondary text ---
1917
+ if (t.body?.color) {
1918
+ rule([
1919
+ '.cm-frontmatter-subtitle',
1920
+ '.cm-frontmatter-author',
1921
+ '.cm-frontmatter-author-affiliation',
1922
+ '.cm-frontmatter-date',
1923
+ '.cm-frontmatter-abstract',
1924
+ '.cm-frontmatter-keyword',
1925
+ ], 'color', t.body.color);
1926
+ }
1927
+
1928
+ // --- Token inheritance ---
1929
+ // Syntax-highlight token spans set color directly, which blocks color
1930
+ // inheritance from parent prose/mark spans. When document styling owns the
1931
+ // preview, prose tokens should inherit from the document template instead.
1932
+ const tokenSel = 'span[class*="ͼ"]';
1933
+ rules.push(`${scopeSelector} .cm-line:not(.cm-codeblock-line):not(.cm-codeblock-fence) ${tokenSel} { color: inherit !important; }`);
1934
+
1935
+ // --- Syntax highlighting: document-template-owned token colors ---
1936
+ //
1937
+ // The tagHighlighter extension stamps deterministic .cm-dt-* classes on
1938
+ // syntax token spans. The ͼN classes from the app theme are neutralised
1939
+ // above (color: inherit !important). Here we output rules that give
1940
+ // our .cm-dt-* classes the template's colors.
1941
+ //
1942
+ // Scoped inside code-block lines so they don't affect prose. The selector
1943
+ // uses the data-attribute on the editor root for maximum specificity.
1944
+ //
1945
+ // Cascade:
1946
+ // code.block.color (inherited base)
1947
+ // → code.highlight.{token} (base token – all code blocks)
1948
+ // → code.highlight.languages.{lang}.{token} (per-language)
1949
+ //
1950
+ const hl = t.code?.highlight;
1951
+ if (hl) {
1952
+ pushSyntaxTokenRules(rules, scopeSelector, hl);
1953
+ }
1954
+
1955
+ // --- Code block font overrides (guaranteed precedence) ---
1956
+ // The built-in codeBlockStyles uses EditorView.theme() which may have
1957
+ // equal specificity to our template theme. Repeat here with !important.
1958
+ if (t.code?.block?.fontSize) {
1959
+ rule(['.cm-codeblock-line', '.cm-codeblock-fence', '.cm-wysiwyg-code-fence-line'], 'font-size', t.code.block.fontSize);
1960
+ }
1961
+ if (t.code?.block?.fontFamily) {
1962
+ rule(['.cm-codeblock-line', '.cm-codeblock-fence', '.cm-wysiwyg-code-fence-line'], 'font-family', t.code.block.fontFamily);
1963
+ }
1964
+ if (t.code?.block?.lineHeight) {
1965
+ rule(['.cm-codeblock-line', '.cm-wysiwyg-code-fence-line'], 'line-height', t.code.block.lineHeight);
1966
+ }
1967
+
1968
+ // --- Output widget font overrides ---
1969
+ if (t.code?.cell?.outputFontFamily) {
1970
+ rule(['.cm-output-widget', '.cm-output-content', '.cm-scroll-output-widget', '.cm-scroll-output-content'], 'font-family', t.code.cell.outputFontFamily);
1971
+ }
1972
+ if (t.code?.cell?.outputFontSize) {
1973
+ rule(['.cm-output-widget', '.cm-output-content', '.cm-scroll-output-widget', '.cm-scroll-output-content'], 'font-size', t.code.cell.outputFontSize);
1974
+ }
1975
+ if (t.code?.cell?.outputLineHeight) {
1976
+ rule(['.cm-output-widget', '.cm-scroll-output-widget'], 'line-height', t.code.cell.outputLineHeight);
1977
+ }
1978
+
1979
+ // --- Table overrides ---
1980
+ if (t.table?.color) {
1981
+ rule('.cm-table-widget td', 'color', t.table.color);
1982
+ }
1983
+ if (t.table?.headerColor) {
1984
+ rule('.cm-table-widget th', 'color', t.table.headerColor);
1985
+ }
1986
+ if (t.table?.headerFontWeight) {
1987
+ rule('.cm-table-widget th', 'font-weight', t.table.headerFontWeight);
1988
+ }
1989
+ if (t.table?.fontSize) {
1990
+ rule('.cm-table-widget table', 'font-size', t.table.fontSize);
1991
+ }
1992
+ if (t.table?.fontFamily) {
1993
+ rule('.cm-table-widget table', 'font-family', t.table.fontFamily);
1994
+ }
1995
+
1996
+ // --- Math overrides ---
1997
+ // KaTeX generates its own elements that set color directly.
1998
+ // Must target .katex inside our containers to override.
1999
+ if (t.math?.color) {
2000
+ rule(['.cm-math-inline', '.cm-math-display'], 'color', t.math.color);
2001
+ rule(['.cm-math-inline .katex', '.cm-math-display .katex'], 'color', t.math.color);
2002
+ rule(['.cm-math-inline .katex *', '.cm-math-display .katex *'], 'color', t.math.color);
2003
+ }
2004
+ if (t.math?.fontSize) {
2005
+ rule(['.cm-math-inline', '.cm-math-display'], 'font-size', t.math.fontSize);
2006
+ }
2007
+ if (t.math?.displayBorderRadius) {
2008
+ rule('.cm-math-display', 'border-radius', t.math.displayBorderRadius);
2009
+ }
2010
+
2011
+ return rules.join('\n');
2012
+ }
2013
+
2014
+ /**
2015
+ * Generate CSS rules targeting the deterministic .cm-dt-* token classes
2016
+ * inside code block lines. Uses !important to win over the neutralised
2017
+ * ͼN classes.
2018
+ *
2019
+ * For base tokens the selector is:
2020
+ * ${scope} .cm-codeblock-line .cm-dt-keyword { color: #xxx !important; }
2021
+ *
2022
+ * For per-language overrides the selector adds [data-lang]:
2023
+ * ${scope} .cm-codeblock-line[data-lang="python"] .cm-dt-keyword { … }
2024
+ */
2025
+ function pushSyntaxTokenRules(rules, scopeSelector, hl) {
2026
+ const CODE_LINE_SCOPES = ['.cm-codeblock-line', '.cm-wysiwyg-code-fence-line'];
2027
+
2028
+ const pushTokenColor = (tokenName, color, styleModifier, langAttr) => {
2029
+ if (!color && !styleModifier) return;
2030
+ const cls = DT_TOKEN_CLASS_MAP[tokenName];
2031
+ if (!cls) return;
2032
+
2033
+ const decls = [];
2034
+ if (color) decls.push(`color: ${color} !important`);
2035
+ if (styleModifier) {
2036
+ const { fontWeight, fontStyle } = parseStyleModifier(styleModifier);
2037
+ if (fontWeight) decls.push(`font-weight: ${fontWeight} !important`);
2038
+ if (fontStyle) decls.push(`font-style: ${fontStyle} !important`);
2039
+ }
2040
+ if (!decls.length) return;
2041
+
2042
+ const body = decls.join('; ');
2043
+ const selectors = CODE_LINE_SCOPES.map((scope) => {
2044
+ const lineScope = langAttr ? `${scope}[data-lang="${langAttr}"]` : scope;
2045
+ return `${scopeSelector} ${lineScope} .${cls}`;
2046
+ });
2047
+ rules.push(`${selectors.join(', ')} { ${body}; }`);
2048
+ };
2049
+
2050
+ // Style modifier lookup: token → style modifier key
2051
+ const STYLE_KEYS = {
2052
+ keyword: 'keywordStyle', controlKeyword: 'keywordStyle',
2053
+ comment: 'commentStyle', function: 'functionStyle', type: 'typeStyle',
2054
+ };
2055
+
2056
+ // --- Base token rules (all code blocks) ---
2057
+ for (const tokenName of Object.keys(DT_TOKEN_CLASS_MAP)) {
2058
+ const color = hl[tokenName] || '';
2059
+ const styleMod = hl[STYLE_KEYS[tokenName]] || '';
2060
+ pushTokenColor(tokenName, color, styleMod, null);
2061
+ }
2062
+
2063
+ // --- Per-language overrides ---
2064
+ const languages = hl.languages;
2065
+ if (languages) {
2066
+ for (const [lang, langTokens] of Object.entries(languages)) {
2067
+ if (!langTokens || typeof langTokens !== 'object') continue;
2068
+ for (const tokenName of Object.keys(DT_TOKEN_CLASS_MAP)) {
2069
+ const color = langTokens[tokenName] || '';
2070
+ const styleMod = langTokens[STYLE_KEYS[tokenName]] || '';
2071
+ if (color || styleMod) {
2072
+ pushTokenColor(tokenName, color, styleMod, lang);
2073
+ }
2074
+ }
2075
+ }
2076
+ }
2077
+ }
2078
+
2079
+ function parseStyleModifier(value) {
2080
+ if (!value) return {};
2081
+ const lower = String(value).toLowerCase().trim();
2082
+ const result = {};
2083
+ if (lower.includes('bold')) result.fontWeight = 'bold';
2084
+ if (lower.includes('italic')) result.fontStyle = 'italic';
2085
+ return result;
2086
+ }
2087
+
2088
+ function createDocumentTemplateOverrideExtension(template) {
2089
+ const t = normalizeDocumentTemplate(template);
2090
+ if (t.editor?.applyDocumentStyles === false) return [];
2091
+
2092
+ return ViewPlugin.fromClass(class {
2093
+ constructor(view) {
2094
+ this.view = view;
2095
+ this.ownerDocument = view.dom.ownerDocument;
2096
+ this.attrName = 'data-mrmd-document-template-id';
2097
+ this.attrValue = `dt-${++documentTemplateOverrideId}`;
2098
+ this.styleEl = this.ownerDocument.createElement('style');
2099
+ this.styleEl.id = `mrmd-document-template-overrides-${this.attrValue}`;
2100
+ view.dom.setAttribute(this.attrName, this.attrValue);
2101
+ this.styleEl.textContent = buildDocumentTemplateOverrideCSS(
2102
+ t,
2103
+ `.cm-editor[${this.attrName}="${this.attrValue}"]`
2104
+ );
2105
+ (this.ownerDocument.head || this.ownerDocument.documentElement).appendChild(this.styleEl);
2106
+ }
2107
+
2108
+ destroy() {
2109
+ this.styleEl?.remove?.();
2110
+ this.view?.dom?.removeAttribute?.(this.attrName);
2111
+ }
2112
+ });
2113
+ }
2114
+
2115
+ // ---------------------------------------------------------------------------
2116
+ // Document-template-owned syntax token classes.
2117
+ //
2118
+ // We use @lezer/highlight's tagHighlighter to stamp deterministic CSS class
2119
+ // names (cm-dt-keyword, cm-dt-string, …) on syntax token spans. These
2120
+ // classes coexist with CodeMirror's generated ͼN classes. The ͼN classes
2121
+ // are neutralised (color: inherit !important) by the theme rules above.
2122
+ // Our cm-dt-* classes are then coloured in the override <style> element
2123
+ // with even higher specificity + !important, so they always win.
2124
+ //
2125
+ // This is the same pattern used for heading colors and inline mark colors.
2126
+ // ---------------------------------------------------------------------------
2127
+
2128
+ /**
2129
+ * Mapping from our template token names to their deterministic CSS class.
2130
+ */
2131
+ const DT_TOKEN_CLASS_MAP = {
2132
+ keyword: 'cm-dt-keyword',
2133
+ controlKeyword: 'cm-dt-control-keyword',
2134
+ string: 'cm-dt-string',
2135
+ number: 'cm-dt-number',
2136
+ comment: 'cm-dt-comment',
2137
+ function: 'cm-dt-function',
2138
+ variable: 'cm-dt-variable',
2139
+ type: 'cm-dt-type',
2140
+ operator: 'cm-dt-operator',
2141
+ punctuation: 'cm-dt-punctuation',
2142
+ property: 'cm-dt-property',
2143
+ constant: 'cm-dt-constant',
2144
+ regexp: 'cm-dt-regexp',
2145
+ escape: 'cm-dt-escape',
2146
+ tag: 'cm-dt-tag',
2147
+ attribute: 'cm-dt-attribute',
2148
+ attributeValue: 'cm-dt-attribute-value',
2149
+ meta: 'cm-dt-meta',
2150
+ inserted: 'cm-dt-inserted',
2151
+ deleted: 'cm-dt-deleted',
2152
+ changed: 'cm-dt-changed',
2153
+ };
2154
+
2155
+ /**
2156
+ * Build the tagHighlighter extension that stamps cm-dt-* classes on
2157
+ * syntax token spans. This is always active when document styles own
2158
+ * the preview — the classes are harmless when no highlight tokens are
2159
+ * set (they just exist on the spans without any matching CSS rule).
2160
+ */
2161
+ function createDocumentTemplateTokenClasses(template) {
2162
+ const t = normalizeDocumentTemplate(template);
2163
+ if (t.editor?.applyDocumentStyles === false) return [];
2164
+
2165
+ const th = tagHighlighter([
2166
+ // Keywords
2167
+ { tag: [lezerTags.keyword, lezerTags.operatorKeyword, lezerTags.definitionKeyword, lezerTags.moduleKeyword],
2168
+ class: DT_TOKEN_CLASS_MAP.keyword },
2169
+ { tag: lezerTags.controlKeyword,
2170
+ class: DT_TOKEN_CLASS_MAP.controlKeyword },
2171
+ // Strings
2172
+ { tag: [lezerTags.string, lezerTags.docString, lezerTags.character, lezerTags.special(lezerTags.string)],
2173
+ class: DT_TOKEN_CLASS_MAP.string },
2174
+ // Numbers
2175
+ { tag: [lezerTags.number, lezerTags.integer, lezerTags.float],
2176
+ class: DT_TOKEN_CLASS_MAP.number },
2177
+ // Comments
2178
+ { tag: [lezerTags.comment, lezerTags.lineComment, lezerTags.blockComment, lezerTags.docComment],
2179
+ class: DT_TOKEN_CLASS_MAP.comment },
2180
+ // Functions
2181
+ { tag: [lezerTags.function(lezerTags.variableName), lezerTags.definition(lezerTags.function(lezerTags.variableName))],
2182
+ class: DT_TOKEN_CLASS_MAP.function },
2183
+ // Variables
2184
+ { tag: [lezerTags.variableName, lezerTags.definition(lezerTags.variableName), lezerTags.local(lezerTags.variableName)],
2185
+ class: DT_TOKEN_CLASS_MAP.variable },
2186
+ // Types & classes
2187
+ { tag: [lezerTags.typeName, lezerTags.className, lezerTags.namespace, lezerTags.macroName],
2188
+ class: DT_TOKEN_CLASS_MAP.type },
2189
+ // Operators
2190
+ { tag: lezerTags.operator,
2191
+ class: DT_TOKEN_CLASS_MAP.operator },
2192
+ // Punctuation
2193
+ { tag: [lezerTags.punctuation, lezerTags.separator, lezerTags.bracket, lezerTags.paren, lezerTags.brace, lezerTags.squareBracket, lezerTags.angleBracket],
2194
+ class: DT_TOKEN_CLASS_MAP.punctuation },
2195
+ // Properties
2196
+ { tag: [lezerTags.propertyName, lezerTags.definition(lezerTags.propertyName), lezerTags.special(lezerTags.propertyName)],
2197
+ class: DT_TOKEN_CLASS_MAP.property },
2198
+ // Constants / booleans / null
2199
+ { tag: [lezerTags.constant(lezerTags.variableName), lezerTags.standard(lezerTags.variableName), lezerTags.bool, lezerTags.null, lezerTags.atom],
2200
+ class: DT_TOKEN_CLASS_MAP.constant },
2201
+ // Regexp
2202
+ { tag: lezerTags.regexp,
2203
+ class: DT_TOKEN_CLASS_MAP.regexp },
2204
+ // Escape sequences
2205
+ { tag: lezerTags.escape,
2206
+ class: DT_TOKEN_CLASS_MAP.escape },
2207
+ // HTML/XML tags
2208
+ { tag: lezerTags.tagName,
2209
+ class: DT_TOKEN_CLASS_MAP.tag },
2210
+ // HTML/XML attributes
2211
+ { tag: lezerTags.attributeName,
2212
+ class: DT_TOKEN_CLASS_MAP.attribute },
2213
+ { tag: lezerTags.attributeValue,
2214
+ class: DT_TOKEN_CLASS_MAP.attributeValue },
2215
+ // Meta / decorators / annotations
2216
+ { tag: [lezerTags.meta, lezerTags.processingInstruction, lezerTags.annotation],
2217
+ class: DT_TOKEN_CLASS_MAP.meta },
2218
+ // Diff
2219
+ { tag: lezerTags.inserted, class: DT_TOKEN_CLASS_MAP.inserted },
2220
+ { tag: lezerTags.deleted, class: DT_TOKEN_CLASS_MAP.deleted },
2221
+ { tag: lezerTags.changed, class: DT_TOKEN_CLASS_MAP.changed },
2222
+ ]);
2223
+
2224
+ return [syntaxHighlighting(th)];
2225
+ }
2226
+
2227
+ export function createDocumentTemplateExtension(template) {
2228
+ return [
2229
+ EditorView.theme(compileDocumentTemplateCSS(template)),
2230
+ createDocumentTemplateOverrideExtension(template),
2231
+ ...createDocumentTemplateTokenClasses(template),
2232
+ ];
2233
+ }
2234
+
2235
+ // Make the class map available for external consumers
2236
+ export { DT_TOKEN_CLASS_MAP };