@embedpdf/plugin-commands 3.0.0-next.13 → 3.0.0-next.15

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/dist/index.cjs CHANGED
@@ -1,30 +1,214 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_contract = require("./contract-DYlaVyVk.cjs");
2
+ const require_contract = require("./contract-BlOznyrj.cjs");
3
+ const require_contract_host = require("./contract/host.cjs");
3
4
  let _embedpdf_core = require("@embedpdf/core");
4
5
  let _embedpdf_plugin_i18n_contract = require("@embedpdf/plugin-i18n/contract");
5
6
  let _embedpdf_plugin_shell_contract = require("@embedpdf/plugin-shell/contract");
6
7
  let _embedpdf_core_ui = require("@embedpdf/core-ui");
7
- //#region src/capability.ts
8
- function registerCommand(registry, def) {
9
- if (registry.has(def.id)) throw new Error(`[commands] duplicate command: ${def.id}`);
10
- const shortcuts = def.shortcut === void 0 ? [] : [].concat(def.shortcut);
11
- registry.set(def.id, {
12
- def,
8
+ //#region src/registry.ts
9
+ /** A command definition with its shortcuts parsed once, and how the registry tells a family apart. */
10
+ const registeredCommand = (definition) => {
11
+ const shortcuts = definition.shortcut === void 0 ? [] : [].concat(definition.shortcut);
12
+ return {
13
+ definition,
13
14
  shortcuts,
14
15
  parsed: shortcuts.map(_embedpdf_core_ui.parseShortcut)
16
+ };
17
+ };
18
+ const isCommandFamily = (item) => "prefix" in item;
19
+ function configuredCommands(items) {
20
+ const commands = /* @__PURE__ */ new Map();
21
+ const families = [];
22
+ for (const item of items) if (isCommandFamily(item)) families.push(item);
23
+ else commands.set(item.id, registeredCommand(item));
24
+ return {
25
+ commands,
26
+ families
27
+ };
28
+ }
29
+ //#endregion
30
+ //#region src/services/availability.ts
31
+ /**
32
+ * Whether a category is off: it, or a category it's under, is in the list. Parts are whole
33
+ * dotted segments, so `annotations` covers `annotations.highlight` and not `annotationsExtra`.
34
+ */
35
+ const isCategoryOff = (category, disabled) => disabled.some((off) => category === off || category.startsWith(`${off}.`));
36
+ /**
37
+ * The label: a language's string under the command's id, else under its `labelKey` (the current
38
+ * language, then the fallback one), else `label`, else the id. So a string for
39
+ * `'document:download'` relabels that one command, whichever key its label is under.
40
+ */
41
+ const labelOf = (definition, context) => {
42
+ const i18n = context.tryGet(_embedpdf_plugin_i18n_contract.I18nToken);
43
+ if (!i18n) return definition.label ?? definition.labelKey ?? definition.id;
44
+ const fallbackKey = definition.labelKey ? { fallbackKey: definition.labelKey } : {};
45
+ return definition.label === void 0 && definition.labelKey ? i18n.t(definition.id, fallbackKey) : i18n.t(definition.id, {
46
+ ...fallbackKey,
47
+ fallback: definition.label ?? definition.id
48
+ });
49
+ };
50
+ /**
51
+ * Whether a document's session holds a permission: what `ctx.allows` answers for a
52
+ * document-scoped plugin, asked here of the command's document, since the commands are one
53
+ * set for the whole workspace.
54
+ */
55
+ const sessionHolds = (security, permission) => permission === "annotations:create" ? security.allowsAnnotation("create") : security.allows(permission);
56
+ /** Where a language keeps the sentence for a permission: `commands.notAllowed.doc-text-copy`. */
57
+ const permissionKeyOf = (permission) => `commands.notAllowed.${permission.replace(/[.:]/g, "-")}`;
58
+ /** The general sentence's key: `{label} isn't allowed`. */
59
+ const DEFAULT_NOT_ALLOWED_KEY = "commands.notAllowed.default";
60
+ /** The sentence for a command the session may not run, under `key`, else the general one. */
61
+ const notAllowedMessage = (key, label, context) => {
62
+ const fallback = `${label} isn't allowed`;
63
+ const i18n = context.tryGet(_embedpdf_plugin_i18n_contract.I18nToken);
64
+ if (!i18n) return fallback;
65
+ const params = { label };
66
+ return i18n.t(key, {
67
+ params,
68
+ fallback: i18n.t(DEFAULT_NOT_ALLOWED_KEY, {
69
+ params,
70
+ fallback
71
+ })
15
72
  });
73
+ };
74
+ /** The sentence for a command that's turned off. */
75
+ const turnedOffMessage = (label, context) => {
76
+ const i18n = context.tryGet(_embedpdf_plugin_i18n_contract.I18nToken);
77
+ return i18n ? i18n.t("commands.turnedOff", {
78
+ params: { label },
79
+ fallback: "{label} isn't available"
80
+ }) : `${label} isn't available`;
81
+ };
82
+ function createAvailability(ctx, settings) {
83
+ /** Whether a command is turned off: by its id, or through one of its categories. */
84
+ const isTurnedOff = (definition) => {
85
+ const { disabledCategories, disabledCommands } = settings.get();
86
+ return disabledCommands.includes(definition.id) || (definition.categories ?? []).some((category) => isCategoryOff(category, disabledCategories));
87
+ };
88
+ /** Whether a command's `allowed` check says yes; one that throws allows it. */
89
+ const passesCheck = (definition, context) => {
90
+ if (!definition.allowed) return true;
91
+ try {
92
+ return definition.allowed(context);
93
+ } catch {
94
+ return true;
95
+ }
96
+ };
97
+ /**
98
+ * Why its document's session may not run a command, as the key of the sentence that says
99
+ * so, or `null` when it may: the first permission `requires` names that the session doesn't
100
+ * hold (`commands.notAllowed.doc-print`), else its `allowed` check saying no (its
101
+ * `notAllowedKey`). With no document open there is no session to ask, and nothing is refused.
102
+ */
103
+ const notAllowedKeyOf = (definition, context) => {
104
+ if (!definition.requires?.length && !definition.allowed) return null;
105
+ const handle = context.documentId === null ? null : ctx.documentHandle(context.documentId);
106
+ if (!handle) return null;
107
+ const missing = definition.requires?.find((permission) => !sessionHolds(handle.security, permission));
108
+ if (missing !== void 0) return permissionKeyOf(missing);
109
+ return passesCheck(definition, context) ? null : definition.notAllowedKey ?? DEFAULT_NOT_ALLOWED_KEY;
110
+ };
111
+ /** Why a command won't run whatever the moment, or `null` when it may run. */
112
+ const refusalOf = (definition, context) => {
113
+ if (isTurnedOff(definition)) return {
114
+ reason: "turned-off",
115
+ message: turnedOffMessage(labelOf(definition, context), context)
116
+ };
117
+ const notAllowed = notAllowedKeyOf(definition, context);
118
+ return notAllowed === null ? null : {
119
+ reason: "not-allowed",
120
+ message: notAllowedMessage(notAllowed, labelOf(definition, context), context)
121
+ };
122
+ };
123
+ return {
124
+ isTurnedOff,
125
+ notAllowedKeyOf,
126
+ refusalOf
127
+ };
128
+ }
129
+ //#endregion
130
+ //#region src/write/turning-off.ts
131
+ function createTurningOff(settings, definitionOf, isTurnedOff) {
132
+ const listOf = (list) => settings.get()[list];
133
+ const replace = (list, names) => settings.api.updateSettings(list === "disabledCategories" ? { disabledCategories: [...names] } : { disabledCommands: [...names] });
134
+ const add = (list, name) => {
135
+ const current = listOf(list);
136
+ if (!current.includes(name)) replace(list, [...current, name]);
137
+ };
138
+ const remove = (list, name) => {
139
+ const current = listOf(list);
140
+ if (current.includes(name)) replace(list, current.filter((other) => other !== name));
141
+ };
142
+ return {
143
+ getDisabledCategories: () => listOf("disabledCategories"),
144
+ isCategoryDisabled: (category) => isCategoryOff(category, listOf("disabledCategories")),
145
+ disableCategory: (category) => add("disabledCategories", category),
146
+ enableCategory: (category) => remove("disabledCategories", category),
147
+ setDisabledCategories: (categories) => replace("disabledCategories", categories),
148
+ getDisabledCommands: () => listOf("disabledCommands"),
149
+ isCommandDisabled: (id) => {
150
+ const definition = definitionOf(id);
151
+ return definition ? isTurnedOff(definition) : listOf("disabledCommands").includes(id);
152
+ },
153
+ disableCommand: (id) => add("disabledCommands", id),
154
+ enableCommand: (id) => remove("disabledCommands", id),
155
+ setDisabledCommands: (ids) => replace("disabledCommands", ids)
156
+ };
16
157
  }
17
- const panelTarget = (def) => def.panel === void 0 ? null : typeof def.panel === "string" ? { id: def.panel } : def.panel;
18
- function createCommandsCapability(ctx, registry) {
19
- /** Bind capability resolution to the command's target document. The kernel
20
- * resolves workspace tokens regardless of the document argument, so one
21
- * code path serves both scopes. */
22
- const commandCtx = (documentId) => {
23
- const target = documentId ?? ctx.core().activeId;
158
+ //#endregion
159
+ //#region src/controller.ts
160
+ /**
161
+ * The commands controller: the commands (from the settings, added while the
162
+ * app runs, and from families), resolution for a document (derivations over
163
+ * live state, safe to throw), the one way to run a command, turning commands
164
+ * off (by category and one by one), what the session may run (the scope's
165
+ * `requires`, the plugins' checks in `allowed`), and the host-only keystroke
166
+ * matcher.
167
+ *
168
+ * Definitions hold functions, so the commands added while the app runs are a
169
+ * map outside state; every change to it calls `ctx.notify()` so readers read
170
+ * again.
171
+ */
172
+ const panelTarget = (definition) => definition.panel === void 0 ? null : typeof definition.panel === "string" ? { id: definition.panel } : definition.panel;
173
+ const NOT_REGISTERED = [
174
+ null,
175
+ "",
176
+ void 0,
177
+ void 0,
178
+ false,
179
+ false,
180
+ false,
181
+ void 0
182
+ ];
183
+ const NO_NAMES = [];
184
+ const accentOf = (primary, secondary) => primary === void 0 && secondary === void 0 ? void 0 : {
185
+ ...primary !== void 0 ? { primary } : {},
186
+ ...secondary !== void 0 ? { secondary } : {}
187
+ };
188
+ /** The memo key of a resolution: the command id and the document named, if any. */
189
+ const resolutionKey = (id, documentId) => JSON.stringify([id, documentId ?? null]);
190
+ function createCommandsController(ctx) {
191
+ const settings = ctx.settings();
192
+ /** Commands added while the app runs, in the order they were added. */
193
+ const registered = /* @__PURE__ */ new Map();
194
+ /** Bumped on every change to `registered`: the input of the reads built from it. */
195
+ let registeredVersion = 0;
196
+ const registeredChanged = () => {
197
+ registeredVersion += 1;
198
+ ctx.notify();
199
+ };
200
+ /** The commands the settings define, rebuilt when the setting changes. */
201
+ const configured = (0, _embedpdf_core.memo)(() => [settings.get().commands], (items) => configuredCommands(items));
202
+ const executed = ctx.events.source();
203
+ const executionFailed = ctx.events.source();
204
+ /** Capabilities for the command's document: the one named, else the active one. */
205
+ const commandContext = (documentId, call = {}) => {
206
+ const target = documentId ?? ctx.get(_embedpdf_core.DocumentsToken).getActiveId();
24
207
  const get = (token) => target ? ctx.forDocument(token, target) : ctx.get(token);
25
208
  return {
26
209
  documentId: target,
27
- core: ctx.core,
210
+ ...call.args === void 0 ? {} : { args: call.args },
211
+ ...call.signal ? { signal: call.signal } : {},
28
212
  get,
29
213
  tryGet: (token) => {
30
214
  try {
@@ -35,153 +219,302 @@ function createCommandsCapability(ctx, registry) {
35
219
  }
36
220
  };
37
221
  };
38
- /** Derivations run against live state; a derivation that throws (e.g. it
39
- * needs a document and none is open) falls back to the safe default —
40
- * the button renders, disabled, exactly like v2's empty state. */
41
- const derive = (fn, c, fallback) => {
42
- if (!fn) return fallback;
222
+ /** A family's command for one name, made once so it keeps its identity. */
223
+ const familyCommands = /* @__PURE__ */ new WeakMap();
224
+ const familyCommand = (family, name) => {
225
+ let byName = familyCommands.get(family);
226
+ if (!byName) {
227
+ byName = /* @__PURE__ */ new Map();
228
+ familyCommands.set(family, byName);
229
+ }
230
+ let entry = byName.get(name);
231
+ if (!entry) {
232
+ entry = registeredCommand({
233
+ ...family.command(name),
234
+ id: `${family.prefix}${name}`
235
+ });
236
+ byName.set(name, entry);
237
+ }
238
+ return entry;
239
+ };
240
+ /** The command with this id: one added while the app runs, else the settings', else a family's. */
241
+ const entryOf = (id) => {
242
+ const { commands, families } = configured();
243
+ const entry = registered.get(id) ?? commands.get(id);
244
+ if (entry) return entry;
245
+ const family = families.find((candidate) => id.startsWith(candidate.prefix) && id.length > candidate.prefix.length);
246
+ return family ? familyCommand(family, id.slice(family.prefix.length)) : null;
247
+ };
248
+ /** Every command with its own definition (not a family's), each id once. */
249
+ const definedCommands = (0, _embedpdf_core.memo)(() => [registeredVersion, configured()], (_version, { commands }) => {
250
+ const entries = new Map(commands);
251
+ for (const [id, entry] of registered) entries.set(id, entry);
252
+ return [...entries.values()];
253
+ });
254
+ const derive = (derivation, context, fallback) => {
255
+ if (!derivation) return fallback;
43
256
  try {
44
- return fn(c);
257
+ return derivation(context);
45
258
  } catch {
46
259
  return fallback;
47
260
  }
48
261
  };
49
- /** Same guard as `derive`, for the icon-accent derivation: a throw (no
50
- * document, provider missing) means "no accent" — the icon renders plain. */
51
- const deriveAccent = (fn, c) => {
52
- if (!fn) return void 0;
262
+ const deriveAccent = (derivation, context) => {
263
+ if (!derivation) return void 0;
53
264
  try {
54
- return fn(c) ?? void 0;
265
+ return derivation(context) ?? void 0;
55
266
  } catch {
56
267
  return;
57
268
  }
58
269
  };
59
- const resolve = (id, documentId) => {
60
- const entry = registry.get(id);
61
- if (!entry) return null;
62
- const { def } = entry;
63
- const c = commandCtx(documentId);
64
- const disabled = ctx.getState().disabledCategories;
65
- const categoryHidden = (def.categories ?? []).some((cat) => disabled.includes(cat));
66
- const i18n = c.tryGet(_embedpdf_plugin_i18n_contract.I18nToken);
67
- const label = i18n ? i18n.t(def.labelKey) : def.labelKey;
270
+ const namesOf = (family, context) => {
271
+ try {
272
+ return family.names(context);
273
+ } catch {
274
+ return NO_NAMES;
275
+ }
276
+ };
277
+ const { isTurnedOff, notAllowedKeyOf, refusalOf } = createAvailability(ctx, settings);
278
+ const derivationsOf = (entry, documentId) => {
279
+ const { definition } = entry;
280
+ const context = commandContext(documentId);
281
+ const label = labelOf(definition, context);
282
+ const turnedOff = isTurnedOff(definition);
283
+ const notAllowed = turnedOff ? null : notAllowedKeyOf(definition, context);
284
+ const shownWhenNotAllowed = settings.get().notAllowed === "disable";
285
+ const ruledOut = turnedOff || notAllowed !== null;
68
286
  let active;
69
- if (def.active) active = derive(def.active, c, false);
287
+ if (definition.active) active = derive(definition.active, context, false);
70
288
  else {
71
- const shell = c.tryGet(_embedpdf_plugin_shell_contract.ShellToken);
72
- const panel = panelTarget(def);
73
- active = shell ? def.menu ? shell.isMenuOpen(def.menu) : panel ? shell.isOpen(panel.id) : def.modal ? shell.isOpen(def.modal) : false : false;
289
+ const shell = context.tryGet(_embedpdf_plugin_shell_contract.ShellToken);
290
+ const panel = panelTarget(definition);
291
+ active = shell ? definition.menu ? shell.isMenuOpen(definition.menu) : panel ? shell.isOpen(panel.id) : definition.modal ? shell.isOpen(definition.modal) : false : false;
74
292
  }
75
- return {
76
- id: def.id,
293
+ const accent = deriveAccent(definition.iconAccent, context);
294
+ return [
295
+ entry,
77
296
  label,
78
- icon: def.icon,
79
- iconAccent: deriveAccent(def.iconAccent, c),
80
- shortcuts: entry.shortcuts,
81
- menu: def.menu,
82
- enabled: derive(def.enabled, c, true) && !categoryHidden,
297
+ accent?.primary,
298
+ accent?.secondary,
299
+ !ruledOut && derive(definition.enabled, context, true),
83
300
  active,
84
- visible: derive(def.visible, c, true) && !categoryHidden,
85
- categories: def.categories ?? []
86
- };
301
+ !turnedOff && (notAllowed === null || shownWhenNotAllowed) && derive(definition.visible, context, true),
302
+ notAllowed !== null && shownWhenNotAllowed ? notAllowedMessage(notAllowed, label, context) : void 0
303
+ ];
87
304
  };
88
- const execute = (id, documentId) => {
89
- const entry = registry.get(id);
90
- if (!entry) return;
91
- const resolved = resolve(id, documentId);
92
- if (!resolved || !resolved.enabled || !resolved.visible) return;
93
- const c = commandCtx(documentId);
94
- if (entry.def.run) {
95
- entry.def.run(c);
305
+ /** A resolved command, the same object until one of its derivations or its entry changes. */
306
+ const resolvedByKey = (0, _embedpdf_core.memoByKey)((key) => {
307
+ const [id, documentId] = JSON.parse(key);
308
+ const entry = entryOf(id);
309
+ return entry ? derivationsOf(entry, documentId ?? void 0) : NOT_REGISTERED;
310
+ }, (_key, entry, label, accentPrimary, accentSecondary, enabled, active, visible, disabledReason) => entry && {
311
+ id: entry.definition.id,
312
+ label,
313
+ icon: entry.definition.icon,
314
+ iconAccent: accentOf(accentPrimary, accentSecondary),
315
+ shortcuts: entry.shortcuts,
316
+ menu: entry.definition.menu,
317
+ enabled,
318
+ active,
319
+ visible,
320
+ categories: entry.definition.categories ?? [],
321
+ ...disabledReason === void 0 ? {} : { disabledReason }
322
+ });
323
+ const resolveCommand = (id, documentId) => entryOf(id) ? resolvedByKey(resolutionKey(id, documentId)) : null;
324
+ /**
325
+ * Every command's id for a document (`''` is the active one): the defined ones in the order
326
+ * they were added, then each family's names that no defined command took.
327
+ */
328
+ const idsByDocument = (0, _embedpdf_core.memoByKey)((documentKey) => {
329
+ const config = configured();
330
+ const context = commandContext(documentKey || void 0);
331
+ return [
332
+ definedCommands(),
333
+ config,
334
+ ...config.families.map((family) => namesOf(family, context))
335
+ ];
336
+ }, (_documentKey, ...inputs) => {
337
+ const [defined, { families }, ...names] = inputs;
338
+ const ids = defined.map((entry) => entry.definition.id);
339
+ const taken = new Set(ids);
340
+ families.forEach((family, index) => {
341
+ for (const name of names[index] ?? NO_NAMES) {
342
+ const id = `${family.prefix}${name}`;
343
+ if (taken.has(id)) continue;
344
+ taken.add(id);
345
+ ids.push(id);
346
+ }
347
+ });
348
+ return ids;
349
+ });
350
+ const commandsByDocument = (0, _embedpdf_core.memoByKey)((documentKey) => idsByDocument(documentKey).map((id) => resolveCommand(id, documentKey || void 0)), (_documentKey, ...resolved) => resolved.filter((command) => command !== null));
351
+ const listCommands = (documentId) => commandsByDocument(documentId ?? "");
352
+ const listShortcuts = (0, _embedpdf_core.memo)(() => [definedCommands()], (entries) => entries.flatMap((entry) => entry.shortcuts.map((shortcut) => ({
353
+ commandId: entry.definition.id,
354
+ shortcut
355
+ }))));
356
+ /** What running a command does: its `run`, or opening what it opens. */
357
+ const runCommand = async (definition, context) => {
358
+ if (definition.run) {
359
+ await definition.run(context);
96
360
  return;
97
361
  }
98
- const shell = c.tryGet(_embedpdf_plugin_shell_contract.ShellToken);
362
+ const shell = context.tryGet(_embedpdf_plugin_shell_contract.ShellToken);
363
+ const panel = panelTarget(definition);
99
364
  if (!shell) return;
100
- const panel = panelTarget(entry.def);
101
- if (entry.def.menu) shell.toggleMenu(entry.def.menu);
102
- else if (panel) shell.toggle(panel.id, { exclusive: panel.exclusive });
103
- else if (entry.def.modal) shell.toggle(entry.def.modal, { exclusive: "modal" });
365
+ if (definition.menu) shell.toggleMenu(definition.menu);
366
+ else if (panel) shell.toggle(panel.id, {
367
+ exclusive: panel.exclusive,
368
+ focus: panel.focus
369
+ });
370
+ else if (definition.modal) shell.toggle(definition.modal, { exclusive: "modal" });
104
371
  };
105
- return {
106
- register: (def) => registerCommand(registry, def),
107
- unregister: (id) => void registry.delete(id),
108
- has: (id) => registry.has(id),
109
- ids: () => [...registry.keys()],
110
- resolve,
111
- search: (query, documentId) => {
112
- const q = query.trim().toLowerCase();
113
- const hits = [];
114
- for (const id of registry.keys()) {
115
- const r = resolve(id, documentId);
116
- if (!r || !r.visible) continue;
117
- if (q === "" || r.label.toLowerCase().includes(q) || r.id.includes(q)) hits.push(r);
118
- }
119
- return hits;
372
+ const execute = async (id, options = {}) => {
373
+ const { signal, documentId, args } = options;
374
+ if (signal?.aborted) throw new _embedpdf_core.PluginError("operation-cancelled", "commands", "operation cancelled");
375
+ const entry = entryOf(id);
376
+ if (!entry) return {
377
+ status: "rejected",
378
+ reason: "not-found"
379
+ };
380
+ const resolved = resolveCommand(id, documentId);
381
+ if (!resolved?.visible) return {
382
+ status: "rejected",
383
+ reason: "hidden"
384
+ };
385
+ if (!resolved.enabled) return {
386
+ status: "rejected",
387
+ reason: "disabled"
388
+ };
389
+ const context = commandContext(documentId, {
390
+ args,
391
+ signal
392
+ });
393
+ try {
394
+ await ctx.cancellable(signal, runCommand(entry.definition, context));
395
+ } catch (error) {
396
+ if (signal?.aborted && (0, _embedpdf_core.isPluginError)(error, "operation-cancelled")) throw error;
397
+ const failure = (0, _embedpdf_core.toPluginError)("commands", error);
398
+ executionFailed.emit({
399
+ commandId: id,
400
+ documentId: context.documentId,
401
+ error: (0, _embedpdf_core.toPluginErrorInfo)(failure)
402
+ });
403
+ throw failure;
404
+ }
405
+ executed.emit({
406
+ commandId: id,
407
+ documentId: context.documentId,
408
+ args
409
+ });
410
+ return { status: "executed" };
411
+ };
412
+ const registerOne = (definition, options) => {
413
+ if ((registered.has(definition.id) || configured().commands.has(definition.id)) && !options?.replace) throw new _embedpdf_core.PluginError("conflict", "commands", `duplicate command '${definition.id}'`);
414
+ const entry = registeredCommand(definition);
415
+ registered.set(definition.id, entry);
416
+ registeredChanged();
417
+ return () => {
418
+ if (registered.get(definition.id) !== entry) return;
419
+ registered.delete(definition.id);
420
+ registeredChanged();
421
+ };
422
+ };
423
+ const turningOff = createTurningOff(settings, (id) => entryOf(id)?.definition ?? null, isTurnedOff);
424
+ return { api: {
425
+ ...settings.api,
426
+ registerCommand: registerOne,
427
+ registerCommands: (definitions, options) => {
428
+ const removers = definitions.map((definition) => registerOne(definition, options));
429
+ return () => removers.forEach((remove) => remove());
120
430
  },
121
- menuTarget: (id) => {
122
- const entry = registry.get(id);
123
- return entry ? { menu: entry.def.menu } : null;
431
+ hasCommand: (id) => entryOf(id) !== null,
432
+ getCommand: (id) => entryOf(id)?.definition ?? null,
433
+ listCommandIds: () => idsByDocument(""),
434
+ resolveCommand,
435
+ listCommands,
436
+ searchCommands: (query, documentId) => {
437
+ const needle = query.trim().toLowerCase();
438
+ return listCommands(documentId).filter((command) => command.visible && (needle === "" || command.label.toLowerCase().includes(needle) || command.id.includes(needle)));
124
439
  },
440
+ listShortcuts,
125
441
  execute,
126
- matchStroke: (stroke, opts) => {
127
- for (const [id, entry] of registry) if (entry.parsed.some((p) => (0, _embedpdf_core_ui.matchShortcut)(p, stroke, opts))) return id;
128
- return null;
442
+ canExecute: (id, documentId) => {
443
+ const resolved = resolveCommand(id, documentId);
444
+ return !!resolved && resolved.enabled && resolved.visible;
129
445
  },
130
- disabledCategories: () => ctx.getState().disabledCategories,
131
- isCategoryDisabled: (category) => ctx.getState().disabledCategories.includes(category),
132
- disableCategory: (category) => ctx.dispatch({
133
- type: "COMMANDS/DISABLE_CATEGORY",
134
- category
135
- }),
136
- enableCategory: (category) => ctx.dispatch({
137
- type: "COMMANDS/ENABLE_CATEGORY",
138
- category
139
- }),
140
- setDisabledCategories: (categories) => ctx.dispatch({
141
- type: "COMMANDS/SET_DISABLED_CATEGORIES",
142
- categories
143
- })
144
- };
145
- }
146
- //#endregion
147
- //#region src/reducer.ts
148
- const initialCommandsState = { disabledCategories: [] };
149
- function commandsReducer(state, action) {
150
- switch (action.type) {
151
- case "COMMANDS/DISABLE_CATEGORY": return state.disabledCategories.includes(action.category) ? state : { disabledCategories: [...state.disabledCategories, action.category] };
152
- case "COMMANDS/ENABLE_CATEGORY": return state.disabledCategories.includes(action.category) ? { disabledCategories: state.disabledCategories.filter((c) => c !== action.category) } : state;
153
- case "COMMANDS/SET_DISABLED_CATEGORIES": return { disabledCategories: [...action.categories] };
154
- default: return state;
155
- }
446
+ getRefusal: (id, documentId) => {
447
+ const entry = entryOf(id);
448
+ return entry ? refusalOf(entry.definition, commandContext(documentId)) : null;
449
+ },
450
+ ...turningOff,
451
+ onExecuted: executed.on,
452
+ onExecutionFailed: executionFailed.on,
453
+ matchStroke: (stroke, options) => {
454
+ let refused = null;
455
+ for (const entry of definedCommands()) {
456
+ if (options.inTextField && !entry.definition.shortcutInTextFields) continue;
457
+ if (!entry.parsed.some((shortcut) => (0, _embedpdf_core_ui.matchShortcut)(shortcut, stroke, options))) continue;
458
+ const context = commandContext();
459
+ const refusal = refusalOf(entry.definition, context);
460
+ if (!refusal) return {
461
+ commandId: entry.definition.id,
462
+ refusal: null
463
+ };
464
+ if (!derive(entry.definition.visible, context, true)) continue;
465
+ refused ?? (refused = {
466
+ commandId: entry.definition.id,
467
+ refusal
468
+ });
469
+ }
470
+ return refused;
471
+ },
472
+ getMenuTarget: (id) => {
473
+ const entry = entryOf(id);
474
+ return entry ? { menu: entry.definition.menu } : null;
475
+ }
476
+ } };
156
477
  }
157
478
  //#endregion
158
479
  //#region src/commands.plugin.ts
159
480
  /**
160
- * The commands plugin: workspace-scoped (one vocabulary for the whole
161
- * workspace; resolution/execution bind to a target document per call).
162
- * Definitions live in this closure — never in the store (they hold
163
- * functions); the store slice holds only `disabledCategories`.
481
+ * The commands: one set for the whole workspace, each resolved and run for a
482
+ * document (the one named, else the document in scope, else the active one).
483
+ * Definitions are settings and app content, never state, since they hold
484
+ * functions. `config` is the settings the app registers, over
485
+ * {@link COMMANDS_DEFAULTS}.
164
486
  */
165
- const commandsPlugin = (config) => {
166
- const registry = /* @__PURE__ */ new Map();
167
- for (const def of config?.commands ?? []) registerCommand(registry, def);
168
- return (0, _embedpdf_core.definePlugin)({
169
- id: "commands",
170
- scope: "workspace",
171
- token: require_contract.CommandsToken,
172
- initialState: {
173
- ...initialCommandsState,
174
- disabledCategories: [...config?.disabledCategories ?? []]
175
- },
176
- reduce: commandsReducer,
177
- capability: (ctx) => createCommandsCapability(ctx, registry)
178
- });
179
- };
487
+ const commandsPlugin = (config) => (0, _embedpdf_core.definePlugin)({
488
+ id: "commands",
489
+ scope: "workspace",
490
+ token: require_contract_host.CommandsToken,
491
+ optional: [_embedpdf_plugin_i18n_contract.I18nToken, _embedpdf_plugin_shell_contract.ShellToken],
492
+ resolvesAnyCapability: true,
493
+ settings: {
494
+ defaults: require_contract.COMMANDS_DEFAULTS,
495
+ registered: config
496
+ },
497
+ create: createCommandsController,
498
+ inScope: (commands, documentId) => ({
499
+ ...commands,
500
+ resolveCommand: (id, target = documentId) => commands.resolveCommand(id, target),
501
+ listCommands: (target = documentId) => commands.listCommands(target),
502
+ searchCommands: (query, target = documentId) => commands.searchCommands(query, target),
503
+ execute: (id, options) => commands.execute(id, {
504
+ ...options,
505
+ documentId: options?.documentId ?? documentId
506
+ }),
507
+ canExecute: (id, target = documentId) => commands.canExecute(id, target),
508
+ getRefusal: (id, target = documentId) => commands.getRefusal(id, target)
509
+ })
510
+ });
180
511
  //#endregion
512
+ exports.COMMANDS_DEFAULTS = require_contract.COMMANDS_DEFAULTS;
181
513
  exports.CommandsToken = require_contract.CommandsToken;
514
+ exports.boundCommandOf = require_contract.boundCommandOf;
182
515
  exports.commandsPlugin = commandsPlugin;
183
- exports.commandsReducer = commandsReducer;
184
- exports.initialCommandsState = initialCommandsState;
185
516
  exports.resolvedCommandsEqual = require_contract.resolvedCommandsEqual;
517
+ exports.unknownCommandWarning = require_contract.unknownCommandWarning;
518
+ exports.unregisteredCommand = require_contract.unregisteredCommand;
186
519
 
187
520
  //# sourceMappingURL=index.cjs.map