mkdnsite 0.0.1 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,92 @@
1
+ import type { MkdnSiteConfig, CspConfig } from '../config/schema.ts'
2
+
3
+ /**
4
+ * Sanitize a CSP source value to prevent directive injection.
5
+ * Strips semicolons which act as directive separators.
6
+ */
7
+ function sanitizeCspValue (val: string): string {
8
+ return val.replace(/;/g, '').trim()
9
+ }
10
+
11
+ /**
12
+ * Build a Content-Security-Policy header value string from the current config.
13
+ * Only includes external sources for features that are actually enabled.
14
+ */
15
+ export function buildCsp (config: MkdnSiteConfig): string {
16
+ const { client, analytics, csp, theme } = config
17
+ const gaEnabled = (analytics?.googleAnalytics?.measurementId ?? '') !== ''
18
+ const useCdn = client.mermaid || client.charts
19
+ const extra: CspConfig = csp ?? { enabled: true }
20
+
21
+ // script-src
22
+ const scriptSrc = ["'self'", "'unsafe-inline'"]
23
+ if (useCdn) scriptSrc.push('https://cdn.jsdelivr.net')
24
+ if (gaEnabled) {
25
+ scriptSrc.push('https://www.googletagmanager.com')
26
+ scriptSrc.push('https://www.google-analytics.com')
27
+ }
28
+ if (extra.extraScriptSrc != null) {
29
+ scriptSrc.push(...extra.extraScriptSrc.map(sanitizeCspValue))
30
+ }
31
+
32
+ // style-src
33
+ const styleSrc = ["'self'", "'unsafe-inline'"]
34
+ if (client.math) styleSrc.push('https://cdn.jsdelivr.net')
35
+ if (theme.customCssUrl != null) {
36
+ try {
37
+ const u = new URL(theme.customCssUrl)
38
+ if (u.protocol === 'https:' || u.protocol === 'http:') {
39
+ styleSrc.push(u.origin)
40
+ }
41
+ } catch {
42
+ // relative URL — 'self' covers it
43
+ }
44
+ }
45
+ if (extra.extraStyleSrc != null) {
46
+ styleSrc.push(...extra.extraStyleSrc.map(sanitizeCspValue))
47
+ }
48
+
49
+ // img-src
50
+ const imgSrc = ["'self'", 'data:', 'https:']
51
+ if (client.mermaid) imgSrc.push('blob:')
52
+ if (extra.extraImgSrc != null) {
53
+ imgSrc.push(...extra.extraImgSrc.map(sanitizeCspValue))
54
+ }
55
+
56
+ // font-src
57
+ const fontSrc = ["'self'", 'https://fonts.gstatic.com']
58
+ if (client.math) fontSrc.push('https://cdn.jsdelivr.net')
59
+ if (extra.extraFontSrc != null) {
60
+ fontSrc.push(...extra.extraFontSrc.map(sanitizeCspValue))
61
+ }
62
+
63
+ // connect-src
64
+ const connectSrc = ["'self'"]
65
+ if (gaEnabled) {
66
+ connectSrc.push('https://www.google-analytics.com')
67
+ connectSrc.push('https://analytics.google.com')
68
+ connectSrc.push('https://region1.google-analytics.com')
69
+ }
70
+ if (extra.extraConnectSrc != null) {
71
+ connectSrc.push(...extra.extraConnectSrc.map(sanitizeCspValue))
72
+ }
73
+
74
+ const directives: string[] = [
75
+ "default-src 'self'",
76
+ 'script-src ' + scriptSrc.join(' '),
77
+ 'style-src ' + styleSrc.join(' '),
78
+ 'img-src ' + imgSrc.join(' '),
79
+ 'font-src ' + fontSrc.join(' '),
80
+ 'connect-src ' + connectSrc.join(' '),
81
+ "frame-src 'none'",
82
+ "object-src 'none'",
83
+ "base-uri 'self'",
84
+ "form-action 'self'"
85
+ ]
86
+
87
+ if (extra.reportUri != null && extra.reportUri !== '') {
88
+ directives.push('report-uri ' + sanitizeCspValue(extra.reportUri))
89
+ }
90
+
91
+ return directives.join('; ')
92
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Default theme CSS for mkdnsite.
2
+ * Base theme CSS for mkdnsite.
3
3
  *
4
4
  * Inspired by @tailwindcss/typography prose styles and GitHub's
5
5
  * markdown rendering. Designed to look beautiful out of the box
@@ -9,54 +9,54 @@
9
9
  * child elements rendered from markdown. The layout classes
10
10
  * (.mkdn-layout, .mkdn-nav, .mkdn-main) handle the page chrome.
11
11
  *
12
- * Users can override this entirely via config.theme.customCss.
13
- * Users on the 'components' theme mode can provide their own
14
- * Tailwind build with custom component styling.
12
+ * Users can extend this via config.theme.colors/fonts/customCss or
13
+ * replace it entirely via config.theme.builtinCss: false.
15
14
  */
16
- export const THEME_CSS = `
15
+ export const BASE_THEME_CSS = `
17
16
  :root {
18
17
  --mkdn-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;
18
+ --mkdn-font-heading: var(--mkdn-font);
19
19
  --mkdn-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
20
+ --mkdn-accent: #0969da;
20
21
  --mkdn-text: #1f2328;
21
22
  --mkdn-text-muted: #656d76;
22
23
  --mkdn-bg: #ffffff;
23
24
  --mkdn-bg-alt: #f6f8fa;
24
25
  --mkdn-border: #d0d7de;
25
- --mkdn-link: #0969da;
26
+ --mkdn-link: var(--mkdn-accent);
26
27
  --mkdn-link-hover: #0550ae;
27
28
  --mkdn-code-bg: rgba(175, 184, 193, 0.2);
28
29
  --mkdn-pre-bg: #f6f8fa;
29
30
  --mkdn-nav-w: 260px;
30
31
  --mkdn-content-max: 880px;
31
- --mkdn-accent: #0969da;
32
32
  }
33
33
 
34
34
  [data-theme="dark"] {
35
+ --mkdn-accent: #58a6ff;
35
36
  --mkdn-text: #e6edf3;
36
37
  --mkdn-text-muted: #8d96a0;
37
38
  --mkdn-bg: #0d1117;
38
39
  --mkdn-bg-alt: #161b22;
39
40
  --mkdn-border: #30363d;
40
- --mkdn-link: #58a6ff;
41
+ --mkdn-link: var(--mkdn-accent);
41
42
  --mkdn-link-hover: #79c0ff;
42
43
  --mkdn-code-bg: rgba(110, 118, 129, 0.4);
43
44
  --mkdn-pre-bg: #161b22;
44
- --mkdn-accent: #58a6ff;
45
45
  }
46
46
 
47
47
  /* No-JS fallback: respect system preference */
48
48
  @media (prefers-color-scheme: dark) {
49
49
  :root:not([data-theme]) {
50
+ --mkdn-accent: #58a6ff;
50
51
  --mkdn-text: #e6edf3;
51
52
  --mkdn-text-muted: #8d96a0;
52
53
  --mkdn-bg: #0d1117;
53
54
  --mkdn-bg-alt: #161b22;
54
55
  --mkdn-border: #30363d;
55
- --mkdn-link: #58a6ff;
56
+ --mkdn-link: var(--mkdn-accent);
56
57
  --mkdn-link-hover: #79c0ff;
57
58
  --mkdn-code-bg: rgba(110, 118, 129, 0.4);
58
59
  --mkdn-pre-bg: #161b22;
59
- --mkdn-accent: #58a6ff;
60
60
  }
61
61
  }
62
62
 
@@ -72,6 +72,11 @@ body {
72
72
  line-height: 1.6;
73
73
  }
74
74
 
75
+ :focus-visible {
76
+ outline: 2px solid var(--mkdn-accent);
77
+ outline-offset: 2px;
78
+ }
79
+
75
80
  /* ---- Layout ---- */
76
81
  .mkdn-layout { display: flex; min-height: 100vh; }
77
82
 
@@ -94,6 +99,8 @@ body {
94
99
  .mkdn-nav-list li.active > a,
95
100
  .mkdn-nav-list a[aria-current="page"] {
96
101
  color: var(--mkdn-text); background: var(--mkdn-code-bg); font-weight: 600;
102
+ border-left: 2px solid var(--mkdn-accent);
103
+ padding-left: calc(0.75rem - 2px);
97
104
  }
98
105
  .mkdn-nav-section-title {
99
106
  display: block; padding: 0.5rem 0.75rem 0.2rem;
@@ -109,6 +116,43 @@ body {
109
116
  margin: 0 auto; padding: 2rem 2.5rem;
110
117
  }
111
118
 
119
+ /* Two-column layout when TOC sidebar is present */
120
+ .mkdn-main:has(.mkdn-toc) {
121
+ display: flex; flex-direction: row;
122
+ gap: 2.5rem; align-items: flex-start;
123
+ max-width: calc(var(--mkdn-content-max) + 260px);
124
+ }
125
+ .mkdn-content-area { flex: 1; min-width: 0; }
126
+
127
+ /* TOC sidebar */
128
+ .mkdn-toc {
129
+ flex: 0 0 220px; position: sticky; top: 2rem;
130
+ max-height: calc(100vh - 4rem); overflow-y: auto;
131
+ font-size: 0.8rem;
132
+ }
133
+ .mkdn-toc-title {
134
+ font-size: 0.7rem; font-weight: 600;
135
+ text-transform: uppercase; letter-spacing: 0.08em;
136
+ color: var(--mkdn-text-muted); margin: 0 0 0.5rem;
137
+ }
138
+ .mkdn-toc ul { list-style: none; padding: 0; margin: 0; }
139
+ .mkdn-toc li { margin: 0; }
140
+ .mkdn-toc a {
141
+ display: block; padding: 0.2rem 0;
142
+ color: var(--mkdn-text-muted); text-decoration: none;
143
+ border-left: 2px solid transparent;
144
+ padding-left: 0.5rem; transition: color 0.15s, border-color 0.15s;
145
+ line-height: 1.4;
146
+ }
147
+ .mkdn-toc a:hover { color: var(--mkdn-text); border-left-color: var(--mkdn-accent); }
148
+ .mkdn-toc-3 a { padding-left: 1rem; font-size: 0.775rem; }
149
+ .mkdn-toc-4 a { padding-left: 1.5rem; font-size: 0.75rem; }
150
+
151
+ @media (max-width: 1200px) {
152
+ .mkdn-main:has(.mkdn-toc) { flex-direction: column; }
153
+ .mkdn-toc { display: none; }
154
+ }
155
+
112
156
  .mkdn-footer {
113
157
  margin-top: 4rem; padding-top: 1.5rem;
114
158
  border-top: 1px solid var(--mkdn-border);
@@ -116,9 +160,59 @@ body {
116
160
  }
117
161
  .mkdn-footer a { color: var(--mkdn-link); }
118
162
 
163
+ /* ---- Page title (from frontmatter) ---- */
164
+ .mkdn-page-title {
165
+ font-size: 2.5rem; font-weight: 800;
166
+ letter-spacing: -0.03em; line-height: 1.1;
167
+ margin-top: 0; margin-bottom: 0.35em;
168
+ }
169
+
170
+ /* ---- Page meta (date + reading time) ---- */
171
+ .mkdn-page-meta {
172
+ display: flex; align-items: center; flex-wrap: wrap; gap: 0.5rem 1rem;
173
+ color: var(--mkdn-text-muted); font-size: 0.875rem;
174
+ margin-bottom: 2rem;
175
+ }
176
+ .mkdn-page-meta time { color: var(--mkdn-text-muted); }
177
+ .mkdn-reading-time { color: var(--mkdn-text-muted); }
178
+
179
+ /* ---- Prev/Next navigation ---- */
180
+ .mkdn-prev-next {
181
+ display: flex; justify-content: space-between; align-items: flex-start;
182
+ gap: 1rem; padding: 1.5rem 0; margin-top: 2.5rem;
183
+ border-top: 1px solid var(--mkdn-border);
184
+ }
185
+ .mkdn-prev-next a {
186
+ display: flex; flex-direction: column; gap: 0.2rem;
187
+ text-decoration: none; color: var(--mkdn-text-muted);
188
+ font-size: 0.875rem; max-width: 45%;
189
+ transition: color 0.15s;
190
+ }
191
+ .mkdn-prev-next a:hover { color: var(--mkdn-accent); }
192
+ .mkdn-prev { align-items: flex-start; }
193
+ .mkdn-next { align-items: flex-end; text-align: right; }
194
+ .mkdn-prev-next .mkdn-pn-label {
195
+ font-size: 0.75rem; text-transform: uppercase;
196
+ letter-spacing: 0.05em; color: var(--mkdn-text-muted); font-weight: 600;
197
+ }
198
+ .mkdn-prev-next .mkdn-pn-title { color: var(--mkdn-link); font-weight: 500; }
199
+
200
+ /* ---- Nav header (logo + site name) ---- */
201
+ .mkdn-nav-header {
202
+ display: flex; align-items: center; gap: 0.6rem;
203
+ padding: 0 1rem 1rem; margin-bottom: 0.5rem;
204
+ border-bottom: 1px solid var(--mkdn-border);
205
+ text-decoration: none; color: var(--mkdn-text);
206
+ }
207
+ .mkdn-nav-header:hover { color: var(--mkdn-text); }
208
+ .mkdn-nav-logo { display: block; flex-shrink: 0; }
209
+ .mkdn-nav-logo img { display: block; border-radius: 4px; }
210
+ .mkdn-nav-title { font-weight: 700; font-size: 0.95rem; line-height: 1.2; }
211
+
119
212
  /* ---- Prose typography (shadcn/Radix-inspired, applied to .mkdn-prose) ---- */
120
213
  .mkdn-prose h1, .mkdn-prose h2, .mkdn-prose h3,
121
214
  .mkdn-prose h4, .mkdn-prose h5, .mkdn-prose h6 {
215
+ font-family: var(--mkdn-font-heading);
122
216
  scroll-margin-top: 1rem;
123
217
  letter-spacing: -0.025em;
124
218
  line-height: 1.2;
@@ -216,6 +310,7 @@ body {
216
310
  .mkdn-prose th { font-weight: 600; }
217
311
  .mkdn-prose tr:last-child td { border-bottom: none; }
218
312
  .mkdn-prose tbody tr:nth-child(even) { background: var(--mkdn-bg-alt); }
313
+ .mkdn-prose thead th { background: var(--mkdn-bg-alt); }
219
314
  .mkdn-prose td[align="center"], .mkdn-prose th[align="center"] { text-align: center; }
220
315
  .mkdn-prose td[align="right"], .mkdn-prose th[align="right"] { text-align: right; }
221
316
 
@@ -374,4 +469,149 @@ body {
374
469
  }
375
470
  .mkdn-main { padding: 1.5rem 1rem; }
376
471
  }
472
+
473
+ /* ---- Search trigger button ---- */
474
+ .mkdn-search-trigger {
475
+ position: fixed; top: 0.75rem; right: 3.25rem; z-index: 200;
476
+ display: flex; align-items: center; justify-content: center;
477
+ width: 36px; height: 36px; border-radius: 50%; border: none;
478
+ background: transparent; cursor: pointer;
479
+ color: var(--mkdn-text-muted);
480
+ transition: color 0.15s, background 0.15s;
481
+ }
482
+ .mkdn-search-trigger:hover { color: var(--mkdn-text); background: var(--mkdn-code-bg); }
483
+
484
+ /* ---- Search modal overlay ---- */
485
+ .mkdn-search-overlay {
486
+ position: fixed; inset: 0; z-index: 1000;
487
+ background: rgba(0, 0, 0, 0.5);
488
+ display: flex; align-items: flex-start; justify-content: center;
489
+ padding-top: 8vh;
490
+ opacity: 0; pointer-events: none;
491
+ transition: opacity 0.15s ease;
492
+ }
493
+ .mkdn-search-overlay--open {
494
+ opacity: 1; pointer-events: auto;
495
+ }
496
+
497
+ /* ---- Search modal box ---- */
498
+ .mkdn-search-modal {
499
+ background: var(--mkdn-bg);
500
+ border: 1px solid var(--mkdn-border);
501
+ border-radius: 12px;
502
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
503
+ width: 100%; max-width: 600px; max-height: 80vh;
504
+ display: flex; flex-direction: column; overflow: hidden;
505
+ }
506
+
507
+ /* ---- Input row ---- */
508
+ .mkdn-search-input-wrap {
509
+ display: flex; align-items: center; gap: 0.5rem;
510
+ padding: 0.75rem 1rem;
511
+ border-bottom: 1px solid var(--mkdn-border);
512
+ }
513
+ .mkdn-search-icon { flex-shrink: 0; color: var(--mkdn-text-muted); }
514
+ .mkdn-search-input {
515
+ flex: 1; border: none; outline: none; background: transparent;
516
+ font-size: 1.05rem; color: var(--mkdn-text);
517
+ font-family: inherit;
518
+ }
519
+ .mkdn-search-input::placeholder { color: var(--mkdn-text-muted); }
520
+ .mkdn-search-kbd {
521
+ flex-shrink: 0; font-size: 0.7rem; padding: 2px 6px;
522
+ border: 1px solid var(--mkdn-border); border-radius: 4px;
523
+ background: var(--mkdn-code-bg); color: var(--mkdn-text-muted);
524
+ font-family: inherit;
525
+ }
526
+
527
+ /* ---- Results list ---- */
528
+ .mkdn-search-results {
529
+ overflow-y: auto; max-height: 400px; padding: 0.5rem 0;
530
+ }
531
+ .mkdn-search-hint {
532
+ padding: 1.5rem 1rem; margin: 0;
533
+ text-align: center; color: var(--mkdn-text-muted); font-size: 0.9rem;
534
+ }
535
+ .mkdn-search-result {
536
+ display: block; padding: 0.65rem 1rem;
537
+ text-decoration: none; color: inherit;
538
+ border-left: 3px solid transparent;
539
+ transition: background 0.1s;
540
+ }
541
+ .mkdn-search-result--active {
542
+ background: var(--mkdn-bg-alt);
543
+ border-left-color: var(--mkdn-accent);
544
+ }
545
+ .mkdn-search-result-title {
546
+ font-weight: 600; font-size: 0.95rem;
547
+ color: var(--mkdn-text); margin-bottom: 0.15rem;
548
+ }
549
+ .mkdn-search-result-excerpt {
550
+ font-size: 0.82rem; color: var(--mkdn-text-muted);
551
+ line-height: 1.5; margin-bottom: 0.15rem;
552
+ }
553
+ .mkdn-search-result-excerpt mark {
554
+ background: transparent; color: var(--mkdn-accent);
555
+ font-weight: 600; padding: 0;
556
+ }
557
+ .mkdn-search-result-slug {
558
+ font-size: 0.75rem; color: var(--mkdn-text-muted); opacity: 0.7;
559
+ font-family: var(--mkdn-font-mono, monospace);
560
+ }
561
+
562
+ @media (max-width: 640px) {
563
+ .mkdn-search-overlay { padding-top: 0; align-items: flex-end; }
564
+ .mkdn-search-modal {
565
+ max-width: 100%; border-radius: 12px 12px 0 0;
566
+ max-height: 70vh;
567
+ }
568
+ .mkdn-search-trigger { right: 3rem; }
569
+ }
570
+
571
+ /* ---- Search result highlighting (on target page) ---- */
572
+ .mkdn-search-highlight {
573
+ background: color-mix(in srgb, var(--mkdn-accent) 20%, transparent);
574
+ border-radius: 2px; padding: 1px 2px;
575
+ transition: background 0.5s ease;
576
+ }
577
+ .mkdn-search-highlight--fading { background: transparent; }
578
+
579
+ /* ---- Sticky table header (JS-cloned) ---- */
580
+ .mkdn-thead-clone {
581
+ position: fixed;
582
+ top: 0;
583
+ z-index: 100;
584
+ pointer-events: none;
585
+ overflow: hidden;
586
+ border-top-left-radius: 8px;
587
+ border-top-right-radius: 8px;
588
+ border: 1px solid var(--mkdn-border);
589
+ border-bottom: none;
590
+ }
591
+ .mkdn-thead-clone table {
592
+ border-collapse: collapse;
593
+ }
594
+
595
+ /* ---- Chart rendering ---- */
596
+ .mkdn-chart {
597
+ max-width: 600px;
598
+ margin: 1.5rem auto;
599
+ padding: 1rem;
600
+ background: var(--mkdn-bg);
601
+ border: 1px solid var(--mkdn-border);
602
+ border-radius: 8px;
603
+ }
604
+ .mkdn-chart canvas {
605
+ width: 100% !important;
606
+ height: auto !important;
607
+ }
608
+ .mkdn-chart-error {
609
+ padding: 1rem;
610
+ margin: 1rem 0;
611
+ border: 1px solid #ef4444;
612
+ border-radius: 8px;
613
+ color: #ef4444;
614
+ font-size: 0.9rem;
615
+ text-align: center;
616
+ }
377
617
  `.trim()
@@ -0,0 +1,74 @@
1
+ import type { MkdnSiteConfig, ColorTokens, FontTokens } from '../config/schema.ts'
2
+ import { BASE_THEME_CSS } from './base-css.ts'
3
+
4
+ /**
5
+ * Build the full CSS string for a page, combining the base theme with
6
+ * any user-defined color, font, and custom CSS overrides from config.
7
+ */
8
+ export function buildThemeCss (config: MkdnSiteConfig): string {
9
+ const { theme } = config
10
+ const parts: string[] = []
11
+
12
+ // Base built-in styles
13
+ if (theme.builtinCss !== false) {
14
+ parts.push(BASE_THEME_CSS)
15
+ }
16
+
17
+ // Light mode color token overrides
18
+ if (theme.colors != null) {
19
+ const tokens = renderColorTokens(theme.colors)
20
+ if (tokens !== '') {
21
+ parts.push(`:root {\n${tokens}\n}`)
22
+ }
23
+ }
24
+
25
+ // Dark mode color token overrides.
26
+ // Uses [data-theme="dark"] as the primary selector and
27
+ // @media (prefers-color-scheme: dark) with :root:not([data-theme]) as no-JS fallback.
28
+ if (theme.colorsDark != null) {
29
+ const tokens = renderColorTokens(theme.colorsDark)
30
+ if (tokens !== '') {
31
+ const indented = tokens.split('\n').map(l => ` ${l}`).join('\n')
32
+ parts.push(`[data-theme="dark"] {\n${tokens}\n}`)
33
+ parts.push(`@media (prefers-color-scheme: dark) {\n :root:not([data-theme]) {\n${indented}\n }\n}`)
34
+ }
35
+ }
36
+
37
+ // Font token overrides
38
+ if (theme.fonts != null) {
39
+ const tokens = renderFontTokens(theme.fonts)
40
+ if (tokens !== '') {
41
+ parts.push(`:root {\n${tokens}\n}`)
42
+ }
43
+ }
44
+
45
+ // Inline custom CSS appended last (highest specificity wins)
46
+ if (theme.customCss != null && theme.customCss.trim() !== '') {
47
+ parts.push(theme.customCss.trim())
48
+ }
49
+
50
+ return parts.join('\n\n')
51
+ }
52
+
53
+ function renderColorTokens (colors: ColorTokens): string {
54
+ const lines: string[] = []
55
+ if (colors.accent != null) lines.push(` --mkdn-accent: ${colors.accent};`)
56
+ if (colors.text != null) lines.push(` --mkdn-text: ${colors.text};`)
57
+ if (colors.textMuted != null) lines.push(` --mkdn-text-muted: ${colors.textMuted};`)
58
+ if (colors.bg != null) lines.push(` --mkdn-bg: ${colors.bg};`)
59
+ if (colors.bgAlt != null) lines.push(` --mkdn-bg-alt: ${colors.bgAlt};`)
60
+ if (colors.border != null) lines.push(` --mkdn-border: ${colors.border};`)
61
+ if (colors.link != null) lines.push(` --mkdn-link: ${colors.link};`)
62
+ if (colors.linkHover != null) lines.push(` --mkdn-link-hover: ${colors.linkHover};`)
63
+ if (colors.codeBg != null) lines.push(` --mkdn-code-bg: ${colors.codeBg};`)
64
+ if (colors.preBg != null) lines.push(` --mkdn-pre-bg: ${colors.preBg};`)
65
+ return lines.join('\n')
66
+ }
67
+
68
+ function renderFontTokens (fonts: FontTokens): string {
69
+ const lines: string[] = []
70
+ if (fonts.body != null) lines.push(` --mkdn-font: ${fonts.body};`)
71
+ if (fonts.mono != null) lines.push(` --mkdn-mono: ${fonts.mono};`)
72
+ if (fonts.heading != null) lines.push(` --mkdn-font-heading: ${fonts.heading};`)
73
+ return lines.join('\n')
74
+ }