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.
- package/package.json +8 -3
- package/src/adapters/cloudflare.ts +202 -15
- package/src/adapters/local.ts +38 -17
- package/src/analytics/classify.ts +65 -0
- package/src/analytics/console.ts +39 -0
- package/src/analytics/noop.ts +15 -0
- package/src/analytics/types.ts +49 -0
- package/src/cache/kv.ts +81 -0
- package/src/cache/memory.ts +46 -0
- package/src/cache/response.ts +24 -0
- package/src/cli.ts +301 -51
- package/src/client/scripts.ts +379 -3
- package/src/config/defaults.ts +66 -5
- package/src/config/schema.ts +200 -2
- package/src/content/assets.ts +202 -0
- package/src/content/cache.ts +232 -0
- package/src/content/filesystem.ts +17 -1
- package/src/content/github.ts +169 -102
- package/src/content/nav-builder.ts +120 -0
- package/src/content/r2.ts +214 -0
- package/src/handler.ts +341 -21
- package/src/index.ts +49 -1
- package/src/mcp/server.ts +164 -0
- package/src/mcp/stdio.ts +29 -0
- package/src/mcp/transport.ts +29 -0
- package/src/negotiate/headers.ts +37 -9
- package/src/render/page-shell.ts +249 -8
- package/src/search/index.ts +342 -0
- package/src/security/csp.ts +92 -0
- package/src/theme/{prose-css.ts → base-css.ts} +251 -11
- package/src/theme/build-css.ts +74 -0
|
@@ -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
|
-
*
|
|
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
|
|
13
|
-
*
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
+
}
|