comarkserv 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +389 -0
- package/dist/THIRD_PARTY_LICENSES.md +932 -0
- package/dist/build-CsToYJrU.mjs +2 -0
- package/dist/build-Cv5aO0CB.mjs +205 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +894 -0
- package/dist/cloudflared-6f_lzz4F.mjs +181 -0
- package/dist/dist-B-IALNns.mjs +2 -0
- package/dist/dist-BK-bpLWg.mjs +2 -0
- package/dist/dist-BQzCbTko.mjs +1693 -0
- package/dist/dist-BWcWG5YB.mjs +671 -0
- package/dist/dist-BiVw4ojz.mjs +592 -0
- package/dist/dist-BnmL9jeq.mjs +538 -0
- package/dist/dist-BtedNLrc.mjs +276 -0
- package/dist/dist-BvdNTrap.mjs +2710 -0
- package/dist/dist-ByijHttv.mjs +69 -0
- package/dist/dist-C2E4zSX1.mjs +347 -0
- package/dist/dist-CWYRQLUT.mjs +2 -0
- package/dist/dist-CXQ7eTfT.mjs +417 -0
- package/dist/dist-CfsjQ0o8.mjs +905 -0
- package/dist/dist-CsTXDvwi.mjs +300 -0
- package/dist/dist-CtONMLh4.mjs +283 -0
- package/dist/dist-DMftjyxU.mjs +370 -0
- package/dist/dist-Dr9uTCX7.mjs +2 -0
- package/dist/dist-Dxvc11TH.mjs +339 -0
- package/dist/dist-GmbcGEAc.mjs +3 -0
- package/dist/dist-V1rkISnf.mjs +220 -0
- package/dist/dist-ZoN94PJ0.mjs +76 -0
- package/dist/dist-fh99cWMN.mjs +524 -0
- package/dist/editor-CHDyOp-8.mjs +721 -0
- package/dist/index.d.mts +301 -0
- package/dist/index.mjs +6 -0
- package/dist/languages-C0kEygG_.mjs +119 -0
- package/dist/markdown-BqKed1Vq.mjs +710 -0
- package/dist/markdown-DWuqfzps.mjs +2 -0
- package/dist/rolldown-runtime-CMFfr-1z.mjs +26 -0
- package/dist/server-Dkjk_D1Y.mjs +2628 -0
- package/dist/terminal-aKr5Xlp3.mjs +169 -0
- package/dist/themes-mEufQ_3e.mjs +2194 -0
- package/dist/twinkleplop.production-Coccf2Q9.mjs +2 -0
- package/dist/twinkleplop.production-DekYmX6O.mjs +4697 -0
- package/dist/twinkleplop.tokens-CgBUTCSg.mjs +76 -0
- package/package.json +133 -0
|
@@ -0,0 +1,2194 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { mkdir, readFile, readdir, stat as stat$1, writeFile } from "node:fs/promises";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { dirname, extname, join } from "node:path";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
//#region node_modules/.pnpm/pathe@2.0.3/node_modules/pathe/dist/shared/pathe.M-eThtNZ.mjs
|
|
8
|
+
const _DRIVE_LETTER_START_RE = /^[A-Za-z]:\//;
|
|
9
|
+
function normalizeWindowsPath(input = "") {
|
|
10
|
+
if (!input) return input;
|
|
11
|
+
return input.replace(/\\/g, "/").replace(_DRIVE_LETTER_START_RE, (r) => r.toUpperCase());
|
|
12
|
+
}
|
|
13
|
+
const _UNC_REGEX = /^[/\\]{2}/;
|
|
14
|
+
const _IS_ABSOLUTE_RE = /^[/\\](?![/\\])|^[/\\]{2}(?!\.)|^[A-Za-z]:[/\\]/;
|
|
15
|
+
const _DRIVE_LETTER_RE = /^[A-Za-z]:$/;
|
|
16
|
+
const _ROOT_FOLDER_RE = /^\/([A-Za-z]:)?$/;
|
|
17
|
+
const _EXTNAME_RE = /.(\.[^./]+|\.)$/;
|
|
18
|
+
const normalize = function(path) {
|
|
19
|
+
if (path.length === 0) return ".";
|
|
20
|
+
path = normalizeWindowsPath(path);
|
|
21
|
+
const isUNCPath = path.match(_UNC_REGEX);
|
|
22
|
+
const isPathAbsolute = isAbsolute(path);
|
|
23
|
+
const trailingSeparator = path[path.length - 1] === "/";
|
|
24
|
+
path = normalizeString(path, !isPathAbsolute);
|
|
25
|
+
if (path.length === 0) {
|
|
26
|
+
if (isPathAbsolute) return "/";
|
|
27
|
+
return trailingSeparator ? "./" : ".";
|
|
28
|
+
}
|
|
29
|
+
if (trailingSeparator) path += "/";
|
|
30
|
+
if (_DRIVE_LETTER_RE.test(path)) path += "/";
|
|
31
|
+
if (isUNCPath) {
|
|
32
|
+
if (!isPathAbsolute) return `//./${path}`;
|
|
33
|
+
return `//${path}`;
|
|
34
|
+
}
|
|
35
|
+
return isPathAbsolute && !isAbsolute(path) ? `/${path}` : path;
|
|
36
|
+
};
|
|
37
|
+
const join$1 = function(...segments) {
|
|
38
|
+
let path = "";
|
|
39
|
+
for (const seg of segments) {
|
|
40
|
+
if (!seg) continue;
|
|
41
|
+
if (path.length > 0) {
|
|
42
|
+
const pathTrailing = path[path.length - 1] === "/";
|
|
43
|
+
const segLeading = seg[0] === "/";
|
|
44
|
+
if (pathTrailing && segLeading) path += seg.slice(1);
|
|
45
|
+
else path += pathTrailing || segLeading ? seg : `/${seg}`;
|
|
46
|
+
} else path += seg;
|
|
47
|
+
}
|
|
48
|
+
return normalize(path);
|
|
49
|
+
};
|
|
50
|
+
function cwd() {
|
|
51
|
+
if (typeof process !== "undefined" && typeof process.cwd === "function") return process.cwd().replace(/\\/g, "/");
|
|
52
|
+
return "/";
|
|
53
|
+
}
|
|
54
|
+
const resolve$1 = function(...arguments_) {
|
|
55
|
+
arguments_ = arguments_.map((argument) => normalizeWindowsPath(argument));
|
|
56
|
+
let resolvedPath = "";
|
|
57
|
+
let resolvedAbsolute = false;
|
|
58
|
+
for (let index = arguments_.length - 1; index >= -1 && !resolvedAbsolute; index--) {
|
|
59
|
+
const path = index >= 0 ? arguments_[index] : cwd();
|
|
60
|
+
if (!path || path.length === 0) continue;
|
|
61
|
+
resolvedPath = `${path}/${resolvedPath}`;
|
|
62
|
+
resolvedAbsolute = isAbsolute(path);
|
|
63
|
+
}
|
|
64
|
+
resolvedPath = normalizeString(resolvedPath, !resolvedAbsolute);
|
|
65
|
+
if (resolvedAbsolute && !isAbsolute(resolvedPath)) return `/${resolvedPath}`;
|
|
66
|
+
return resolvedPath.length > 0 ? resolvedPath : ".";
|
|
67
|
+
};
|
|
68
|
+
function normalizeString(path, allowAboveRoot) {
|
|
69
|
+
let res = "";
|
|
70
|
+
let lastSegmentLength = 0;
|
|
71
|
+
let lastSlash = -1;
|
|
72
|
+
let dots = 0;
|
|
73
|
+
let char = null;
|
|
74
|
+
for (let index = 0; index <= path.length; ++index) {
|
|
75
|
+
if (index < path.length) char = path[index];
|
|
76
|
+
else if (char === "/") break;
|
|
77
|
+
else char = "/";
|
|
78
|
+
if (char === "/") {
|
|
79
|
+
if (lastSlash === index - 1 || dots === 1);
|
|
80
|
+
else if (dots === 2) {
|
|
81
|
+
if (res.length < 2 || lastSegmentLength !== 2 || res[res.length - 1] !== "." || res[res.length - 2] !== ".") {
|
|
82
|
+
if (res.length > 2) {
|
|
83
|
+
const lastSlashIndex = res.lastIndexOf("/");
|
|
84
|
+
if (lastSlashIndex === -1) {
|
|
85
|
+
res = "";
|
|
86
|
+
lastSegmentLength = 0;
|
|
87
|
+
} else {
|
|
88
|
+
res = res.slice(0, lastSlashIndex);
|
|
89
|
+
lastSegmentLength = res.length - 1 - res.lastIndexOf("/");
|
|
90
|
+
}
|
|
91
|
+
lastSlash = index;
|
|
92
|
+
dots = 0;
|
|
93
|
+
continue;
|
|
94
|
+
} else if (res.length > 0) {
|
|
95
|
+
res = "";
|
|
96
|
+
lastSegmentLength = 0;
|
|
97
|
+
lastSlash = index;
|
|
98
|
+
dots = 0;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (allowAboveRoot) {
|
|
103
|
+
res += res.length > 0 ? "/.." : "..";
|
|
104
|
+
lastSegmentLength = 2;
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
if (res.length > 0) res += `/${path.slice(lastSlash + 1, index)}`;
|
|
108
|
+
else res = path.slice(lastSlash + 1, index);
|
|
109
|
+
lastSegmentLength = index - lastSlash - 1;
|
|
110
|
+
}
|
|
111
|
+
lastSlash = index;
|
|
112
|
+
dots = 0;
|
|
113
|
+
} else if (char === "." && dots !== -1) ++dots;
|
|
114
|
+
else dots = -1;
|
|
115
|
+
}
|
|
116
|
+
return res;
|
|
117
|
+
}
|
|
118
|
+
const isAbsolute = function(p) {
|
|
119
|
+
return _IS_ABSOLUTE_RE.test(p);
|
|
120
|
+
};
|
|
121
|
+
const extname$1 = function(p) {
|
|
122
|
+
if (p === "..") return "";
|
|
123
|
+
const match = _EXTNAME_RE.exec(normalizeWindowsPath(p));
|
|
124
|
+
return match && match[1] || "";
|
|
125
|
+
};
|
|
126
|
+
const relative = function(from, to) {
|
|
127
|
+
const _from = resolve$1(from).replace(_ROOT_FOLDER_RE, "$1").split("/");
|
|
128
|
+
const _to = resolve$1(to).replace(_ROOT_FOLDER_RE, "$1").split("/");
|
|
129
|
+
if (_to[0][1] === ":" && _from[0][1] === ":" && _from[0] !== _to[0]) return _to.join("/");
|
|
130
|
+
const _fromCopy = [..._from];
|
|
131
|
+
for (const segment of _fromCopy) {
|
|
132
|
+
if (_to[0] !== segment) break;
|
|
133
|
+
_from.shift();
|
|
134
|
+
_to.shift();
|
|
135
|
+
}
|
|
136
|
+
return [..._from.map(() => ".."), ..._to].join("/");
|
|
137
|
+
};
|
|
138
|
+
const dirname$1 = function(p) {
|
|
139
|
+
const segments = normalizeWindowsPath(p).replace(/\/$/, "").split("/").slice(0, -1);
|
|
140
|
+
if (segments.length === 1 && _DRIVE_LETTER_RE.test(segments[0])) segments[0] += "/";
|
|
141
|
+
return segments.join("/") || (isAbsolute(p) ? "/" : ".");
|
|
142
|
+
};
|
|
143
|
+
const basename$1 = function(p, extension) {
|
|
144
|
+
const segments = normalizeWindowsPath(p).split("/");
|
|
145
|
+
let lastSegment = "";
|
|
146
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
147
|
+
const val = segments[i];
|
|
148
|
+
if (val) {
|
|
149
|
+
lastSegment = val;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return extension && lastSegment.endsWith(extension) ? lastSegment.slice(0, -extension.length) : lastSegment;
|
|
154
|
+
};
|
|
155
|
+
//#endregion
|
|
156
|
+
//#region src/assets/palette.ts
|
|
157
|
+
const TOKEN_SLOTS = {
|
|
158
|
+
background: "01",
|
|
159
|
+
comment: "03",
|
|
160
|
+
doctype: "03",
|
|
161
|
+
hash: "03",
|
|
162
|
+
label: "03",
|
|
163
|
+
strike: "03",
|
|
164
|
+
hr: "03",
|
|
165
|
+
hard_break: "03",
|
|
166
|
+
doc_marker: "03",
|
|
167
|
+
blockquote_marker: "03",
|
|
168
|
+
code_fence: "03",
|
|
169
|
+
front_matter_marker: "03",
|
|
170
|
+
prompt_prefix: "03",
|
|
171
|
+
carriage_return: "03",
|
|
172
|
+
newline: "03",
|
|
173
|
+
space: "03",
|
|
174
|
+
tab: "03",
|
|
175
|
+
output: "04",
|
|
176
|
+
identifier: "05",
|
|
177
|
+
punctuation: "05",
|
|
178
|
+
expression: "05",
|
|
179
|
+
lifetime: "08",
|
|
180
|
+
parameter: "08",
|
|
181
|
+
property: "08",
|
|
182
|
+
variable: "08",
|
|
183
|
+
tag_name: "08",
|
|
184
|
+
css_variable: "08",
|
|
185
|
+
deleted: "08",
|
|
186
|
+
deleted_marker: "08",
|
|
187
|
+
link_text: "08",
|
|
188
|
+
boolean: "09",
|
|
189
|
+
number: "09",
|
|
190
|
+
constant: "09",
|
|
191
|
+
null: "09",
|
|
192
|
+
unit: "09",
|
|
193
|
+
bit: "09",
|
|
194
|
+
datetime: "09",
|
|
195
|
+
attribute: "09",
|
|
196
|
+
attr_name: "09",
|
|
197
|
+
variant: "09",
|
|
198
|
+
autolink: "09",
|
|
199
|
+
url: "09",
|
|
200
|
+
url_link: "09",
|
|
201
|
+
type: "0A",
|
|
202
|
+
class_name: "0A",
|
|
203
|
+
namespace: "0A",
|
|
204
|
+
decorator: "0A",
|
|
205
|
+
selector_class: "0A",
|
|
206
|
+
array_table_header: "0A",
|
|
207
|
+
tag: "0A",
|
|
208
|
+
bold: "0A",
|
|
209
|
+
code_language: "0A",
|
|
210
|
+
string: "0B",
|
|
211
|
+
template: "0B",
|
|
212
|
+
inserted: "0B",
|
|
213
|
+
inserted_marker: "0B",
|
|
214
|
+
code: "0B",
|
|
215
|
+
code_block: "0B",
|
|
216
|
+
url_title: "0B",
|
|
217
|
+
plain_scalar: "0B",
|
|
218
|
+
operator: "0C",
|
|
219
|
+
regex: "0C",
|
|
220
|
+
builtin: "0C",
|
|
221
|
+
entity: "0C",
|
|
222
|
+
escape: "0C",
|
|
223
|
+
string_escape: "0C",
|
|
224
|
+
format: "0C",
|
|
225
|
+
attr_sigil: "0C",
|
|
226
|
+
selector_pseudo: "0C",
|
|
227
|
+
svelte_directive: "0C",
|
|
228
|
+
function: "0D",
|
|
229
|
+
heading: "0D",
|
|
230
|
+
heading_marker: "0D",
|
|
231
|
+
selector_id: "0D",
|
|
232
|
+
prompt: "0D",
|
|
233
|
+
keyword: "0E",
|
|
234
|
+
selector: "0E",
|
|
235
|
+
changed: "0E",
|
|
236
|
+
changed_marker: "0E",
|
|
237
|
+
italic: "0E",
|
|
238
|
+
list_marker: "0E",
|
|
239
|
+
task_marker: "0E",
|
|
240
|
+
svelte_block: "0E",
|
|
241
|
+
block_scalar_header: "0E",
|
|
242
|
+
directive: "0E"
|
|
243
|
+
};
|
|
244
|
+
const PAGE_COLORS = {
|
|
245
|
+
"--cms-bg": "var(--b00)",
|
|
246
|
+
"--cms-subtle": "var(--b01)",
|
|
247
|
+
"--cms-hover": "color-mix(in srgb, var(--b02) 55%, var(--b00))",
|
|
248
|
+
"--cms-border": "var(--b02)",
|
|
249
|
+
"--cms-muted": "var(--b04)",
|
|
250
|
+
"--cms-fg": "var(--b05)",
|
|
251
|
+
"--cms-accent": "var(--b-accent, var(--b0D))",
|
|
252
|
+
"--cms-accent-soft": "color-mix(in srgb, var(--cms-accent) 16%, var(--b00))",
|
|
253
|
+
"--cms-note": "var(--b0D)",
|
|
254
|
+
"--cms-tip": "var(--b0B)",
|
|
255
|
+
"--cms-important": "var(--b0E)",
|
|
256
|
+
"--cms-warning": "var(--b0A)",
|
|
257
|
+
"--cms-caution": "var(--b08)"
|
|
258
|
+
};
|
|
259
|
+
/** Returns the color tokens of a twinkleplop theme. Style tokens, such as `bold-font-weight`, have a `-`. */
|
|
260
|
+
function themeTokenNames(themeCss) {
|
|
261
|
+
const names = [...(/:root\s*\{([^}]*)\}/.exec(themeCss)?.[1] ?? "").matchAll(/--twp-([a-z0-9_-]+)\s*:/g)].map((match) => match[1] ?? "");
|
|
262
|
+
return [...new Set(names)].filter((name) => name && !name.includes("-"));
|
|
263
|
+
}
|
|
264
|
+
function slotFor(name) {
|
|
265
|
+
if (Object.hasOwn(TOKEN_SLOTS, name)) return TOKEN_SLOTS[name] ?? "05";
|
|
266
|
+
if (/_(open|close|marker)$/.test(name)) return "03";
|
|
267
|
+
return "05";
|
|
268
|
+
}
|
|
269
|
+
/** Returns the CSS that maps the page and token colors to the slots of the active palette. */
|
|
270
|
+
function paletteStyles(tokenNames) {
|
|
271
|
+
return `:root[data-cms-palette] {
|
|
272
|
+
${[...Object.entries(PAGE_COLORS).map(([name, value]) => ` ${name}: ${value};`), ...tokenNames.map((name) => ` --twp-${name}: var(--b${slotFor(name)});`)].join("\n")}
|
|
273
|
+
}
|
|
274
|
+
:root[data-cms-palette] ::selection { background: color-mix(in srgb, var(--b02) 85%, transparent); }
|
|
275
|
+
:root[data-cms-palette] .cms-markdown mark { background: color-mix(in srgb, var(--b0A) 35%, transparent); }
|
|
276
|
+
`;
|
|
277
|
+
}
|
|
278
|
+
//#endregion
|
|
279
|
+
//#region src/assets/styles.ts
|
|
280
|
+
const styles = String.raw`
|
|
281
|
+
:root {
|
|
282
|
+
color-scheme: light;
|
|
283
|
+
--cms-bg: #ffffff;
|
|
284
|
+
--cms-fg: #1f2328;
|
|
285
|
+
--cms-muted: #59636e;
|
|
286
|
+
--cms-border: #d1d9e0;
|
|
287
|
+
--cms-subtle: #f6f8fa;
|
|
288
|
+
--cms-hover: #eff2f5;
|
|
289
|
+
--cms-accent: #0969da;
|
|
290
|
+
--cms-accent-soft: #ddf4ff;
|
|
291
|
+
--cms-note: #0969da;
|
|
292
|
+
--cms-tip: #1a7f37;
|
|
293
|
+
--cms-important: #8250df;
|
|
294
|
+
--cms-warning: #9a6700;
|
|
295
|
+
--cms-caution: #cf222e;
|
|
296
|
+
--cms-shadow: 0 16px 48px rgb(31 35 40 / 0.18), 0 2px 6px rgb(31 35 40 / 0.08);
|
|
297
|
+
--cms-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
|
298
|
+
--cms-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
|
|
299
|
+
--cms-width: 860px;
|
|
300
|
+
--cms-radius: 8px;
|
|
301
|
+
}
|
|
302
|
+
.dark {
|
|
303
|
+
color-scheme: dark;
|
|
304
|
+
--cms-bg: #0d1117;
|
|
305
|
+
--cms-fg: #e6edf3;
|
|
306
|
+
--cms-muted: #9198a1;
|
|
307
|
+
--cms-border: #30363d;
|
|
308
|
+
--cms-subtle: #151b23;
|
|
309
|
+
--cms-hover: #1f2630;
|
|
310
|
+
--cms-accent: #4493f8;
|
|
311
|
+
--cms-accent-soft: #121d2f;
|
|
312
|
+
--cms-note: #4493f8;
|
|
313
|
+
--cms-tip: #3fb950;
|
|
314
|
+
--cms-important: #ab7df8;
|
|
315
|
+
--cms-warning: #d29922;
|
|
316
|
+
--cms-caution: #f85149;
|
|
317
|
+
--cms-shadow: 0 16px 48px rgb(1 4 9 / 0.6), 0 0 0 1px #30363d;
|
|
318
|
+
}
|
|
319
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
320
|
+
html { background: var(--cms-bg); scroll-padding-top: 72px; }
|
|
321
|
+
body {
|
|
322
|
+
margin: 0;
|
|
323
|
+
background: var(--cms-bg);
|
|
324
|
+
color: var(--cms-fg);
|
|
325
|
+
font: 16px/1.6 var(--cms-font);
|
|
326
|
+
-webkit-font-smoothing: antialiased;
|
|
327
|
+
text-rendering: optimizeLegibility;
|
|
328
|
+
}
|
|
329
|
+
a { color: var(--cms-accent); text-decoration: none; }
|
|
330
|
+
a:hover { text-decoration: underline; text-underline-offset: 0.2em; }
|
|
331
|
+
kbd {
|
|
332
|
+
display: inline-block;
|
|
333
|
+
padding: 0.1em 0.4em;
|
|
334
|
+
font: 0.75em/1.4 var(--cms-mono);
|
|
335
|
+
color: var(--cms-fg);
|
|
336
|
+
background: var(--cms-subtle);
|
|
337
|
+
border: 1px solid var(--cms-border);
|
|
338
|
+
border-bottom-width: 2px;
|
|
339
|
+
border-radius: 6px;
|
|
340
|
+
}
|
|
341
|
+
.cms-icon { width: 1em; height: 1em; flex: none; vertical-align: -0.125em; }
|
|
342
|
+
|
|
343
|
+
/* Top bar */
|
|
344
|
+
.cms-bar {
|
|
345
|
+
position: sticky;
|
|
346
|
+
top: 0;
|
|
347
|
+
z-index: 10;
|
|
348
|
+
display: flex;
|
|
349
|
+
align-items: center;
|
|
350
|
+
gap: 12px;
|
|
351
|
+
height: 56px;
|
|
352
|
+
padding: 0 max(16px, env(safe-area-inset-left));
|
|
353
|
+
background: color-mix(in srgb, var(--cms-bg) 82%, transparent);
|
|
354
|
+
backdrop-filter: saturate(180%) blur(12px);
|
|
355
|
+
-webkit-backdrop-filter: saturate(180%) blur(12px);
|
|
356
|
+
border-bottom: 1px solid var(--cms-border);
|
|
357
|
+
}
|
|
358
|
+
.cms-logo { display: flex; color: var(--cms-fg); }
|
|
359
|
+
.cms-logo svg { width: 22px; height: 22px; }
|
|
360
|
+
.cms-crumbs {
|
|
361
|
+
display: flex;
|
|
362
|
+
align-items: center;
|
|
363
|
+
min-width: 0;
|
|
364
|
+
gap: 2px;
|
|
365
|
+
font-size: 14px;
|
|
366
|
+
white-space: nowrap;
|
|
367
|
+
overflow: hidden;
|
|
368
|
+
mask-image: linear-gradient(to left, transparent, #000 24px);
|
|
369
|
+
-webkit-mask-image: linear-gradient(to left, transparent, #000 24px);
|
|
370
|
+
padding-right: 24px;
|
|
371
|
+
}
|
|
372
|
+
.cms-crumbs a, .cms-crumbs span { padding: 4px 6px; border-radius: 6px; color: var(--cms-muted); }
|
|
373
|
+
.cms-crumbs a:hover { background: var(--cms-hover); color: var(--cms-fg); text-decoration: none; }
|
|
374
|
+
.cms-crumbs span:last-child { color: var(--cms-fg); font-weight: 600; }
|
|
375
|
+
.cms-crumbs .cms-sep { padding: 0; color: var(--cms-border); }
|
|
376
|
+
.cms-actions { display: flex; align-items: center; gap: 4px; margin-left: auto; }
|
|
377
|
+
.cms-button {
|
|
378
|
+
display: inline-flex;
|
|
379
|
+
align-items: center;
|
|
380
|
+
gap: 8px;
|
|
381
|
+
height: 32px;
|
|
382
|
+
padding: 0 10px;
|
|
383
|
+
font: 500 13px var(--cms-font);
|
|
384
|
+
color: var(--cms-muted);
|
|
385
|
+
background: transparent;
|
|
386
|
+
border: 1px solid transparent;
|
|
387
|
+
border-radius: 6px;
|
|
388
|
+
cursor: pointer;
|
|
389
|
+
}
|
|
390
|
+
.cms-button:hover { color: var(--cms-fg); background: var(--cms-hover); text-decoration: none; }
|
|
391
|
+
.cms-search {
|
|
392
|
+
min-width: 200px;
|
|
393
|
+
justify-content: flex-start;
|
|
394
|
+
border-color: var(--cms-border);
|
|
395
|
+
background: var(--cms-subtle);
|
|
396
|
+
}
|
|
397
|
+
.cms-search kbd { margin-left: auto; }
|
|
398
|
+
.cms-edit[data-state="ok"] { color: var(--cms-tip); }
|
|
399
|
+
.cms-share-button[data-state="on"] { color: var(--cms-tip); }
|
|
400
|
+
.cms-share-button[data-state="starting"] { animation: cms-pulse 1s ease-in-out infinite alternate; }
|
|
401
|
+
@keyframes cms-pulse { to { opacity: 0.35; } }
|
|
402
|
+
.cms-edit[data-state="error"] { color: var(--cms-caution); }
|
|
403
|
+
.cms-live {
|
|
404
|
+
width: 8px;
|
|
405
|
+
height: 8px;
|
|
406
|
+
margin: 0 6px;
|
|
407
|
+
border-radius: 50%;
|
|
408
|
+
background: var(--cms-border);
|
|
409
|
+
transition: background 0.3s;
|
|
410
|
+
}
|
|
411
|
+
.cms-live[data-state="open"] { background: var(--cms-tip); box-shadow: 0 0 0 3px color-mix(in srgb, var(--cms-tip) 25%, transparent); }
|
|
412
|
+
.cms-live[data-state="closed"] { background: var(--cms-caution); }
|
|
413
|
+
|
|
414
|
+
/* Layout */
|
|
415
|
+
.cms-layout {
|
|
416
|
+
display: grid;
|
|
417
|
+
grid-template-columns: minmax(0, 1fr);
|
|
418
|
+
justify-content: center;
|
|
419
|
+
gap: 48px;
|
|
420
|
+
max-width: calc(var(--cms-width) + 320px);
|
|
421
|
+
margin: 0 auto;
|
|
422
|
+
padding: 40px max(16px, env(safe-area-inset-left)) 64px;
|
|
423
|
+
}
|
|
424
|
+
.cms-main { min-width: 0; max-width: var(--cms-width); width: 100%; margin: 0 auto; }
|
|
425
|
+
.cms-toc { display: none; }
|
|
426
|
+
@media (min-width: 1200px) {
|
|
427
|
+
.cms-layout:has(.cms-toc nav:not(:empty)) { grid-template-columns: minmax(0, var(--cms-width)) 240px; }
|
|
428
|
+
.cms-toc { display: block; }
|
|
429
|
+
}
|
|
430
|
+
.cms-toc nav {
|
|
431
|
+
position: sticky;
|
|
432
|
+
top: 88px;
|
|
433
|
+
max-height: calc(100vh - 120px);
|
|
434
|
+
overflow-y: auto;
|
|
435
|
+
font-size: 13px;
|
|
436
|
+
}
|
|
437
|
+
.cms-toc p { margin: 0 0 8px; font-weight: 600; color: var(--cms-fg); }
|
|
438
|
+
.cms-toc ul { margin: 0; padding: 0; list-style: none; }
|
|
439
|
+
.cms-toc ul ul { padding-left: 12px; }
|
|
440
|
+
.cms-toc a {
|
|
441
|
+
display: block;
|
|
442
|
+
padding: 4px 0 4px 12px;
|
|
443
|
+
color: var(--cms-muted);
|
|
444
|
+
border-left: 2px solid var(--cms-border);
|
|
445
|
+
line-height: 1.4;
|
|
446
|
+
}
|
|
447
|
+
.cms-toc a:hover { color: var(--cms-fg); text-decoration: none; }
|
|
448
|
+
.cms-toc a[aria-current="true"] { color: var(--cms-accent); border-left-color: var(--cms-accent); }
|
|
449
|
+
.cms-footer {
|
|
450
|
+
max-width: var(--cms-width);
|
|
451
|
+
margin: 0 auto;
|
|
452
|
+
padding: 24px 16px 48px;
|
|
453
|
+
font-size: 12px;
|
|
454
|
+
color: var(--cms-muted);
|
|
455
|
+
text-align: center;
|
|
456
|
+
}
|
|
457
|
+
.cms-footer a { color: inherit; }
|
|
458
|
+
|
|
459
|
+
/* Markdown */
|
|
460
|
+
.cms-markdown { overflow-wrap: break-word; }
|
|
461
|
+
.cms-markdown > :first-child { margin-top: 0 !important; }
|
|
462
|
+
.cms-markdown > :last-child { margin-bottom: 0 !important; }
|
|
463
|
+
.cms-markdown :is(p, ul, ol, blockquote, table, pre, details, figure, dl, .cms-alert, .cms-code-group, .cms-code) { margin: 0 0 16px; }
|
|
464
|
+
.cms-markdown :is(h1, h2, h3, h4, h5, h6) {
|
|
465
|
+
position: relative;
|
|
466
|
+
margin: 32px 0 16px;
|
|
467
|
+
font-weight: 600;
|
|
468
|
+
line-height: 1.25;
|
|
469
|
+
letter-spacing: -0.01em;
|
|
470
|
+
}
|
|
471
|
+
.cms-markdown h1 { font-size: 2em; padding-bottom: 0.3em; border-bottom: 1px solid var(--cms-border); letter-spacing: -0.02em; }
|
|
472
|
+
.cms-markdown h2 { font-size: 1.5em; padding-bottom: 0.3em; border-bottom: 1px solid var(--cms-border); }
|
|
473
|
+
.cms-markdown h3 { font-size: 1.25em; }
|
|
474
|
+
.cms-markdown h4 { font-size: 1em; }
|
|
475
|
+
.cms-markdown h5 { font-size: 0.875em; }
|
|
476
|
+
.cms-markdown h6 { font-size: 0.85em; color: var(--cms-muted); }
|
|
477
|
+
.cms-anchor {
|
|
478
|
+
position: absolute;
|
|
479
|
+
left: -1.1em;
|
|
480
|
+
padding-right: 0.3em;
|
|
481
|
+
color: var(--cms-muted);
|
|
482
|
+
opacity: 0;
|
|
483
|
+
font-weight: 400;
|
|
484
|
+
transition: opacity 0.15s;
|
|
485
|
+
}
|
|
486
|
+
.cms-markdown :is(h1, h2, h3, h4, h5, h6):hover .cms-anchor, .cms-anchor:focus { opacity: 1; text-decoration: none; }
|
|
487
|
+
.cms-markdown :is(ul, ol) { padding-left: 2em; }
|
|
488
|
+
.cms-markdown li + li { margin-top: 0.25em; }
|
|
489
|
+
.cms-markdown li > :is(ul, ol) { margin: 0.25em 0 0; }
|
|
490
|
+
.cms-markdown .contains-task-list { list-style: none; padding-left: 1.2em; }
|
|
491
|
+
.cms-markdown .task-list-item-checkbox { margin: 0 0.4em 0 -1.2em; vertical-align: middle; accent-color: var(--cms-accent); }
|
|
492
|
+
.cms-markdown blockquote { margin-left: 0; padding: 0 1em; color: var(--cms-muted); border-left: 0.25em solid var(--cms-border); }
|
|
493
|
+
.cms-markdown hr { height: 0.25em; margin: 24px 0; padding: 0; background: var(--cms-border); border: 0; border-radius: 2px; }
|
|
494
|
+
.cms-markdown img { max-width: 100%; height: auto; border-radius: 4px; }
|
|
495
|
+
.cms-markdown table { display: block; width: max-content; max-width: 100%; overflow: auto; border-spacing: 0; border-collapse: collapse; font-variant-numeric: tabular-nums; }
|
|
496
|
+
.cms-markdown :is(th, td) { padding: 6px 13px; border: 1px solid var(--cms-border); }
|
|
497
|
+
.cms-markdown th { font-weight: 600; text-align: start; background: var(--cms-subtle); }
|
|
498
|
+
.cms-markdown tr:nth-child(2n) td { background: color-mix(in srgb, var(--cms-subtle) 60%, transparent); }
|
|
499
|
+
.cms-markdown :not(pre) > code {
|
|
500
|
+
padding: 0.2em 0.4em;
|
|
501
|
+
font: 85%/1 var(--cms-mono);
|
|
502
|
+
background: color-mix(in srgb, var(--cms-muted) 14%, transparent);
|
|
503
|
+
border-radius: 6px;
|
|
504
|
+
}
|
|
505
|
+
.cms-markdown details { padding: 8px 16px; border: 1px solid var(--cms-border); border-radius: var(--cms-radius); }
|
|
506
|
+
.cms-markdown summary { cursor: pointer; font-weight: 600; }
|
|
507
|
+
.cms-markdown details[open] summary { margin-bottom: 8px; }
|
|
508
|
+
.cms-markdown mark { background: #fff8c5; color: inherit; padding: 0 2px; border-radius: 2px; }
|
|
509
|
+
.dark .cms-markdown mark { background: #bb800966; }
|
|
510
|
+
.cms-markdown .footnotes { font-size: 0.875em; color: var(--cms-muted); }
|
|
511
|
+
.cms-markdown .footnotes h2 { font-size: 1em; border: 0; }
|
|
512
|
+
.cms-markdown .footnotes hr { height: 1px; }
|
|
513
|
+
.cms-markdown .footnote-ref a { font-size: 0.75em; }
|
|
514
|
+
|
|
515
|
+
/* Code */
|
|
516
|
+
.cms-markdown pre, .cms-code-error pre {
|
|
517
|
+
position: relative;
|
|
518
|
+
padding: 16px 0;
|
|
519
|
+
overflow: auto;
|
|
520
|
+
font: 13.5px/1.6 var(--cms-mono);
|
|
521
|
+
color: var(--twp-identifier, var(--cms-fg));
|
|
522
|
+
background: var(--twp-background, var(--cms-subtle));
|
|
523
|
+
border: 1px solid var(--cms-border);
|
|
524
|
+
border-radius: var(--cms-radius);
|
|
525
|
+
tab-size: 2;
|
|
526
|
+
}
|
|
527
|
+
.cms-markdown pre > code { display: block; min-width: max-content; padding: 0 16px; }
|
|
528
|
+
.twinkleplop code:has(> .l) { padding: 0; }
|
|
529
|
+
.twinkleplop .l { display: inline-block; min-width: 100%; padding: 0 16px; }
|
|
530
|
+
.twinkleplop .l.highlight {
|
|
531
|
+
background: color-mix(in srgb, var(--cms-accent) 12%, transparent);
|
|
532
|
+
box-shadow: inset 3px 0 var(--cms-accent);
|
|
533
|
+
}
|
|
534
|
+
.twinkleplop.has-highlight .l:not(.highlight) { opacity: 0.72; }
|
|
535
|
+
.twinkleplop .ln {
|
|
536
|
+
display: inline-block;
|
|
537
|
+
width: 2.5ch;
|
|
538
|
+
margin-right: 20px;
|
|
539
|
+
color: var(--cms-muted);
|
|
540
|
+
text-align: right;
|
|
541
|
+
opacity: 0.6;
|
|
542
|
+
user-select: none;
|
|
543
|
+
}
|
|
544
|
+
.twinkleplop .highlighted-word {
|
|
545
|
+
padding: 1px 3px;
|
|
546
|
+
margin: -1px -3px;
|
|
547
|
+
background: color-mix(in srgb, var(--cms-warning) 22%, transparent);
|
|
548
|
+
border-radius: 4px;
|
|
549
|
+
}
|
|
550
|
+
.twinkleplop-block { margin: 0 0 16px; }
|
|
551
|
+
.twinkleplop-block > pre { margin: 0 !important; border-top-left-radius: 0; border-top-right-radius: 0; }
|
|
552
|
+
.twinkleplop-title {
|
|
553
|
+
display: flex;
|
|
554
|
+
align-items: center;
|
|
555
|
+
gap: 8px;
|
|
556
|
+
padding: 8px 16px;
|
|
557
|
+
font: 12px/1.5 var(--cms-mono);
|
|
558
|
+
color: var(--cms-muted);
|
|
559
|
+
background: var(--cms-subtle);
|
|
560
|
+
border: 1px solid var(--cms-border);
|
|
561
|
+
border-bottom: 0;
|
|
562
|
+
border-radius: var(--cms-radius) var(--cms-radius) 0 0;
|
|
563
|
+
}
|
|
564
|
+
.twinkleplop-caption { margin-top: 6px; font-size: 13px; color: var(--cms-muted); text-align: center; }
|
|
565
|
+
.cms-code { position: relative; }
|
|
566
|
+
.cms-code > pre { margin: 0; }
|
|
567
|
+
.cms-copy {
|
|
568
|
+
position: absolute;
|
|
569
|
+
top: 8px;
|
|
570
|
+
right: 8px;
|
|
571
|
+
z-index: 1;
|
|
572
|
+
display: grid;
|
|
573
|
+
place-items: center;
|
|
574
|
+
width: 32px;
|
|
575
|
+
height: 32px;
|
|
576
|
+
color: var(--cms-muted);
|
|
577
|
+
background: var(--cms-bg);
|
|
578
|
+
border: 1px solid var(--cms-border);
|
|
579
|
+
border-radius: 6px;
|
|
580
|
+
opacity: 0;
|
|
581
|
+
cursor: pointer;
|
|
582
|
+
transition: opacity 0.15s, color 0.15s;
|
|
583
|
+
}
|
|
584
|
+
.cms-code:hover .cms-copy, .cms-copy:focus-visible { opacity: 1; }
|
|
585
|
+
.cms-copy:hover { color: var(--cms-fg); }
|
|
586
|
+
.cms-copy[data-copied] { color: var(--cms-tip); opacity: 1; }
|
|
587
|
+
.cms-copy .cms-icon { width: 16px; height: 16px; }
|
|
588
|
+
.twinkleplop-block .cms-copy { top: 44px; }
|
|
589
|
+
.cms-code-error { margin: 0 0 16px; }
|
|
590
|
+
.cms-code-error figcaption {
|
|
591
|
+
padding: 8px 12px;
|
|
592
|
+
font: 12px/1.5 var(--cms-mono);
|
|
593
|
+
color: var(--cms-caution);
|
|
594
|
+
background: color-mix(in srgb, var(--cms-caution) 10%, transparent);
|
|
595
|
+
border: 1px solid color-mix(in srgb, var(--cms-caution) 40%, transparent);
|
|
596
|
+
border-bottom: 0;
|
|
597
|
+
border-radius: var(--cms-radius) var(--cms-radius) 0 0;
|
|
598
|
+
}
|
|
599
|
+
.cms-code-error pre { margin: 0; border-top-left-radius: 0; border-top-right-radius: 0; }
|
|
600
|
+
.cms-code-error pre > code { padding: 0 16px; }
|
|
601
|
+
|
|
602
|
+
/* Code groups */
|
|
603
|
+
.cms-code-group { overflow: hidden; border: 1px solid var(--cms-border); border-radius: var(--cms-radius); }
|
|
604
|
+
.cms-tabs { display: flex; overflow-x: auto; background: var(--cms-subtle); border-bottom: 1px solid var(--cms-border); }
|
|
605
|
+
.cms-tabs button {
|
|
606
|
+
padding: 8px 14px;
|
|
607
|
+
font: 12px/1.5 var(--cms-mono);
|
|
608
|
+
color: var(--cms-muted);
|
|
609
|
+
background: none;
|
|
610
|
+
border: 0;
|
|
611
|
+
border-bottom: 2px solid transparent;
|
|
612
|
+
margin-bottom: -1px;
|
|
613
|
+
cursor: pointer;
|
|
614
|
+
white-space: nowrap;
|
|
615
|
+
}
|
|
616
|
+
.cms-tabs button:hover { color: var(--cms-fg); }
|
|
617
|
+
.cms-tabs button[aria-selected="true"] { color: var(--cms-fg); border-bottom-color: var(--cms-accent); }
|
|
618
|
+
.cms-code-group :is(pre, .cms-code, .twinkleplop-block) { margin: 0 !important; }
|
|
619
|
+
.cms-code-group pre { border: 0; border-radius: 0; }
|
|
620
|
+
.cms-code-group .twinkleplop-title { display: none; }
|
|
621
|
+
.cms-code-group .twinkleplop-block .cms-copy { top: 8px; }
|
|
622
|
+
|
|
623
|
+
/* Alerts */
|
|
624
|
+
.cms-alert {
|
|
625
|
+
--cms-alert: var(--cms-note);
|
|
626
|
+
padding: 8px 16px;
|
|
627
|
+
border-left: 0.25em solid var(--cms-alert);
|
|
628
|
+
border-radius: 0 var(--cms-radius) var(--cms-radius) 0;
|
|
629
|
+
background: color-mix(in srgb, var(--cms-alert) 7%, transparent);
|
|
630
|
+
}
|
|
631
|
+
.cms-alert > :last-child { margin-bottom: 0; }
|
|
632
|
+
.cms-alert-title { display: flex; align-items: center; gap: 8px; margin: 0 0 4px !important; font-weight: 600; color: var(--cms-alert); }
|
|
633
|
+
.cms-alert-tip { --cms-alert: var(--cms-tip); }
|
|
634
|
+
.cms-alert-important { --cms-alert: var(--cms-important); }
|
|
635
|
+
.cms-alert-warning { --cms-alert: var(--cms-warning); }
|
|
636
|
+
.cms-alert-caution { --cms-alert: var(--cms-caution); }
|
|
637
|
+
|
|
638
|
+
/* Math and diagrams */
|
|
639
|
+
.cms-math-block { margin: 0 0 16px; overflow-x: auto; overflow-y: hidden; padding: 4px 0; }
|
|
640
|
+
.cms-mermaid { --accent: var(--cms-accent); display: flex; justify-content: center; margin: 0 0 16px; padding: 16px; overflow-x: auto; border: 1px solid var(--cms-border); border-radius: var(--cms-radius); }
|
|
641
|
+
.cms-mermaid svg { max-width: 100%; height: auto; }
|
|
642
|
+
|
|
643
|
+
/* Directory listing */
|
|
644
|
+
.cms-listing { width: 100%; margin: 0 0 32px; border: 1px solid var(--cms-border); border-radius: var(--cms-radius); border-spacing: 0; overflow: hidden; font-size: 14px; }
|
|
645
|
+
.cms-listing th { padding: 10px 16px; font-weight: 600; text-align: left; background: var(--cms-subtle); border-bottom: 1px solid var(--cms-border); }
|
|
646
|
+
.cms-listing td { padding: 8px 16px; border-top: 1px solid var(--cms-border); }
|
|
647
|
+
.cms-listing tbody tr:first-child td { border-top: 0; }
|
|
648
|
+
.cms-listing tbody tr:hover td { background: var(--cms-hover); }
|
|
649
|
+
.cms-listing td:first-child a { display: flex; align-items: center; gap: 10px; color: var(--cms-fg); }
|
|
650
|
+
.cms-listing td:first-child .cms-icon { color: var(--cms-muted); width: 16px; height: 16px; }
|
|
651
|
+
.cms-listing tr[data-kind="dir"] .cms-icon { color: var(--cms-accent); }
|
|
652
|
+
.cms-listing tr[data-kind="markdown"] .cms-icon { color: var(--cms-important); }
|
|
653
|
+
.cms-listing :is(th, td):not(:first-child) { width: 1%; color: var(--cms-muted); text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
654
|
+
.cms-listing .cms-empty { color: var(--cms-muted); text-align: center; }
|
|
655
|
+
.cms-readme { border: 1px solid var(--cms-border); border-radius: var(--cms-radius); }
|
|
656
|
+
.cms-readme > header { display: flex; align-items: center; gap: 8px; padding: 10px 16px; font-size: 14px; font-weight: 600; border-bottom: 1px solid var(--cms-border); }
|
|
657
|
+
.cms-readme > .cms-markdown { padding: 24px 32px 32px; }
|
|
658
|
+
|
|
659
|
+
/* Status pages */
|
|
660
|
+
.cms-status { padding: 64px 0; text-align: center; }
|
|
661
|
+
.cms-status h1 { margin: 0 0 8px; font-size: 64px; letter-spacing: -0.04em; border: 0; }
|
|
662
|
+
.cms-status p { color: var(--cms-muted); }
|
|
663
|
+
.cms-status pre { text-align: left; }
|
|
664
|
+
|
|
665
|
+
/* Search palette */
|
|
666
|
+
.cms-palette {
|
|
667
|
+
width: min(640px, calc(100vw - 32px));
|
|
668
|
+
max-height: min(560px, 72vh);
|
|
669
|
+
margin: 12vh auto auto;
|
|
670
|
+
padding: 0;
|
|
671
|
+
overflow: hidden;
|
|
672
|
+
color: var(--cms-fg);
|
|
673
|
+
background: var(--cms-bg);
|
|
674
|
+
border: 1px solid var(--cms-border);
|
|
675
|
+
border-radius: 12px;
|
|
676
|
+
box-shadow: var(--cms-shadow);
|
|
677
|
+
}
|
|
678
|
+
.cms-palette[open] { display: flex; flex-direction: column; }
|
|
679
|
+
.cms-palette::backdrop { background: rgb(1 4 9 / 0.4); backdrop-filter: blur(2px); }
|
|
680
|
+
.cms-palette input {
|
|
681
|
+
width: 100%;
|
|
682
|
+
padding: 16px 18px;
|
|
683
|
+
font: 16px var(--cms-font);
|
|
684
|
+
color: inherit;
|
|
685
|
+
background: transparent;
|
|
686
|
+
border: 0;
|
|
687
|
+
border-bottom: 1px solid var(--cms-border);
|
|
688
|
+
outline: none;
|
|
689
|
+
}
|
|
690
|
+
.cms-palette ul { flex: 1; margin: 0; padding: 6px; overflow-y: auto; list-style: none; }
|
|
691
|
+
.cms-palette .cms-hit { display: flex; flex-direction: column; gap: 2px; padding: 8px 12px; color: inherit; border-radius: 8px; cursor: pointer; }
|
|
692
|
+
.cms-palette .cms-hit:hover { text-decoration: none; }
|
|
693
|
+
.cms-palette li[aria-selected="true"] > .cms-hit { background: var(--cms-accent-soft); }
|
|
694
|
+
.cms-palette li[aria-selected="true"] .cms-hit-title { color: var(--cms-accent); }
|
|
695
|
+
.cms-palette .cms-theme-hit { flex-direction: row; align-items: center; justify-content: space-between; gap: 12px; }
|
|
696
|
+
.cms-palette .cms-hit-title small { margin-left: 8px; font: 11px var(--cms-mono); color: var(--cms-muted); }
|
|
697
|
+
.cms-palette li[data-active] .cms-hit-title::after { content: "✓"; margin-left: 8px; color: var(--cms-tip); }
|
|
698
|
+
.cms-swatches { display: flex; flex: none; gap: 3px; min-height: 14px; }
|
|
699
|
+
.cms-swatches i { width: 14px; height: 14px; border-radius: 4px; box-shadow: inset 0 0 0 1px rgb(127 127 127 / 0.3); }
|
|
700
|
+
.cms-palette .cms-hit-title { font-weight: 500; }
|
|
701
|
+
.cms-palette .cms-hit-path { font: 12px var(--cms-mono); color: var(--cms-muted); }
|
|
702
|
+
.cms-palette mark { color: var(--cms-accent); background: none; font-weight: 700; }
|
|
703
|
+
.cms-palette .cms-hint { display: flex; gap: 16px; padding: 8px 16px; font-size: 12px; color: var(--cms-muted); border-top: 1px solid var(--cms-border); }
|
|
704
|
+
.cms-palette .cms-none { padding: 24px; color: var(--cms-muted); text-align: center; }
|
|
705
|
+
|
|
706
|
+
/* Share dialog */
|
|
707
|
+
.cms-share {
|
|
708
|
+
width: min(460px, calc(100vw - 32px));
|
|
709
|
+
margin: 12vh auto auto;
|
|
710
|
+
padding: 20px 22px;
|
|
711
|
+
color: var(--cms-fg);
|
|
712
|
+
background: var(--cms-bg);
|
|
713
|
+
border: 1px solid var(--cms-border);
|
|
714
|
+
border-radius: 12px;
|
|
715
|
+
box-shadow: var(--cms-shadow);
|
|
716
|
+
font-size: 14px;
|
|
717
|
+
}
|
|
718
|
+
.cms-share::backdrop { background: rgb(1 4 9 / 0.4); backdrop-filter: blur(2px); }
|
|
719
|
+
.cms-share h2 { margin: 0 0 8px; font-size: 17px; }
|
|
720
|
+
.cms-share p { margin: 0 0 12px; color: var(--cms-muted); line-height: 1.5; }
|
|
721
|
+
.cms-share .cms-share-error { color: var(--cms-caution); }
|
|
722
|
+
.cms-share .cms-share-wait { animation: cms-pulse 1.2s ease-in-out infinite alternate; }
|
|
723
|
+
.cms-share-link { display: flex; gap: 8px; margin: 0 0 14px; }
|
|
724
|
+
.cms-share-link input {
|
|
725
|
+
flex: 1;
|
|
726
|
+
min-width: 0;
|
|
727
|
+
padding: 0 10px;
|
|
728
|
+
font: 13px var(--cms-mono);
|
|
729
|
+
color: inherit;
|
|
730
|
+
background: var(--cms-subtle);
|
|
731
|
+
border: 1px solid var(--cms-border);
|
|
732
|
+
border-radius: 6px;
|
|
733
|
+
}
|
|
734
|
+
.cms-qr { display: flex; justify-content: center; margin: 0 0 16px; }
|
|
735
|
+
.cms-qr svg { width: 180px; height: 180px; padding: 8px; background: #fff; border-radius: 8px; }
|
|
736
|
+
.cms-share-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
737
|
+
.cms-share .cms-button { border-color: var(--cms-border); }
|
|
738
|
+
.cms-share .cms-primary { color: #fff; background: var(--cms-accent); border-color: var(--cms-accent); }
|
|
739
|
+
.cms-share .cms-primary:hover { color: #fff; background: color-mix(in srgb, var(--cms-accent) 85%, #000); }
|
|
740
|
+
.cms-share .cms-danger { color: var(--cms-caution); }
|
|
741
|
+
|
|
742
|
+
/* Live reload */
|
|
743
|
+
@keyframes cms-flash {
|
|
744
|
+
from { background: color-mix(in srgb, var(--cms-accent) 20%, transparent); box-shadow: 0 0 0 6px color-mix(in srgb, var(--cms-accent) 20%, transparent); }
|
|
745
|
+
to { background: transparent; box-shadow: 0 0 0 6px transparent; }
|
|
746
|
+
}
|
|
747
|
+
.cms-changed { animation: cms-flash 1.4s ease-out; border-radius: 4px; }
|
|
748
|
+
@media (prefers-reduced-motion: reduce) { .cms-changed { animation: none; } }
|
|
749
|
+
|
|
750
|
+
@media (max-width: 720px) {
|
|
751
|
+
.cms-search { min-width: 0; }
|
|
752
|
+
.cms-search span, .cms-search kbd { display: none; }
|
|
753
|
+
.cms-layout { padding-top: 24px; }
|
|
754
|
+
.cms-readme > .cms-markdown { padding: 16px; }
|
|
755
|
+
.cms-listing :is(th, td):nth-child(3) { display: none; }
|
|
756
|
+
}
|
|
757
|
+
@media print {
|
|
758
|
+
.cms-bar, .cms-toc, .cms-footer, .cms-copy { display: none !important; }
|
|
759
|
+
.cms-layout { display: block; padding: 0; }
|
|
760
|
+
.cms-markdown pre { white-space: pre-wrap; }
|
|
761
|
+
}
|
|
762
|
+
`;
|
|
763
|
+
//#endregion
|
|
764
|
+
//#region src/theme-parse.ts
|
|
765
|
+
/**
|
|
766
|
+
* Reads a base16 or base24 scheme (YAML) or an Omarchy `colors.toml`.
|
|
767
|
+
* Returns `undefined` when the text is not a complete scheme.
|
|
768
|
+
*
|
|
769
|
+
* The colors go into CSS, and the text can come from the network, so the
|
|
770
|
+
* function accepts only hex colors. The browser gets this function with
|
|
771
|
+
* `toString()`, so it must not use anything from outside its body.
|
|
772
|
+
*/
|
|
773
|
+
function parseTheme(text, id, fallbackName) {
|
|
774
|
+
const hex = (value) => {
|
|
775
|
+
const match = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec((value ?? "").trim());
|
|
776
|
+
if (!match?.[1]) return void 0;
|
|
777
|
+
const digits = match[1].toLowerCase();
|
|
778
|
+
return `#${digits.length === 3 ? digits.replace(/./g, "$&$&") : digits}`;
|
|
779
|
+
};
|
|
780
|
+
const unquote = (value) => value.replace(/\s+#.*$/, "").trim().replace(/^(["'])(.*)\1$/, "$2");
|
|
781
|
+
const isDark = (color) => {
|
|
782
|
+
const [r, g, b] = [
|
|
783
|
+
1,
|
|
784
|
+
3,
|
|
785
|
+
5
|
|
786
|
+
].map((start) => parseInt(color.slice(start, start + 2), 16) / 255);
|
|
787
|
+
return .2126 * (r ?? 0) + .7152 * (g ?? 0) + .0722 * (b ?? 0) < .5;
|
|
788
|
+
};
|
|
789
|
+
let colors;
|
|
790
|
+
let name;
|
|
791
|
+
let variant;
|
|
792
|
+
let accent;
|
|
793
|
+
if (/^\s*base0[0-9a-f]\s*:/im.test(text)) {
|
|
794
|
+
const slots = {};
|
|
795
|
+
for (const match of text.matchAll(/^\s*base0([0-9a-f])\s*:\s*(["']?)#?([0-9a-f]+)\2/gim)) slots[(match[1] ?? "").toLowerCase()] = hex(match[3]);
|
|
796
|
+
colors = "0123456789abcdef".split("").map((slot) => slots[slot]);
|
|
797
|
+
name = /^(?:name|scheme)\s*:\s*(.+?)\s*$/m.exec(text)?.[1];
|
|
798
|
+
variant = /^variant\s*:\s*(.+?)\s*$/m.exec(text)?.[1];
|
|
799
|
+
} else if (/^\s*background\s*=/m.test(text)) {
|
|
800
|
+
const keys = {};
|
|
801
|
+
for (const match of text.matchAll(/^\s*([a-z_]+)\s*=\s*(.+?)\s*$/gm)) keys[match[1] ?? ""] = unquote(match[2] ?? "");
|
|
802
|
+
const first = (...names) => {
|
|
803
|
+
for (const key of names) {
|
|
804
|
+
const color = hex(keys[key]);
|
|
805
|
+
if (color) return color;
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
const background = first("background");
|
|
809
|
+
const foreground = first("foreground");
|
|
810
|
+
colors = [
|
|
811
|
+
background,
|
|
812
|
+
first("lighter_background", "dark_background", "selection") ?? background,
|
|
813
|
+
first("selection", "lighter_background") ?? background,
|
|
814
|
+
first("muted", "dark_foreground", "bright_black"),
|
|
815
|
+
first("dark_foreground", "muted", "light_foreground") ?? foreground,
|
|
816
|
+
foreground,
|
|
817
|
+
first("light_foreground", "bright_foreground") ?? foreground,
|
|
818
|
+
first("bright_foreground", "light_foreground") ?? foreground,
|
|
819
|
+
first("red"),
|
|
820
|
+
first("orange", "bright_red", "red"),
|
|
821
|
+
first("yellow"),
|
|
822
|
+
first("green"),
|
|
823
|
+
first("cyan"),
|
|
824
|
+
first("blue"),
|
|
825
|
+
first("magenta"),
|
|
826
|
+
first("brown", "orange", "red")
|
|
827
|
+
];
|
|
828
|
+
colors[3] ??= colors[4] ?? colors[2];
|
|
829
|
+
const fallback = colors[13] ?? colors[5];
|
|
830
|
+
for (const index of [
|
|
831
|
+
8,
|
|
832
|
+
9,
|
|
833
|
+
10,
|
|
834
|
+
11,
|
|
835
|
+
12,
|
|
836
|
+
13,
|
|
837
|
+
14,
|
|
838
|
+
15
|
|
839
|
+
]) colors[index] ??= fallback;
|
|
840
|
+
variant = keys.mode;
|
|
841
|
+
accent = first("accent");
|
|
842
|
+
} else return;
|
|
843
|
+
if (colors.length !== 16 || colors.some((color) => color === void 0)) return void 0;
|
|
844
|
+
const complete = colors;
|
|
845
|
+
const cleanName = unquote(name ?? "").slice(0, 80) || fallbackName;
|
|
846
|
+
const mode = unquote(variant ?? "").toLowerCase();
|
|
847
|
+
return {
|
|
848
|
+
id,
|
|
849
|
+
name: cleanName,
|
|
850
|
+
variant: mode === "light" || mode === "dark" ? mode : isDark(complete[0] ?? "") ? "dark" : "light",
|
|
851
|
+
colors: complete,
|
|
852
|
+
...accent ? { accent } : {}
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
//#endregion
|
|
856
|
+
//#region src/client.ts
|
|
857
|
+
/**
|
|
858
|
+
* Applies the theme before the first paint, so the page does not flash. The page
|
|
859
|
+
* puts it inline in the head, with the default theme of the server as its argument.
|
|
860
|
+
* It must not use anything from outside its body.
|
|
861
|
+
*/
|
|
862
|
+
function themeBoot(defaultTheme) {
|
|
863
|
+
const root = document.documentElement;
|
|
864
|
+
const media = matchMedia("(prefers-color-scheme: dark)");
|
|
865
|
+
const hex = /^#[0-9a-f]{6}$/i;
|
|
866
|
+
const slots = "0123456789ABCDEF".split("").map((slot) => `--b0${slot}`);
|
|
867
|
+
const isPalette = (theme) => !!theme && "colors" in theme && Array.isArray(theme.colors) && theme.colors.length === 16 && theme.colors.every((color) => hex.test(color));
|
|
868
|
+
const read = () => {
|
|
869
|
+
try {
|
|
870
|
+
const raw = localStorage.getItem("cms-theme");
|
|
871
|
+
if (!raw) return null;
|
|
872
|
+
return JSON.parse(raw);
|
|
873
|
+
} catch {
|
|
874
|
+
return null;
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
const apply = (theme) => {
|
|
878
|
+
for (const slot of slots) root.style.removeProperty(slot);
|
|
879
|
+
root.style.removeProperty("--b-accent");
|
|
880
|
+
if (isPalette(theme)) {
|
|
881
|
+
theme.colors.forEach((color, index) => root.style.setProperty(slots[index] ?? "", color));
|
|
882
|
+
if (theme.accent && hex.test(theme.accent)) root.style.setProperty("--b-accent", theme.accent);
|
|
883
|
+
root.dataset.cmsPalette = theme.id;
|
|
884
|
+
root.dataset.theme = "palette";
|
|
885
|
+
root.classList.toggle("dark", theme.variant === "dark");
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
const mode = theme && "mode" in theme ? theme.mode : "system";
|
|
889
|
+
delete root.dataset.cmsPalette;
|
|
890
|
+
root.dataset.theme = mode;
|
|
891
|
+
root.classList.toggle("dark", mode === "dark" || mode === "system" && media.matches);
|
|
892
|
+
};
|
|
893
|
+
const runtime = {
|
|
894
|
+
defaultTheme,
|
|
895
|
+
read,
|
|
896
|
+
apply,
|
|
897
|
+
current: () => read() ?? runtime.defaultTheme ?? { mode: "system" }
|
|
898
|
+
};
|
|
899
|
+
window.__cms = runtime;
|
|
900
|
+
media.addEventListener("change", () => apply(runtime.current()));
|
|
901
|
+
apply(runtime.current());
|
|
902
|
+
}
|
|
903
|
+
function client(parse) {
|
|
904
|
+
const doc = document;
|
|
905
|
+
const configText = doc.getElementById("cms-config")?.textContent;
|
|
906
|
+
const config = JSON.parse(configText ?? "{}");
|
|
907
|
+
const themes = window.__cms;
|
|
908
|
+
const all = (selector, scope = doc) => [...scope.querySelectorAll(selector)];
|
|
909
|
+
const escape = (text) => text.replace(/[&<>"']/g, (char) => `&#${char.charCodeAt(0)};`);
|
|
910
|
+
const svg = (paths) => `<svg class="cms-icon" viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${paths}</svg>`;
|
|
911
|
+
const ICON_COPY = svg("<rect x=\"9\" y=\"9\" width=\"12\" height=\"12\" rx=\"2\"/><path d=\"M5 15V5a2 2 0 0 1 2-2h10\"/>");
|
|
912
|
+
const ICON_CHECK = svg("<path d=\"m5 12 5 5L20 7\"/>");
|
|
913
|
+
const store = {
|
|
914
|
+
get: (key) => {
|
|
915
|
+
try {
|
|
916
|
+
return localStorage.getItem(key);
|
|
917
|
+
} catch {
|
|
918
|
+
return null;
|
|
919
|
+
}
|
|
920
|
+
},
|
|
921
|
+
set: (key, value) => {
|
|
922
|
+
try {
|
|
923
|
+
if (value === null) localStorage.removeItem(key);
|
|
924
|
+
else localStorage.setItem(key, value);
|
|
925
|
+
} catch {}
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
const content = doc.getElementById("cms-content");
|
|
929
|
+
let pristine = content ? [...content.children].map((child) => child.outerHTML) : [];
|
|
930
|
+
const copyText = async (text) => {
|
|
931
|
+
if (navigator.clipboard && isSecureContext) return navigator.clipboard.writeText(text);
|
|
932
|
+
const area = doc.createElement("textarea");
|
|
933
|
+
area.value = text;
|
|
934
|
+
area.style.position = "fixed";
|
|
935
|
+
area.style.opacity = "0";
|
|
936
|
+
doc.body.append(area);
|
|
937
|
+
area.select();
|
|
938
|
+
doc.execCommand("copy");
|
|
939
|
+
area.remove();
|
|
940
|
+
};
|
|
941
|
+
const enhance = (scope) => {
|
|
942
|
+
for (const heading of all(":is(h1, h2, h3, h4, h5, h6)[id]", scope)) {
|
|
943
|
+
if (heading.querySelector(".cms-anchor")) continue;
|
|
944
|
+
const anchor = doc.createElement("a");
|
|
945
|
+
anchor.className = "cms-anchor";
|
|
946
|
+
anchor.href = `#${heading.id}`;
|
|
947
|
+
anchor.textContent = "#";
|
|
948
|
+
anchor.setAttribute("aria-label", "Link to this section");
|
|
949
|
+
heading.prepend(anchor);
|
|
950
|
+
}
|
|
951
|
+
for (const pre of all("pre", scope)) {
|
|
952
|
+
if (pre.closest(".cms-code-error, .cms-code") || !pre.querySelector("code")) continue;
|
|
953
|
+
let host = pre.parentElement;
|
|
954
|
+
if (!host?.matches("figure.twinkleplop-block")) {
|
|
955
|
+
host = doc.createElement("div");
|
|
956
|
+
host.className = "cms-code";
|
|
957
|
+
pre.replaceWith(host);
|
|
958
|
+
host.append(pre);
|
|
959
|
+
}
|
|
960
|
+
if (host.querySelector(":scope > .cms-copy")) continue;
|
|
961
|
+
const button = doc.createElement("button");
|
|
962
|
+
button.className = "cms-copy";
|
|
963
|
+
button.type = "button";
|
|
964
|
+
button.title = "Copy";
|
|
965
|
+
button.innerHTML = ICON_COPY;
|
|
966
|
+
host.append(button);
|
|
967
|
+
}
|
|
968
|
+
const tab = store.get("cms-tab");
|
|
969
|
+
if (tab) selectTab(tab, scope);
|
|
970
|
+
};
|
|
971
|
+
const selectTab = (label, scope = doc) => {
|
|
972
|
+
for (const group of all(".cms-code-group", scope)) {
|
|
973
|
+
const buttons = all(".cms-tabs button", group);
|
|
974
|
+
const index = buttons.findIndex((button) => button.textContent === label);
|
|
975
|
+
if (index < 0) continue;
|
|
976
|
+
const panels = all(":scope > [role=\"tabpanel\"]", group);
|
|
977
|
+
buttons.forEach((button, i) => button.setAttribute("aria-selected", String(i === index)));
|
|
978
|
+
panels.forEach((panel, i) => panel.hidden = i !== index);
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
doc.addEventListener("click", (event) => {
|
|
982
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
983
|
+
const copy = target?.closest(".cms-copy");
|
|
984
|
+
if (copy) {
|
|
985
|
+
const code = copy.parentElement?.querySelector("pre code")?.cloneNode(true);
|
|
986
|
+
if (!(code instanceof Element)) return;
|
|
987
|
+
for (const number of all(".ln", code)) number.remove();
|
|
988
|
+
copyText(code.textContent ?? "").then(() => {
|
|
989
|
+
copy.innerHTML = ICON_CHECK;
|
|
990
|
+
copy.dataset.copied = "";
|
|
991
|
+
setTimeout(() => {
|
|
992
|
+
copy.innerHTML = ICON_COPY;
|
|
993
|
+
delete copy.dataset.copied;
|
|
994
|
+
}, 1500);
|
|
995
|
+
});
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
const tabButton = target?.closest(".cms-tabs button");
|
|
999
|
+
if (tabButton) {
|
|
1000
|
+
const label = tabButton.textContent ?? "";
|
|
1001
|
+
store.set("cms-tab", label);
|
|
1002
|
+
selectTab(label);
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
if (target?.closest("[data-cms-edit]")) openEditor();
|
|
1006
|
+
else if (target?.closest("[data-cms-share]")) openShare();
|
|
1007
|
+
else if (target?.closest("[data-cms-theme]")) openPalette("theme");
|
|
1008
|
+
else if (target?.closest("[data-cms-search]")) openPalette("search");
|
|
1009
|
+
});
|
|
1010
|
+
let spyFrame = 0;
|
|
1011
|
+
const spy = () => {
|
|
1012
|
+
spyFrame = 0;
|
|
1013
|
+
const links = all("#cms-toc a");
|
|
1014
|
+
let active;
|
|
1015
|
+
for (const link of links) {
|
|
1016
|
+
const target = doc.getElementById(decodeURIComponent(link.hash.slice(1)));
|
|
1017
|
+
if (target && target.getBoundingClientRect().top < 120) active = link;
|
|
1018
|
+
}
|
|
1019
|
+
active ??= links[0];
|
|
1020
|
+
for (const link of links) link.setAttribute("aria-current", String(link === active));
|
|
1021
|
+
};
|
|
1022
|
+
addEventListener("scroll", () => spyFrame ||= requestAnimationFrame(spy), { passive: true });
|
|
1023
|
+
const editButton = doc.querySelector("[data-cms-edit]");
|
|
1024
|
+
const currentLine = () => {
|
|
1025
|
+
let line = 1;
|
|
1026
|
+
for (const heading of all(":is(h1, h2, h3, h4, h5, h6)[id]", content ?? doc)) {
|
|
1027
|
+
if (heading.getBoundingClientRect().top > 120) break;
|
|
1028
|
+
line = config.lines?.[heading.id] ?? line;
|
|
1029
|
+
}
|
|
1030
|
+
return line;
|
|
1031
|
+
};
|
|
1032
|
+
const openEditor = async () => {
|
|
1033
|
+
if (!config.edit || config.kind !== "markdown" || !editButton || editButton.hidden) return;
|
|
1034
|
+
const response = await fetch(config.edit, {
|
|
1035
|
+
method: "POST",
|
|
1036
|
+
headers: {
|
|
1037
|
+
"content-type": "application/json",
|
|
1038
|
+
"x-comarkserv-action": "1"
|
|
1039
|
+
},
|
|
1040
|
+
body: JSON.stringify({
|
|
1041
|
+
path: config.source,
|
|
1042
|
+
line: currentLine()
|
|
1043
|
+
})
|
|
1044
|
+
}).catch(() => void 0);
|
|
1045
|
+
if (!editButton) return;
|
|
1046
|
+
const error = response?.ok ? void 0 : (await response?.json().catch(() => void 0))?.error;
|
|
1047
|
+
editButton.dataset.state = response?.ok ? "ok" : "error";
|
|
1048
|
+
editButton.title = response?.ok ? "Edit in your editor (E)" : error ?? "The editor did not open";
|
|
1049
|
+
setTimeout(() => delete editButton.dataset.state, 1500);
|
|
1050
|
+
};
|
|
1051
|
+
const shareButton = doc.querySelector("[data-cms-share]");
|
|
1052
|
+
let share = {
|
|
1053
|
+
state: "off",
|
|
1054
|
+
needsConsent: false
|
|
1055
|
+
};
|
|
1056
|
+
let shareDialog;
|
|
1057
|
+
const renderShare = () => {
|
|
1058
|
+
if (!shareDialog) return;
|
|
1059
|
+
const links = share.links;
|
|
1060
|
+
const link = (href, text) => href ? `<a href="${escape(href)}" target="_blank" rel="noreferrer">${text}</a>` : text;
|
|
1061
|
+
if (share.state === "on") shareDialog.innerHTML = `<h2>Sharing</h2><p>Anyone with this link can read the files in this folder. They cannot use the Edit button.</p><div class="cms-share-link"><input readonly value="${escape(share.page ?? share.url ?? "")}" aria-label="The public link"><button class="cms-button cms-primary" type="button" data-share-copy>Copy</button></div><div class="cms-qr">${share.qr ?? ""}</div><div class="cms-share-actions"><button class="cms-button cms-danger" type="button" data-share-stop>Stop sharing</button><button class="cms-button" type="button" data-share-close>Close</button></div>`;
|
|
1062
|
+
else if (share.state === "starting") shareDialog.innerHTML = "<h2>Sharing</h2><p class=\"cms-share-wait\">Starting a Cloudflare tunnel. This can take some seconds. The first time, comarkserv also downloads cloudflared.</p><div class=\"cms-share-actions\"><button class=\"cms-button\" type=\"button\" data-share-close>Close</button></div>";
|
|
1063
|
+
else shareDialog.innerHTML = "<h2>Share this folder</h2><p>comarkserv gives the pages a public URL, through a free Cloudflare tunnel. Anyone with the URL can read the files in this folder, until you stop sharing or stop comarkserv. Visitors cannot use the Edit button.</p>" + (share.needsConsent ? `<p class="cms-share-consent">To share, comarkserv installs cloudflared first. Before that, you must accept the Cloudflare ${link(links?.license, "License")}, ${link(links?.terms, "Terms")} and ${link(links?.privacy, "Privacy Policy")}.</p>` : "") + (share.error ? `<p class="cms-share-error">${escape(share.error)}</p>` : "") + `<div class="cms-share-actions"><button class="cms-button cms-primary" type="button" data-share-start>${share.needsConsent ? "Accept and share" : "Share"}</button><button class="cms-button" type="button" data-share-close>Cancel</button></div>`;
|
|
1064
|
+
};
|
|
1065
|
+
const setShare = (next) => {
|
|
1066
|
+
share = {
|
|
1067
|
+
...share,
|
|
1068
|
+
...next
|
|
1069
|
+
};
|
|
1070
|
+
if (next.state !== "on") {
|
|
1071
|
+
delete share.page;
|
|
1072
|
+
delete share.qr;
|
|
1073
|
+
}
|
|
1074
|
+
if (shareButton) {
|
|
1075
|
+
shareButton.dataset.state = share.state;
|
|
1076
|
+
shareButton.title = share.state === "on" ? `Sharing at ${share.url ?? ""}` : "Share this folder";
|
|
1077
|
+
}
|
|
1078
|
+
if (shareDialog?.open) renderShare();
|
|
1079
|
+
};
|
|
1080
|
+
const shareRequest = async (action) => {
|
|
1081
|
+
const next = await (await fetch(config.share, {
|
|
1082
|
+
method: "POST",
|
|
1083
|
+
headers: {
|
|
1084
|
+
"content-type": "application/json",
|
|
1085
|
+
"x-comarkserv-action": "1"
|
|
1086
|
+
},
|
|
1087
|
+
body: JSON.stringify({
|
|
1088
|
+
action,
|
|
1089
|
+
accept: action === "start" && share.needsConsent,
|
|
1090
|
+
path: location.pathname + location.search
|
|
1091
|
+
})
|
|
1092
|
+
}).catch(() => void 0))?.json().catch(() => void 0);
|
|
1093
|
+
setShare(next ?? {
|
|
1094
|
+
...share,
|
|
1095
|
+
state: "off",
|
|
1096
|
+
error: "The server does not answer."
|
|
1097
|
+
});
|
|
1098
|
+
};
|
|
1099
|
+
const openShare = () => {
|
|
1100
|
+
if (!config.share || !shareButton || shareButton.hidden) return;
|
|
1101
|
+
if (!shareDialog) {
|
|
1102
|
+
shareDialog = doc.createElement("dialog");
|
|
1103
|
+
shareDialog.className = "cms-share";
|
|
1104
|
+
doc.body.append(shareDialog);
|
|
1105
|
+
shareDialog.addEventListener("click", (event) => {
|
|
1106
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
1107
|
+
if (event.target === shareDialog || target?.closest("[data-share-close]")) shareDialog?.close();
|
|
1108
|
+
else if (target?.closest("[data-share-start]")) {
|
|
1109
|
+
setShare({
|
|
1110
|
+
...share,
|
|
1111
|
+
state: "starting"
|
|
1112
|
+
});
|
|
1113
|
+
shareRequest("start");
|
|
1114
|
+
} else if (target?.closest("[data-share-stop]")) shareRequest("stop");
|
|
1115
|
+
else if (target?.closest("[data-share-copy]")) {
|
|
1116
|
+
const copy = target.closest("button");
|
|
1117
|
+
copyText(share.page ?? share.url ?? "").then(() => {
|
|
1118
|
+
if (copy) copy.textContent = "Copied";
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
renderShare();
|
|
1124
|
+
shareDialog.showModal();
|
|
1125
|
+
shareRequest("status");
|
|
1126
|
+
};
|
|
1127
|
+
if (config.local) fetch(config.local, { cache: "no-store" }).then((response) => response.ok ? response.json() : void 0).then((local) => {
|
|
1128
|
+
if (!local) return;
|
|
1129
|
+
if (local.edit && editButton) editButton.hidden = false;
|
|
1130
|
+
if (local.share && shareButton) {
|
|
1131
|
+
shareButton.hidden = false;
|
|
1132
|
+
setShare(local.share);
|
|
1133
|
+
}
|
|
1134
|
+
}).catch(() => {});
|
|
1135
|
+
const isBoundary = (char) => !char || /[\s/._\-›:]/.test(char);
|
|
1136
|
+
const fuzzy = (query, text) => {
|
|
1137
|
+
const lower = text.toLowerCase();
|
|
1138
|
+
const at = lower.indexOf(query);
|
|
1139
|
+
if (at >= 0) {
|
|
1140
|
+
const hits = Array.from({ length: query.length }, (_, i) => at + i);
|
|
1141
|
+
return {
|
|
1142
|
+
score: 100 + (isBoundary(lower[at - 1]) ? 40 : 0) - at * .5 - lower.length * .05,
|
|
1143
|
+
hits
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
const hits = [];
|
|
1147
|
+
let score = 0;
|
|
1148
|
+
let from = 0;
|
|
1149
|
+
for (const char of query) {
|
|
1150
|
+
if (char === " ") continue;
|
|
1151
|
+
const i = lower.indexOf(char, from);
|
|
1152
|
+
if (i < 0) return void 0;
|
|
1153
|
+
const last = hits.at(-1);
|
|
1154
|
+
score += last !== void 0 && i === last + 1 ? 6 : isBoundary(lower[i - 1]) ? 4 : 1;
|
|
1155
|
+
hits.push(i);
|
|
1156
|
+
from = i + 1;
|
|
1157
|
+
}
|
|
1158
|
+
return {
|
|
1159
|
+
score: score - lower.length * .05,
|
|
1160
|
+
hits
|
|
1161
|
+
};
|
|
1162
|
+
};
|
|
1163
|
+
const mark = (text, hits) => {
|
|
1164
|
+
const set = new Set(hits);
|
|
1165
|
+
let html = "";
|
|
1166
|
+
for (let i = 0; i < text.length; i++) {
|
|
1167
|
+
const char = escape(text.charAt(i));
|
|
1168
|
+
html += set.has(i) ? `<mark>${char}</mark>` : char;
|
|
1169
|
+
}
|
|
1170
|
+
return html;
|
|
1171
|
+
};
|
|
1172
|
+
let index;
|
|
1173
|
+
const loadIndex = () => index ??= fetch(config.search).then((response) => response.json()).then((entries) => entries.flatMap((entry) => [{
|
|
1174
|
+
title: entry.title,
|
|
1175
|
+
path: entry.url,
|
|
1176
|
+
url: config.root + entry.url.slice(1),
|
|
1177
|
+
page: true
|
|
1178
|
+
}, ...entry.headings.filter((heading) => heading.depth > 1).map((heading) => ({
|
|
1179
|
+
title: heading.text,
|
|
1180
|
+
path: `${entry.title} › ${entry.url}`,
|
|
1181
|
+
url: `${config.root}${entry.url.slice(1)}#${heading.id}`,
|
|
1182
|
+
page: false
|
|
1183
|
+
}))])).catch(() => {
|
|
1184
|
+
index = void 0;
|
|
1185
|
+
return [];
|
|
1186
|
+
});
|
|
1187
|
+
const searchResults = async (needle) => {
|
|
1188
|
+
const items = await loadIndex();
|
|
1189
|
+
if (!needle) return items.filter((item) => item.page).map((item) => ({
|
|
1190
|
+
item,
|
|
1191
|
+
titleHits: [],
|
|
1192
|
+
pathHits: []
|
|
1193
|
+
}));
|
|
1194
|
+
const results = [];
|
|
1195
|
+
for (const item of items) {
|
|
1196
|
+
const title = fuzzy(needle, item.title);
|
|
1197
|
+
const path = fuzzy(needle, item.path);
|
|
1198
|
+
if (!title && !path) continue;
|
|
1199
|
+
const score = Math.max((title?.score ?? -Infinity) + 10, path?.score ?? -Infinity) + (item.page ? 8 : 0);
|
|
1200
|
+
results.push({
|
|
1201
|
+
item,
|
|
1202
|
+
score,
|
|
1203
|
+
titleHits: title?.hits ?? [],
|
|
1204
|
+
pathHits: title ? [] : path?.hits ?? []
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
return results.sort((a, b) => b.score - a.score);
|
|
1208
|
+
};
|
|
1209
|
+
const GITHUB_LIGHT = [
|
|
1210
|
+
"#ffffff",
|
|
1211
|
+
"#1f2328",
|
|
1212
|
+
"#cf222e",
|
|
1213
|
+
"#953800",
|
|
1214
|
+
"#9a6700",
|
|
1215
|
+
"#1a7f37",
|
|
1216
|
+
"#0969da",
|
|
1217
|
+
"#8250df"
|
|
1218
|
+
];
|
|
1219
|
+
const GITHUB_DARK = [
|
|
1220
|
+
"#0d1117",
|
|
1221
|
+
"#e6edf3",
|
|
1222
|
+
"#ff7b72",
|
|
1223
|
+
"#ffa657",
|
|
1224
|
+
"#d29922",
|
|
1225
|
+
"#3fb950",
|
|
1226
|
+
"#58a6ff",
|
|
1227
|
+
"#d2a8ff"
|
|
1228
|
+
];
|
|
1229
|
+
const builtIns = () => [
|
|
1230
|
+
...themes?.defaultTheme ? [{
|
|
1231
|
+
id: "default",
|
|
1232
|
+
name: `Default: ${themes.defaultTheme.name}`,
|
|
1233
|
+
source: "server",
|
|
1234
|
+
choice: themes.defaultTheme
|
|
1235
|
+
}] : [],
|
|
1236
|
+
{
|
|
1237
|
+
id: "github-system",
|
|
1238
|
+
name: "GitHub",
|
|
1239
|
+
source: "system",
|
|
1240
|
+
choice: { mode: "system" },
|
|
1241
|
+
colors: GITHUB_LIGHT
|
|
1242
|
+
},
|
|
1243
|
+
{
|
|
1244
|
+
id: "github-light",
|
|
1245
|
+
name: "GitHub Light",
|
|
1246
|
+
source: "built-in",
|
|
1247
|
+
choice: { mode: "light" },
|
|
1248
|
+
colors: GITHUB_LIGHT
|
|
1249
|
+
},
|
|
1250
|
+
{
|
|
1251
|
+
id: "github-dark",
|
|
1252
|
+
name: "GitHub Dark",
|
|
1253
|
+
source: "built-in",
|
|
1254
|
+
choice: { mode: "dark" },
|
|
1255
|
+
colors: GITHUB_DARK
|
|
1256
|
+
}
|
|
1257
|
+
];
|
|
1258
|
+
let catalog;
|
|
1259
|
+
const loadCatalog = () => catalog ??= (config.themes ? fetch(config.themes) : Promise.reject(/* @__PURE__ */ new Error("no catalog"))).then((response) => response.ok ? response.json() : []).catch(() => {
|
|
1260
|
+
catalog = void 0;
|
|
1261
|
+
return [];
|
|
1262
|
+
});
|
|
1263
|
+
const palettes = /* @__PURE__ */ new Map();
|
|
1264
|
+
const loadTheme = (entry) => {
|
|
1265
|
+
if (entry.choice) return Promise.resolve(entry.choice);
|
|
1266
|
+
if (!entry.url) return Promise.resolve(void 0);
|
|
1267
|
+
let palette = palettes.get(entry.url);
|
|
1268
|
+
if (!palette) {
|
|
1269
|
+
palette = fetch(entry.url).then((response) => response.ok ? response.text() : "").then((text) => parse(text, entry.id, entry.name)).catch(() => void 0);
|
|
1270
|
+
palettes.set(entry.url, palette);
|
|
1271
|
+
}
|
|
1272
|
+
return palette;
|
|
1273
|
+
};
|
|
1274
|
+
const swatches = (colors) => colors.map((color) => `<i style="background:${escape(color)}"></i>`).join("");
|
|
1275
|
+
const paletteSwatches = (theme) => "colors" in theme ? [
|
|
1276
|
+
0,
|
|
1277
|
+
5,
|
|
1278
|
+
8,
|
|
1279
|
+
9,
|
|
1280
|
+
10,
|
|
1281
|
+
11,
|
|
1282
|
+
13,
|
|
1283
|
+
14
|
|
1284
|
+
].map((slot) => theme.colors[slot] ?? "") : theme.mode === "dark" ? GITHUB_DARK : GITHUB_LIGHT;
|
|
1285
|
+
const choiceId = (theme) => {
|
|
1286
|
+
if (!theme) return themes?.defaultTheme ? "default" : "github-system";
|
|
1287
|
+
return "colors" in theme ? theme.id : `github-${theme.mode}`;
|
|
1288
|
+
};
|
|
1289
|
+
let themeEntries = [];
|
|
1290
|
+
const themeResults = async (needle) => {
|
|
1291
|
+
themeEntries = [...builtIns(), ...await loadCatalog()];
|
|
1292
|
+
if (!needle) return themeEntries.map((entry) => ({
|
|
1293
|
+
entry,
|
|
1294
|
+
hits: []
|
|
1295
|
+
}));
|
|
1296
|
+
const results = [];
|
|
1297
|
+
for (const entry of themeEntries) {
|
|
1298
|
+
const match = fuzzy(needle, `${entry.name} ${entry.source}`);
|
|
1299
|
+
const pinned = !entry.url || entry.source === "omarchy-live";
|
|
1300
|
+
if (match) results.push({
|
|
1301
|
+
entry,
|
|
1302
|
+
score: match.score + (pinned ? 50 : 0),
|
|
1303
|
+
hits: match.hits.filter((hit) => hit < entry.name.length)
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
return results.sort((a, b) => b.score - a.score);
|
|
1307
|
+
};
|
|
1308
|
+
let palette;
|
|
1309
|
+
let mode = "search";
|
|
1310
|
+
let selected = 0;
|
|
1311
|
+
let committed = false;
|
|
1312
|
+
let previewTimer;
|
|
1313
|
+
let swatchObserver;
|
|
1314
|
+
const items = () => all("li[role=option]", palette);
|
|
1315
|
+
const select = (next) => {
|
|
1316
|
+
const list = items();
|
|
1317
|
+
selected = (next + list.length) % Math.max(list.length, 1);
|
|
1318
|
+
list.forEach((item, i) => item.setAttribute("aria-selected", String(i === selected)));
|
|
1319
|
+
list[selected]?.scrollIntoView({ block: "nearest" });
|
|
1320
|
+
if (mode === "theme") {
|
|
1321
|
+
clearTimeout(previewTimer);
|
|
1322
|
+
previewTimer = setTimeout(() => void preview(list[selected]), 40);
|
|
1323
|
+
}
|
|
1324
|
+
};
|
|
1325
|
+
const entryOf = (item) => themeEntries[Number(item?.dataset.entry)];
|
|
1326
|
+
const preview = async (item) => {
|
|
1327
|
+
const entry = entryOf(item);
|
|
1328
|
+
const theme = entry && await loadTheme(entry);
|
|
1329
|
+
if (theme && item?.getAttribute("aria-selected") === "true") themes?.apply(theme);
|
|
1330
|
+
};
|
|
1331
|
+
const commitTheme = async (item) => {
|
|
1332
|
+
const entry = entryOf(item);
|
|
1333
|
+
const theme = entry && await loadTheme(entry);
|
|
1334
|
+
if (!entry || !theme) return;
|
|
1335
|
+
committed = true;
|
|
1336
|
+
store.set("cms-theme", entry.id === "default" ? null : JSON.stringify(theme));
|
|
1337
|
+
themes?.apply(themes.current());
|
|
1338
|
+
palette?.close();
|
|
1339
|
+
};
|
|
1340
|
+
const openPalette = (next) => {
|
|
1341
|
+
if (next === "theme" && !themes) return;
|
|
1342
|
+
if (!palette) {
|
|
1343
|
+
palette = doc.createElement("dialog");
|
|
1344
|
+
palette.className = "cms-palette";
|
|
1345
|
+
palette.innerHTML = "<input type=\"search\" aria-label=\"Search\" autocomplete=\"off\" spellcheck=\"false\"><ul role=\"listbox\"></ul><div class=\"cms-hint\"><span><kbd>↑</kbd> <kbd>↓</kbd> move</span><span><kbd>↵</kbd> <span class=\"cms-hint-enter\">open</span></span><span><kbd>esc</kbd> close</span></div>";
|
|
1346
|
+
doc.body.append(palette);
|
|
1347
|
+
const input = palette.querySelector("input");
|
|
1348
|
+
input.addEventListener("input", () => void renderResults(input.value));
|
|
1349
|
+
input.addEventListener("keydown", (event) => {
|
|
1350
|
+
if (event.key === "Escape") {
|
|
1351
|
+
event.preventDefault();
|
|
1352
|
+
palette?.close();
|
|
1353
|
+
} else if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
1354
|
+
event.preventDefault();
|
|
1355
|
+
select(selected + (event.key === "ArrowDown" ? 1 : -1));
|
|
1356
|
+
} else if (event.key === "Enter") {
|
|
1357
|
+
event.preventDefault();
|
|
1358
|
+
const item = items()[selected];
|
|
1359
|
+
if (mode === "theme") return void commitTheme(item);
|
|
1360
|
+
const link = item?.querySelector("a");
|
|
1361
|
+
if (!link) return;
|
|
1362
|
+
if (event.metaKey || event.ctrlKey) open(link.href, "_blank");
|
|
1363
|
+
else location.href = link.href;
|
|
1364
|
+
palette?.close();
|
|
1365
|
+
}
|
|
1366
|
+
});
|
|
1367
|
+
palette.addEventListener("click", (event) => {
|
|
1368
|
+
if (event.target === palette) return palette.close();
|
|
1369
|
+
const item = event.target instanceof Element ? event.target.closest("li[role=option]") : null;
|
|
1370
|
+
if (mode === "theme" && item) commitTheme(item);
|
|
1371
|
+
});
|
|
1372
|
+
palette.addEventListener("mousemove", (event) => {
|
|
1373
|
+
const item = event.target instanceof Element ? event.target.closest("li[role=option]") : null;
|
|
1374
|
+
const index = item ? items().indexOf(item) : -1;
|
|
1375
|
+
if (index >= 0 && index !== selected) select(index);
|
|
1376
|
+
});
|
|
1377
|
+
palette.addEventListener("close", () => {
|
|
1378
|
+
clearTimeout(previewTimer);
|
|
1379
|
+
if (mode === "theme" && !committed) themes?.apply(themes.current());
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
mode = next;
|
|
1383
|
+
committed = false;
|
|
1384
|
+
const input = palette.querySelector("input");
|
|
1385
|
+
input.value = "";
|
|
1386
|
+
input.placeholder = mode === "theme" ? "Search 580+ themes" : "Search pages and headings";
|
|
1387
|
+
const enter = palette.querySelector(".cms-hint-enter");
|
|
1388
|
+
if (enter) enter.textContent = mode === "theme" ? "keep" : "open";
|
|
1389
|
+
palette.showModal();
|
|
1390
|
+
input.focus();
|
|
1391
|
+
renderResults("");
|
|
1392
|
+
};
|
|
1393
|
+
const observeSwatches = (list) => {
|
|
1394
|
+
swatchObserver?.disconnect();
|
|
1395
|
+
swatchObserver = new IntersectionObserver((records) => {
|
|
1396
|
+
for (const record of records) {
|
|
1397
|
+
if (!record.isIntersecting) continue;
|
|
1398
|
+
swatchObserver?.unobserve(record.target);
|
|
1399
|
+
const entry = entryOf(record.target);
|
|
1400
|
+
const target = record.target.querySelector(".cms-swatches");
|
|
1401
|
+
if (!entry || !target) continue;
|
|
1402
|
+
if (entry.colors) target.innerHTML = swatches(entry.colors);
|
|
1403
|
+
else loadTheme(entry).then((theme) => theme && (target.innerHTML = swatches(paletteSwatches(theme))));
|
|
1404
|
+
}
|
|
1405
|
+
}, { root: list });
|
|
1406
|
+
for (const item of items()) swatchObserver.observe(item);
|
|
1407
|
+
};
|
|
1408
|
+
const renderResults = async (query) => {
|
|
1409
|
+
const list = palette?.querySelector("ul");
|
|
1410
|
+
if (!list) return;
|
|
1411
|
+
const needle = query.trim().toLowerCase();
|
|
1412
|
+
const current = mode;
|
|
1413
|
+
let html;
|
|
1414
|
+
if (current === "theme") {
|
|
1415
|
+
const results = await themeResults(needle);
|
|
1416
|
+
const active = choiceId(themes?.read() ?? null);
|
|
1417
|
+
html = results.map(({ entry, hits }, i) => `<li role="option" aria-selected="${i === 0}" data-entry="${themeEntries.indexOf(entry)}"${entry.id === active ? " data-active" : ""}><div class="cms-hit cms-theme-hit"><span class="cms-hit-title">${mark(entry.name, hits)}<small>${escape(entry.source)}</small></span><span class="cms-swatches"></span></div></li>`).join("");
|
|
1418
|
+
} else html = (await searchResults(needle)).slice(0, 60).map(({ item, titleHits, pathHits }, i) => `<li role="option" aria-selected="${i === 0}"><a class="cms-hit" href="${escape(item.url)}"><span class="cms-hit-title">${item.page ? "" : "# "}${mark(item.title, titleHits)}</span><span class="cms-hit-path">${mark(item.path, pathHits)}</span></a></li>`).join("");
|
|
1419
|
+
if (current !== mode) return;
|
|
1420
|
+
selected = 0;
|
|
1421
|
+
list.innerHTML = html || "<li class=\"cms-none\">No results</li>";
|
|
1422
|
+
list.scrollTop = 0;
|
|
1423
|
+
if (current !== "theme") return;
|
|
1424
|
+
observeSwatches(list);
|
|
1425
|
+
const active = items().findIndex((item) => item.hasAttribute("data-active"));
|
|
1426
|
+
if (!needle && active > 0) select(active);
|
|
1427
|
+
};
|
|
1428
|
+
const isMac = /Mac|iPhone|iPad/.test(navigator.platform);
|
|
1429
|
+
for (const key of all("[data-cms-search] kbd")) key.textContent = isMac ? "⌘K" : "Ctrl K";
|
|
1430
|
+
addEventListener("keydown", (event) => {
|
|
1431
|
+
const typing = event.target instanceof HTMLElement && event.target.closest("input, textarea, select, [contenteditable]");
|
|
1432
|
+
const modifier = event.metaKey || event.ctrlKey || event.altKey;
|
|
1433
|
+
if (event.key === "e" && !typing && !modifier && !palette?.open) {
|
|
1434
|
+
event.preventDefault();
|
|
1435
|
+
openEditor();
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
if (event.key === "k" && (event.metaKey || event.ctrlKey) || event.key === "/" && !typing) {
|
|
1439
|
+
event.preventDefault();
|
|
1440
|
+
if (palette?.open) palette.close();
|
|
1441
|
+
else openPalette("search");
|
|
1442
|
+
}
|
|
1443
|
+
});
|
|
1444
|
+
const parentOf = (path) => path.slice(0, path.lastIndexOf("/") + 1);
|
|
1445
|
+
let updating;
|
|
1446
|
+
let again = false;
|
|
1447
|
+
const update = async () => {
|
|
1448
|
+
const started = performance.now();
|
|
1449
|
+
const response = await fetch(location.href, { cache: "no-store" });
|
|
1450
|
+
const next = new DOMParser().parseFromString(await response.text(), "text/html");
|
|
1451
|
+
const incoming = next.getElementById("cms-content");
|
|
1452
|
+
if (!content || !incoming) return location.reload();
|
|
1453
|
+
const nextPristine = [...incoming.children].map((child) => child.outerHTML);
|
|
1454
|
+
let start = 0;
|
|
1455
|
+
while (start < pristine.length && start < nextPristine.length && pristine[start] === nextPristine[start]) start++;
|
|
1456
|
+
let endOld = pristine.length;
|
|
1457
|
+
let endNew = nextPristine.length;
|
|
1458
|
+
while (endOld > start && endNew > start && pristine[endOld - 1] === nextPristine[endNew - 1]) {
|
|
1459
|
+
endOld--;
|
|
1460
|
+
endNew--;
|
|
1461
|
+
}
|
|
1462
|
+
const current = [...content.children];
|
|
1463
|
+
const added = [...incoming.children].slice(start, endNew);
|
|
1464
|
+
for (const child of current.slice(start, endOld)) child.remove();
|
|
1465
|
+
const anchor = content.children[start] ?? null;
|
|
1466
|
+
for (const child of added) content.insertBefore(doc.importNode(child, true), anchor);
|
|
1467
|
+
pristine = nextPristine;
|
|
1468
|
+
const nextConfig = next.getElementById("cms-config")?.textContent;
|
|
1469
|
+
if (nextConfig) config.lines = JSON.parse(nextConfig).lines;
|
|
1470
|
+
enhance(content);
|
|
1471
|
+
doc.title = next.title;
|
|
1472
|
+
const toc = doc.getElementById("cms-toc");
|
|
1473
|
+
const nextToc = next.getElementById("cms-toc");
|
|
1474
|
+
if (toc && nextToc) toc.innerHTML = nextToc.innerHTML;
|
|
1475
|
+
spy();
|
|
1476
|
+
const inserted = [...content.children].slice(start, start + added.length);
|
|
1477
|
+
for (const block of inserted) {
|
|
1478
|
+
block.classList.remove("cms-changed");
|
|
1479
|
+
block.offsetWidth;
|
|
1480
|
+
block.classList.add("cms-changed");
|
|
1481
|
+
}
|
|
1482
|
+
const box = inserted[0]?.getBoundingClientRect();
|
|
1483
|
+
if (box && (box.bottom < 64 || box.top > innerHeight)) inserted[0]?.scrollIntoView({
|
|
1484
|
+
block: "center",
|
|
1485
|
+
behavior: "smooth"
|
|
1486
|
+
});
|
|
1487
|
+
console.info(`[comarkserv] updated in ${Math.round(performance.now() - started)}ms`);
|
|
1488
|
+
};
|
|
1489
|
+
const scheduleUpdate = () => {
|
|
1490
|
+
if (updating) {
|
|
1491
|
+
again = true;
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
updating = update().finally(() => {
|
|
1495
|
+
updating = void 0;
|
|
1496
|
+
if (again) {
|
|
1497
|
+
again = false;
|
|
1498
|
+
scheduleUpdate();
|
|
1499
|
+
}
|
|
1500
|
+
});
|
|
1501
|
+
};
|
|
1502
|
+
const onChange = (paths) => {
|
|
1503
|
+
index = void 0;
|
|
1504
|
+
const source = config.source;
|
|
1505
|
+
if (paths.some((path) => path === source || config.kind === "directory" && parentOf(path) === source)) {
|
|
1506
|
+
if (config.kind === "markdown" || config.kind === "directory") scheduleUpdate();
|
|
1507
|
+
else location.reload();
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
const changed = new Set(paths);
|
|
1511
|
+
for (const link of all("link[rel=\"stylesheet\"]")) {
|
|
1512
|
+
const url = new URL(link.href);
|
|
1513
|
+
if (!changed.has(url.pathname)) continue;
|
|
1514
|
+
const fresh = link.cloneNode();
|
|
1515
|
+
url.searchParams.set("t", String(Date.now()));
|
|
1516
|
+
fresh.href = url.href;
|
|
1517
|
+
fresh.addEventListener("load", () => link.remove(), { once: true });
|
|
1518
|
+
link.after(fresh);
|
|
1519
|
+
}
|
|
1520
|
+
for (const image of all("img")) {
|
|
1521
|
+
const url = new URL(image.src);
|
|
1522
|
+
if (!changed.has(decodeURIComponent(url.pathname))) continue;
|
|
1523
|
+
url.searchParams.set("t", String(Date.now()));
|
|
1524
|
+
image.src = url.href;
|
|
1525
|
+
}
|
|
1526
|
+
};
|
|
1527
|
+
const onTheme = (theme) => {
|
|
1528
|
+
if (!themes) return;
|
|
1529
|
+
const chosen = themes.read();
|
|
1530
|
+
if (chosen && "colors" in chosen && chosen.id === theme.id) store.set("cms-theme", JSON.stringify(theme));
|
|
1531
|
+
else if (!chosen && themes.defaultTheme?.id === theme.id) themes.defaultTheme = theme;
|
|
1532
|
+
else return;
|
|
1533
|
+
if (!(mode === "theme" && palette?.open)) themes.apply(themes.current());
|
|
1534
|
+
};
|
|
1535
|
+
if (config.events) {
|
|
1536
|
+
const indicator = doc.querySelector(".cms-live");
|
|
1537
|
+
let serverId;
|
|
1538
|
+
const events = new EventSource(config.events);
|
|
1539
|
+
events.addEventListener("hello", (event) => {
|
|
1540
|
+
const id = JSON.parse(event.data).id;
|
|
1541
|
+
if (serverId && serverId !== id) location.reload();
|
|
1542
|
+
serverId = id;
|
|
1543
|
+
if (indicator) {
|
|
1544
|
+
indicator.dataset.state = "open";
|
|
1545
|
+
indicator.title = "Live reload is on";
|
|
1546
|
+
}
|
|
1547
|
+
});
|
|
1548
|
+
events.addEventListener("change", (event) => onChange(JSON.parse(event.data).paths));
|
|
1549
|
+
events.addEventListener("share", () => {
|
|
1550
|
+
if (shareButton && !shareButton.hidden) shareRequest("status");
|
|
1551
|
+
});
|
|
1552
|
+
events.addEventListener("theme", (event) => onTheme(JSON.parse(event.data).theme));
|
|
1553
|
+
events.addEventListener("error", () => {
|
|
1554
|
+
if (indicator) {
|
|
1555
|
+
indicator.dataset.state = "closed";
|
|
1556
|
+
indicator.title = "Live reload is off: the server does not answer";
|
|
1557
|
+
}
|
|
1558
|
+
});
|
|
1559
|
+
addEventListener("pagehide", () => events.close());
|
|
1560
|
+
}
|
|
1561
|
+
if (content) enhance(content);
|
|
1562
|
+
spy();
|
|
1563
|
+
}
|
|
1564
|
+
/** The client script, ready to serve. */
|
|
1565
|
+
const clientScript = `(${client.toString()})(${parseTheme.toString()});\n`;
|
|
1566
|
+
/** The inline head script, with the default theme of the server. */
|
|
1567
|
+
function themeScript(defaultTheme) {
|
|
1568
|
+
const json = JSON.stringify(defaultTheme ?? null).replace(/</g, "\\u003c");
|
|
1569
|
+
return `(${themeBoot.toString()})(${json});`;
|
|
1570
|
+
}
|
|
1571
|
+
//#endregion
|
|
1572
|
+
//#region src/assets.ts
|
|
1573
|
+
const require = createRequire(import.meta.url);
|
|
1574
|
+
function hash(text) {
|
|
1575
|
+
return createHash("sha1").update(text).digest("hex").slice(0, 10);
|
|
1576
|
+
}
|
|
1577
|
+
function asset(body, type) {
|
|
1578
|
+
return {
|
|
1579
|
+
body,
|
|
1580
|
+
type,
|
|
1581
|
+
version: hash(body)
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
let assets;
|
|
1585
|
+
/** Returns the assets. The first call reads the theme from disk. Later calls use the cache. */
|
|
1586
|
+
function getAssets() {
|
|
1587
|
+
if (assets) return assets;
|
|
1588
|
+
const katexDir = dirname(require.resolve("katex"));
|
|
1589
|
+
const themeCss = readFileSync(require.resolve("@twinkleplop/theme-github"), "utf8");
|
|
1590
|
+
assets = {
|
|
1591
|
+
"app.css": asset(`${styles}\n${themeCss}\n${paletteStyles(themeTokenNames(themeCss))}`, "text/css; charset=utf-8"),
|
|
1592
|
+
"app.js": asset(clientScript, "text/javascript; charset=utf-8"),
|
|
1593
|
+
katexDir,
|
|
1594
|
+
katexVersion: hash(readFileSync(join(katexDir, "katex.min.css"), "utf8"))
|
|
1595
|
+
};
|
|
1596
|
+
return assets;
|
|
1597
|
+
}
|
|
1598
|
+
//#endregion
|
|
1599
|
+
//#region src/page.ts
|
|
1600
|
+
function escape(text) {
|
|
1601
|
+
return text.replace(/[&<>"']/g, (char) => `&#${char.charCodeAt(0)};`);
|
|
1602
|
+
}
|
|
1603
|
+
function icon(paths) {
|
|
1604
|
+
return `<svg class="cms-icon" viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${paths}</svg>`;
|
|
1605
|
+
}
|
|
1606
|
+
const LOGO_PATHS = "<rect x=\"2\" y=\"5\" width=\"20\" height=\"14\" rx=\"3\"/><path d=\"M6 15V9l3 3 3-3v6m4-6v6m-2.5-2.5L16 15l2.5-2.5\"/>";
|
|
1607
|
+
const icons = {
|
|
1608
|
+
logo: icon(LOGO_PATHS),
|
|
1609
|
+
search: icon("<circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m20 20-3.5-3.5\"/>"),
|
|
1610
|
+
palette: icon("<path d=\"M12 22a10 10 0 1 1 10-10c0 2.8-2.2 4-4.5 4H16a2 2 0 0 0-1.5 3.3c.4.5.6 1 .6 1.5 0 .7-.6 1.2-1.3 1.2Z\"/><circle cx=\"7.5\" cy=\"10.5\" r=\"1.2\"/><circle cx=\"12\" cy=\"7\" r=\"1.2\"/><circle cx=\"16.5\" cy=\"10.5\" r=\"1.2\"/>"),
|
|
1611
|
+
code: icon("<path d=\"m16 18 6-6-6-6M8 6l-6 6 6 6\"/>"),
|
|
1612
|
+
edit: icon("<path d=\"M12 20h9\"/><path d=\"M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z\"/>"),
|
|
1613
|
+
share: icon("<circle cx=\"18\" cy=\"5\" r=\"3\"/><circle cx=\"6\" cy=\"12\" r=\"3\"/><circle cx=\"18\" cy=\"19\" r=\"3\"/><path d=\"m8.6 13.5 6.8 4M15.4 6.5l-6.8 4\"/>"),
|
|
1614
|
+
folder: icon("<path d=\"M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z\"/>"),
|
|
1615
|
+
file: icon("<path d=\"M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8Z\"/><path d=\"M14 3v5h5\"/>"),
|
|
1616
|
+
markdown: icon("<rect x=\"2\" y=\"5\" width=\"20\" height=\"14\" rx=\"3\"/><path d=\"M6 15V9l3 3 3-3v6m4-6v6m-2.5-2.5L16 15l2.5-2.5\"/>"),
|
|
1617
|
+
image: icon("<rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"/><circle cx=\"9\" cy=\"9\" r=\"2\"/><path d=\"m21 15-5-5L5 21\"/>"),
|
|
1618
|
+
up: icon("<path d=\"M12 19V5m-7 7 7-7 7 7\"/>"),
|
|
1619
|
+
book: icon("<path d=\"M4 19.5A2.5 2.5 0 0 1 6.5 17H20V3H6.5A2.5 2.5 0 0 0 4 5.5Z\"/><path d=\"M4 19.5A2.5 2.5 0 0 0 6.5 22H20v-5\"/>")
|
|
1620
|
+
};
|
|
1621
|
+
const FAVICON = "data:image/svg+xml," + encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#8250df" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round">${LOGO_PATHS}</svg>`);
|
|
1622
|
+
function renderToc(toc) {
|
|
1623
|
+
if (!toc || toc.length === 0) return "";
|
|
1624
|
+
const list = (links) => `<ul>${links.map((link) => `<li><a href="#${escape(link.id)}">${escape(link.text)}</a>${link.children?.length ? list(link.children) : ""}</li>`).join("")}</ul>`;
|
|
1625
|
+
return `<p>On this page</p>${list(toc)}`;
|
|
1626
|
+
}
|
|
1627
|
+
function renderCrumbs(crumbs) {
|
|
1628
|
+
return crumbs.map((crumb) => crumb.href === void 0 ? `<span>${escape(crumb.name)}</span>` : `<a href="${escape(crumb.href)}">${escape(crumb.name)}</a>`).join("<span class=\"cms-sep\">/</span>");
|
|
1629
|
+
}
|
|
1630
|
+
function configScript(config) {
|
|
1631
|
+
return `<script type="application/json" id="cms-config">${JSON.stringify(config).replace(/</g, "\\u003c")}<\/script>`;
|
|
1632
|
+
}
|
|
1633
|
+
function renderPage(input) {
|
|
1634
|
+
const { assets, config } = input;
|
|
1635
|
+
const toc = renderToc(input.toc);
|
|
1636
|
+
const rootHref = input.home ?? input.crumbs[0]?.href ?? "./";
|
|
1637
|
+
return `<!doctype html>
|
|
1638
|
+
<html lang="en">
|
|
1639
|
+
<head>
|
|
1640
|
+
<meta charset="utf-8">
|
|
1641
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1642
|
+
<title>${escape(input.title)}</title>
|
|
1643
|
+
${input.description ? `<meta name="description" content="${escape(input.description)}">\n` : ""}<meta name="generator" content="comarkserv">
|
|
1644
|
+
<link rel="icon" href="${FAVICON}">
|
|
1645
|
+
<script>${themeScript(input.theme)}<\/script>
|
|
1646
|
+
<link rel="stylesheet" href="${escape(assets.css)}">
|
|
1647
|
+
${input.features?.math ? `<link rel="stylesheet" href="${escape(assets.katex)}">\n` : ""}<script src="${escape(assets.js)}" defer><\/script>
|
|
1648
|
+
</head>
|
|
1649
|
+
<body>
|
|
1650
|
+
<header class="cms-bar">
|
|
1651
|
+
<a class="cms-logo" href="${escape(rootHref)}" title="Home">${icons.logo}</a>
|
|
1652
|
+
<nav class="cms-crumbs" aria-label="Breadcrumb">${renderCrumbs(input.crumbs)}</nav>
|
|
1653
|
+
<div class="cms-actions">
|
|
1654
|
+
<button class="cms-button cms-search" type="button" data-cms-search>${icons.search}<span>Search</span><kbd>⌘K</kbd></button>
|
|
1655
|
+
${input.rawHref ? `<a class="cms-button" href="${escape(input.rawHref)}" title="View the source">${icons.code}</a>\n` : ""}${config.edit && config.kind === "markdown" ? `<button class="cms-button cms-edit" type="button" data-cms-edit title="Edit in your editor (E)" hidden>${icons.edit}</button>\n` : ""}${config.share ? `<button class="cms-button cms-share-button" type="button" data-cms-share title="Share this folder" hidden>${icons.share}</button>\n` : ""}<button class="cms-button" type="button" data-cms-theme title="Change the theme">${icons.palette}</button>
|
|
1656
|
+
${config.events ? "<span class=\"cms-live\" title=\"Live reload is connecting\"></span>\n" : ""}</div>
|
|
1657
|
+
</header>
|
|
1658
|
+
<div class="cms-layout">
|
|
1659
|
+
<main class="cms-main"><div id="cms-content" class="${input.contentClass ?? "cms-markdown"}">
|
|
1660
|
+
${input.content}
|
|
1661
|
+
</div></main>
|
|
1662
|
+
<aside class="cms-toc"><nav id="cms-toc" aria-label="Table of contents">${toc}</nav></aside>
|
|
1663
|
+
</div>
|
|
1664
|
+
<footer class="cms-footer">Served by <a href="https://github.com/Rigo-m/comarkserv">comarkserv</a>${input.footer ? ` · ${escape(input.footer)}` : ""}</footer>
|
|
1665
|
+
${configScript(config)}
|
|
1666
|
+
</body>
|
|
1667
|
+
</html>
|
|
1668
|
+
`;
|
|
1669
|
+
}
|
|
1670
|
+
/** Adds the client script to an HTML file that the server does not render, for live reload. */
|
|
1671
|
+
function injectClient(html, config, assets) {
|
|
1672
|
+
const tags = `${configScript(config)}<script src="${escape(assets.js)}" defer><\/script>`;
|
|
1673
|
+
const at = html.search(/<\/body>/i);
|
|
1674
|
+
return at < 0 ? html + tags : html.slice(0, at) + tags + html.slice(at);
|
|
1675
|
+
}
|
|
1676
|
+
const KIND_ICONS = {
|
|
1677
|
+
dir: icons.folder,
|
|
1678
|
+
markdown: icons.markdown,
|
|
1679
|
+
image: icons.image,
|
|
1680
|
+
file: icons.file
|
|
1681
|
+
};
|
|
1682
|
+
function formatSize(bytes) {
|
|
1683
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
1684
|
+
const units = [
|
|
1685
|
+
"KB",
|
|
1686
|
+
"MB",
|
|
1687
|
+
"GB",
|
|
1688
|
+
"TB"
|
|
1689
|
+
];
|
|
1690
|
+
let value = bytes / 1024;
|
|
1691
|
+
let unit = 0;
|
|
1692
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
1693
|
+
value /= 1024;
|
|
1694
|
+
unit++;
|
|
1695
|
+
}
|
|
1696
|
+
return `${value < 10 ? value.toFixed(1) : Math.round(value)} ${units[unit]}`;
|
|
1697
|
+
}
|
|
1698
|
+
function formatAge(date, now = Date.now()) {
|
|
1699
|
+
const seconds = Math.round((now - date.getTime()) / 1e3);
|
|
1700
|
+
if (seconds < 60) return "just now";
|
|
1701
|
+
const steps = [
|
|
1702
|
+
[60, "minute"],
|
|
1703
|
+
[24, "hour"],
|
|
1704
|
+
[30, "day"],
|
|
1705
|
+
[12, "month"]
|
|
1706
|
+
];
|
|
1707
|
+
let value = seconds / 60;
|
|
1708
|
+
for (const [limit, unit] of steps) {
|
|
1709
|
+
if (value < limit) {
|
|
1710
|
+
const count = Math.floor(value);
|
|
1711
|
+
return `${count} ${unit}${count === 1 ? "" : "s"} ago`;
|
|
1712
|
+
}
|
|
1713
|
+
value /= limit;
|
|
1714
|
+
}
|
|
1715
|
+
const years = Math.floor(value);
|
|
1716
|
+
return `${years} year${years === 1 ? "" : "s"} ago`;
|
|
1717
|
+
}
|
|
1718
|
+
function renderListing(entries, parentHref) {
|
|
1719
|
+
const rows = entries.map((entry) => `<tr data-kind="${entry.kind}"><td><a href="${escape(entry.href)}">${KIND_ICONS[entry.kind]}${escape(entry.name)}${entry.kind === "dir" ? "/" : ""}</a></td><td>${entry.kind === "dir" ? "" : formatSize(entry.size)}</td><td><time datetime="${entry.modified.toISOString()}" title="${entry.modified.toLocaleString("en")}">${formatAge(entry.modified)}</time></td></tr>`);
|
|
1720
|
+
if (parentHref !== void 0) rows.unshift(`<tr data-kind="dir"><td><a href="${escape(parentHref)}">${icons.up}..</a></td><td></td><td></td></tr>`);
|
|
1721
|
+
if (entries.length === 0) rows.push("<tr><td class=\"cms-empty\" colspan=\"3\">This directory is empty.</td></tr>");
|
|
1722
|
+
return `<table class="cms-listing"><thead><tr><th>Name</th><th>Size</th><th>Modified</th></tr></thead><tbody>${rows.join("")}</tbody></table>`;
|
|
1723
|
+
}
|
|
1724
|
+
function renderReadme(name, html) {
|
|
1725
|
+
return `<section class="cms-readme"><header>${icons.book}${escape(name)}</header><div class="cms-markdown">${html}</div></section>`;
|
|
1726
|
+
}
|
|
1727
|
+
function renderStatus(code, title, message, detail) {
|
|
1728
|
+
return `<section class="cms-status"><h1>${code}</h1><h2>${escape(title)}</h2><p>${escape(message)}</p>${detail ? `<pre><code>${escape(detail)}</code></pre>` : ""}</section>`;
|
|
1729
|
+
}
|
|
1730
|
+
//#endregion
|
|
1731
|
+
//#region src/site.ts
|
|
1732
|
+
const MARKDOWN_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1733
|
+
".md",
|
|
1734
|
+
".markdown",
|
|
1735
|
+
".mdown",
|
|
1736
|
+
".mkd",
|
|
1737
|
+
".mdc"
|
|
1738
|
+
]);
|
|
1739
|
+
const IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1740
|
+
".apng",
|
|
1741
|
+
".avif",
|
|
1742
|
+
".bmp",
|
|
1743
|
+
".gif",
|
|
1744
|
+
".ico",
|
|
1745
|
+
".jpeg",
|
|
1746
|
+
".jpg",
|
|
1747
|
+
".png",
|
|
1748
|
+
".svg",
|
|
1749
|
+
".webp"
|
|
1750
|
+
]);
|
|
1751
|
+
/** Directories that the listing, the search index and the watcher skip. */
|
|
1752
|
+
const SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set(["node_modules", ".git"]);
|
|
1753
|
+
/** Glob patterns for the skipped directories, for tinyglobby. */
|
|
1754
|
+
const SKIPPED_GLOBS = [...SKIPPED_DIRECTORIES].map((name) => `**/${name}/**`);
|
|
1755
|
+
/** A glob pattern for markdown files, for tinyglobby. */
|
|
1756
|
+
const MARKDOWN_GLOB = `**/*.{${[...MARKDOWN_EXTENSIONS].map((extension) => extension.slice(1)).join(",")}}`;
|
|
1757
|
+
function isMarkdown(name) {
|
|
1758
|
+
return MARKDOWN_EXTENSIONS.has(extname(name).toLowerCase());
|
|
1759
|
+
}
|
|
1760
|
+
function entryKind(name, directory) {
|
|
1761
|
+
if (directory) return "dir";
|
|
1762
|
+
const extension = extname(name).toLowerCase();
|
|
1763
|
+
if (MARKDOWN_EXTENSIONS.has(extension)) return "markdown";
|
|
1764
|
+
return IMAGE_EXTENSIONS.has(extension) ? "image" : "file";
|
|
1765
|
+
}
|
|
1766
|
+
/** Returns true when a path segment is hidden: a dotfile other than `.well-known`. */
|
|
1767
|
+
function isHiddenSegment(segment) {
|
|
1768
|
+
return segment.startsWith(".") && segment !== ".well-known";
|
|
1769
|
+
}
|
|
1770
|
+
/** Returns true when a URL path, with `/` separators, has a hidden segment. */
|
|
1771
|
+
function hasHiddenSegment(path) {
|
|
1772
|
+
return path.split("/").some(isHiddenSegment);
|
|
1773
|
+
}
|
|
1774
|
+
/** Returns true when the watcher and the search index must skip a path from the root. */
|
|
1775
|
+
function isSkippedPath(path, dotfiles) {
|
|
1776
|
+
return path.split("/").some((segment) => SKIPPED_DIRECTORIES.has(segment) || !dotfiles && isHiddenSegment(segment));
|
|
1777
|
+
}
|
|
1778
|
+
/** A relative link that goes up the given number of directories. */
|
|
1779
|
+
function up(levels) {
|
|
1780
|
+
return levels === 0 ? "./" : "../".repeat(levels);
|
|
1781
|
+
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Returns the breadcrumbs for a URL path. The links are relative, so they work
|
|
1784
|
+
* on the server and in a static build.
|
|
1785
|
+
*/
|
|
1786
|
+
function crumbsFor(pathname, rootName) {
|
|
1787
|
+
const isDirectory = pathname.endsWith("/");
|
|
1788
|
+
const segments = pathname.split("/").filter(Boolean);
|
|
1789
|
+
const directories = isDirectory ? segments : segments.slice(0, -1);
|
|
1790
|
+
const crumbs = [{
|
|
1791
|
+
name: rootName,
|
|
1792
|
+
href: up(directories.length)
|
|
1793
|
+
}];
|
|
1794
|
+
directories.forEach((name, index) => {
|
|
1795
|
+
crumbs.push({
|
|
1796
|
+
name,
|
|
1797
|
+
href: up(directories.length - 1 - index)
|
|
1798
|
+
});
|
|
1799
|
+
});
|
|
1800
|
+
if (!isDirectory && segments.length > 0) crumbs.push({ name: segments.at(-1) ?? "" });
|
|
1801
|
+
const last = crumbs.at(-1);
|
|
1802
|
+
if (last) delete last.href;
|
|
1803
|
+
return crumbs;
|
|
1804
|
+
}
|
|
1805
|
+
const serverLinks = {
|
|
1806
|
+
directory: (name) => `${encodeURIComponent(name)}/`,
|
|
1807
|
+
markdown: (name) => encodeURIComponent(name)
|
|
1808
|
+
};
|
|
1809
|
+
function compareEntries(a, b) {
|
|
1810
|
+
if (a.kind === "dir" !== (b.kind === "dir")) return a.kind === "dir" ? -1 : 1;
|
|
1811
|
+
return a.name.localeCompare(b.name, "en", {
|
|
1812
|
+
numeric: true,
|
|
1813
|
+
sensitivity: "base"
|
|
1814
|
+
});
|
|
1815
|
+
}
|
|
1816
|
+
/** Reads a directory for the listing: directories first, then files, in natural order. */
|
|
1817
|
+
async function readDirectory(path, options) {
|
|
1818
|
+
const visible = (await readdir(path, { withFileTypes: true })).filter((dirent) => !SKIPPED_DIRECTORIES.has(dirent.name) && (options.dotfiles || !isHiddenSegment(dirent.name)));
|
|
1819
|
+
const list = (await Promise.all(visible.map(async (dirent) => {
|
|
1820
|
+
const info = await stat$1(join(path, dirent.name)).catch(() => void 0);
|
|
1821
|
+
if (!info) return void 0;
|
|
1822
|
+
const directory = info.isDirectory();
|
|
1823
|
+
const kind = entryKind(dirent.name, directory);
|
|
1824
|
+
const href = directory ? options.links.directory(dirent.name) : kind === "markdown" ? options.links.markdown(dirent.name) : encodeURIComponent(dirent.name);
|
|
1825
|
+
return {
|
|
1826
|
+
name: dirent.name,
|
|
1827
|
+
href,
|
|
1828
|
+
kind,
|
|
1829
|
+
size: info.size,
|
|
1830
|
+
modified: info.mtime
|
|
1831
|
+
};
|
|
1832
|
+
}))).filter((entry) => entry !== void 0).sort(compareEntries);
|
|
1833
|
+
return {
|
|
1834
|
+
entries: list,
|
|
1835
|
+
readme: list.find((entry) => entry.kind === "markdown" && /^readme\.[a-z]+$/i.test(entry.name))?.name
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
/** Maps the items with at most `limit` calls at the same time. The results keep the order of the items. */
|
|
1839
|
+
async function mapLimit(items, limit, map) {
|
|
1840
|
+
const results = Array.from({ length: items.length });
|
|
1841
|
+
let next = 0;
|
|
1842
|
+
const worker = async () => {
|
|
1843
|
+
while (next < items.length) {
|
|
1844
|
+
const index = next++;
|
|
1845
|
+
results[index] = await map(items[index]);
|
|
1846
|
+
}
|
|
1847
|
+
};
|
|
1848
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
1849
|
+
return results;
|
|
1850
|
+
}
|
|
1851
|
+
const README_EXTENSIONS = [
|
|
1852
|
+
"md",
|
|
1853
|
+
"markdown",
|
|
1854
|
+
"mdc",
|
|
1855
|
+
"mdown",
|
|
1856
|
+
"mkd"
|
|
1857
|
+
];
|
|
1858
|
+
/**
|
|
1859
|
+
* Returns the closest README: in `start`, or else in the first parent directory
|
|
1860
|
+
* that has one. The name can have any case. `README.md` comes before the other
|
|
1861
|
+
* markdown extensions.
|
|
1862
|
+
*/
|
|
1863
|
+
async function findReadme(start) {
|
|
1864
|
+
for (let directory = start;; directory = dirname(directory)) {
|
|
1865
|
+
const names = await readdir(directory).catch(() => []);
|
|
1866
|
+
for (const extension of README_EXTENSIONS) {
|
|
1867
|
+
const name = names.find((entry) => entry.toLowerCase() === `readme.${extension}`);
|
|
1868
|
+
if (name) return join(directory, name);
|
|
1869
|
+
}
|
|
1870
|
+
if (dirname(directory) === directory) return void 0;
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
//#endregion
|
|
1874
|
+
//#region src/search.ts
|
|
1875
|
+
const MAX_FILES = 1e4;
|
|
1876
|
+
const READ_CONCURRENCY = 32;
|
|
1877
|
+
function slugify(text) {
|
|
1878
|
+
let slug = text.toLowerCase().trim().replace(/\s+/g, "-").replace(/[^\w-]+/g, "").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
|
|
1879
|
+
if (/^\d/.test(slug)) slug = `_${slug}`;
|
|
1880
|
+
return slug;
|
|
1881
|
+
}
|
|
1882
|
+
/** Removes the inline markdown from heading text. */
|
|
1883
|
+
function plainText(markdown) {
|
|
1884
|
+
return markdown.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\[([^\]]*)\]\[[^\]]*\]/g, "$1").replace(/<[^>]+>/g, "").replace(/`([^`]*)`/g, "$1").replace(/(\*\*|__)(.+?)\1/g, "$2").replace(/(^|[^\w*])[*_]([^*_]+)[*_](?=$|[^\w*])/g, "$1$2").replace(/~~(.+?)~~/g, "$1").replace(/\\([\\`*_{}[\]()#+\-.!|])/g, "$1").replace(/\s+/g, " ").trim();
|
|
1885
|
+
}
|
|
1886
|
+
const ATX = /^ {0,3}(#{1,6})(?:[ \t]+(.*?))?(?:[ \t]+#+)?[ \t]*$/;
|
|
1887
|
+
const FENCE = /^ {0,3}(`{3,}|~{3,})/;
|
|
1888
|
+
const ATTRIBUTES = /\s*\{([^{}]*)\}\s*$/;
|
|
1889
|
+
const FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
|
|
1890
|
+
function customId(attributes) {
|
|
1891
|
+
if (!attributes) return void 0;
|
|
1892
|
+
return /(?:^|\s)#([\w-]+)/.exec(attributes)?.[1] ?? /\bid=["']?([\w-]+)/.exec(attributes)?.[1];
|
|
1893
|
+
}
|
|
1894
|
+
function frontmatterTitle(yaml) {
|
|
1895
|
+
const match = /^title:[ \t]*(.+?)[ \t]*$/m.exec(yaml);
|
|
1896
|
+
if (!match?.[1]) return void 0;
|
|
1897
|
+
return match[1].replace(/^(["'])(.*)\1$/, "$2").trim() || void 0;
|
|
1898
|
+
}
|
|
1899
|
+
/** Reads the title and the headings of a markdown source, with no full parse. */
|
|
1900
|
+
function extractOutline(source) {
|
|
1901
|
+
const frontmatter = FRONTMATTER.exec(source);
|
|
1902
|
+
const body = frontmatter ? source.slice(frontmatter[0].length) : source;
|
|
1903
|
+
const headings = [];
|
|
1904
|
+
const stack = [];
|
|
1905
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1906
|
+
let fence;
|
|
1907
|
+
let previous = "";
|
|
1908
|
+
const add = (level, raw, line) => {
|
|
1909
|
+
const attributes = ATTRIBUTES.exec(raw);
|
|
1910
|
+
const text = plainText(attributes ? raw.slice(0, attributes.index) : raw);
|
|
1911
|
+
if (!text) return;
|
|
1912
|
+
let slug = slugify(text);
|
|
1913
|
+
while (stack.length > 0 && (stack.at(-1)?.level ?? 0) >= level) stack.pop();
|
|
1914
|
+
const parent = stack.at(-1);
|
|
1915
|
+
if (parent && parent.level >= 2) slug = `${parent.id}-${slug}`;
|
|
1916
|
+
stack.push({
|
|
1917
|
+
level,
|
|
1918
|
+
id: slug
|
|
1919
|
+
});
|
|
1920
|
+
const count = counts.get(slug) ?? 0;
|
|
1921
|
+
counts.set(slug, count + 1);
|
|
1922
|
+
const id = customId(attributes?.[1]) ?? (count === 0 ? slug : `${slug}-${count}`);
|
|
1923
|
+
headings.push({
|
|
1924
|
+
id,
|
|
1925
|
+
text,
|
|
1926
|
+
depth: level,
|
|
1927
|
+
line
|
|
1928
|
+
});
|
|
1929
|
+
};
|
|
1930
|
+
const offset = frontmatter ? frontmatter[0].split(/\r?\n/).length - (frontmatter[0].endsWith("\n") ? 1 : 0) : 0;
|
|
1931
|
+
const lines = body.split(/\r?\n/);
|
|
1932
|
+
for (const [index, line] of lines.entries()) {
|
|
1933
|
+
const number = offset + index + 1;
|
|
1934
|
+
const fenceMatch = FENCE.exec(line);
|
|
1935
|
+
if (fence) {
|
|
1936
|
+
if (fenceMatch?.[1] && fenceMatch[1][0] === fence[0] && fenceMatch[1].length >= fence.length) fence = void 0;
|
|
1937
|
+
previous = "";
|
|
1938
|
+
continue;
|
|
1939
|
+
}
|
|
1940
|
+
if (fenceMatch?.[1]) {
|
|
1941
|
+
fence = fenceMatch[1];
|
|
1942
|
+
previous = "";
|
|
1943
|
+
continue;
|
|
1944
|
+
}
|
|
1945
|
+
const atx = ATX.exec(line);
|
|
1946
|
+
if (atx?.[1]) {
|
|
1947
|
+
add(atx[1].length, atx[2] ?? "", number);
|
|
1948
|
+
previous = "";
|
|
1949
|
+
continue;
|
|
1950
|
+
}
|
|
1951
|
+
if (previous && /^ {0,3}(=+|-+)[ \t]*$/.test(line) && !/^ {0,3}[-*+>]|^ {4}/.test(previous)) {
|
|
1952
|
+
add(line.trim().startsWith("=") ? 1 : 2, previous.trim(), number - 1);
|
|
1953
|
+
previous = "";
|
|
1954
|
+
continue;
|
|
1955
|
+
}
|
|
1956
|
+
previous = line.trim() ? line : "";
|
|
1957
|
+
}
|
|
1958
|
+
return {
|
|
1959
|
+
title: (frontmatter?.[1] ? frontmatterTitle(frontmatter[1]) : void 0) ?? headings.find((heading) => heading.depth === 1)?.text,
|
|
1960
|
+
headings
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
async function findMarkdown(root, dotfiles, ignore) {
|
|
1964
|
+
const { glob } = await import("./dist-CWYRQLUT.mjs");
|
|
1965
|
+
return glob(MARKDOWN_GLOB, {
|
|
1966
|
+
cwd: root,
|
|
1967
|
+
dot: dotfiles,
|
|
1968
|
+
ignore: [...SKIPPED_GLOBS, ...ignore],
|
|
1969
|
+
caseSensitiveMatch: false,
|
|
1970
|
+
expandDirectories: false
|
|
1971
|
+
}).then((paths) => paths.sort().slice(0, MAX_FILES));
|
|
1972
|
+
}
|
|
1973
|
+
function createSearchIndex(options) {
|
|
1974
|
+
const toUrl = options.toUrl ?? ((path) => path);
|
|
1975
|
+
let entries;
|
|
1976
|
+
let building;
|
|
1977
|
+
let changed = /* @__PURE__ */ new Set();
|
|
1978
|
+
let stale = false;
|
|
1979
|
+
const read = async (path) => {
|
|
1980
|
+
const source = await readFile(join$1(options.root, path), "utf8").catch(() => void 0);
|
|
1981
|
+
if (source === void 0) return void 0;
|
|
1982
|
+
const outline = extractOutline(source);
|
|
1983
|
+
const title = outline.title ?? basename$1(path, extname$1(path));
|
|
1984
|
+
return {
|
|
1985
|
+
url: toUrl(`/${path}`),
|
|
1986
|
+
title,
|
|
1987
|
+
headings: outline.headings
|
|
1988
|
+
};
|
|
1989
|
+
};
|
|
1990
|
+
const build = async (previous) => {
|
|
1991
|
+
const dirty = changed;
|
|
1992
|
+
changed = /* @__PURE__ */ new Set();
|
|
1993
|
+
const paths = await findMarkdown(options.root, options.dotfiles, options.ignore ?? []);
|
|
1994
|
+
const list = await mapLimit(paths, READ_CONCURRENCY, (path) => {
|
|
1995
|
+
const known = previous?.get(path);
|
|
1996
|
+
return known && !dirty.has(path) ? Promise.resolve(known) : read(path);
|
|
1997
|
+
});
|
|
1998
|
+
const map = /* @__PURE__ */ new Map();
|
|
1999
|
+
paths.forEach((path, index) => {
|
|
2000
|
+
const entry = list[index];
|
|
2001
|
+
if (entry) map.set(path, entry);
|
|
2002
|
+
});
|
|
2003
|
+
return map;
|
|
2004
|
+
};
|
|
2005
|
+
return {
|
|
2006
|
+
get: async () => {
|
|
2007
|
+
while (!entries || stale) {
|
|
2008
|
+
stale = false;
|
|
2009
|
+
building ??= build(entries).finally(() => building = void 0);
|
|
2010
|
+
entries = await building;
|
|
2011
|
+
}
|
|
2012
|
+
return [...entries.values()].sort((a, b) => a.url.localeCompare(b.url));
|
|
2013
|
+
},
|
|
2014
|
+
invalidate: (paths) => {
|
|
2015
|
+
for (const path of paths) changed.add(path.replace(/^\//, ""));
|
|
2016
|
+
if (paths.length > 0) stale = true;
|
|
2017
|
+
}
|
|
2018
|
+
};
|
|
2019
|
+
}
|
|
2020
|
+
//#endregion
|
|
2021
|
+
//#region src/themes.ts
|
|
2022
|
+
/** The remote theme sources. Each theme is one file in a GitHub repository. */
|
|
2023
|
+
const THEME_SOURCES = {
|
|
2024
|
+
base16: {
|
|
2025
|
+
repo: "tinted-theming/schemes",
|
|
2026
|
+
dir: "base16/",
|
|
2027
|
+
suffix: ".yaml"
|
|
2028
|
+
},
|
|
2029
|
+
base24: {
|
|
2030
|
+
repo: "tinted-theming/schemes",
|
|
2031
|
+
dir: "base24/",
|
|
2032
|
+
suffix: ".yaml"
|
|
2033
|
+
},
|
|
2034
|
+
omarchy: {
|
|
2035
|
+
repo: "basecamp/omarchy",
|
|
2036
|
+
dir: "themes/",
|
|
2037
|
+
suffix: "/colors.toml"
|
|
2038
|
+
}
|
|
2039
|
+
};
|
|
2040
|
+
/** The `colors.toml` of the current Omarchy theme. `omarchy-theme-set` replaces it on each change. */
|
|
2041
|
+
const OMARCHY_CURRENT = join$1(homedir(), ".local/state/omarchy/current/theme/colors.toml");
|
|
2042
|
+
const UNGH = "https://ungh.cc/repos/";
|
|
2043
|
+
const RAW = "https://raw.githubusercontent.com/";
|
|
2044
|
+
const DAY = 864e5;
|
|
2045
|
+
const LISTING_TTL = DAY;
|
|
2046
|
+
const THEME_TTL = 7 * DAY;
|
|
2047
|
+
const TIMEOUT = 8e3;
|
|
2048
|
+
const THEME_ID = /^[a-z0-9][a-z0-9._-]*$/i;
|
|
2049
|
+
const REMOTE_SPEC = /^(base16|base24|omarchy):(.+)$/;
|
|
2050
|
+
function defaultCacheDir() {
|
|
2051
|
+
return join$1(process.env.XDG_CACHE_HOME || join$1(homedir(), ".cache"), "comarkserv", "themes");
|
|
2052
|
+
}
|
|
2053
|
+
function isThemeSource(value) {
|
|
2054
|
+
return Object.hasOwn(THEME_SOURCES, value);
|
|
2055
|
+
}
|
|
2056
|
+
/** Returns true for a theme id that is a plain file name, so it cannot change the path. */
|
|
2057
|
+
function isThemeId(id) {
|
|
2058
|
+
return THEME_ID.test(id) && !id.includes("..");
|
|
2059
|
+
}
|
|
2060
|
+
function titleCase(id) {
|
|
2061
|
+
return id.split(/[-_]/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
2062
|
+
}
|
|
2063
|
+
/** Returns the file to watch for a `--theme` value that can change while the server runs. */
|
|
2064
|
+
function themeWatchPath(spec, cwd, omarchyPath = OMARCHY_CURRENT) {
|
|
2065
|
+
if (spec === "omarchy") return omarchyPath;
|
|
2066
|
+
if (spec === "github" || REMOTE_SPEC.test(spec) || /^https?:\/\//.test(spec)) return void 0;
|
|
2067
|
+
return resolve$1(cwd, spec);
|
|
2068
|
+
}
|
|
2069
|
+
function createThemeStore(options = {}) {
|
|
2070
|
+
const cacheDir = options.cacheDir ?? defaultCacheDir();
|
|
2071
|
+
const request = options.fetch ?? ((input) => fetch(input, { signal: AbortSignal.timeout(TIMEOUT) }));
|
|
2072
|
+
const now = options.now ?? Date.now;
|
|
2073
|
+
const omarchyPath = options.omarchyPath ?? OMARCHY_CURRENT;
|
|
2074
|
+
const pending = /* @__PURE__ */ new Map();
|
|
2075
|
+
const cachePath = (url) => join$1(cacheDir, `${createHash("sha1").update(url).digest("hex").slice(0, 20)}.json`);
|
|
2076
|
+
const readCache = async (url) => {
|
|
2077
|
+
try {
|
|
2078
|
+
return JSON.parse(await readFile(cachePath(url), "utf8"));
|
|
2079
|
+
} catch {
|
|
2080
|
+
return;
|
|
2081
|
+
}
|
|
2082
|
+
};
|
|
2083
|
+
const cached = (url, ttl) => {
|
|
2084
|
+
let promise = pending.get(url);
|
|
2085
|
+
if (!promise) {
|
|
2086
|
+
promise = (async () => {
|
|
2087
|
+
const record = await readCache(url);
|
|
2088
|
+
if (record && now() - record.time < ttl) return record.text;
|
|
2089
|
+
try {
|
|
2090
|
+
const response = await request(url);
|
|
2091
|
+
if (response.status === 404) return void 0;
|
|
2092
|
+
if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
|
|
2093
|
+
const text = await response.text();
|
|
2094
|
+
await mkdir(cacheDir, { recursive: true });
|
|
2095
|
+
await writeFile(cachePath(url), JSON.stringify({
|
|
2096
|
+
time: now(),
|
|
2097
|
+
text
|
|
2098
|
+
}));
|
|
2099
|
+
return text;
|
|
2100
|
+
} catch (error) {
|
|
2101
|
+
if (record) return record.text;
|
|
2102
|
+
throw error;
|
|
2103
|
+
}
|
|
2104
|
+
})();
|
|
2105
|
+
pending.set(url, promise);
|
|
2106
|
+
const done = () => pending.delete(url);
|
|
2107
|
+
promise.then(done, done);
|
|
2108
|
+
}
|
|
2109
|
+
return promise;
|
|
2110
|
+
};
|
|
2111
|
+
const json = async (url, ttl) => {
|
|
2112
|
+
const text = await cached(url, ttl);
|
|
2113
|
+
if (text === void 0) throw new Error(`${url} does not exist`);
|
|
2114
|
+
return JSON.parse(text);
|
|
2115
|
+
};
|
|
2116
|
+
const branch = async (repo) => (await json(`${UNGH}${repo}`, LISTING_TTL)).repo.defaultBranch;
|
|
2117
|
+
const listing = async (repo) => {
|
|
2118
|
+
const ref = await branch(repo);
|
|
2119
|
+
const { files } = await json(`${UNGH}${repo}/files/${ref}`, LISTING_TTL);
|
|
2120
|
+
return {
|
|
2121
|
+
ref,
|
|
2122
|
+
paths: files.map((file) => file.path)
|
|
2123
|
+
};
|
|
2124
|
+
};
|
|
2125
|
+
const rawUrl = async (source, id) => {
|
|
2126
|
+
const { repo, dir, suffix } = THEME_SOURCES[source];
|
|
2127
|
+
return `${RAW}${repo}/${await branch(repo)}/${dir}${id}${suffix}`;
|
|
2128
|
+
};
|
|
2129
|
+
const raw = async (source, id) => {
|
|
2130
|
+
if (!isThemeId(id)) return void 0;
|
|
2131
|
+
return cached(await rawUrl(source, id), THEME_TTL);
|
|
2132
|
+
};
|
|
2133
|
+
const catalog = async (urlFor) => {
|
|
2134
|
+
const repos = [...new Set(Object.values(THEME_SOURCES).map((source) => source.repo))];
|
|
2135
|
+
const listings = new Map(await Promise.all(repos.map(async (repo) => [repo, await listing(repo)])));
|
|
2136
|
+
const entries = [];
|
|
2137
|
+
for (const [source, { repo, dir, suffix }] of Object.entries(THEME_SOURCES)) {
|
|
2138
|
+
const found = listings.get(repo);
|
|
2139
|
+
if (!found || !isThemeSource(source)) continue;
|
|
2140
|
+
const sorted = found.paths.filter((path) => path.startsWith(dir) && path.endsWith(suffix)).map((path) => path.slice(dir.length, path.length - suffix.length)).filter(isThemeId).map((id) => ({
|
|
2141
|
+
id,
|
|
2142
|
+
name: titleCase(id)
|
|
2143
|
+
})).sort((a, b) => a.name.localeCompare(b.name, "en"));
|
|
2144
|
+
for (const { id, name } of sorted) {
|
|
2145
|
+
const url = urlFor ? urlFor(source, id) : `${RAW}${repo}/${found.ref}/${dir}${id}${suffix}`;
|
|
2146
|
+
entries.push({
|
|
2147
|
+
id: `${source}:${id}`,
|
|
2148
|
+
name,
|
|
2149
|
+
source,
|
|
2150
|
+
url
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
return entries;
|
|
2155
|
+
};
|
|
2156
|
+
const parse = (text, id, name, origin) => {
|
|
2157
|
+
const theme = parseTheme(text, id, name);
|
|
2158
|
+
if (!theme) throw new Error(`${origin} is not a base16, base24 or Omarchy theme.`);
|
|
2159
|
+
return theme;
|
|
2160
|
+
};
|
|
2161
|
+
const load = async (spec, cwd = process.cwd()) => {
|
|
2162
|
+
if (!spec || spec === "github") return void 0;
|
|
2163
|
+
const remote = REMOTE_SPEC.exec(spec);
|
|
2164
|
+
if (remote?.[1] && remote[2] && isThemeSource(remote[1])) {
|
|
2165
|
+
const text = await raw(remote[1], remote[2]);
|
|
2166
|
+
if (text === void 0) throw new Error(`There is no theme "${spec}". Run \`comarkserv themes\` for the list.`);
|
|
2167
|
+
return parse(text, spec, titleCase(remote[2]), spec);
|
|
2168
|
+
}
|
|
2169
|
+
if (spec === "omarchy") {
|
|
2170
|
+
const text = await readFile(omarchyPath, "utf8").catch(() => {
|
|
2171
|
+
throw new Error(`There is no current Omarchy theme at ${omarchyPath}.`);
|
|
2172
|
+
});
|
|
2173
|
+
return parse(text, "omarchy-live", "Omarchy", omarchyPath);
|
|
2174
|
+
}
|
|
2175
|
+
if (/^https?:\/\//.test(spec)) {
|
|
2176
|
+
const text = await cached(spec, LISTING_TTL);
|
|
2177
|
+
if (text === void 0) throw new Error(`There is no theme at ${spec}.`);
|
|
2178
|
+
return parse(text, spec, titleCase(basename$1(spec, extname$1(spec))), spec);
|
|
2179
|
+
}
|
|
2180
|
+
const path = resolve$1(cwd, spec);
|
|
2181
|
+
const text = await readFile(path, "utf8").catch(() => {
|
|
2182
|
+
throw new Error(`There is no theme file at ${path}.`);
|
|
2183
|
+
});
|
|
2184
|
+
const name = basename$1(path) === "colors.toml" ? basename$1(dirname$1(path)) : basename$1(path, extname$1(path));
|
|
2185
|
+
return parse(text, `file:${basename$1(path)}`, titleCase(name), path);
|
|
2186
|
+
};
|
|
2187
|
+
return {
|
|
2188
|
+
catalog,
|
|
2189
|
+
raw,
|
|
2190
|
+
load
|
|
2191
|
+
};
|
|
2192
|
+
}
|
|
2193
|
+
//#endregion
|
|
2194
|
+
export { parseTheme as C, join$1 as D, extname$1 as E, relative as O, getAssets as S, dirname$1 as T, injectClient as _, themeWatchPath as a, renderReadme as b, SKIPPED_GLOBS as c, hasHiddenSegment as d, isMarkdown as f, serverLinks as g, readDirectory as h, isThemeSource as i, resolve$1 as k, crumbsFor as l, mapLimit as m, THEME_SOURCES as n, createSearchIndex as o, isSkippedPath as p, createThemeStore as r, extractOutline as s, OMARCHY_CURRENT as t, findReadme as u, renderListing as v, basename$1 as w, renderStatus as x, renderPage as y };
|