dsh-plugin-term-dictionary 0.0.0-stage → 1.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/CHANGELOG.md +189 -0
- package/LICENSE +21 -0
- package/README.md +776 -2
- package/cordis.patch.yml +14 -0
- package/icon.svg +13 -0
- package/lib/ROADMAP-lexicon.md +52 -0
- package/lib/client.js +13354 -0
- package/lib/core/api.js +278 -0
- package/lib/core/bus.js +98 -0
- package/lib/core/copy.js +614 -0
- package/lib/core/core.js +309 -0
- package/lib/core/dictionary.js +1187 -0
- package/lib/core/entries.js +454 -0
- package/lib/core/highlight.js +282 -0
- package/lib/core/hover.js +470 -0
- package/lib/core/hovercard.js +173 -0
- package/lib/core/interact.js +1802 -0
- package/lib/core/lexicon.en.js +872 -0
- package/lib/core/lexicon.zh.js +249 -0
- package/lib/core/overlay.js +239 -0
- package/lib/core/pack.js +372 -0
- package/lib/core/package.json +4 -0
- package/lib/core/selection.js +83 -0
- package/lib/core/settings.js +366 -0
- package/lib/core/shell.js +1003 -0
- package/lib/core/stopwords.js +147 -0
- package/lib/core/store.js +397 -0
- package/lib/core/styles.js +574 -0
- package/lib/core/terms.js +398 -0
- package/lib/core/transfer.js +382 -0
- package/lib/core/views.js +2428 -0
- package/lib/index.js +1110 -0
- package/lib/pack-code.js +84 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +71 -3
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The plugin's switches, persisted in the page.
|
|
5
|
+
*
|
|
6
|
+
* Why these live beside the dictionary instead of inside it
|
|
7
|
+
* ---------------------------------------------------------
|
|
8
|
+
* The dictionary document is MERGED: it travels to the host, comes back, and every
|
|
9
|
+
* reader has to agree on it. A preference is not like that. There is one user and one
|
|
10
|
+
* page, nothing to reconcile, and putting a switch in the merged document would drag it
|
|
11
|
+
* through the whole tombstone/revival machinery — and would make two windows fight over
|
|
12
|
+
* a setting neither of them shares a meaning for. So the switches get their own
|
|
13
|
+
* `localStorage` key and the document keeps only entries.
|
|
14
|
+
*
|
|
15
|
+
* Failure is not fatal: an unreadable or unavailable store yields the defaults, because
|
|
16
|
+
* a plugin that cannot remember "auto-collect is off" must still collect nothing rather
|
|
17
|
+
* than refuse to start.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** The storage key. Versioned so a future shape change can migrate rather than guess. */
|
|
21
|
+
const SETTINGS_KEY = "dsh-plugin-term-dictionary:settings:v1";
|
|
22
|
+
|
|
23
|
+
/** The most sources the list will hold. A UI bound, not a security one. */
|
|
24
|
+
const MAX_SOURCES = 12;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The source a fresh install starts with: this plugin's own repository, as a static file.
|
|
28
|
+
*
|
|
29
|
+
* Why ship one at all: without it the packs page opens on an empty list, and a first-time reader has
|
|
30
|
+
* to be TOLD a URL before they can see what a term pack even is. The alternative — fetching from a
|
|
31
|
+
* server we run — is the thing this design refuses to have, so the default is an ordinary source like
|
|
32
|
+
* any other: visible in the list, removable in one press, and fetched only when the reader asks (the
|
|
33
|
+
* page refreshes it on first open, which is a request to the source THEY have configured).
|
|
34
|
+
*
|
|
35
|
+
* `@main` rather than a tag because a default should follow the packs the repository actually has:
|
|
36
|
+
* pinned to a tag, a new pack would need a plugin release before anyone could see it.
|
|
37
|
+
*/
|
|
38
|
+
const DEFAULT_PACK_SOURCE = "https://cdn.jsdelivr.net/gh/lakerian/dsh-plugin-term-dictionary@main/packs/index.json";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The URL rule, imported rather than restated: the settings store, the page and the host's transport
|
|
42
|
+
* must agree on what a usable source is, and two copies of that rule is how one of them drifts.
|
|
43
|
+
*/
|
|
44
|
+
const { refuseUrl } = require("./pack.js");
|
|
45
|
+
|
|
46
|
+
const ENTRY_COLUMNS = ["auto", "1", "2", "3"];
|
|
47
|
+
|
|
48
|
+
/** The two row-height readings: equal cards, or each card as tall as its own text. */
|
|
49
|
+
const ENTRY_ROWS = ["uniform", "compact"];
|
|
50
|
+
|
|
51
|
+
/** The narrowest a column may be when the layout is choosing. Below this a gloss wraps every other word. */
|
|
52
|
+
const MIN_COLUMN_PX = 260;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The three switches and their defaults.
|
|
56
|
+
*
|
|
57
|
+
* `autoExplain` defaults ON because the complaint that produced it was the opposite:
|
|
58
|
+
* collected terms arrived with no explanation and stayed "待补充" forever, which made the
|
|
59
|
+
* dictionary a list of words rather than a dictionary.
|
|
60
|
+
*/
|
|
61
|
+
const DEFAULT_SETTINGS = {
|
|
62
|
+
/** Collect rare terminology from agent replies automatically. */
|
|
63
|
+
autoCollect: true,
|
|
64
|
+
/** Mark collected terms in the transcript (the "词条块"). */
|
|
65
|
+
autoAnnotate: true,
|
|
66
|
+
/** Ask the model for an explanation as soon as a term is collected. */
|
|
67
|
+
autoExplain: true,
|
|
68
|
+
/** Which language an explanation is written in: the UI's, or one forced. */
|
|
69
|
+
explainLang: "auto",
|
|
70
|
+
/** How much an explanation says. */
|
|
71
|
+
explainDepth: "normal",
|
|
72
|
+
/** Whether the model also gets the paragraph the term appeared in, not just its sentence. */
|
|
73
|
+
explainParagraph: true,
|
|
74
|
+
/**
|
|
75
|
+
* The shortest term automatic collection will record.
|
|
76
|
+
*
|
|
77
|
+
* Four by default: below that a "term" is a fragment (`12-0D`, `id`), and the dictionary is
|
|
78
|
+
* worse for holding it.
|
|
79
|
+
*/
|
|
80
|
+
collectMinLength: 4,
|
|
81
|
+
/**
|
|
82
|
+
* Whether code-shaped names are collected (`data-conversation-region`, `Set-Content`).
|
|
83
|
+
*
|
|
84
|
+
* OFF by default. When the dictionary was cleaned out, every junk entry was code-shaped and
|
|
85
|
+
* none of the real ones were — so this is the switch that would have prevented it.
|
|
86
|
+
*/
|
|
87
|
+
collectIdentifiers: false,
|
|
88
|
+
/** Whether Chinese terms are collected. */
|
|
89
|
+
collectCjk: true,
|
|
90
|
+
/**
|
|
91
|
+
* How long the pointer must rest on a term before the hover card appears, in milliseconds.
|
|
92
|
+
*
|
|
93
|
+
* The hard-coded 110ms, exposed: a slower reader wants a steadier card, a faster one is
|
|
94
|
+
* annoyed by it.
|
|
95
|
+
*/
|
|
96
|
+
hoverInMs: 110,
|
|
97
|
+
/**
|
|
98
|
+
* How long the card lingers after the pointer leaves the term, in milliseconds.
|
|
99
|
+
*
|
|
100
|
+
* ZERO by default, and that default is a decision rather than an omission: a delay on the EXIT
|
|
101
|
+
* was removed once already because it read as lag — the card sat there for the whole delay after
|
|
102
|
+
* the pointer had gone. Anyone who wants a grace period may have one; nobody gets it by accident.
|
|
103
|
+
*/
|
|
104
|
+
hoverOutMs: 0,
|
|
105
|
+
/**
|
|
106
|
+
* How many columns the entry list is laid out in.
|
|
107
|
+
*
|
|
108
|
+
* `auto` fits as many as the panel is wide enough for, and is the default: a dictionary row is
|
|
109
|
+
* short, a single column of them is a very long strip to read, and a narrow panel simply gets one
|
|
110
|
+
* column back. The fixed values exist for a reader who wants the layout to stop moving under them.
|
|
111
|
+
*/
|
|
112
|
+
entryColumns: "auto",
|
|
113
|
+
/**
|
|
114
|
+
* How tall a row of the entry list is.
|
|
115
|
+
*
|
|
116
|
+
* `uniform` gives every card in a grid row the height of the tallest, so the cards line up and no
|
|
117
|
+
* empty strip is left under the short ones. `compact` lets each keep its own height, which reads
|
|
118
|
+
* tighter in a single column and leaves exactly those strips in a grid. Both are defensible, which
|
|
119
|
+
* is why it is a preference rather than a constant.
|
|
120
|
+
*/
|
|
121
|
+
entryRows: "uniform",
|
|
122
|
+
/**
|
|
123
|
+
* The term-pack sources the user added: https URLs of static `index.json` files.
|
|
124
|
+
*
|
|
125
|
+
* A LIST, which is a fourth value shape next to the switches, the option bars and the numbers —
|
|
126
|
+
* and configuration rather than page state, because a source that disappears when the panel closes
|
|
127
|
+
* is not configuration. Validated by the same rule the host applies before it fetches anything
|
|
128
|
+
* ({@link module:core/pack.refuseUrl}), so the page and the transport cannot disagree about what a
|
|
129
|
+
* usable source is.
|
|
130
|
+
*
|
|
131
|
+
* The default is not empty: see {@link DEFAULT_PACK_SOURCE}. An ABSENT key means "never touched, use
|
|
132
|
+
* the default"; an empty array means "the reader removed them all", which is honoured (§
|
|
133
|
+
* `normalizeSettings`) — the two are different statements and a store that conflated them would
|
|
134
|
+
* resurrect the default every time somebody cleared the list.
|
|
135
|
+
*/
|
|
136
|
+
packSources: [DEFAULT_PACK_SOURCE]
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/** The accepted minimum term lengths, for the panel's cycling control. */
|
|
140
|
+
const COLLECT_LENGTHS = [2, 3, 4, 6, 8];
|
|
141
|
+
|
|
142
|
+
/** The accepted hover delays, in milliseconds: the bounds for both the slider and manual entry. */
|
|
143
|
+
const HOVER_DELAY_MIN = 0;
|
|
144
|
+
const HOVER_DELAY_MAX = 200;
|
|
145
|
+
|
|
146
|
+
/** The step the hover slider moves in. */
|
|
147
|
+
const HOVER_DELAY_STEP = 10;
|
|
148
|
+
|
|
149
|
+
/** The accepted explanation languages. `auto` follows the active UI language. */
|
|
150
|
+
const EXPLAIN_LANGS = ["auto", "zh", "en"];
|
|
151
|
+
|
|
152
|
+
/** The accepted explanation depths. */
|
|
153
|
+
const EXPLAIN_DEPTHS = ["brief", "normal", "detailed"];
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Read one switch's value from an unknown record.
|
|
157
|
+
* @param value - the raw stored value.
|
|
158
|
+
* @param fallback - the default.
|
|
159
|
+
* @returns a boolean.
|
|
160
|
+
*/
|
|
161
|
+
function readFlag(value, fallback) {
|
|
162
|
+
return typeof value === "boolean" ? value : fallback;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Read one choice from an unknown record.
|
|
167
|
+
*
|
|
168
|
+
* An unrecognized value falls back rather than being stored: a hand-edited or older record
|
|
169
|
+
* must not be able to put a value in the prompt builder that it has no branch for.
|
|
170
|
+
*
|
|
171
|
+
* @param value - the raw stored value.
|
|
172
|
+
* @param allowed - the accepted values.
|
|
173
|
+
* @param fallback - the default.
|
|
174
|
+
* @returns one of `allowed`.
|
|
175
|
+
*/
|
|
176
|
+
function readChoice(value, allowed, fallback) {
|
|
177
|
+
// `includes` is a strict comparison, so this admits a NUMBER list (the minimum term length)
|
|
178
|
+
// as well as a string one, and an object or array can still never match a member.
|
|
179
|
+
return allowed.includes(value) ? value : fallback;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Read the source list from an unknown record.
|
|
184
|
+
*
|
|
185
|
+
* Every entry is put through the host's own URL rule, so a stored list can never contain something the
|
|
186
|
+
* fetch would later refuse — a source that cannot work is not kept. Duplicates are collapsed and the
|
|
187
|
+
* list is capped: the cap is a UI bound (a list of twenty sources is a list nobody reads), not a
|
|
188
|
+
* security one.
|
|
189
|
+
*
|
|
190
|
+
* @param value - the raw stored value.
|
|
191
|
+
* @returns a fresh array of https URLs.
|
|
192
|
+
*/
|
|
193
|
+
function readSources(value) {
|
|
194
|
+
if (!Array.isArray(value)) return [];
|
|
195
|
+
const sources = [];
|
|
196
|
+
for (const entry of value) {
|
|
197
|
+
if (typeof entry !== "string") continue;
|
|
198
|
+
const url = entry.trim();
|
|
199
|
+
if (refuseUrl(url) !== null) continue;
|
|
200
|
+
if (sources.includes(url)) continue;
|
|
201
|
+
sources.push(url);
|
|
202
|
+
if (sources.length >= MAX_SOURCES) break;
|
|
203
|
+
}
|
|
204
|
+
return sources;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Read one delay from an unknown record, as a whole number of milliseconds inside the range. *
|
|
209
|
+
* Clamped rather than rejected, and rounded rather than trusted: a slider and a text field feed the
|
|
210
|
+
* same setter, so `"120"`, `120.4` and `5000` all arrive here, and a delay outside the range is a
|
|
211
|
+
* worse outcome than the nearest valid one.
|
|
212
|
+
*
|
|
213
|
+
* @param value - the raw stored value.
|
|
214
|
+
* @param fallback - the default.
|
|
215
|
+
* @returns an integer between {@link HOVER_DELAY_MIN} and {@link HOVER_DELAY_MAX}.
|
|
216
|
+
*/
|
|
217
|
+
function readDelay(value, fallback) {
|
|
218
|
+
const number = typeof value === "number" ? value : Number.parseFloat(value);
|
|
219
|
+
if (!Number.isFinite(number)) return fallback;
|
|
220
|
+
return Math.min(HOVER_DELAY_MAX, Math.max(HOVER_DELAY_MIN, Math.round(number)));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Normalize a stored settings record.
|
|
225
|
+
* @param raw - the parsed record, or anything.
|
|
226
|
+
* @returns a complete settings object.
|
|
227
|
+
*/
|
|
228
|
+
function normalizeSettings(raw) {
|
|
229
|
+
const value = raw !== null && typeof raw === "object" ? raw : {};
|
|
230
|
+
return {
|
|
231
|
+
autoCollect: readFlag(value.autoCollect, DEFAULT_SETTINGS.autoCollect),
|
|
232
|
+
autoAnnotate: readFlag(value.autoAnnotate, DEFAULT_SETTINGS.autoAnnotate),
|
|
233
|
+
autoExplain: readFlag(value.autoExplain, DEFAULT_SETTINGS.autoExplain),
|
|
234
|
+
explainLang: readChoice(value.explainLang, EXPLAIN_LANGS, DEFAULT_SETTINGS.explainLang),
|
|
235
|
+
explainDepth: readChoice(value.explainDepth, EXPLAIN_DEPTHS, DEFAULT_SETTINGS.explainDepth),
|
|
236
|
+
explainParagraph: readFlag(value.explainParagraph, DEFAULT_SETTINGS.explainParagraph),
|
|
237
|
+
collectMinLength: readChoice(value.collectMinLength, COLLECT_LENGTHS, DEFAULT_SETTINGS.collectMinLength),
|
|
238
|
+
collectIdentifiers: readFlag(value.collectIdentifiers, DEFAULT_SETTINGS.collectIdentifiers),
|
|
239
|
+
collectCjk: readFlag(value.collectCjk, DEFAULT_SETTINGS.collectCjk),
|
|
240
|
+
hoverInMs: readDelay(value.hoverInMs, DEFAULT_SETTINGS.hoverInMs),
|
|
241
|
+
hoverOutMs: readDelay(value.hoverOutMs, DEFAULT_SETTINGS.hoverOutMs),
|
|
242
|
+
entryColumns: readChoice(value.entryColumns, ENTRY_COLUMNS, DEFAULT_SETTINGS.entryColumns),
|
|
243
|
+
entryRows: readChoice(value.entryRows, ENTRY_ROWS, DEFAULT_SETTINGS.entryRows),
|
|
244
|
+
// Absent means "never touched" and gets the shipped default; an empty array means the reader
|
|
245
|
+
// removed every source, and is kept as it is. A fresh array either way: a snapshot is compared by
|
|
246
|
+
// identity, so handing back the shared default array would make one page's edit appear in
|
|
247
|
+
// another page's defaults.
|
|
248
|
+
packSources: value.packSources === undefined ? [...DEFAULT_SETTINGS.packSources] : readSources(value.packSources)
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Build the page's settings store.
|
|
254
|
+
*
|
|
255
|
+
* @param options - `storage` (a `Storage`-alike) and an optional `logger`.
|
|
256
|
+
* @returns `{ getSnapshot, subscribe, set, reset, dispose }`.
|
|
257
|
+
*/
|
|
258
|
+
function createSettingsStore(options) {
|
|
259
|
+
const storage = options?.storage ?? null;
|
|
260
|
+
const logger = options?.logger ?? null;
|
|
261
|
+
let current = DEFAULT_SETTINGS;
|
|
262
|
+
|
|
263
|
+
/** Read what was persisted. Anything unusable leaves the defaults in place. */
|
|
264
|
+
function load() {
|
|
265
|
+
if (storage === null || typeof storage.getItem !== "function") return;
|
|
266
|
+
try {
|
|
267
|
+
const text = storage.getItem(SETTINGS_KEY);
|
|
268
|
+
if (typeof text !== "string" || text === "") return;
|
|
269
|
+
current = normalizeSettings(JSON.parse(text));
|
|
270
|
+
} catch (error) {
|
|
271
|
+
logger?.warn?.(`term-dictionary: reading the settings failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const listeners = new Set();
|
|
276
|
+
|
|
277
|
+
/** Tell every subscriber the settings changed. */
|
|
278
|
+
function emit() {
|
|
279
|
+
for (const listener of [...listeners]) {
|
|
280
|
+
try {
|
|
281
|
+
listener();
|
|
282
|
+
} catch (error) {
|
|
283
|
+
logger?.warn?.(`term-dictionary: a settings listener failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Write the current settings, tolerating a storage that refuses. */
|
|
289
|
+
function persist() {
|
|
290
|
+
if (storage === null || typeof storage.setItem !== "function") return;
|
|
291
|
+
try {
|
|
292
|
+
storage.setItem(SETTINGS_KEY, JSON.stringify(current));
|
|
293
|
+
} catch (error) {
|
|
294
|
+
logger?.warn?.(`term-dictionary: saving the settings failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
load();
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
/**
|
|
302
|
+
* The current settings.
|
|
303
|
+
*
|
|
304
|
+
* Returns the SAME object until something changes, because
|
|
305
|
+
* `useSyncExternalStore` compares snapshots by identity and a fresh object each
|
|
306
|
+
* call would re-render forever.
|
|
307
|
+
* @returns the settings.
|
|
308
|
+
*/
|
|
309
|
+
getSnapshot() {
|
|
310
|
+
return current;
|
|
311
|
+
},
|
|
312
|
+
/**
|
|
313
|
+
* Observe changes.
|
|
314
|
+
* @param listener - the callback.
|
|
315
|
+
* @returns the unsubscribe function.
|
|
316
|
+
*/
|
|
317
|
+
subscribe(listener) {
|
|
318
|
+
listeners.add(listener);
|
|
319
|
+
return () => listeners.delete(listener);
|
|
320
|
+
},
|
|
321
|
+
/**
|
|
322
|
+
* Set one switch.
|
|
323
|
+
* @param key - the setting's name.
|
|
324
|
+
* @param value - the new value.
|
|
325
|
+
* @returns the new settings.
|
|
326
|
+
*/
|
|
327
|
+
set(key, value) {
|
|
328
|
+
if (!Object.prototype.hasOwnProperty.call(DEFAULT_SETTINGS, key)) return current;
|
|
329
|
+
const next = normalizeSettings({ ...current, [key]: value });
|
|
330
|
+
if (next[key] === current[key]) return current;
|
|
331
|
+
current = next;
|
|
332
|
+
persist();
|
|
333
|
+
emit();
|
|
334
|
+
return current;
|
|
335
|
+
},
|
|
336
|
+
/** Put every switch back to its default. */
|
|
337
|
+
reset() {
|
|
338
|
+
current = DEFAULT_SETTINGS;
|
|
339
|
+
persist();
|
|
340
|
+
emit();
|
|
341
|
+
return current;
|
|
342
|
+
},
|
|
343
|
+
/** Drop every listener. */
|
|
344
|
+
dispose() {
|
|
345
|
+
listeners.clear();
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
module.exports = {
|
|
351
|
+
createSettingsStore,
|
|
352
|
+
SETTINGS_KEY,
|
|
353
|
+
DEFAULT_SETTINGS,
|
|
354
|
+
normalizeSettings,
|
|
355
|
+
MAX_SOURCES,
|
|
356
|
+
DEFAULT_PACK_SOURCE,
|
|
357
|
+
ENTRY_COLUMNS,
|
|
358
|
+
ENTRY_ROWS,
|
|
359
|
+
MIN_COLUMN_PX,
|
|
360
|
+
EXPLAIN_LANGS,
|
|
361
|
+
EXPLAIN_DEPTHS,
|
|
362
|
+
COLLECT_LENGTHS,
|
|
363
|
+
HOVER_DELAY_MIN,
|
|
364
|
+
HOVER_DELAY_MAX,
|
|
365
|
+
HOVER_DELAY_STEP
|
|
366
|
+
};
|