@hraness/wordcell 0.21.2 → 0.22.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/README.md +196 -66
- package/dist/{authority-4sp9d552.js → authority-1pgftmwq.js} +55 -10
- package/dist/benchmark.js +3 -3
- package/dist/capture.js +6 -5
- package/dist/cli.js +61 -3800
- package/dist/clip/acquire.js +3 -2
- package/dist/clip/cli.js +6 -5
- package/dist/clip/doctor.js +4 -3
- package/dist/clip/jobs.js +3 -3
- package/dist/clip/refresh.js +1 -1
- package/dist/evaluation-builder.js +18 -17
- package/dist/evaluation-kb.js +14 -13
- package/dist/graph-authority.js +3 -3
- package/dist/graph-percolation.js +5 -5
- package/dist/index-0dqfnrz9.js +1985 -0
- package/dist/{index-0k2x4nn9.js → index-1tm7bgx7.js} +16 -4
- package/dist/{index-vdw5yjfa.js → index-2hf27mws.js} +108 -18
- package/dist/index-3agn8scn.js +306 -0
- package/dist/index-4ag64rc1.js +4299 -0
- package/dist/index-4knsp9qj.js +34 -0
- package/dist/index-66pshdtx.js +341 -0
- package/dist/index-9rf81m0p.js +368 -0
- package/dist/{index-59ssmqqm.js → index-a6kbv718.js} +1 -1
- package/dist/{index-51bap0mq.js → index-avbce90h.js} +9 -9
- package/dist/{index-180zbkkt.js → index-de2w8crk.js} +14 -13
- package/dist/{index-pbr6rv47.js → index-e6p9e926.js} +6 -6
- package/dist/{index-bcfn9xah.js → index-famy7fhs.js} +5 -5
- package/dist/{index-by0pbant.js → index-fc4dr114.js} +5 -2
- package/dist/{index-x6m57nfc.js → index-g5vsqmdy.js} +28 -57
- package/dist/{index-115b07ap.js → index-hcw140eb.js} +5 -5
- package/dist/index-j70m75wd.js +217 -0
- package/dist/{index-3ehtt59c.js → index-qwgsmtsz.js} +1 -1
- package/dist/{index-bgfzwt4h.js → index-t2bs9xdr.js} +4 -4
- package/dist/index.js +285 -93
- package/dist/portfolio.js +14 -13
- package/dist/publish-model.js +36 -0
- package/dist/publish-reader/reader.css +1042 -0
- package/dist/publish-reader/reader.js +1534 -0
- package/dist/publish-reader/theme.js +92 -0
- package/dist/publish-search.js +52 -0
- package/dist/publish.js +36 -0
- package/dist/rerank-typesafe.js +13 -0
- package/dist/rerank.js +14 -0
- package/dist/sdk.js +14 -11
- package/dist/search.js +2 -2
- package/dist/semantic.js +4 -4
- package/dist/support-mywv1em0.js +140 -0
- package/dist/workflows/decision-context.js +13 -12
- package/dist/workflows/index.js +13 -12
- package/package.json +68 -7
- package/skills/wordcell/AGENTS.md +1 -0
- package/skills/wordcell/SKILL.md +21 -1
- package/skills/wordcell/agents/openai.yaml +1 -1
- package/skills/wordcell/references/publish.md +93 -0
- package/skills/wordcell/references/query.md +22 -0
- package/src/cli-program.ts +353 -9
- package/src/cli.ts +12 -1
- package/src/clip/acquire.ts +2 -0
- package/src/clip/doctor.ts +2 -0
- package/src/clip/media.ts +2 -0
- package/src/index.ts +4 -0
- package/src/oh/authority.ts +14 -5
- package/src/oh/canonical-rust.ts +127 -0
- package/src/oh/projection-rust.ts +63 -0
- package/src/oh-adoption.ts +7 -1
- package/src/publish-graph.ts +0 -0
- package/src/publish-index.ts +195 -0
- package/src/publish-markdown.ts +617 -0
- package/src/publish-model.ts +599 -0
- package/src/publish-nav.ts +204 -0
- package/src/publish-pages.ts +382 -0
- package/src/publish-reader/reader.css +1042 -0
- package/src/publish-reader/reader.ts +1017 -0
- package/src/publish-reader/theme.ts +97 -0
- package/src/publish-search.ts +418 -0
- package/src/publish-select.ts +419 -0
- package/src/publish-theme.ts +106 -0
- package/src/publish.ts +904 -0
- package/src/rerank-credentials.ts +56 -0
- package/src/rerank-typesafe.ts +472 -0
- package/src/rerank.ts +389 -0
- package/src/sdk.ts +157 -6
- package/src/serve.ts +180 -0
- package/src/support-profile.ts +7 -0
- package/src/support.ts +73 -0
- package/src/vault.ts +23 -9
- package/dist/{index-why54q5p.js → index-gm9t95d9.js} +3 -3
|
@@ -0,0 +1,1534 @@
|
|
|
1
|
+
// src/publish-model.ts
|
|
2
|
+
var WORDCELL_SITE_FORMAT_V1 = "hraness.wordcell.site.v1";
|
|
3
|
+
var WORDCELL_SITE_CATALOG_FORMAT_V1 = "hraness.wordcell.site-catalog.v1";
|
|
4
|
+
var WORDCELL_SITE_DOCS_FORMAT_V1 = "hraness.wordcell.site-docs.v1";
|
|
5
|
+
var WORDCELL_SITE_TERMS_FORMAT_V1 = "hraness.wordcell.site-terms.v1";
|
|
6
|
+
var WORDCELL_SITE_POSTINGS_FORMAT_V1 = "hraness.wordcell.site-postings.v1";
|
|
7
|
+
var WORDCELL_SITE_NOTE_FORMAT_V1 = "hraness.wordcell.site-note.v1";
|
|
8
|
+
var WORDCELL_SITE_GRAPH_FORMAT_V1 = "hraness.wordcell.site-graph.v1";
|
|
9
|
+
var WORDCELL_SITE_LIMITS_V1 = Object.freeze({
|
|
10
|
+
notes: 1e4,
|
|
11
|
+
assets: 1e4,
|
|
12
|
+
assetBytes: 16 * 1024 * 1024,
|
|
13
|
+
totalAssetBytes: 512 * 1024 * 1024,
|
|
14
|
+
indexTerms: 250000,
|
|
15
|
+
contentShards: 256,
|
|
16
|
+
shardPairs: 250000,
|
|
17
|
+
docPreviewBytes: 240,
|
|
18
|
+
fieldTextBytes: 4 * 1024,
|
|
19
|
+
inlineTextBytes: 16 * 1024,
|
|
20
|
+
inlineTotalBytes: 2 * 1024 * 1024,
|
|
21
|
+
noteTextBytes: 32 * 1024,
|
|
22
|
+
titleBytes: 512,
|
|
23
|
+
descriptionBytes: 4 * 1024,
|
|
24
|
+
searchHydration: 50,
|
|
25
|
+
searchResults: 100
|
|
26
|
+
});
|
|
27
|
+
var textEncoder = new TextEncoder;
|
|
28
|
+
function isRecord(value) {
|
|
29
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
30
|
+
}
|
|
31
|
+
function boundedText(value, maximumBytes, field) {
|
|
32
|
+
if (typeof value !== "string" || textEncoder.encode(value).byteLength > maximumBytes) {
|
|
33
|
+
throw new TypeError(`${field} must be a string of at most ${maximumBytes} bytes`);
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
function boundedCount(value, maximum, field) {
|
|
38
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0 || value > maximum) {
|
|
39
|
+
throw new TypeError(`${field} must be a safe integer from 0 through ${maximum}`);
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
function exactKeys(value, required, optional, field) {
|
|
44
|
+
const allowed = new Set([...required, ...optional]);
|
|
45
|
+
for (const key of Object.keys(value)) {
|
|
46
|
+
if (!allowed.has(key))
|
|
47
|
+
throw new TypeError(`${field} has unexpected key ${JSON.stringify(key)}`);
|
|
48
|
+
}
|
|
49
|
+
for (const key of required) {
|
|
50
|
+
if (!(key in value))
|
|
51
|
+
throw new TypeError(`${field} is missing ${JSON.stringify(key)}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function stringArray(value, maximum, field) {
|
|
55
|
+
if (!Array.isArray(value) || value.length > maximum) {
|
|
56
|
+
throw new TypeError(`${field} must be an array of at most ${maximum} strings`);
|
|
57
|
+
}
|
|
58
|
+
for (const item of value) {
|
|
59
|
+
if (typeof item !== "string")
|
|
60
|
+
throw new TypeError(`${field} entries must be strings`);
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
function contentSearch(value) {
|
|
65
|
+
if (value === "inline" || value === "shards" || value === "none")
|
|
66
|
+
return value;
|
|
67
|
+
throw new TypeError("search.content must be inline, shards, or none");
|
|
68
|
+
}
|
|
69
|
+
function parseSiteManifestV1(value) {
|
|
70
|
+
if (!isRecord(value))
|
|
71
|
+
throw new TypeError("manifest must be an object");
|
|
72
|
+
exactKeys(value, ["format", "site", "generated", "source", "paths", "search", "counts", "truncated"], [], "manifest");
|
|
73
|
+
if (value.format !== WORDCELL_SITE_FORMAT_V1) {
|
|
74
|
+
throw new TypeError(`manifest format must be ${WORDCELL_SITE_FORMAT_V1}`);
|
|
75
|
+
}
|
|
76
|
+
const { site, generated, source, paths, search, counts, truncated } = value;
|
|
77
|
+
if (!isRecord(site))
|
|
78
|
+
throw new TypeError("manifest.site must be an object");
|
|
79
|
+
exactKeys(site, ["title", "basePath"], ["description"], "manifest.site");
|
|
80
|
+
if (!isRecord(generated))
|
|
81
|
+
throw new TypeError("manifest.generated must be an object");
|
|
82
|
+
exactKeys(generated, ["by", "version"], ["at"], "manifest.generated");
|
|
83
|
+
if (generated.by !== "@hraness/wordcell") {
|
|
84
|
+
throw new TypeError("manifest.generated.by must be @hraness/wordcell");
|
|
85
|
+
}
|
|
86
|
+
if (!isRecord(source))
|
|
87
|
+
throw new TypeError("manifest.source must be an object");
|
|
88
|
+
exactKeys(source, ["selection", "notes", "digest"], [], "manifest.source");
|
|
89
|
+
if (!isRecord(paths))
|
|
90
|
+
throw new TypeError("manifest.paths must be an object");
|
|
91
|
+
exactKeys(paths, ["catalog", "graph", "docs", "terms", "postingsPrefix", "notePrefix", "assetPrefix", "readerPrefix"], [], "manifest.paths");
|
|
92
|
+
if (!isRecord(search))
|
|
93
|
+
throw new TypeError("manifest.search must be an object");
|
|
94
|
+
exactKeys(search, ["mode", "content", "shards", "hash"], [], "manifest.search");
|
|
95
|
+
if (search.mode !== "exact")
|
|
96
|
+
throw new TypeError("manifest.search.mode must be exact");
|
|
97
|
+
if (search.hash !== "fnv1a32-8bit") {
|
|
98
|
+
throw new TypeError("manifest.search.hash must be fnv1a32-8bit");
|
|
99
|
+
}
|
|
100
|
+
if (!isRecord(counts))
|
|
101
|
+
throw new TypeError("manifest.counts must be an object");
|
|
102
|
+
exactKeys(counts, ["notes", "assets", "bytes"], [], "manifest.counts");
|
|
103
|
+
if (!isRecord(truncated))
|
|
104
|
+
throw new TypeError("manifest.truncated must be an object");
|
|
105
|
+
exactKeys(truncated, [], ["terms", "text", "assets"], "manifest.truncated");
|
|
106
|
+
for (const flag of Object.values(truncated)) {
|
|
107
|
+
if (flag !== true)
|
|
108
|
+
throw new TypeError("manifest.truncated flags must be true");
|
|
109
|
+
}
|
|
110
|
+
const digest = source.digest;
|
|
111
|
+
if (typeof digest !== "string" || !/^sha256:[0-9a-f]{64}$/u.test(digest)) {
|
|
112
|
+
throw new TypeError("manifest.source.digest must be a sha256 hex digest");
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
format: WORDCELL_SITE_FORMAT_V1,
|
|
116
|
+
site: Object.freeze({
|
|
117
|
+
title: boundedText(site.title, WORDCELL_SITE_LIMITS_V1.titleBytes, "site.title"),
|
|
118
|
+
basePath: boundedText(site.basePath, 1024, "site.basePath"),
|
|
119
|
+
...site.description === undefined ? {} : { description: boundedText(site.description, WORDCELL_SITE_LIMITS_V1.descriptionBytes, "site.description") }
|
|
120
|
+
}),
|
|
121
|
+
generated: Object.freeze({
|
|
122
|
+
by: "@hraness/wordcell",
|
|
123
|
+
version: boundedText(generated.version, 64, "generated.version"),
|
|
124
|
+
...generated.at === undefined ? {} : { at: boundedText(generated.at, 64, "generated.at") }
|
|
125
|
+
}),
|
|
126
|
+
source: Object.freeze({
|
|
127
|
+
selection: source.selection,
|
|
128
|
+
notes: boundedCount(source.notes, WORDCELL_SITE_LIMITS_V1.notes, "source.notes"),
|
|
129
|
+
digest
|
|
130
|
+
}),
|
|
131
|
+
paths: Object.freeze({
|
|
132
|
+
catalog: boundedText(paths.catalog, 256, "paths.catalog"),
|
|
133
|
+
graph: boundedText(paths.graph, 256, "paths.graph"),
|
|
134
|
+
docs: boundedText(paths.docs, 256, "paths.docs"),
|
|
135
|
+
terms: boundedText(paths.terms, 256, "paths.terms"),
|
|
136
|
+
postingsPrefix: boundedText(paths.postingsPrefix, 256, "paths.postingsPrefix"),
|
|
137
|
+
notePrefix: boundedText(paths.notePrefix, 256, "paths.notePrefix"),
|
|
138
|
+
assetPrefix: boundedText(paths.assetPrefix, 256, "paths.assetPrefix"),
|
|
139
|
+
readerPrefix: boundedText(paths.readerPrefix, 256, "paths.readerPrefix")
|
|
140
|
+
}),
|
|
141
|
+
search: Object.freeze({
|
|
142
|
+
mode: "exact",
|
|
143
|
+
content: contentSearch(search.content),
|
|
144
|
+
shards: boundedCount(search.shards, WORDCELL_SITE_LIMITS_V1.contentShards, "search.shards"),
|
|
145
|
+
hash: "fnv1a32-8bit"
|
|
146
|
+
}),
|
|
147
|
+
counts: Object.freeze({
|
|
148
|
+
notes: boundedCount(counts.notes, WORDCELL_SITE_LIMITS_V1.notes, "counts.notes"),
|
|
149
|
+
assets: boundedCount(counts.assets, WORDCELL_SITE_LIMITS_V1.assets, "counts.assets"),
|
|
150
|
+
bytes: boundedCount(counts.bytes, Number.MAX_SAFE_INTEGER, "counts.bytes")
|
|
151
|
+
}),
|
|
152
|
+
truncated: Object.freeze({ ...truncated })
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function parseSiteCatalogV1(value) {
|
|
156
|
+
if (!isRecord(value))
|
|
157
|
+
throw new TypeError("catalog must be an object");
|
|
158
|
+
exactKeys(value, ["format", "entries"], [], "catalog");
|
|
159
|
+
if (value.format !== WORDCELL_SITE_CATALOG_FORMAT_V1) {
|
|
160
|
+
throw new TypeError(`catalog format must be ${WORDCELL_SITE_CATALOG_FORMAT_V1}`);
|
|
161
|
+
}
|
|
162
|
+
if (!Array.isArray(value.entries) || value.entries.length > WORDCELL_SITE_LIMITS_V1.notes) {
|
|
163
|
+
throw new TypeError("catalog.entries must be a bounded array");
|
|
164
|
+
}
|
|
165
|
+
const entries = value.entries.map((entry, index) => {
|
|
166
|
+
if (!isRecord(entry))
|
|
167
|
+
throw new TypeError(`catalog entry ${index} must be an object`);
|
|
168
|
+
exactKeys(entry, ["i", "s", "t"], ["type", "g"], `catalog entry ${index}`);
|
|
169
|
+
return {
|
|
170
|
+
i: boundedCount(entry.i, WORDCELL_SITE_LIMITS_V1.notes, `catalog entry ${index}.i`),
|
|
171
|
+
s: boundedText(entry.s, 1024, `catalog entry ${index}.s`),
|
|
172
|
+
t: boundedText(entry.t, WORDCELL_SITE_LIMITS_V1.titleBytes, `catalog entry ${index}.t`),
|
|
173
|
+
...entry.type === undefined ? {} : { type: boundedText(entry.type, 256, `catalog entry ${index}.type`) },
|
|
174
|
+
...entry.g === undefined ? {} : { g: stringArray(entry.g, 128, `catalog entry ${index}.g`) }
|
|
175
|
+
};
|
|
176
|
+
});
|
|
177
|
+
return { format: WORDCELL_SITE_CATALOG_FORMAT_V1, entries };
|
|
178
|
+
}
|
|
179
|
+
function docFields(value, field) {
|
|
180
|
+
if (!isRecord(value))
|
|
181
|
+
throw new TypeError(`${field} must be an object`);
|
|
182
|
+
exactKeys(value, ["t", "a", "p", "g", "m"], [], field);
|
|
183
|
+
return {
|
|
184
|
+
t: boundedText(value.t, WORDCELL_SITE_LIMITS_V1.fieldTextBytes, `${field}.t`),
|
|
185
|
+
a: boundedText(value.a, WORDCELL_SITE_LIMITS_V1.fieldTextBytes, `${field}.a`),
|
|
186
|
+
p: boundedText(value.p, WORDCELL_SITE_LIMITS_V1.fieldTextBytes, `${field}.p`),
|
|
187
|
+
g: boundedText(value.g, WORDCELL_SITE_LIMITS_V1.fieldTextBytes, `${field}.g`),
|
|
188
|
+
m: boundedText(value.m, WORDCELL_SITE_LIMITS_V1.fieldTextBytes, `${field}.m`)
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function parseSiteDocsV1(value) {
|
|
192
|
+
if (!isRecord(value))
|
|
193
|
+
throw new TypeError("docs must be an object");
|
|
194
|
+
exactKeys(value, ["format", "content", "docs"], [], "docs");
|
|
195
|
+
if (value.format !== WORDCELL_SITE_DOCS_FORMAT_V1) {
|
|
196
|
+
throw new TypeError(`docs format must be ${WORDCELL_SITE_DOCS_FORMAT_V1}`);
|
|
197
|
+
}
|
|
198
|
+
const content = contentSearch(value.content);
|
|
199
|
+
if (!Array.isArray(value.docs) || value.docs.length > WORDCELL_SITE_LIMITS_V1.notes) {
|
|
200
|
+
throw new TypeError("docs.docs must be a bounded array");
|
|
201
|
+
}
|
|
202
|
+
const docs = value.docs.map((entry, index) => {
|
|
203
|
+
const field = `docs entry ${index}`;
|
|
204
|
+
if (!isRecord(entry))
|
|
205
|
+
throw new TypeError(`${field} must be an object`);
|
|
206
|
+
exactKeys(entry, ["i", "s", "t", "p", "f"], ["x"], field);
|
|
207
|
+
const inline = entry.x === undefined ? {} : { x: boundedText(entry.x, WORDCELL_SITE_LIMITS_V1.inlineTextBytes, `${field}.x`) };
|
|
208
|
+
return {
|
|
209
|
+
i: boundedCount(entry.i, WORDCELL_SITE_LIMITS_V1.notes, `${field}.i`),
|
|
210
|
+
s: boundedText(entry.s, 1024, `${field}.s`),
|
|
211
|
+
t: boundedText(entry.t, WORDCELL_SITE_LIMITS_V1.titleBytes, `${field}.t`),
|
|
212
|
+
p: boundedText(entry.p, WORDCELL_SITE_LIMITS_V1.docPreviewBytes + 16, `${field}.p`),
|
|
213
|
+
f: docFields(entry.f, `${field}.f`),
|
|
214
|
+
...inline
|
|
215
|
+
};
|
|
216
|
+
});
|
|
217
|
+
return { format: WORDCELL_SITE_DOCS_FORMAT_V1, content, docs };
|
|
218
|
+
}
|
|
219
|
+
function parseSiteTermsV1(value) {
|
|
220
|
+
if (!isRecord(value))
|
|
221
|
+
throw new TypeError("terms must be an object");
|
|
222
|
+
exactKeys(value, ["format", "terms"], [], "terms");
|
|
223
|
+
if (value.format !== WORDCELL_SITE_TERMS_FORMAT_V1) {
|
|
224
|
+
throw new TypeError(`terms format must be ${WORDCELL_SITE_TERMS_FORMAT_V1}`);
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
format: WORDCELL_SITE_TERMS_FORMAT_V1,
|
|
228
|
+
terms: stringArray(value.terms, WORDCELL_SITE_LIMITS_V1.indexTerms, "terms.terms")
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function parseSitePostingsV1(value) {
|
|
232
|
+
if (!isRecord(value))
|
|
233
|
+
throw new TypeError("postings must be an object");
|
|
234
|
+
exactKeys(value, ["format", "shard", "postings"], [], "postings");
|
|
235
|
+
if (value.format !== WORDCELL_SITE_POSTINGS_FORMAT_V1) {
|
|
236
|
+
throw new TypeError(`postings format must be ${WORDCELL_SITE_POSTINGS_FORMAT_V1}`);
|
|
237
|
+
}
|
|
238
|
+
const shard = value.shard;
|
|
239
|
+
if (typeof shard !== "string" || !/^[0-9a-f]{2}$/u.test(shard)) {
|
|
240
|
+
throw new TypeError("postings.shard must be a two-digit lowercase hex string");
|
|
241
|
+
}
|
|
242
|
+
if (!isRecord(value.postings))
|
|
243
|
+
throw new TypeError("postings.postings must be an object");
|
|
244
|
+
const entries = Object.entries(value.postings);
|
|
245
|
+
if (entries.length > WORDCELL_SITE_LIMITS_V1.shardPairs) {
|
|
246
|
+
throw new TypeError("postings.postings exceeds the shard pair limit");
|
|
247
|
+
}
|
|
248
|
+
const postings = Object.create(null);
|
|
249
|
+
for (const [term, docs] of entries) {
|
|
250
|
+
if (!Array.isArray(docs) || docs.length > WORDCELL_SITE_LIMITS_V1.notes) {
|
|
251
|
+
throw new TypeError(`postings for ${JSON.stringify(term)} must be a bounded array`);
|
|
252
|
+
}
|
|
253
|
+
postings[term] = docs.map((doc) => boundedCount(doc, WORDCELL_SITE_LIMITS_V1.notes, "posting document id"));
|
|
254
|
+
}
|
|
255
|
+
return { format: WORDCELL_SITE_POSTINGS_FORMAT_V1, shard, postings: Object.freeze(postings) };
|
|
256
|
+
}
|
|
257
|
+
function noteLinks(value, field) {
|
|
258
|
+
if (!Array.isArray(value) || value.length > WORDCELL_SITE_LIMITS_V1.notes) {
|
|
259
|
+
throw new TypeError(`${field} must be a bounded array`);
|
|
260
|
+
}
|
|
261
|
+
return value.map((entry, index) => {
|
|
262
|
+
if (!isRecord(entry))
|
|
263
|
+
throw new TypeError(`${field} ${index} must be an object`);
|
|
264
|
+
exactKeys(entry, ["s", "t"], [], `${field} ${index}`);
|
|
265
|
+
return {
|
|
266
|
+
s: boundedText(entry.s, 1024, `${field} ${index}.s`),
|
|
267
|
+
t: boundedText(entry.t, WORDCELL_SITE_LIMITS_V1.titleBytes, `${field} ${index}.t`)
|
|
268
|
+
};
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
function parseSiteNoteV1(value) {
|
|
272
|
+
if (!isRecord(value))
|
|
273
|
+
throw new TypeError("note payload must be an object");
|
|
274
|
+
exactKeys(value, ["format", "id", "slug", "title", "aliases", "tags", "summary", "text", "textTruncated", "links", "backlinks", "relations", "relationBacklinks"], ["type"], "note payload");
|
|
275
|
+
if (value.format !== WORDCELL_SITE_NOTE_FORMAT_V1) {
|
|
276
|
+
throw new TypeError(`note payload format must be ${WORDCELL_SITE_NOTE_FORMAT_V1}`);
|
|
277
|
+
}
|
|
278
|
+
if (value.textTruncated !== true && value.textTruncated !== false) {
|
|
279
|
+
throw new TypeError("note payload textTruncated must be boolean");
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
format: WORDCELL_SITE_NOTE_FORMAT_V1,
|
|
283
|
+
id: boundedText(value.id, 1024, "note.id"),
|
|
284
|
+
slug: boundedText(value.slug, 1024, "note.slug"),
|
|
285
|
+
title: boundedText(value.title, WORDCELL_SITE_LIMITS_V1.titleBytes, "note.title"),
|
|
286
|
+
aliases: stringArray(value.aliases, 128, "note.aliases"),
|
|
287
|
+
...value.type === undefined ? {} : { type: boundedText(value.type, 256, "note.type") },
|
|
288
|
+
tags: stringArray(value.tags, 128, "note.tags"),
|
|
289
|
+
summary: boundedText(value.summary, WORDCELL_SITE_LIMITS_V1.descriptionBytes, "note.summary"),
|
|
290
|
+
text: boundedText(value.text, WORDCELL_SITE_LIMITS_V1.noteTextBytes, "note.text"),
|
|
291
|
+
textTruncated: value.textTruncated,
|
|
292
|
+
links: noteLinks(value.links, "note.links"),
|
|
293
|
+
backlinks: noteLinks(value.backlinks, "note.backlinks"),
|
|
294
|
+
relations: noteRelations(value.relations, "note.relations"),
|
|
295
|
+
relationBacklinks: noteRelations(value.relationBacklinks, "note.relationBacklinks")
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function noteRelations(value, field) {
|
|
299
|
+
if (!Array.isArray(value) || value.length > WORDCELL_SITE_LIMITS_V1.notes) {
|
|
300
|
+
throw new TypeError(`${field} must be a bounded array`);
|
|
301
|
+
}
|
|
302
|
+
return value.map((entry, index) => {
|
|
303
|
+
if (!isRecord(entry))
|
|
304
|
+
throw new TypeError(`${field} ${index} must be an object`);
|
|
305
|
+
exactKeys(entry, ["p", "s", "t"], [], `${field} ${index}`);
|
|
306
|
+
return {
|
|
307
|
+
p: boundedText(entry.p, 256, `${field} ${index}.p`),
|
|
308
|
+
s: boundedText(entry.s, 1024, `${field} ${index}.s`),
|
|
309
|
+
t: boundedText(entry.t, WORDCELL_SITE_LIMITS_V1.titleBytes, `${field} ${index}.t`)
|
|
310
|
+
};
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
function parseSiteGraphV1(value) {
|
|
314
|
+
if (!isRecord(value))
|
|
315
|
+
throw new TypeError("graph must be an object");
|
|
316
|
+
exactKeys(value, ["format", "edges"], [], "graph");
|
|
317
|
+
if (value.format !== WORDCELL_SITE_GRAPH_FORMAT_V1) {
|
|
318
|
+
throw new TypeError(`graph format must be ${WORDCELL_SITE_GRAPH_FORMAT_V1}`);
|
|
319
|
+
}
|
|
320
|
+
if (!Array.isArray(value.edges) || value.edges.length > 1e6) {
|
|
321
|
+
throw new TypeError("graph.edges must be a bounded array");
|
|
322
|
+
}
|
|
323
|
+
const edges = value.edges.map((entry, index) => {
|
|
324
|
+
if (!isRecord(entry))
|
|
325
|
+
throw new TypeError(`graph edge ${index} must be an object`);
|
|
326
|
+
exactKeys(entry, ["s", "t", "k"], ["p"], `graph edge ${index}`);
|
|
327
|
+
if (entry.k !== "link" && entry.k !== "relation") {
|
|
328
|
+
throw new TypeError(`graph edge ${index}.k must be link or relation`);
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
s: boundedCount(entry.s, WORDCELL_SITE_LIMITS_V1.notes, `graph edge ${index}.s`),
|
|
332
|
+
t: boundedCount(entry.t, WORDCELL_SITE_LIMITS_V1.notes, `graph edge ${index}.t`),
|
|
333
|
+
k: entry.k,
|
|
334
|
+
...entry.p === undefined ? {} : { p: boundedText(entry.p, 256, `graph edge ${index}.p`) }
|
|
335
|
+
};
|
|
336
|
+
});
|
|
337
|
+
return { format: WORDCELL_SITE_GRAPH_FORMAT_V1, edges };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// src/publish-graph.ts
|
|
341
|
+
var WORDCELL_SITE_GRAPH_VIEW_LIMIT = 1000;
|
|
342
|
+
function mulberry32(seed) {
|
|
343
|
+
let state = seed | 0;
|
|
344
|
+
return () => {
|
|
345
|
+
state = state + 1831565813 | 0;
|
|
346
|
+
let value = Math.imul(state ^ state >>> 15, 1 | state);
|
|
347
|
+
value = value + Math.imul(value ^ value >>> 7, 61 | value) ^ value;
|
|
348
|
+
return ((value ^ value >>> 14) >>> 0) / 4294967296;
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
function siteGraphSeed(slugs, edges) {
|
|
352
|
+
let hash = 2166136261;
|
|
353
|
+
const mix = (text) => {
|
|
354
|
+
for (let index = 0;index < text.length; index += 1) {
|
|
355
|
+
hash ^= text.charCodeAt(index);
|
|
356
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
for (const slug of slugs) {
|
|
360
|
+
mix(slug);
|
|
361
|
+
mix(`
|
|
362
|
+
`);
|
|
363
|
+
}
|
|
364
|
+
mix("\x00");
|
|
365
|
+
for (const edge of edges) {
|
|
366
|
+
mix(`${String(edge.s)}-${String(edge.t)},`);
|
|
367
|
+
}
|
|
368
|
+
return hash >>> 0;
|
|
369
|
+
}
|
|
370
|
+
function layoutSiteGraph(nodeCount, edges, options) {
|
|
371
|
+
if (nodeCount <= 0)
|
|
372
|
+
return [];
|
|
373
|
+
const width = options.width ?? 1000;
|
|
374
|
+
const height = options.height ?? 700;
|
|
375
|
+
const iterations = options.iterations ?? Math.max(80, Math.min(260, 24000 / nodeCount));
|
|
376
|
+
const random = mulberry32(options.seed);
|
|
377
|
+
const x = new Float64Array(nodeCount);
|
|
378
|
+
const y = new Float64Array(nodeCount);
|
|
379
|
+
const dx = new Float64Array(nodeCount);
|
|
380
|
+
const dy = new Float64Array(nodeCount);
|
|
381
|
+
for (let index = 0;index < nodeCount; index += 1) {
|
|
382
|
+
x[index] = (random() - 0.5) * width;
|
|
383
|
+
y[index] = (random() - 0.5) * height;
|
|
384
|
+
}
|
|
385
|
+
if (nodeCount === 1) {
|
|
386
|
+
x[0] = 0;
|
|
387
|
+
y[0] = 0;
|
|
388
|
+
return [{ x: 0, y: 0 }];
|
|
389
|
+
}
|
|
390
|
+
const pairs = new Set;
|
|
391
|
+
const deduped = [];
|
|
392
|
+
for (const edge of edges) {
|
|
393
|
+
const s = Math.min(edge.s, edge.t);
|
|
394
|
+
const t = Math.max(edge.s, edge.t);
|
|
395
|
+
if (s === t || s < 0 || t >= nodeCount)
|
|
396
|
+
continue;
|
|
397
|
+
const key = s * nodeCount + t;
|
|
398
|
+
if (pairs.has(key))
|
|
399
|
+
continue;
|
|
400
|
+
pairs.add(key);
|
|
401
|
+
deduped.push({ s, t });
|
|
402
|
+
}
|
|
403
|
+
const area = width * height;
|
|
404
|
+
const k = Math.sqrt(area / nodeCount);
|
|
405
|
+
const centerX = 0;
|
|
406
|
+
const centerY = 0;
|
|
407
|
+
const epsilon = 0.01;
|
|
408
|
+
const gravity = 0.05;
|
|
409
|
+
let temperature = k * 2;
|
|
410
|
+
for (let iteration = 0;iteration < iterations; iteration += 1) {
|
|
411
|
+
dx.fill(0);
|
|
412
|
+
dy.fill(0);
|
|
413
|
+
for (let i = 0;i < nodeCount; i += 1) {
|
|
414
|
+
for (let j = i + 1;j < nodeCount; j += 1) {
|
|
415
|
+
const deltaX = (x[i] ?? 0) - (x[j] ?? 0);
|
|
416
|
+
const deltaY = (y[i] ?? 0) - (y[j] ?? 0);
|
|
417
|
+
const distance = Math.max(Math.sqrt(deltaX * deltaX + deltaY * deltaY), epsilon);
|
|
418
|
+
const force = k * k / distance;
|
|
419
|
+
const ux = deltaX / distance * force;
|
|
420
|
+
const uy = deltaY / distance * force;
|
|
421
|
+
dx[i] = (dx[i] ?? 0) + ux;
|
|
422
|
+
dy[i] = (dy[i] ?? 0) + uy;
|
|
423
|
+
dx[j] = (dx[j] ?? 0) - ux;
|
|
424
|
+
dy[j] = (dy[j] ?? 0) - uy;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
for (const edge of deduped) {
|
|
428
|
+
const deltaX = (x[edge.s] ?? 0) - (x[edge.t] ?? 0);
|
|
429
|
+
const deltaY = (y[edge.s] ?? 0) - (y[edge.t] ?? 0);
|
|
430
|
+
const distance = Math.max(Math.sqrt(deltaX * deltaX + deltaY * deltaY), epsilon);
|
|
431
|
+
const force = distance * distance / k;
|
|
432
|
+
const ux = deltaX / distance * force;
|
|
433
|
+
const uy = deltaY / distance * force;
|
|
434
|
+
dx[edge.s] = (dx[edge.s] ?? 0) - ux;
|
|
435
|
+
dy[edge.s] = (dy[edge.s] ?? 0) - uy;
|
|
436
|
+
dx[edge.t] = (dx[edge.t] ?? 0) + ux;
|
|
437
|
+
dy[edge.t] = (dy[edge.t] ?? 0) + uy;
|
|
438
|
+
}
|
|
439
|
+
for (let i = 0;i < nodeCount; i += 1) {
|
|
440
|
+
dx[i] = (dx[i] ?? 0) + (centerX - (x[i] ?? 0)) * gravity;
|
|
441
|
+
dy[i] = (dy[i] ?? 0) + (centerY - (y[i] ?? 0)) * gravity;
|
|
442
|
+
}
|
|
443
|
+
for (let i = 0;i < nodeCount; i += 1) {
|
|
444
|
+
const moveX = dx[i] ?? 0;
|
|
445
|
+
const moveY = dy[i] ?? 0;
|
|
446
|
+
const length = Math.sqrt(moveX * moveX + moveY * moveY);
|
|
447
|
+
if (length === 0)
|
|
448
|
+
continue;
|
|
449
|
+
const bounded = Math.min(length, temperature);
|
|
450
|
+
x[i] = (x[i] ?? 0) + moveX / length * bounded;
|
|
451
|
+
y[i] = (y[i] ?? 0) + moveY / length * bounded;
|
|
452
|
+
}
|
|
453
|
+
temperature *= 1 - (iteration + 1) / iterations;
|
|
454
|
+
}
|
|
455
|
+
const points = [];
|
|
456
|
+
for (let index = 0;index < nodeCount; index += 1) {
|
|
457
|
+
points.push({ x: x[index] ?? 0, y: y[index] ?? 0 });
|
|
458
|
+
}
|
|
459
|
+
return points;
|
|
460
|
+
}
|
|
461
|
+
function siteGraphDegrees(nodeCount, edges) {
|
|
462
|
+
const degree = new Array(Math.max(0, nodeCount)).fill(0);
|
|
463
|
+
for (const edge of edges) {
|
|
464
|
+
if (edge.s === edge.t || edge.s < 0 || edge.t < 0)
|
|
465
|
+
continue;
|
|
466
|
+
if (edge.s >= nodeCount || edge.t >= nodeCount)
|
|
467
|
+
continue;
|
|
468
|
+
degree[edge.s] = (degree[edge.s] ?? 0) + 1;
|
|
469
|
+
degree[edge.t] = (degree[edge.t] ?? 0) + 1;
|
|
470
|
+
}
|
|
471
|
+
return degree;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// src/publish-search.ts
|
|
475
|
+
var PUBLISH_FIELD_TITLE = 1;
|
|
476
|
+
var PUBLISH_FIELD_ALIAS = 2;
|
|
477
|
+
var PUBLISH_FIELD_PATH = 4;
|
|
478
|
+
var PUBLISH_FIELD_TAG = 8;
|
|
479
|
+
var PUBLISH_FIELD_METADATA = 16;
|
|
480
|
+
var PUBLISH_FIELD_CONTENT = 32;
|
|
481
|
+
var PUBLISH_SEARCH_WEIGHTS_V1 = Object.freeze({
|
|
482
|
+
identity: Object.freeze({ title: 1000, alias: 950, path: 900 }),
|
|
483
|
+
phrase: Object.freeze({
|
|
484
|
+
title: 400,
|
|
485
|
+
alias: 350,
|
|
486
|
+
path: 300,
|
|
487
|
+
tag: 250,
|
|
488
|
+
content: 150,
|
|
489
|
+
metadata: 100
|
|
490
|
+
}),
|
|
491
|
+
term: Object.freeze({
|
|
492
|
+
title: 40,
|
|
493
|
+
alias: 35,
|
|
494
|
+
path: 30,
|
|
495
|
+
tag: 30,
|
|
496
|
+
metadata: 10,
|
|
497
|
+
content: 5
|
|
498
|
+
}),
|
|
499
|
+
coverage: 100
|
|
500
|
+
});
|
|
501
|
+
var MAX_PUBLISH_QUERY_BYTES = 16 * 1024;
|
|
502
|
+
var MAX_PUBLISH_QUERY_TERMS = 64;
|
|
503
|
+
var MAX_PUBLISH_PREFIX_EXPANSIONS = 32;
|
|
504
|
+
var TERM_PATTERN = /[\p{L}\p{N}][\p{L}\p{N}._/-]*/gu;
|
|
505
|
+
var textEncoder2 = new TextEncoder;
|
|
506
|
+
function publishUtf8Bytes(value) {
|
|
507
|
+
return textEncoder2.encode(value).byteLength;
|
|
508
|
+
}
|
|
509
|
+
function publishNormalize(value) {
|
|
510
|
+
return value.normalize("NFC").toLocaleLowerCase("en-US");
|
|
511
|
+
}
|
|
512
|
+
var MAX_PUBLISH_QUERY_FILTERS = 8;
|
|
513
|
+
var FILTER_PATTERN = /^(tag|type|path):(\S+)$/u;
|
|
514
|
+
function trimSlashes(value) {
|
|
515
|
+
let start = 0;
|
|
516
|
+
let end = value.length;
|
|
517
|
+
while (start < end && value.charCodeAt(start) === 47)
|
|
518
|
+
start += 1;
|
|
519
|
+
while (end > start && value.charCodeAt(end - 1) === 47)
|
|
520
|
+
end -= 1;
|
|
521
|
+
return value.slice(start, end);
|
|
522
|
+
}
|
|
523
|
+
function publishQueryParts(raw) {
|
|
524
|
+
const tags = [];
|
|
525
|
+
const types = [];
|
|
526
|
+
const paths = [];
|
|
527
|
+
const rest = [];
|
|
528
|
+
for (const token of raw.split(/\s+/u)) {
|
|
529
|
+
if (token === "")
|
|
530
|
+
continue;
|
|
531
|
+
const match = FILTER_PATTERN.exec(token);
|
|
532
|
+
const value = match === null ? "" : publishNormalize(trimSlashes(match[2] ?? ""));
|
|
533
|
+
if (match === null || value === "") {
|
|
534
|
+
rest.push(token);
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
const bucket = match[1] === "tag" ? tags : match[1] === "type" ? types : paths;
|
|
538
|
+
if (bucket.length < MAX_PUBLISH_QUERY_FILTERS)
|
|
539
|
+
bucket.push(value);
|
|
540
|
+
else
|
|
541
|
+
rest.push(token);
|
|
542
|
+
}
|
|
543
|
+
return { filters: { tags, types, paths }, text: rest.join(" ") };
|
|
544
|
+
}
|
|
545
|
+
function publishDocMatchesFilters(doc, filters, type) {
|
|
546
|
+
if (filters.tags.length > 0) {
|
|
547
|
+
const tags = doc.f.g === "" ? [] : doc.f.g.split(`
|
|
548
|
+
`);
|
|
549
|
+
for (const tag of filters.tags) {
|
|
550
|
+
if (!tags.includes(publishNormalize(tag)))
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
if (filters.types.length > 0) {
|
|
555
|
+
const docType = publishNormalize(type === undefined || type === "" ? "note" : type);
|
|
556
|
+
for (const wanted of filters.types) {
|
|
557
|
+
if (docType !== publishNormalize(wanted))
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
if (filters.paths.length > 0) {
|
|
562
|
+
const slug = publishNormalize(doc.s);
|
|
563
|
+
const fields = doc.f.p === "" ? [] : doc.f.p.split(`
|
|
564
|
+
`);
|
|
565
|
+
for (const value of filters.paths) {
|
|
566
|
+
const prefix = publishNormalize(trimSlashes(value));
|
|
567
|
+
const boundary = `${prefix}/`;
|
|
568
|
+
const inSlug = slug === prefix || slug.startsWith(boundary);
|
|
569
|
+
const inFields = fields.some((line) => line === prefix || line.startsWith(boundary));
|
|
570
|
+
if (!inSlug && !inFields)
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return true;
|
|
575
|
+
}
|
|
576
|
+
var MAX_PUBLISH_MARK_RANGES = 32;
|
|
577
|
+
function publishMarkRanges(text, terms, maximum = MAX_PUBLISH_MARK_RANGES) {
|
|
578
|
+
const display = text.normalize("NFC");
|
|
579
|
+
let normalized = "";
|
|
580
|
+
const map = [];
|
|
581
|
+
for (let offset = 0;offset < display.length; ) {
|
|
582
|
+
const point = display.codePointAt(offset) ?? 0;
|
|
583
|
+
const character = String.fromCodePoint(point);
|
|
584
|
+
const lowered = character.toLocaleLowerCase("en-US");
|
|
585
|
+
for (let index = 0;index < lowered.length; index += 1)
|
|
586
|
+
map.push(offset);
|
|
587
|
+
normalized += lowered;
|
|
588
|
+
offset += character.length;
|
|
589
|
+
}
|
|
590
|
+
const ranges = [];
|
|
591
|
+
for (const term of terms) {
|
|
592
|
+
const needle = publishNormalize(term);
|
|
593
|
+
if (needle === "")
|
|
594
|
+
continue;
|
|
595
|
+
let from = 0;
|
|
596
|
+
while (ranges.length < maximum) {
|
|
597
|
+
const hit = normalized.indexOf(needle, from);
|
|
598
|
+
if (hit === -1)
|
|
599
|
+
break;
|
|
600
|
+
const start = map[hit] ?? 0;
|
|
601
|
+
const tail = hit + needle.length;
|
|
602
|
+
const end = tail >= map.length ? display.length : map[tail] ?? display.length;
|
|
603
|
+
if (end > start)
|
|
604
|
+
ranges.push({ start, end });
|
|
605
|
+
from = tail === 0 ? 1 : tail;
|
|
606
|
+
}
|
|
607
|
+
if (ranges.length >= maximum)
|
|
608
|
+
break;
|
|
609
|
+
}
|
|
610
|
+
ranges.sort((left, right) => left.start - right.start || left.end - right.end);
|
|
611
|
+
const merged = [];
|
|
612
|
+
for (const range of ranges) {
|
|
613
|
+
const last = merged[merged.length - 1];
|
|
614
|
+
if (last !== undefined && range.start <= last.end) {
|
|
615
|
+
merged[merged.length - 1] = { start: last.start, end: Math.max(last.end, range.end) };
|
|
616
|
+
} else {
|
|
617
|
+
merged.push(range);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return merged;
|
|
621
|
+
}
|
|
622
|
+
function publishQuery(value) {
|
|
623
|
+
if (typeof value !== "string") {
|
|
624
|
+
throw new TypeError("Search query must be a string.");
|
|
625
|
+
}
|
|
626
|
+
if (publishUtf8Bytes(value) > MAX_PUBLISH_QUERY_BYTES) {
|
|
627
|
+
throw new RangeError(`Search query exceeds the ${MAX_PUBLISH_QUERY_BYTES}-byte limit.`);
|
|
628
|
+
}
|
|
629
|
+
const raw = value.trim();
|
|
630
|
+
const normalized = publishNormalize(raw);
|
|
631
|
+
const terms = [];
|
|
632
|
+
const seen = new Set;
|
|
633
|
+
for (const match of normalized.matchAll(TERM_PATTERN)) {
|
|
634
|
+
if (seen.has(match[0]))
|
|
635
|
+
continue;
|
|
636
|
+
seen.add(match[0]);
|
|
637
|
+
terms.push(match[0]);
|
|
638
|
+
if (terms.length > MAX_PUBLISH_QUERY_TERMS) {
|
|
639
|
+
throw new RangeError(`Search query may contain at most ${MAX_PUBLISH_QUERY_TERMS} unique normalized terms.`);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return { raw, normalized, terms };
|
|
643
|
+
}
|
|
644
|
+
function publishShardName(term) {
|
|
645
|
+
let hash = 2166136261;
|
|
646
|
+
for (let index = 0;index < term.length; index += 1) {
|
|
647
|
+
hash ^= term.charCodeAt(index);
|
|
648
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
649
|
+
}
|
|
650
|
+
return (hash >>> 24).toString(16).padStart(2, "0");
|
|
651
|
+
}
|
|
652
|
+
function publishPrefixTerms(dictionary, prefix, maximum = MAX_PUBLISH_PREFIX_EXPANSIONS) {
|
|
653
|
+
if (prefix === "")
|
|
654
|
+
return [];
|
|
655
|
+
let low = 0;
|
|
656
|
+
let high = dictionary.length;
|
|
657
|
+
while (low < high) {
|
|
658
|
+
const middle = low + high >> 1;
|
|
659
|
+
if ((dictionary[middle] ?? "") < prefix)
|
|
660
|
+
low = middle + 1;
|
|
661
|
+
else
|
|
662
|
+
high = middle;
|
|
663
|
+
}
|
|
664
|
+
const found = [];
|
|
665
|
+
for (let index = low;index < dictionary.length && found.length < maximum; index += 1) {
|
|
666
|
+
const term = dictionary[index] ?? "";
|
|
667
|
+
if (!term.startsWith(prefix))
|
|
668
|
+
break;
|
|
669
|
+
found.push(term);
|
|
670
|
+
}
|
|
671
|
+
return found;
|
|
672
|
+
}
|
|
673
|
+
function fieldMasks(term, fields) {
|
|
674
|
+
let mask = 0;
|
|
675
|
+
if (fields.t.includes(term))
|
|
676
|
+
mask |= PUBLISH_FIELD_TITLE;
|
|
677
|
+
if (fields.a.includes(term))
|
|
678
|
+
mask |= PUBLISH_FIELD_ALIAS;
|
|
679
|
+
if (fields.p.includes(term))
|
|
680
|
+
mask |= PUBLISH_FIELD_PATH;
|
|
681
|
+
if (fields.g.includes(term))
|
|
682
|
+
mask |= PUBLISH_FIELD_TAG;
|
|
683
|
+
if (fields.m.includes(term))
|
|
684
|
+
mask |= PUBLISH_FIELD_METADATA;
|
|
685
|
+
return mask;
|
|
686
|
+
}
|
|
687
|
+
function scorePublishDocument(input, query) {
|
|
688
|
+
const fields = input.doc.f;
|
|
689
|
+
const phrase = query.normalized;
|
|
690
|
+
const weights = PUBLISH_SEARCH_WEIGHTS_V1;
|
|
691
|
+
let score = 0;
|
|
692
|
+
let identity = false;
|
|
693
|
+
let phraseMatched = false;
|
|
694
|
+
if (phrase !== "") {
|
|
695
|
+
if (fields.t === phrase) {
|
|
696
|
+
identity = true;
|
|
697
|
+
score += weights.identity.title;
|
|
698
|
+
}
|
|
699
|
+
if (fields.a.split(`
|
|
700
|
+
`).includes(phrase)) {
|
|
701
|
+
identity = true;
|
|
702
|
+
score += weights.identity.alias;
|
|
703
|
+
}
|
|
704
|
+
if (fields.p.split(`
|
|
705
|
+
`).includes(phrase)) {
|
|
706
|
+
identity = true;
|
|
707
|
+
score += weights.identity.path;
|
|
708
|
+
}
|
|
709
|
+
const phraseFields = [
|
|
710
|
+
[PUBLISH_FIELD_TITLE, fields.t, weights.phrase.title],
|
|
711
|
+
[PUBLISH_FIELD_ALIAS, fields.a, weights.phrase.alias],
|
|
712
|
+
[PUBLISH_FIELD_PATH, fields.p, weights.phrase.path],
|
|
713
|
+
[PUBLISH_FIELD_TAG, fields.g, weights.phrase.tag],
|
|
714
|
+
[PUBLISH_FIELD_METADATA, fields.m, weights.phrase.metadata]
|
|
715
|
+
];
|
|
716
|
+
for (const [, value, weight] of phraseFields) {
|
|
717
|
+
if (value.includes(phrase)) {
|
|
718
|
+
phraseMatched = true;
|
|
719
|
+
score += weight;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
if (input.contentText !== undefined && input.contentText.includes(phrase)) {
|
|
723
|
+
phraseMatched = true;
|
|
724
|
+
score += weights.phrase.content;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
let matchedTerms = 0;
|
|
728
|
+
for (const term of query.terms) {
|
|
729
|
+
const mask = fieldMasks(term, fields) | (input.contentTerms.has(term) ? PUBLISH_FIELD_CONTENT : 0);
|
|
730
|
+
if (mask === 0)
|
|
731
|
+
continue;
|
|
732
|
+
matchedTerms += 1;
|
|
733
|
+
if (mask & PUBLISH_FIELD_TITLE)
|
|
734
|
+
score += weights.term.title;
|
|
735
|
+
if (mask & PUBLISH_FIELD_ALIAS)
|
|
736
|
+
score += weights.term.alias;
|
|
737
|
+
if (mask & PUBLISH_FIELD_PATH)
|
|
738
|
+
score += weights.term.path;
|
|
739
|
+
if (mask & PUBLISH_FIELD_TAG)
|
|
740
|
+
score += weights.term.tag;
|
|
741
|
+
if (mask & PUBLISH_FIELD_METADATA)
|
|
742
|
+
score += weights.term.metadata;
|
|
743
|
+
if (mask & PUBLISH_FIELD_CONTENT)
|
|
744
|
+
score += weights.term.content;
|
|
745
|
+
}
|
|
746
|
+
const requiredTerms = query.terms.length <= 1 ? query.terms.length : Math.min(3, Math.ceil(query.terms.length / 2));
|
|
747
|
+
if (!identity && !phraseMatched && matchedTerms < requiredTerms)
|
|
748
|
+
return null;
|
|
749
|
+
if (query.terms.length > 0) {
|
|
750
|
+
score += Math.round(matchedTerms / query.terms.length * weights.coverage);
|
|
751
|
+
}
|
|
752
|
+
if (score === 0)
|
|
753
|
+
return null;
|
|
754
|
+
return { score, identity, phraseMatched, matchedTerms };
|
|
755
|
+
}
|
|
756
|
+
function comparePublishScores(left, right) {
|
|
757
|
+
return Number(right.identity) - Number(left.identity) || right.score - left.score || left.i - right.i;
|
|
758
|
+
}
|
|
759
|
+
function publishSnippet(text, query, fallback, windowBytes = 160) {
|
|
760
|
+
const normalized = publishNormalize(text);
|
|
761
|
+
let offset = query.normalized === "" ? -1 : normalized.indexOf(query.normalized);
|
|
762
|
+
if (offset < 0) {
|
|
763
|
+
for (const term of query.terms) {
|
|
764
|
+
offset = normalized.indexOf(term);
|
|
765
|
+
if (offset >= 0)
|
|
766
|
+
break;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
if (offset < 0)
|
|
770
|
+
return fallback;
|
|
771
|
+
const half = Math.floor(windowBytes / 2);
|
|
772
|
+
const start = Math.max(0, offset - half);
|
|
773
|
+
const end = Math.min(text.length, offset + half);
|
|
774
|
+
const snippet = text.slice(start, end).replace(/\s+/gu, " ").trim();
|
|
775
|
+
return `${start > 0 ? "…" : ""}${snippet}${end < text.length ? "…" : ""}`;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// src/publish-theme.ts
|
|
779
|
+
var wordcellPalettes = [
|
|
780
|
+
"wordcell",
|
|
781
|
+
"catppuccin",
|
|
782
|
+
"gruvbox",
|
|
783
|
+
"rose-pine",
|
|
784
|
+
"tokyo-night"
|
|
785
|
+
];
|
|
786
|
+
var wordcellPaletteLabels = {
|
|
787
|
+
wordcell: "Wordcell",
|
|
788
|
+
catppuccin: "Catppuccin",
|
|
789
|
+
gruvbox: "Gruvbox",
|
|
790
|
+
"rose-pine": "Rosé Pine",
|
|
791
|
+
"tokyo-night": "Tokyo Night"
|
|
792
|
+
};
|
|
793
|
+
var wordcellAppearanceModes = ["system", "light", "dark"];
|
|
794
|
+
var wordcellAppearanceModeLabels = {
|
|
795
|
+
system: "System",
|
|
796
|
+
light: "Light",
|
|
797
|
+
dark: "Dark"
|
|
798
|
+
};
|
|
799
|
+
function wordcellAppearanceBridge() {
|
|
800
|
+
return globalThis.wordcellAppearance;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// src/publish-reader/reader.ts
|
|
804
|
+
var dom = globalThis;
|
|
805
|
+
async function fetchJson(base, path) {
|
|
806
|
+
const response = await dom.fetch(`${base}${path}`);
|
|
807
|
+
if (!response.ok)
|
|
808
|
+
return;
|
|
809
|
+
return response.json();
|
|
810
|
+
}
|
|
811
|
+
async function loadIndex(base) {
|
|
812
|
+
const manifestRaw = await fetchJson(base, "manifest.json");
|
|
813
|
+
if (manifestRaw === undefined)
|
|
814
|
+
return;
|
|
815
|
+
const manifest = parseSiteManifestV1(manifestRaw);
|
|
816
|
+
const docsRaw = await fetchJson(base, manifest.paths.docs);
|
|
817
|
+
const termsRaw = await fetchJson(base, manifest.paths.terms);
|
|
818
|
+
if (docsRaw === undefined || termsRaw === undefined)
|
|
819
|
+
return;
|
|
820
|
+
const docs = parseSiteDocsV1(docsRaw);
|
|
821
|
+
const terms = parseSiteTermsV1(termsRaw);
|
|
822
|
+
return {
|
|
823
|
+
manifest,
|
|
824
|
+
docs: docs.docs,
|
|
825
|
+
terms: terms.terms,
|
|
826
|
+
postings: new Map,
|
|
827
|
+
notes: new Map,
|
|
828
|
+
catalog: undefined
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
async function loadShard(index, base, shard) {
|
|
832
|
+
const cached = index.postings.get(shard);
|
|
833
|
+
if (cached !== undefined)
|
|
834
|
+
return cached;
|
|
835
|
+
const raw = await fetchJson(base, `${index.manifest.paths.postingsPrefix}${shard}.json`);
|
|
836
|
+
const postings = raw === undefined ? new Map : new Map(Object.entries(parseSitePostingsV1(raw).postings));
|
|
837
|
+
index.postings.set(shard, postings);
|
|
838
|
+
return postings;
|
|
839
|
+
}
|
|
840
|
+
async function loadCatalog(index, base) {
|
|
841
|
+
if (index.catalog !== undefined)
|
|
842
|
+
return index.catalog;
|
|
843
|
+
const raw = await fetchJson(base, index.manifest.paths.catalog);
|
|
844
|
+
if (raw === undefined)
|
|
845
|
+
return;
|
|
846
|
+
index.catalog = parseSiteCatalogV1(raw);
|
|
847
|
+
return index.catalog;
|
|
848
|
+
}
|
|
849
|
+
async function hydrate(index, base, slug) {
|
|
850
|
+
const cached = index.notes.get(slug);
|
|
851
|
+
if (cached !== undefined)
|
|
852
|
+
return cached;
|
|
853
|
+
const path = slug === "" ? "index.json" : `${index.manifest.paths.notePrefix}${slug}.json`;
|
|
854
|
+
const raw = await fetchJson(base, path);
|
|
855
|
+
if (raw === undefined)
|
|
856
|
+
return;
|
|
857
|
+
const note = parseSiteNoteV1(raw);
|
|
858
|
+
index.notes.set(slug, note);
|
|
859
|
+
return note;
|
|
860
|
+
}
|
|
861
|
+
var FILTER_ONLY_SCORE = Object.freeze({
|
|
862
|
+
score: 0,
|
|
863
|
+
identity: false,
|
|
864
|
+
phraseMatched: false,
|
|
865
|
+
matchedTerms: 0
|
|
866
|
+
});
|
|
867
|
+
function hasFilters(filters) {
|
|
868
|
+
return filters.tags.length + filters.types.length + filters.paths.length > 0;
|
|
869
|
+
}
|
|
870
|
+
async function search(index, base, raw) {
|
|
871
|
+
const { filters, text } = publishQueryParts(raw);
|
|
872
|
+
const filtered = hasFilters(filters);
|
|
873
|
+
const query = publishQuery(text);
|
|
874
|
+
if (!filtered && query.terms.length === 0 && query.normalized === "")
|
|
875
|
+
return [];
|
|
876
|
+
let typeByIndex;
|
|
877
|
+
if (filters.types.length > 0) {
|
|
878
|
+
const catalog = await loadCatalog(index, base);
|
|
879
|
+
typeByIndex = new Map((catalog?.entries ?? []).map((entry) => [entry.i, entry.type ?? "note"]));
|
|
880
|
+
}
|
|
881
|
+
const postingsHits = new Map;
|
|
882
|
+
if (index.manifest.search.content === "shards" && query.terms.length > 0) {
|
|
883
|
+
const expandedByTerm = new Map;
|
|
884
|
+
const shards = new Set;
|
|
885
|
+
for (const term of query.terms) {
|
|
886
|
+
const expanded = publishPrefixTerms(index.terms, term, MAX_PUBLISH_PREFIX_EXPANSIONS);
|
|
887
|
+
expandedByTerm.set(term, expanded);
|
|
888
|
+
for (const candidate of expanded)
|
|
889
|
+
shards.add(publishShardName(candidate));
|
|
890
|
+
}
|
|
891
|
+
const tables = new Map;
|
|
892
|
+
for (const shard of shards)
|
|
893
|
+
tables.set(shard, await loadShard(index, base, shard));
|
|
894
|
+
for (const doc of index.docs) {
|
|
895
|
+
const hits2 = new Set;
|
|
896
|
+
for (const term of query.terms) {
|
|
897
|
+
const matched = (expandedByTerm.get(term) ?? []).some((candidate) => tables.get(publishShardName(candidate))?.get(candidate)?.includes(doc.i) === true);
|
|
898
|
+
if (matched)
|
|
899
|
+
hits2.add(term);
|
|
900
|
+
}
|
|
901
|
+
postingsHits.set(doc.i, hits2);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
const hits = [];
|
|
905
|
+
for (const doc of index.docs) {
|
|
906
|
+
if (!publishDocMatchesFilters(doc, filters, typeByIndex?.get(doc.i)))
|
|
907
|
+
continue;
|
|
908
|
+
if (query.terms.length === 0 && query.normalized === "") {
|
|
909
|
+
hits.push({ doc, score: FILTER_ONLY_SCORE });
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
const inline = index.manifest.search.content === "inline" ? doc.x : undefined;
|
|
913
|
+
const contentTerms = inline !== undefined ? new Set(query.terms.filter((term) => inline.includes(term))) : postingsHits.get(doc.i) ?? new Set;
|
|
914
|
+
const scored = scorePublishDocument({ doc, contentTerms, ...inline === undefined ? {} : { contentText: inline } }, query);
|
|
915
|
+
if (scored === null)
|
|
916
|
+
continue;
|
|
917
|
+
hits.push({ doc, score: scored });
|
|
918
|
+
}
|
|
919
|
+
hits.sort((left, right) => comparePublishScores({ i: left.doc.i, ...left.score }, { i: right.doc.i, ...right.score }));
|
|
920
|
+
return hits.slice(0, WORDCELL_SITE_LIMITS_V1.searchResults);
|
|
921
|
+
}
|
|
922
|
+
function resultHref(base, slug) {
|
|
923
|
+
return slug === "" ? base : `${base}n/${encodeURI(slug)}/`;
|
|
924
|
+
}
|
|
925
|
+
function snippetFor(index, doc, query) {
|
|
926
|
+
const hydrated = index.notes.get(doc.s);
|
|
927
|
+
const text = hydrated?.text ?? doc.x;
|
|
928
|
+
return text === undefined ? doc.p : publishSnippet(text, query, doc.p);
|
|
929
|
+
}
|
|
930
|
+
function appendMarked(parent, text, terms) {
|
|
931
|
+
const display = text.normalize("NFC");
|
|
932
|
+
const ranges = publishMarkRanges(display, terms);
|
|
933
|
+
if (ranges.length === 0) {
|
|
934
|
+
parent.appendChild(dom.document.createTextNode(display));
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
let cursor = 0;
|
|
938
|
+
for (const range of ranges) {
|
|
939
|
+
if (range.start > cursor) {
|
|
940
|
+
parent.appendChild(dom.document.createTextNode(display.slice(cursor, range.start)));
|
|
941
|
+
}
|
|
942
|
+
const mark = dom.document.createElement("mark");
|
|
943
|
+
mark.textContent = display.slice(range.start, range.end);
|
|
944
|
+
parent.appendChild(mark);
|
|
945
|
+
cursor = range.end;
|
|
946
|
+
}
|
|
947
|
+
if (cursor < display.length) {
|
|
948
|
+
parent.appendChild(dom.document.createTextNode(display.slice(cursor)));
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
function buildOverlay(base) {
|
|
952
|
+
const overlay = dom.document.createElement("div");
|
|
953
|
+
overlay.className = "wordcell-search-overlay";
|
|
954
|
+
overlay.setAttribute("role", "dialog");
|
|
955
|
+
overlay.setAttribute("aria-modal", "true");
|
|
956
|
+
overlay.setAttribute("aria-label", "Search");
|
|
957
|
+
const panel = dom.document.createElement("div");
|
|
958
|
+
panel.className = "wordcell-search-panel";
|
|
959
|
+
const input = dom.document.createElement("input");
|
|
960
|
+
input.type = "search";
|
|
961
|
+
input.placeholder = "Search this site… (tag:, type:, path:)";
|
|
962
|
+
input.setAttribute("aria-label", "Search this site");
|
|
963
|
+
input.setAttribute("autocomplete", "off");
|
|
964
|
+
input.setAttribute("spellcheck", "false");
|
|
965
|
+
const status = dom.document.createElement("div");
|
|
966
|
+
status.className = "wordcell-search-status";
|
|
967
|
+
status.setAttribute("role", "status");
|
|
968
|
+
const list = dom.document.createElement("ul");
|
|
969
|
+
list.className = "wordcell-search-results";
|
|
970
|
+
panel.appendChild(input);
|
|
971
|
+
panel.appendChild(status);
|
|
972
|
+
panel.appendChild(list);
|
|
973
|
+
overlay.appendChild(panel);
|
|
974
|
+
let index;
|
|
975
|
+
let loading;
|
|
976
|
+
let hits = [];
|
|
977
|
+
let active = -1;
|
|
978
|
+
let debounce = 0;
|
|
979
|
+
const render = () => {
|
|
980
|
+
list.innerHTML = "";
|
|
981
|
+
let query;
|
|
982
|
+
try {
|
|
983
|
+
query = publishQuery(publishQueryParts(input.value).text);
|
|
984
|
+
} catch {
|
|
985
|
+
query = undefined;
|
|
986
|
+
}
|
|
987
|
+
const terms = query?.terms ?? [];
|
|
988
|
+
for (const [position, hit] of hits.entries()) {
|
|
989
|
+
const item = dom.document.createElement("li");
|
|
990
|
+
const link = dom.document.createElement("a");
|
|
991
|
+
link.href = resultHref(base, hit.doc.s);
|
|
992
|
+
link.className = position === active ? "active" : "";
|
|
993
|
+
const title = dom.document.createElement("span");
|
|
994
|
+
title.className = "title";
|
|
995
|
+
appendMarked(title, hit.doc.t, terms);
|
|
996
|
+
const snippet = dom.document.createElement("span");
|
|
997
|
+
snippet.className = "snippet";
|
|
998
|
+
const snippetText = index === undefined || query === undefined || query.terms.length === 0 ? hit.doc.p : snippetFor(index, hit.doc, query);
|
|
999
|
+
appendMarked(snippet, snippetText, terms);
|
|
1000
|
+
link.appendChild(title);
|
|
1001
|
+
link.appendChild(snippet);
|
|
1002
|
+
item.appendChild(link);
|
|
1003
|
+
list.appendChild(item);
|
|
1004
|
+
}
|
|
1005
|
+
status.textContent = hits.length === 0 ? input.value.trim() === "" ? "Type to search. Filter with tag:, type:, path:." : "No results." : `${hits.length} result${hits.length === 1 ? "" : "s"}`;
|
|
1006
|
+
};
|
|
1007
|
+
const run = () => {
|
|
1008
|
+
(async () => {
|
|
1009
|
+
if (index === undefined) {
|
|
1010
|
+
loading ??= loadIndex(base);
|
|
1011
|
+
index = await loading;
|
|
1012
|
+
if (index === undefined) {
|
|
1013
|
+
status.textContent = "Search is unavailable for this site.";
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
try {
|
|
1018
|
+
hits = await search(index, base, input.value);
|
|
1019
|
+
} catch {
|
|
1020
|
+
hits = [];
|
|
1021
|
+
}
|
|
1022
|
+
active = hits.length === 0 ? -1 : 0;
|
|
1023
|
+
render();
|
|
1024
|
+
const current = index;
|
|
1025
|
+
Promise.all(hits.slice(0, WORDCELL_SITE_LIMITS_V1.searchHydration).map((hit) => hydrate(current, base, hit.doc.s))).then(() => render());
|
|
1026
|
+
})();
|
|
1027
|
+
};
|
|
1028
|
+
input.addEventListener("input", () => {
|
|
1029
|
+
dom.clearTimeout(debounce);
|
|
1030
|
+
debounce = dom.setTimeout(run, 80);
|
|
1031
|
+
});
|
|
1032
|
+
input.addEventListener("keydown", (event) => {
|
|
1033
|
+
if (event.key === "ArrowDown") {
|
|
1034
|
+
event.preventDefault();
|
|
1035
|
+
if (hits.length > 0) {
|
|
1036
|
+
active = Math.min(hits.length - 1, active + 1);
|
|
1037
|
+
render();
|
|
1038
|
+
}
|
|
1039
|
+
} else if (event.key === "ArrowUp") {
|
|
1040
|
+
event.preventDefault();
|
|
1041
|
+
if (hits.length > 0) {
|
|
1042
|
+
active = Math.max(0, active - 1);
|
|
1043
|
+
render();
|
|
1044
|
+
}
|
|
1045
|
+
} else if (event.key === "Enter") {
|
|
1046
|
+
const hit = hits[active];
|
|
1047
|
+
if (hit !== undefined) {
|
|
1048
|
+
event.preventDefault();
|
|
1049
|
+
dom.location.assign(resultHref(base, hit.doc.s));
|
|
1050
|
+
}
|
|
1051
|
+
} else if (event.key === "Escape") {
|
|
1052
|
+
event.preventDefault();
|
|
1053
|
+
overlay.remove();
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
overlay.addEventListener("click", (event) => {
|
|
1057
|
+
if (event.target === overlay)
|
|
1058
|
+
overlay.remove();
|
|
1059
|
+
});
|
|
1060
|
+
return {
|
|
1061
|
+
open() {
|
|
1062
|
+
dom.document.body.appendChild(overlay);
|
|
1063
|
+
hits = [];
|
|
1064
|
+
active = -1;
|
|
1065
|
+
render();
|
|
1066
|
+
input.focus();
|
|
1067
|
+
if (index === undefined || input.value.trim() !== "")
|
|
1068
|
+
run();
|
|
1069
|
+
},
|
|
1070
|
+
close() {
|
|
1071
|
+
overlay.remove();
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
var GRAPH_MIN_SCALE = 0.05;
|
|
1076
|
+
var GRAPH_MAX_SCALE = 8;
|
|
1077
|
+
function cssVar(shell, name, fallback) {
|
|
1078
|
+
const value = dom.getComputedStyle?.(shell).getPropertyValue(name).trim();
|
|
1079
|
+
return value === undefined || value === "" ? fallback : value;
|
|
1080
|
+
}
|
|
1081
|
+
function graphTheme(shell) {
|
|
1082
|
+
return {
|
|
1083
|
+
bg: cssVar(shell, "--wordcell-bg", "#ffffff"),
|
|
1084
|
+
fg: cssVar(shell, "--wordcell-fg", "#1a1a1a"),
|
|
1085
|
+
muted: cssVar(shell, "--wordcell-muted", "#5f6368"),
|
|
1086
|
+
border: cssVar(shell, "--wordcell-border", "#e1e4e8"),
|
|
1087
|
+
accent: cssVar(shell, "--wordcell-accent", "#0b5bd3")
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
function fitView(points, width, height) {
|
|
1091
|
+
let minX = 0;
|
|
1092
|
+
let minY = 0;
|
|
1093
|
+
let maxX = 0;
|
|
1094
|
+
let maxY = 0;
|
|
1095
|
+
for (const point of points) {
|
|
1096
|
+
minX = Math.min(minX, point.x);
|
|
1097
|
+
maxX = Math.max(maxX, point.x);
|
|
1098
|
+
minY = Math.min(minY, point.y);
|
|
1099
|
+
maxY = Math.max(maxY, point.y);
|
|
1100
|
+
}
|
|
1101
|
+
const spanX = Math.max(maxX - minX, 40);
|
|
1102
|
+
const spanY = Math.max(maxY - minY, 40);
|
|
1103
|
+
const margin = 48;
|
|
1104
|
+
const scale = Math.min(GRAPH_MAX_SCALE, Math.max(GRAPH_MIN_SCALE, Math.min((width - margin * 2) / spanX, (height - margin * 2) / spanY)));
|
|
1105
|
+
return {
|
|
1106
|
+
scale,
|
|
1107
|
+
tx: width / 2 - (minX + maxX) / 2 * scale,
|
|
1108
|
+
ty: height / 2 - (minY + maxY) / 2 * scale,
|
|
1109
|
+
hover: -1,
|
|
1110
|
+
focus: -1
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
function drawGraph(context, ratio, theme, view, points, titles, degrees, edges, neighborFocus) {
|
|
1114
|
+
const { scale, tx, ty } = view;
|
|
1115
|
+
context.setTransform(scale * ratio, 0, 0, scale * ratio, tx * ratio, ty * ratio);
|
|
1116
|
+
const inv = 1 / scale;
|
|
1117
|
+
for (const edge of edges) {
|
|
1118
|
+
const a = points[edge.s];
|
|
1119
|
+
const b = points[edge.t];
|
|
1120
|
+
if (a === undefined || b === undefined)
|
|
1121
|
+
continue;
|
|
1122
|
+
const focused = view.focus >= 0 && (edge.s === view.focus || edge.t === view.focus);
|
|
1123
|
+
const hovered = view.hover >= 0 && (edge.s === view.hover || edge.t === view.hover);
|
|
1124
|
+
context.beginPath();
|
|
1125
|
+
context.moveTo(a.x, a.y);
|
|
1126
|
+
context.lineTo(b.x, b.y);
|
|
1127
|
+
context.lineWidth = (focused || hovered ? 1.8 : 1) * inv;
|
|
1128
|
+
context.strokeStyle = focused || hovered ? theme.accent : theme.border;
|
|
1129
|
+
context.globalAlpha = focused || hovered ? 1 : 0.7;
|
|
1130
|
+
context.setLineDash(edge.kind === "relation" ? [4 * inv, 3 * inv] : []);
|
|
1131
|
+
context.stroke();
|
|
1132
|
+
}
|
|
1133
|
+
context.setLineDash([]);
|
|
1134
|
+
context.globalAlpha = 1;
|
|
1135
|
+
const labelEverywhere = points.length <= 48;
|
|
1136
|
+
for (const [index, point] of points.entries()) {
|
|
1137
|
+
const degree = degrees[index] ?? 0;
|
|
1138
|
+
const radius = Math.min(9, 3 + degree * 0.8) * inv;
|
|
1139
|
+
const active = index === view.hover || index === view.focus || neighborFocus.has(index);
|
|
1140
|
+
context.beginPath();
|
|
1141
|
+
context.arc(point.x, point.y, radius, 0, Math.PI * 2);
|
|
1142
|
+
context.fillStyle = active ? theme.accent : theme.muted;
|
|
1143
|
+
context.fill();
|
|
1144
|
+
context.lineWidth = 1.2 * inv;
|
|
1145
|
+
context.strokeStyle = theme.bg;
|
|
1146
|
+
context.stroke();
|
|
1147
|
+
}
|
|
1148
|
+
context.font = `${11 / scale}px ui-sans-serif, system-ui, sans-serif`;
|
|
1149
|
+
context.textAlign = "center";
|
|
1150
|
+
context.textBaseline = "top";
|
|
1151
|
+
for (const [index, point] of points.entries()) {
|
|
1152
|
+
const degree = degrees[index] ?? 0;
|
|
1153
|
+
const active = index === view.hover || index === view.focus || neighborFocus.has(index);
|
|
1154
|
+
if (!labelEverywhere && !active && degree < 3)
|
|
1155
|
+
continue;
|
|
1156
|
+
context.fillStyle = active ? theme.fg : theme.muted;
|
|
1157
|
+
context.fillText(titles[index] ?? "", point.x, point.y + (Math.min(9, 3 + degree * 0.8) + 4) * inv);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
function initGraph(base) {
|
|
1161
|
+
const shell = dom.document.querySelector("[data-wordcell-graph]");
|
|
1162
|
+
if (shell === null)
|
|
1163
|
+
return;
|
|
1164
|
+
const canvas = shell.querySelector("[data-wordcell-graph-canvas]");
|
|
1165
|
+
const status = shell.querySelector("[data-wordcell-graph-status]");
|
|
1166
|
+
const reset = shell.querySelector("[data-wordcell-graph-reset]");
|
|
1167
|
+
if (canvas === null)
|
|
1168
|
+
return;
|
|
1169
|
+
const context = canvas.getContext("2d");
|
|
1170
|
+
if (context === null) {
|
|
1171
|
+
if (status !== null)
|
|
1172
|
+
status.textContent = "Canvas is unavailable; every note is listed below.";
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
(async () => {
|
|
1176
|
+
const manifestRaw = await fetchJson(base, "manifest.json");
|
|
1177
|
+
if (manifestRaw === undefined)
|
|
1178
|
+
throw new Error("manifest fetch failed");
|
|
1179
|
+
const manifest = parseSiteManifestV1(manifestRaw);
|
|
1180
|
+
const [catalogRaw, graphRaw] = await Promise.all([
|
|
1181
|
+
fetchJson(base, manifest.paths.catalog),
|
|
1182
|
+
fetchJson(base, manifest.paths.graph)
|
|
1183
|
+
]);
|
|
1184
|
+
if (catalogRaw === undefined || graphRaw === undefined)
|
|
1185
|
+
throw new Error("graph data fetch failed");
|
|
1186
|
+
const catalog = parseSiteCatalogV1(catalogRaw);
|
|
1187
|
+
const graph = parseSiteGraphV1(graphRaw);
|
|
1188
|
+
const nodes = catalog.entries;
|
|
1189
|
+
if (nodes.length > WORDCELL_SITE_GRAPH_VIEW_LIMIT) {
|
|
1190
|
+
if (status !== null) {
|
|
1191
|
+
status.textContent = `This site has ${nodes.length} notes — over the ${WORDCELL_SITE_GRAPH_VIEW_LIMIT}-node interactive limit. Every note is listed below.`;
|
|
1192
|
+
}
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
const edges = [];
|
|
1196
|
+
const drawn = [];
|
|
1197
|
+
for (const edge of graph.edges) {
|
|
1198
|
+
if (edge.s === edge.t || edge.s < 0 || edge.t < 0)
|
|
1199
|
+
continue;
|
|
1200
|
+
if (edge.s >= nodes.length || edge.t >= nodes.length)
|
|
1201
|
+
continue;
|
|
1202
|
+
edges.push({ s: edge.s, t: edge.t });
|
|
1203
|
+
drawn.push({ s: edge.s, t: edge.t, kind: edge.k });
|
|
1204
|
+
}
|
|
1205
|
+
const slugs = nodes.map((entry) => entry.s);
|
|
1206
|
+
const titles = nodes.map((entry) => entry.t);
|
|
1207
|
+
const seed = siteGraphSeed(slugs, edges);
|
|
1208
|
+
const points = layoutSiteGraph(nodes.length, edges, { seed });
|
|
1209
|
+
const degrees = siteGraphDegrees(nodes.length, edges);
|
|
1210
|
+
const hrefFor = (index) => resultHref(base, slugs[index] ?? "");
|
|
1211
|
+
const rect = canvas.getBoundingClientRect();
|
|
1212
|
+
const ratio = dom.devicePixelRatio ?? 1;
|
|
1213
|
+
const cssWidth = Math.max(320, rect.width || 960);
|
|
1214
|
+
const cssHeight = Math.max(280, rect.height || 560);
|
|
1215
|
+
canvas.width = Math.round(cssWidth * ratio);
|
|
1216
|
+
canvas.height = Math.round(cssHeight * ratio);
|
|
1217
|
+
let view = fitView(points, cssWidth, cssHeight);
|
|
1218
|
+
const theme = { current: graphTheme(shell) };
|
|
1219
|
+
let focusNeighbors = new Set;
|
|
1220
|
+
const neighborsOf = (index) => {
|
|
1221
|
+
const neighbors = new Set;
|
|
1222
|
+
for (const edge of drawn) {
|
|
1223
|
+
if (edge.s === index)
|
|
1224
|
+
neighbors.add(edge.t);
|
|
1225
|
+
if (edge.t === index)
|
|
1226
|
+
neighbors.add(edge.s);
|
|
1227
|
+
}
|
|
1228
|
+
return neighbors;
|
|
1229
|
+
};
|
|
1230
|
+
const draw = () => {
|
|
1231
|
+
context.setTransform(1, 0, 0, 1, 0, 0);
|
|
1232
|
+
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
1233
|
+
context.setTransform(ratio, 0, 0, ratio, 0, 0);
|
|
1234
|
+
context.fillStyle = theme.current.bg;
|
|
1235
|
+
context.fillRect(0, 0, cssWidth, cssHeight);
|
|
1236
|
+
drawGraph(context, ratio, theme.current, view, points, titles, degrees, drawn, focusNeighbors);
|
|
1237
|
+
};
|
|
1238
|
+
let scheduled = false;
|
|
1239
|
+
const redraw = () => {
|
|
1240
|
+
if (dom.requestAnimationFrame === undefined) {
|
|
1241
|
+
draw();
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
if (scheduled)
|
|
1245
|
+
return;
|
|
1246
|
+
scheduled = true;
|
|
1247
|
+
dom.requestAnimationFrame(() => {
|
|
1248
|
+
scheduled = false;
|
|
1249
|
+
draw();
|
|
1250
|
+
});
|
|
1251
|
+
};
|
|
1252
|
+
const nodeAt = (clientX, clientY) => {
|
|
1253
|
+
const bounds = canvas.getBoundingClientRect();
|
|
1254
|
+
const px = clientX - bounds.left;
|
|
1255
|
+
const py = clientY - bounds.top;
|
|
1256
|
+
let best = -1;
|
|
1257
|
+
let bestDistance = 14;
|
|
1258
|
+
for (const [index, point] of points.entries()) {
|
|
1259
|
+
const sx = point.x * view.scale + view.tx;
|
|
1260
|
+
const sy = point.y * view.scale + view.ty;
|
|
1261
|
+
const distance = Math.sqrt((sx - px) * (sx - px) + (sy - py) * (sy - py));
|
|
1262
|
+
if (distance < bestDistance) {
|
|
1263
|
+
bestDistance = distance;
|
|
1264
|
+
best = index;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
return best;
|
|
1268
|
+
};
|
|
1269
|
+
const focusSlug = () => {
|
|
1270
|
+
const hash = dom.location.hash ?? "";
|
|
1271
|
+
const match = /^#n=(.+)$/u.exec(hash);
|
|
1272
|
+
if (match === null)
|
|
1273
|
+
return;
|
|
1274
|
+
let slug = match[1] ?? "";
|
|
1275
|
+
try {
|
|
1276
|
+
slug = decodeURIComponent(slug);
|
|
1277
|
+
} catch {
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
const index = slugs.indexOf(slug);
|
|
1281
|
+
if (index === -1)
|
|
1282
|
+
return;
|
|
1283
|
+
view.focus = index;
|
|
1284
|
+
focusNeighbors = neighborsOf(index);
|
|
1285
|
+
const point = points[index];
|
|
1286
|
+
if (point !== undefined) {
|
|
1287
|
+
view.tx = cssWidth / 2 - point.x * view.scale;
|
|
1288
|
+
view.ty = cssHeight / 2 - point.y * view.scale;
|
|
1289
|
+
}
|
|
1290
|
+
redraw();
|
|
1291
|
+
};
|
|
1292
|
+
let dragFrom;
|
|
1293
|
+
let dragged = false;
|
|
1294
|
+
canvas.addEventListener("pointerdown", (event) => {
|
|
1295
|
+
dragFrom = { x: event.clientX ?? 0, y: event.clientY ?? 0 };
|
|
1296
|
+
dragged = false;
|
|
1297
|
+
canvas.style.cursor = "grabbing";
|
|
1298
|
+
});
|
|
1299
|
+
canvas.addEventListener("pointermove", (event) => {
|
|
1300
|
+
const clientX = event.clientX ?? 0;
|
|
1301
|
+
const clientY = event.clientY ?? 0;
|
|
1302
|
+
if (dragFrom !== undefined) {
|
|
1303
|
+
const deltaX = clientX - dragFrom.x;
|
|
1304
|
+
const deltaY = clientY - dragFrom.y;
|
|
1305
|
+
if (Math.abs(deltaX) + Math.abs(deltaY) > 4)
|
|
1306
|
+
dragged = true;
|
|
1307
|
+
if (dragged) {
|
|
1308
|
+
view.tx += deltaX;
|
|
1309
|
+
view.ty += deltaY;
|
|
1310
|
+
dragFrom = { x: clientX, y: clientY };
|
|
1311
|
+
redraw();
|
|
1312
|
+
}
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
const hit = nodeAt(clientX, clientY);
|
|
1316
|
+
if (hit !== view.hover) {
|
|
1317
|
+
view.hover = hit;
|
|
1318
|
+
canvas.style.cursor = hit >= 0 ? "pointer" : "grab";
|
|
1319
|
+
redraw();
|
|
1320
|
+
}
|
|
1321
|
+
});
|
|
1322
|
+
canvas.addEventListener("pointerup", (event) => {
|
|
1323
|
+
canvas.style.cursor = "grab";
|
|
1324
|
+
const wasDrag = dragged;
|
|
1325
|
+
dragFrom = undefined;
|
|
1326
|
+
dragged = false;
|
|
1327
|
+
if (wasDrag)
|
|
1328
|
+
return;
|
|
1329
|
+
const hit = nodeAt(event.clientX ?? 0, event.clientY ?? 0);
|
|
1330
|
+
if (hit >= 0)
|
|
1331
|
+
dom.location.assign(hrefFor(hit));
|
|
1332
|
+
});
|
|
1333
|
+
canvas.addEventListener("pointerleave", () => {
|
|
1334
|
+
dragFrom = undefined;
|
|
1335
|
+
dragged = false;
|
|
1336
|
+
if (view.hover !== -1) {
|
|
1337
|
+
view.hover = -1;
|
|
1338
|
+
redraw();
|
|
1339
|
+
}
|
|
1340
|
+
});
|
|
1341
|
+
canvas.addEventListener("wheel", (event) => {
|
|
1342
|
+
event.preventDefault();
|
|
1343
|
+
const bounds = canvas.getBoundingClientRect();
|
|
1344
|
+
const px = (event.clientX ?? 0) - bounds.left;
|
|
1345
|
+
const py = (event.clientY ?? 0) - bounds.top;
|
|
1346
|
+
const factor = Math.exp(-(event.deltaY ?? 0) * 0.0015);
|
|
1347
|
+
const next = Math.min(GRAPH_MAX_SCALE, Math.max(GRAPH_MIN_SCALE, view.scale * factor));
|
|
1348
|
+
const applied = next / view.scale;
|
|
1349
|
+
view.tx = px - (px - view.tx) * applied;
|
|
1350
|
+
view.ty = py - (py - view.ty) * applied;
|
|
1351
|
+
view.scale = next;
|
|
1352
|
+
redraw();
|
|
1353
|
+
});
|
|
1354
|
+
canvas.addEventListener("keydown", (event) => {
|
|
1355
|
+
const panStep = 48;
|
|
1356
|
+
if (event.key === "ArrowLeft")
|
|
1357
|
+
view.tx += panStep;
|
|
1358
|
+
else if (event.key === "ArrowRight")
|
|
1359
|
+
view.tx -= panStep;
|
|
1360
|
+
else if (event.key === "ArrowUp")
|
|
1361
|
+
view.ty += panStep;
|
|
1362
|
+
else if (event.key === "ArrowDown")
|
|
1363
|
+
view.ty -= panStep;
|
|
1364
|
+
else if (event.key === "+" || event.key === "=") {
|
|
1365
|
+
view.scale = Math.min(GRAPH_MAX_SCALE, view.scale * 1.25);
|
|
1366
|
+
} else if (event.key === "-" || event.key === "_") {
|
|
1367
|
+
view.scale = Math.max(GRAPH_MIN_SCALE, view.scale / 1.25);
|
|
1368
|
+
} else if (event.key === "0" || event.key === "Escape") {
|
|
1369
|
+
view = fitView(points, cssWidth, cssHeight);
|
|
1370
|
+
} else if (event.key === "Enter") {
|
|
1371
|
+
const target = view.hover >= 0 ? view.hover : view.focus;
|
|
1372
|
+
if (target < 0)
|
|
1373
|
+
return;
|
|
1374
|
+
dom.location.assign(hrefFor(target));
|
|
1375
|
+
} else {
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
event.preventDefault();
|
|
1379
|
+
redraw();
|
|
1380
|
+
});
|
|
1381
|
+
reset?.addEventListener("click", () => {
|
|
1382
|
+
view = fitView(points, cssWidth, cssHeight);
|
|
1383
|
+
redraw();
|
|
1384
|
+
});
|
|
1385
|
+
const repaintTheme = () => {
|
|
1386
|
+
theme.current = graphTheme(shell);
|
|
1387
|
+
redraw();
|
|
1388
|
+
};
|
|
1389
|
+
wordcellAppearanceBridge()?.subscribe(repaintTheme);
|
|
1390
|
+
dom.matchMedia?.("(prefers-color-scheme: dark)").addEventListener("change", repaintTheme);
|
|
1391
|
+
if (status !== null)
|
|
1392
|
+
status.hidden = true;
|
|
1393
|
+
dom.addEventListener?.("hashchange", focusSlug);
|
|
1394
|
+
focusSlug();
|
|
1395
|
+
draw();
|
|
1396
|
+
})().catch(() => {
|
|
1397
|
+
if (status !== null) {
|
|
1398
|
+
status.hidden = false;
|
|
1399
|
+
status.textContent = "The graph data could not be loaded; every note is listed below.";
|
|
1400
|
+
}
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
function initAppearance() {
|
|
1404
|
+
const bridge = wordcellAppearanceBridge();
|
|
1405
|
+
const actions = dom.document.querySelector(".site-actions");
|
|
1406
|
+
if (bridge === undefined || actions === null)
|
|
1407
|
+
return;
|
|
1408
|
+
const wrap = dom.document.createElement("div");
|
|
1409
|
+
wrap.className = "appearance";
|
|
1410
|
+
const trigger = dom.document.createElement("button");
|
|
1411
|
+
trigger.type = "button";
|
|
1412
|
+
trigger.className = "appearance-trigger";
|
|
1413
|
+
trigger.setAttribute("aria-haspopup", "dialog");
|
|
1414
|
+
trigger.setAttribute("aria-expanded", "false");
|
|
1415
|
+
trigger.setAttribute("aria-label", "Appearance");
|
|
1416
|
+
trigger.innerHTML = '<svg width="15" height="15" viewBox="0 0 16 16" aria-hidden="true">' + '<circle cx="8" cy="8" r="6.6" fill="none" stroke="currentColor" stroke-width="1.4"/>' + '<path d="M8 1.4a6.6 6.6 0 0 1 0 13.2z" fill="currentColor"/></svg>';
|
|
1417
|
+
const menu = dom.document.createElement("div");
|
|
1418
|
+
menu.className = "appearance-menu";
|
|
1419
|
+
menu.hidden = true;
|
|
1420
|
+
menu.setAttribute("role", "dialog");
|
|
1421
|
+
menu.setAttribute("aria-label", "Appearance");
|
|
1422
|
+
const paletteInputs = new Map;
|
|
1423
|
+
const modeInputs = new Map;
|
|
1424
|
+
const modeGroup = dom.document.createElement("div");
|
|
1425
|
+
modeGroup.className = "appearance-group";
|
|
1426
|
+
const modeLabel = dom.document.createElement("div");
|
|
1427
|
+
modeLabel.className = "appearance-label";
|
|
1428
|
+
modeLabel.textContent = "Appearance";
|
|
1429
|
+
modeGroup.appendChild(modeLabel);
|
|
1430
|
+
for (const mode of wordcellAppearanceModes) {
|
|
1431
|
+
const label = dom.document.createElement("label");
|
|
1432
|
+
label.className = "appearance-option";
|
|
1433
|
+
const input = dom.document.createElement("input");
|
|
1434
|
+
input.type = "radio";
|
|
1435
|
+
input.name = "wordcell-appearance-mode";
|
|
1436
|
+
input.value = mode;
|
|
1437
|
+
input.addEventListener("change", () => {
|
|
1438
|
+
bridge.set({ palette: bridge.get().palette, mode });
|
|
1439
|
+
});
|
|
1440
|
+
modeInputs.set(mode, input);
|
|
1441
|
+
const text = dom.document.createElement("span");
|
|
1442
|
+
text.textContent = wordcellAppearanceModeLabels[mode];
|
|
1443
|
+
label.appendChild(input);
|
|
1444
|
+
label.appendChild(text);
|
|
1445
|
+
modeGroup.appendChild(label);
|
|
1446
|
+
}
|
|
1447
|
+
const paletteGroup = dom.document.createElement("div");
|
|
1448
|
+
paletteGroup.className = "appearance-group";
|
|
1449
|
+
const paletteLabel = dom.document.createElement("div");
|
|
1450
|
+
paletteLabel.className = "appearance-label";
|
|
1451
|
+
paletteLabel.textContent = "Palette";
|
|
1452
|
+
paletteGroup.appendChild(paletteLabel);
|
|
1453
|
+
for (const palette of wordcellPalettes) {
|
|
1454
|
+
const label = dom.document.createElement("label");
|
|
1455
|
+
label.className = "appearance-option";
|
|
1456
|
+
const input = dom.document.createElement("input");
|
|
1457
|
+
input.type = "radio";
|
|
1458
|
+
input.name = "wordcell-appearance-palette";
|
|
1459
|
+
input.value = palette;
|
|
1460
|
+
input.addEventListener("change", () => {
|
|
1461
|
+
bridge.set({ palette, mode: bridge.get().mode });
|
|
1462
|
+
});
|
|
1463
|
+
paletteInputs.set(palette, input);
|
|
1464
|
+
const swatch = dom.document.createElement("span");
|
|
1465
|
+
swatch.className = `swatch swatch-${palette}`;
|
|
1466
|
+
const text = dom.document.createElement("span");
|
|
1467
|
+
text.textContent = wordcellPaletteLabels[palette];
|
|
1468
|
+
label.appendChild(input);
|
|
1469
|
+
label.appendChild(swatch);
|
|
1470
|
+
label.appendChild(text);
|
|
1471
|
+
paletteGroup.appendChild(label);
|
|
1472
|
+
}
|
|
1473
|
+
menu.appendChild(modeGroup);
|
|
1474
|
+
menu.appendChild(paletteGroup);
|
|
1475
|
+
wrap.appendChild(trigger);
|
|
1476
|
+
wrap.appendChild(menu);
|
|
1477
|
+
actions.appendChild(wrap);
|
|
1478
|
+
const sync = () => {
|
|
1479
|
+
const preference = bridge.get();
|
|
1480
|
+
for (const [mode, input] of modeInputs)
|
|
1481
|
+
input.checked = mode === preference.mode;
|
|
1482
|
+
for (const [palette, input] of paletteInputs)
|
|
1483
|
+
input.checked = palette === preference.palette;
|
|
1484
|
+
};
|
|
1485
|
+
const close = () => {
|
|
1486
|
+
menu.hidden = true;
|
|
1487
|
+
trigger.setAttribute("aria-expanded", "false");
|
|
1488
|
+
};
|
|
1489
|
+
const open = () => {
|
|
1490
|
+
sync();
|
|
1491
|
+
menu.hidden = false;
|
|
1492
|
+
trigger.setAttribute("aria-expanded", "true");
|
|
1493
|
+
paletteInputs.get(bridge.get().palette)?.focus();
|
|
1494
|
+
};
|
|
1495
|
+
trigger.addEventListener("click", () => {
|
|
1496
|
+
if (menu.hidden)
|
|
1497
|
+
open();
|
|
1498
|
+
else
|
|
1499
|
+
close();
|
|
1500
|
+
});
|
|
1501
|
+
dom.document.addEventListener("keydown", (event) => {
|
|
1502
|
+
if (event.key === "Escape" && !menu.hidden) {
|
|
1503
|
+
close();
|
|
1504
|
+
trigger.focus();
|
|
1505
|
+
}
|
|
1506
|
+
});
|
|
1507
|
+
dom.document.addEventListener("click", (event) => {
|
|
1508
|
+
if (!menu.hidden && event.target?.closest?.(".appearance") == null)
|
|
1509
|
+
close();
|
|
1510
|
+
});
|
|
1511
|
+
bridge.subscribe(sync);
|
|
1512
|
+
sync();
|
|
1513
|
+
}
|
|
1514
|
+
function start() {
|
|
1515
|
+
const content = dom.document.querySelector('meta[name="wordcell:base"]')?.getAttribute("content") ?? "./";
|
|
1516
|
+
const base = content.endsWith("/") ? content : `${content}/`;
|
|
1517
|
+
initAppearance();
|
|
1518
|
+
const overlay = buildOverlay(base);
|
|
1519
|
+
for (const button of Array.from(dom.document.querySelectorAll("[data-wordcell-search]"))) {
|
|
1520
|
+
button.addEventListener("click", () => overlay.open());
|
|
1521
|
+
}
|
|
1522
|
+
dom.document.addEventListener("keydown", (event) => {
|
|
1523
|
+
if (event.key === "/" && !/INPUT|TEXTAREA|SELECT/u.test(event.target?.tagName ?? "")) {
|
|
1524
|
+
event.preventDefault();
|
|
1525
|
+
overlay.open();
|
|
1526
|
+
}
|
|
1527
|
+
if (event.key === "Escape")
|
|
1528
|
+
overlay.close();
|
|
1529
|
+
});
|
|
1530
|
+
initGraph(base);
|
|
1531
|
+
}
|
|
1532
|
+
if (dom.document.querySelector("[data-wordcell-search]") !== null) {
|
|
1533
|
+
start();
|
|
1534
|
+
}
|