@domternal/core 0.15.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -3,10 +3,10 @@
3
3
  var state = require('@domternal/pm/state');
4
4
  var view = require('@domternal/pm/view');
5
5
  var model = require('@domternal/pm/model');
6
+ var transform = require('@domternal/pm/transform');
6
7
  var keymap = require('@domternal/pm/keymap');
7
8
  var commands = require('@domternal/pm/commands');
8
9
  var inputrules = require('@domternal/pm/inputrules');
9
- var transform = require('@domternal/pm/transform');
10
10
  var schemaList = require('@domternal/pm/schema-list');
11
11
  var dom = require('@floating-ui/dom');
12
12
  var linkifyjs = require('linkifyjs');
@@ -225,6 +225,90 @@ var ExtensionConfigurationError = class extends Error {
225
225
  }
226
226
  };
227
227
 
228
+ // src/utils/prosemirrorSingleton.ts
229
+ var REGISTRY_KEY = /* @__PURE__ */ Symbol.for("domternal.prosemirror.copies");
230
+ var REPORTED_KEY = /* @__PURE__ */ Symbol.for("domternal.prosemirror.copies.reported");
231
+ function registry() {
232
+ const host = globalThis;
233
+ const existing = host[REGISTRY_KEY];
234
+ if (existing instanceof Map) return existing;
235
+ const fresh = /* @__PURE__ */ new Map();
236
+ host[REGISTRY_KEY] = fresh;
237
+ return fresh;
238
+ }
239
+ function reported() {
240
+ const host = globalThis;
241
+ const existing = host[REPORTED_KEY];
242
+ if (existing instanceof Set) return existing;
243
+ const fresh = /* @__PURE__ */ new Set();
244
+ host[REPORTED_KEY] = fresh;
245
+ return fresh;
246
+ }
247
+ var FIXES = [
248
+ 'pnpm: add the package to "pnpm.overrides" in package.json, then run `pnpm dedupe`',
249
+ 'npm: add the package to "overrides" in package.json, then reinstall',
250
+ 'yarn: add the package to "resolutions" in package.json, then reinstall',
251
+ "Vite: depend on '<module>' in the app, then resolve.dedupe: ['<module>']",
252
+ "webpack: depend on '<module>' in the app, then resolve.alias it to that one copy"
253
+ ];
254
+ var DOCS_URL = "https://domternal.dev/v1/guides/single-prosemirror-copy/";
255
+ function describeConflict(module, first, second) {
256
+ return [
257
+ `Two different copies of "${module}" are loaded on this page.`,
258
+ `"${first}" registered one, "${second}" arrived with another.`,
259
+ "ProseMirror compares classes by identity, so objects made by one copy are",
260
+ "rejected by the other and the editor fails as soon as the two meet.",
261
+ "",
262
+ "Force a single copy:",
263
+ ...FIXES.map((line) => ` ${line.replaceAll("<module>", module)}`),
264
+ "",
265
+ DOCS_URL
266
+ ].join("\n");
267
+ }
268
+ function describeForeignExtension(name) {
269
+ return [
270
+ `The extension "${name}" was built by a different copy of "@domternal/core"`,
271
+ "than the one building this editor.",
272
+ "Extensions are bound to the core that created them: their base class, schema",
273
+ "and plugin keys all belong to that copy, so an editor cannot use one built",
274
+ "elsewhere. Two copies usually mean a linked or nested install.",
275
+ "",
276
+ "Force a single copy:",
277
+ ...FIXES.map((line) => ` ${line.replaceAll("<module>", "@domternal/core")}`),
278
+ "",
279
+ DOCS_URL
280
+ ].join("\n");
281
+ }
282
+ function registerProseMirrorCopy(module, copy, consumer) {
283
+ const entries = registry();
284
+ const existing = entries.get(module);
285
+ if (!existing) {
286
+ entries.set(module, { copy, consumer });
287
+ return null;
288
+ }
289
+ if (existing.copy === copy) return null;
290
+ return {
291
+ module,
292
+ firstConsumer: existing.consumer,
293
+ secondConsumer: consumer,
294
+ message: describeConflict(module, existing.consumer, consumer)
295
+ };
296
+ }
297
+ function assertSingleProseMirrorCopy(module, copy, consumer) {
298
+ const conflict = registerProseMirrorCopy(module, copy, consumer);
299
+ if (conflict) throw new ExtensionConfigurationError(conflict.message);
300
+ }
301
+ function warnOnDuplicateProseMirrorCopy(module, copy, consumer) {
302
+ const conflict = registerProseMirrorCopy(module, copy, consumer);
303
+ if (!conflict) return null;
304
+ const seen = reported();
305
+ if (!seen.has(module)) {
306
+ seen.add(module);
307
+ console.warn(conflict.message);
308
+ }
309
+ return conflict;
310
+ }
311
+
228
312
  // src/helpers/callOrReturn.ts
229
313
  function callOrReturn(value, context, ...args) {
230
314
  if (typeof value === "function") {
@@ -233,6 +317,183 @@ function callOrReturn(value, context, ...args) {
233
317
  return value;
234
318
  }
235
319
 
320
+ // src/Extension.ts
321
+ function mergeConfigWithParentBinding(parentConfig, extendedConfig) {
322
+ const parent = parentConfig;
323
+ const merged = { ...parent };
324
+ for (const [key, value] of Object.entries(extendedConfig)) {
325
+ if (typeof value === "function" && typeof parent[key] === "function") {
326
+ const parentFn = parent[key];
327
+ const childFn = value;
328
+ merged[key] = function(...args) {
329
+ const previousParent = this.parent;
330
+ this.parent = (...pArgs) => parentFn.call(this, ...pArgs);
331
+ const result = childFn.call(this, ...args);
332
+ this.parent = previousParent;
333
+ return result;
334
+ };
335
+ } else {
336
+ merged[key] = value;
337
+ }
338
+ }
339
+ return merged;
340
+ }
341
+ var EXTENSION_BRAND = /* @__PURE__ */ Symbol.for("domternal.core.extension");
342
+ var Extension = class _Extension {
343
+ /**
344
+ * Brand read by `ExtensionManager` to tell an extension built by another
345
+ * copy of `@domternal/core` from a plain object. See `EXTENSION_BRAND`.
346
+ */
347
+ [EXTENSION_BRAND] = true;
348
+ /**
349
+ * Extension type identifier
350
+ * Used to distinguish between Extension, Node, and Mark
351
+ * Subclasses override this to 'node' or 'mark'
352
+ */
353
+ type = "extension";
354
+ /**
355
+ * Unique extension name
356
+ */
357
+ name;
358
+ /**
359
+ * Extension options (immutable after creation)
360
+ */
361
+ options;
362
+ /**
363
+ * Extension storage (mutable state)
364
+ * Accessible via editor.storage[extensionName]
365
+ */
366
+ storage;
367
+ /**
368
+ * The original configuration object
369
+ */
370
+ config;
371
+ /**
372
+ * Editor instance (set by ExtensionManager after creation)
373
+ * null until ExtensionManager binds it
374
+ */
375
+ editor = null;
376
+ /**
377
+ * Reference to the parent config method when using extend().
378
+ * Set temporarily during config method execution so overridden
379
+ * methods can call `this.parent?.()` to invoke the original.
380
+ */
381
+ parent;
382
+ /**
383
+ * Protected constructor - use Extension.create() instead
384
+ */
385
+ constructor(config) {
386
+ if (!/^[a-z][a-zA-Z0-9]*$/.test(config.name)) {
387
+ throw new Error(
388
+ `Extension name '${config.name}' is invalid. Names must be camelCase starting with a lowercase letter (e.g., 'myExtension').`
389
+ );
390
+ }
391
+ this.config = config;
392
+ this.name = config.name;
393
+ const defaultOptions = callOrReturn(config.addOptions, this);
394
+ this.options = defaultOptions ?? {};
395
+ const defaultStorage = callOrReturn(config.addStorage, this);
396
+ this.storage = defaultStorage ?? {};
397
+ }
398
+ /**
399
+ * Creates a new extension instance
400
+ *
401
+ * @param config - Extension configuration
402
+ * @returns New extension instance
403
+ *
404
+ * @example
405
+ * const MyExtension = Extension.create({
406
+ * name: 'myExtension',
407
+ * addOptions() {
408
+ * return { enabled: true };
409
+ * },
410
+ * });
411
+ */
412
+ static create(config) {
413
+ return new _Extension(config);
414
+ }
415
+ /**
416
+ * Creates a new extension with merged options
417
+ * Original extension is not modified
418
+ *
419
+ * **Note:** Options are merged shallowly using object spread (`...`).
420
+ * Nested objects are replaced entirely, not deeply merged.
421
+ *
422
+ * @param options - Options to merge with existing options
423
+ * @returns New extension instance with merged options
424
+ *
425
+ * @example
426
+ * const configured = MyExtension.configure({ enabled: false });
427
+ *
428
+ * @example
429
+ * // Shallow merge behavior with nested objects:
430
+ * // Given: options = { nested: { a: 1, b: 2 } }
431
+ * // configure({ nested: { b: 3 } })
432
+ * // Result: { nested: { b: 3 } } - 'a' is lost!
433
+ * // To preserve nested values, spread manually:
434
+ * // configure({ nested: { ...original.options.nested, b: 3 } })
435
+ */
436
+ configure(options) {
437
+ const newConfig = {
438
+ ...this.config,
439
+ // Override addOptions to return merged options
440
+ addOptions: () => ({
441
+ ...this.options,
442
+ ...options
443
+ })
444
+ };
445
+ return new _Extension(newConfig);
446
+ }
447
+ /**
448
+ * Returns a fresh, unbound copy built from the same `config`: `editor` reset to
449
+ * null and `options`/`storage` re-derived, while `configure()`/`extend()`
450
+ * results are preserved (they live in `config`). Polymorphic: a `Node`/`Mark`
451
+ * clones to its own subclass.
452
+ *
453
+ * `ExtensionManager` clones every extension so each editor owns its instances
454
+ * and binding one editor can't mutate extensions shared with another.
455
+ */
456
+ clone() {
457
+ const Ctor = this.constructor;
458
+ return new Ctor(this.config);
459
+ }
460
+ /**
461
+ * Creates a new extension with extended configuration
462
+ * Original extension is not modified
463
+ *
464
+ * **Note:** Config is merged shallowly using object spread (`...`).
465
+ * Config properties (like `addCommands`, `addKeyboardShortcuts`) are
466
+ * replaced entirely, not combined with the base extension's config.
467
+ *
468
+ * @param extendedConfig - Configuration to extend/override
469
+ * @returns New extension instance with extended config
470
+ *
471
+ * @example
472
+ * const Extended = MyExtension.extend({
473
+ * name: 'extendedExtension',
474
+ * addCommands() {
475
+ * return { customCommand: () => ({ tr }) => true };
476
+ * },
477
+ * });
478
+ *
479
+ * @example
480
+ * // To preserve base extension's commands while adding new ones:
481
+ * const Extended = BaseExtension.extend({
482
+ * addCommands() {
483
+ * const baseCommands = BaseExtension.config.addCommands?.call(this) ?? {};
484
+ * return {
485
+ * ...baseCommands,
486
+ * newCommand: () => ({ tr }) => true,
487
+ * };
488
+ * },
489
+ * });
490
+ */
491
+ extend(extendedConfig) {
492
+ const newConfig = mergeConfigWithParentBinding(this.config, extendedConfig);
493
+ return new _Extension(newConfig);
494
+ }
495
+ };
496
+
236
497
  // src/ExtensionManager.ts
237
498
  function mergeHTMLAttrs(target, source) {
238
499
  const result = { ...target };
@@ -247,6 +508,13 @@ function mergeHTMLAttrs(target, source) {
247
508
  }
248
509
  return result;
249
510
  }
511
+ function assertOwnExtension(ext) {
512
+ if (ext instanceof Extension) return;
513
+ const foreign = ext?.[EXTENSION_BRAND];
514
+ if (foreign !== true) return;
515
+ const name = typeof ext.name === "string" ? ext.name : "unknown";
516
+ throw new ExtensionConfigurationError(describeForeignExtension(name));
517
+ }
250
518
  var ExtensionManager = class {
251
519
  /**
252
520
  * Processed extensions (flattened, sorted by priority)
@@ -306,8 +574,9 @@ var ExtensionManager = class {
306
574
  "ExtensionManager requires either extensions or schema. Provide at least Document, Text, and Paragraph extensions."
307
575
  );
308
576
  }
309
- const flattened = this.flattenExtensions(options.extensions);
310
- const deduped = this.deduplicateExtensions(flattened);
577
+ const autoIncluded = /* @__PURE__ */ new Set();
578
+ const flattened = this.flattenExtensions(options.extensions, autoIncluded);
579
+ const deduped = this.deduplicateExtensions(flattened, autoIncluded);
311
580
  const cloned = this.cloneExtensions(deduped);
312
581
  this._extensions = this.resolveExtensions(cloned);
313
582
  this.detectConflicts();
@@ -389,33 +658,57 @@ var ExtensionManager = class {
389
658
  /**
390
659
  * Recursively flattens extensions by expanding addExtensions()
391
660
  * This allows extension bundles like StarterKit to work
661
+ *
662
+ * `autoIncluded` collects everything that arrived through an
663
+ * `addExtensions()` rather than from the caller's own list, which is what
664
+ * lets deduplication tell a default apart from a choice.
392
665
  */
393
- flattenExtensions(extensions) {
666
+ flattenExtensions(extensions, autoIncluded, fromBundle = false) {
394
667
  const result = [];
395
668
  for (const ext of extensions) {
669
+ assertOwnExtension(ext);
670
+ if (fromBundle) autoIncluded.add(ext);
396
671
  result.push(ext);
397
672
  const nested = callOrReturn(
398
673
  ext.config.addExtensions,
399
674
  ext
400
675
  );
401
676
  if (nested && nested.length > 0) {
402
- result.push(...this.flattenExtensions(nested));
677
+ result.push(...this.flattenExtensions(nested, autoIncluded, true));
403
678
  }
404
679
  }
405
680
  return result;
406
681
  }
407
682
  /**
408
- * Removes duplicate extensions by name, keeping the last occurrence.
409
- * This allows parent extensions to auto-include children via addExtensions()
410
- * while letting users override with explicitly configured versions.
683
+ * Removes duplicate extensions by name.
684
+ *
685
+ * A version the caller listed themselves always wins over one a bundle
686
+ * included on their behalf, and position does not enter into it. Keeping
687
+ * the last occurrence alone said the same thing only while every bundle was
688
+ * listed first, which is the habit for StarterKit and no rule at all: an
689
+ * extension that includes a default and is written LOWER in the list, as
690
+ * `Export` and its `Print` are, silently replaced the configured copy
691
+ * above it and the caller's options went missing with it.
692
+ *
693
+ * Between two of the same kind the later one still wins, so two bundles
694
+ * offering the same default resolve as they always have.
411
695
  */
412
- deduplicateExtensions(extensions) {
413
- const seen = /* @__PURE__ */ new Map();
696
+ deduplicateExtensions(extensions, autoIncluded) {
697
+ const winners = /* @__PURE__ */ new Map();
414
698
  for (let i = 0; i < extensions.length; i++) {
415
699
  const ext = extensions[i];
416
- if (ext) seen.set(ext.name, i);
700
+ if (!ext) continue;
701
+ const held = winners.get(ext.name);
702
+ if (held === void 0) {
703
+ winners.set(ext.name, i);
704
+ continue;
705
+ }
706
+ const heldIsAuto = autoIncluded.has(extensions[held]);
707
+ const nextIsAuto = autoIncluded.has(ext);
708
+ if (nextIsAuto && !heldIsAuto) continue;
709
+ winners.set(ext.name, i);
417
710
  }
418
- return extensions.filter((ext, i) => seen.get(ext.name) === i);
711
+ return extensions.filter((ext, i) => winners.get(ext.name) === i);
419
712
  }
420
713
  /**
421
714
  * Clone every extension so this editor owns its instances. Extensions hold
@@ -1474,177 +1767,6 @@ var insertContent = (content) => ({ state, tr, dispatch }) => {
1474
1767
  return true;
1475
1768
  };
1476
1769
 
1477
- // src/Extension.ts
1478
- function mergeConfigWithParentBinding(parentConfig, extendedConfig) {
1479
- const parent = parentConfig;
1480
- const merged = { ...parent };
1481
- for (const [key, value] of Object.entries(extendedConfig)) {
1482
- if (typeof value === "function" && typeof parent[key] === "function") {
1483
- const parentFn = parent[key];
1484
- const childFn = value;
1485
- merged[key] = function(...args) {
1486
- const previousParent = this.parent;
1487
- this.parent = (...pArgs) => parentFn.call(this, ...pArgs);
1488
- const result = childFn.call(this, ...args);
1489
- this.parent = previousParent;
1490
- return result;
1491
- };
1492
- } else {
1493
- merged[key] = value;
1494
- }
1495
- }
1496
- return merged;
1497
- }
1498
- var Extension = class _Extension {
1499
- /**
1500
- * Extension type identifier
1501
- * Used to distinguish between Extension, Node, and Mark
1502
- * Subclasses override this to 'node' or 'mark'
1503
- */
1504
- type = "extension";
1505
- /**
1506
- * Unique extension name
1507
- */
1508
- name;
1509
- /**
1510
- * Extension options (immutable after creation)
1511
- */
1512
- options;
1513
- /**
1514
- * Extension storage (mutable state)
1515
- * Accessible via editor.storage[extensionName]
1516
- */
1517
- storage;
1518
- /**
1519
- * The original configuration object
1520
- */
1521
- config;
1522
- /**
1523
- * Editor instance (set by ExtensionManager after creation)
1524
- * null until ExtensionManager binds it
1525
- */
1526
- editor = null;
1527
- /**
1528
- * Reference to the parent config method when using extend().
1529
- * Set temporarily during config method execution so overridden
1530
- * methods can call `this.parent?.()` to invoke the original.
1531
- */
1532
- parent;
1533
- /**
1534
- * Protected constructor - use Extension.create() instead
1535
- */
1536
- constructor(config) {
1537
- if (!/^[a-z][a-zA-Z0-9]*$/.test(config.name)) {
1538
- throw new Error(
1539
- `Extension name '${config.name}' is invalid. Names must be camelCase starting with a lowercase letter (e.g., 'myExtension').`
1540
- );
1541
- }
1542
- this.config = config;
1543
- this.name = config.name;
1544
- const defaultOptions = callOrReturn(config.addOptions, this);
1545
- this.options = defaultOptions ?? {};
1546
- const defaultStorage = callOrReturn(config.addStorage, this);
1547
- this.storage = defaultStorage ?? {};
1548
- }
1549
- /**
1550
- * Creates a new extension instance
1551
- *
1552
- * @param config - Extension configuration
1553
- * @returns New extension instance
1554
- *
1555
- * @example
1556
- * const MyExtension = Extension.create({
1557
- * name: 'myExtension',
1558
- * addOptions() {
1559
- * return { enabled: true };
1560
- * },
1561
- * });
1562
- */
1563
- static create(config) {
1564
- return new _Extension(config);
1565
- }
1566
- /**
1567
- * Creates a new extension with merged options
1568
- * Original extension is not modified
1569
- *
1570
- * **Note:** Options are merged shallowly using object spread (`...`).
1571
- * Nested objects are replaced entirely, not deeply merged.
1572
- *
1573
- * @param options - Options to merge with existing options
1574
- * @returns New extension instance with merged options
1575
- *
1576
- * @example
1577
- * const configured = MyExtension.configure({ enabled: false });
1578
- *
1579
- * @example
1580
- * // Shallow merge behavior with nested objects:
1581
- * // Given: options = { nested: { a: 1, b: 2 } }
1582
- * // configure({ nested: { b: 3 } })
1583
- * // Result: { nested: { b: 3 } } - 'a' is lost!
1584
- * // To preserve nested values, spread manually:
1585
- * // configure({ nested: { ...original.options.nested, b: 3 } })
1586
- */
1587
- configure(options) {
1588
- const newConfig = {
1589
- ...this.config,
1590
- // Override addOptions to return merged options
1591
- addOptions: () => ({
1592
- ...this.options,
1593
- ...options
1594
- })
1595
- };
1596
- return new _Extension(newConfig);
1597
- }
1598
- /**
1599
- * Returns a fresh, unbound copy built from the same `config`: `editor` reset to
1600
- * null and `options`/`storage` re-derived, while `configure()`/`extend()`
1601
- * results are preserved (they live in `config`). Polymorphic: a `Node`/`Mark`
1602
- * clones to its own subclass.
1603
- *
1604
- * `ExtensionManager` clones every extension so each editor owns its instances
1605
- * and binding one editor can't mutate extensions shared with another.
1606
- */
1607
- clone() {
1608
- const Ctor = this.constructor;
1609
- return new Ctor(this.config);
1610
- }
1611
- /**
1612
- * Creates a new extension with extended configuration
1613
- * Original extension is not modified
1614
- *
1615
- * **Note:** Config is merged shallowly using object spread (`...`).
1616
- * Config properties (like `addCommands`, `addKeyboardShortcuts`) are
1617
- * replaced entirely, not combined with the base extension's config.
1618
- *
1619
- * @param extendedConfig - Configuration to extend/override
1620
- * @returns New extension instance with extended config
1621
- *
1622
- * @example
1623
- * const Extended = MyExtension.extend({
1624
- * name: 'extendedExtension',
1625
- * addCommands() {
1626
- * return { customCommand: () => ({ tr }) => true };
1627
- * },
1628
- * });
1629
- *
1630
- * @example
1631
- * // To preserve base extension's commands while adding new ones:
1632
- * const Extended = BaseExtension.extend({
1633
- * addCommands() {
1634
- * const baseCommands = BaseExtension.config.addCommands?.call(this) ?? {};
1635
- * return {
1636
- * ...baseCommands,
1637
- * newCommand: () => ({ tr }) => true,
1638
- * };
1639
- * },
1640
- * });
1641
- */
1642
- extend(extendedConfig) {
1643
- const newConfig = mergeConfigWithParentBinding(this.config, extendedConfig);
1644
- return new _Extension(newConfig);
1645
- }
1646
- };
1647
-
1648
1770
  // src/helpers/specBuilder.ts
1649
1771
  function buildProseMirrorAttrs(attributeSpecs) {
1650
1772
  const attrs = {};
@@ -2682,8 +2804,7 @@ var builtInCommands = {
2682
2804
  function buildCommandProps(options) {
2683
2805
  const { editor, tr, dispatch, chain, can, commands } = options;
2684
2806
  return {
2685
- // Cast required: CommandPropsEditor is a minimal interface, but CommandProps
2686
- // expects full Editor. Callers ensure the actual editor instance is passed.
2807
+ // CommandPropsEditor is intentionally the minimal shape CommandProps consumes here.
2687
2808
  editor,
2688
2809
  state: editor.view.state,
2689
2810
  tr,
@@ -3111,8 +3232,7 @@ var CommandManager = class {
3111
3232
  buildCommandProps(tr, dispatch) {
3112
3233
  const { editor } = this;
3113
3234
  return {
3114
- // Cast needed: CommandManagerEditor is a minimal interface for dependency injection,
3115
- // but CommandProps expects the full Editor type. Callers pass the actual Editor instance.
3235
+ // CommandManagerEditor is intentionally the minimal shape CommandProps consumes here.
3116
3236
  editor,
3117
3237
  state: editor.state,
3118
3238
  tr,
@@ -3264,6 +3384,17 @@ function resolveOverrides(overrides) {
3264
3384
  if (!overrides) return DEFAULTS;
3265
3385
  return { ...DEFAULTS, ...overrides };
3266
3386
  }
3387
+ var BULLET_MARKERS = ["disc", "circle", "square"];
3388
+ var ORDERED_MARKERS = ["decimal", "lower-alpha", "lower-roman"];
3389
+ function listMarkerDepth(el, container) {
3390
+ let depth = 0;
3391
+ for (let parent = el.parentElement; parent !== null && parent !== container; parent = parent.parentElement) {
3392
+ const tag = parent.tagName;
3393
+ if (tag === "TD" || tag === "TH") break;
3394
+ if (tag === "UL" || tag === "OL") depth += 1;
3395
+ }
3396
+ return depth;
3397
+ }
3267
3398
  function applyInlineStyles(container, overrides) {
3268
3399
  const v = resolveOverrides(overrides);
3269
3400
  if (overrides?.codeHighlighter) {
@@ -3345,16 +3476,27 @@ function applyInlineStyles(container, overrides) {
3345
3476
  case "H6":
3346
3477
  styles = "font-size: 0.9em; font-weight: 700; line-height: 1.25; margin: 1.5em 0 0.5em;";
3347
3478
  break;
3479
+ // `type` is not a schema attribute on either list node, so no document
3480
+ // this editor produced carries one.
3348
3481
  case "UL":
3349
3482
  if (el.getAttribute("data-type") === "taskList") {
3350
3483
  styles = "list-style: none; padding-left: 0; margin: 0.75em 0;";
3351
- } else {
3484
+ } else if (el.hasAttribute("type")) {
3352
3485
  styles = "margin: 0.75em 0; padding-left: 1.5em;";
3486
+ } else {
3487
+ const bullet = BULLET_MARKERS[listMarkerDepth(el, container) % 3] ?? "disc";
3488
+ styles = `margin: 0.75em 0; padding-left: 1.5em; list-style-type: ${bullet};`;
3353
3489
  }
3354
3490
  break;
3355
- case "OL":
3356
- styles = "margin: 0.75em 0; padding-left: 1.5em;";
3491
+ case "OL": {
3492
+ if (el.hasAttribute("type")) {
3493
+ styles = "margin: 0.75em 0; padding-left: 1.5em;";
3494
+ break;
3495
+ }
3496
+ const number = ORDERED_MARKERS[listMarkerDepth(el, container) % 3] ?? "decimal";
3497
+ styles = `margin: 0.75em 0; padding-left: 1.5em; list-style-type: ${number};`;
3357
3498
  break;
3499
+ }
3358
3500
  case "LI":
3359
3501
  if (el.getAttribute("data-type") === "taskItem") {
3360
3502
  styles = "display: flex; align-items: flex-start; gap: 0.5em; margin: 0.25em 0;";
@@ -3541,6 +3683,15 @@ var Editor = class _Editor extends EventEmitter {
3541
3683
  "Editor requires either schema or extensions. Provide a ProseMirror schema directly, or use extensions like [Document, Paragraph, Text]."
3542
3684
  );
3543
3685
  }
3686
+ warnOnDuplicateProseMirrorCopy("prosemirror-model", model.Fragment, "@domternal/core");
3687
+ warnOnDuplicateProseMirrorCopy("prosemirror-state", state.Plugin, "@domternal/core");
3688
+ warnOnDuplicateProseMirrorCopy("prosemirror-view", view.EditorView, "@domternal/core");
3689
+ warnOnDuplicateProseMirrorCopy("prosemirror-transform", transform.Transform, "@domternal/core");
3690
+ warnOnDuplicateProseMirrorCopy(
3691
+ "@domternal/core",
3692
+ ExtensionConfigurationError,
3693
+ "@domternal/core"
3694
+ );
3544
3695
  this.options = {
3545
3696
  editable: true,
3546
3697
  ...options
@@ -3835,11 +3986,7 @@ var Editor = class _Editor extends EventEmitter {
3835
3986
  */
3836
3987
  getText(options = {}) {
3837
3988
  const { blockSeparator = "\n\n" } = options;
3838
- return this.state.doc.textBetween(
3839
- 0,
3840
- this.state.doc.content.size,
3841
- blockSeparator
3842
- );
3989
+ return this.state.doc.textBetween(0, this.state.doc.content.size, blockSeparator);
3843
3990
  }
3844
3991
  /**
3845
3992
  * Executes a command with proper CommandProps
@@ -3998,10 +4145,7 @@ var Editor = class _Editor extends EventEmitter {
3998
4145
  this._extensionManager.validateSchema();
3999
4146
  let doc;
4000
4147
  try {
4001
- doc = createDocument(
4002
- this.options.content ?? null,
4003
- this._extensionManager.schema
4004
- );
4148
+ doc = createDocument(this.options.content ?? null, this._extensionManager.schema);
4005
4149
  } catch (error) {
4006
4150
  const contentError = error instanceof Error ? error : new Error(String(error));
4007
4151
  this.emit("contentError", {
@@ -4037,7 +4181,10 @@ var Editor = class _Editor extends EventEmitter {
4037
4181
  }),
4038
4182
  ...Object.keys(nodeViews).length > 0 ? { nodeViews } : {},
4039
4183
  // Clipboard transform - apply user-provided transform (e.g. inlineStyles) on copy/cut
4040
- ...this.options.clipboardHTMLTransform ? this.buildClipboardSerializer(this.options.clipboardHTMLTransform, this._extensionManager.schema) : {},
4184
+ ...this.options.clipboardHTMLTransform ? this.buildClipboardSerializer(
4185
+ this.options.clipboardHTMLTransform,
4186
+ this._extensionManager.schema
4187
+ ) : {},
4041
4188
  // Handle focus/blur events
4042
4189
  handleDOMEvents: {
4043
4190
  focus: (_view, event) => {
@@ -4293,20 +4440,19 @@ function refocusEditorAfterCommand(view) {
4293
4440
 
4294
4441
  // src/utils/defaultBubbleContexts.ts
4295
4442
  var NOTION_TEXT_CONTEXT = Object.freeze([
4296
- // `ai` leads (Notion's "Ask AI"); skipped with its leading separator when
4297
- // the pro extension is absent, exactly like `mathInline`.
4298
4443
  "ai",
4444
+ "comment",
4445
+ "|",
4446
+ "heading",
4447
+ "|",
4448
+ "link",
4299
4449
  "|",
4300
4450
  "bold",
4301
4451
  "italic",
4302
4452
  "underline",
4303
4453
  "strike",
4304
4454
  "code",
4305
- "mathInline",
4306
- "|",
4307
- "link",
4308
- "|",
4309
- "textAlign"
4455
+ "mathInline"
4310
4456
  ]);
4311
4457
  var STANDARD_TEXT_CONTEXT = Object.freeze([
4312
4458
  "bold",
@@ -4323,6 +4469,206 @@ function defaultBubbleContexts(editor) {
4323
4469
  return { text: [...text] };
4324
4470
  }
4325
4471
 
4472
+ // src/utils/collapseSeparators.ts
4473
+ function collapseSeparators(items) {
4474
+ const out = [];
4475
+ for (const item of items) {
4476
+ if (item.type !== "separator") {
4477
+ out.push(item);
4478
+ continue;
4479
+ }
4480
+ if (out.length === 0) continue;
4481
+ if (out[out.length - 1]?.type === "separator") continue;
4482
+ out.push(item);
4483
+ }
4484
+ while (out.length > 0 && out[out.length - 1]?.type === "separator") out.pop();
4485
+ return out.length === items.length ? items : out;
4486
+ }
4487
+
4488
+ // src/utils/bubbleMenuResolver.ts
4489
+ function buildBubbleItemMaps(editor) {
4490
+ const itemMap = /* @__PURE__ */ new Map();
4491
+ const dropdownMap = /* @__PURE__ */ new Map();
4492
+ for (const item of editor.toolbarItems) {
4493
+ if (item.type === "button") {
4494
+ itemMap.set(item.name, item);
4495
+ } else if (item.type === "dropdown") {
4496
+ dropdownMap.set(item.name, item);
4497
+ for (const sub of item.items) {
4498
+ itemMap.set(sub.name, sub);
4499
+ }
4500
+ }
4501
+ }
4502
+ return {
4503
+ itemMap,
4504
+ dropdownMap,
4505
+ bubbleDefaults: buildBubbleDefaults(editor)
4506
+ };
4507
+ }
4508
+ function buildBubbleDefaults(editor) {
4509
+ const byCtx = /* @__PURE__ */ new Map();
4510
+ const addItem = (btn) => {
4511
+ const ctx = btn["bubbleMenu"];
4512
+ if (!ctx) return;
4513
+ let arr = byCtx.get(ctx);
4514
+ if (!arr) {
4515
+ arr = [];
4516
+ byCtx.set(ctx, arr);
4517
+ }
4518
+ arr.push(btn);
4519
+ };
4520
+ for (const item of editor.toolbarItems) {
4521
+ if (item.type === "button") addItem(item);
4522
+ else if (item.type === "dropdown") {
4523
+ for (const sub of item.items) addItem(sub);
4524
+ }
4525
+ }
4526
+ const result = /* @__PURE__ */ new Map();
4527
+ for (const [ctx, ctxItems] of byCtx) {
4528
+ ctxItems.sort((a, b) => (b.priority ?? 100) - (a.priority ?? 100));
4529
+ const list = [];
4530
+ let lastGroup;
4531
+ let sepIdx = 0;
4532
+ for (const item of ctxItems) {
4533
+ if (lastGroup !== void 0 && item.group !== lastGroup) {
4534
+ list.push({ type: "separator", name: `bsep-${String(sepIdx++)}` });
4535
+ }
4536
+ list.push(item);
4537
+ lastGroup = item.group;
4538
+ }
4539
+ result.set(ctx, list);
4540
+ }
4541
+ return result;
4542
+ }
4543
+ function resolveBubbleNames(names, itemMap, dropdownMap) {
4544
+ const result = [];
4545
+ let sepIdx = 0;
4546
+ for (const name of names) {
4547
+ if (name === "|") {
4548
+ result.push({ type: "separator", name: `sep-${String(sepIdx++)}` });
4549
+ continue;
4550
+ }
4551
+ const dropdown = dropdownMap.get(name);
4552
+ if (dropdown) {
4553
+ result.push(dropdown);
4554
+ continue;
4555
+ }
4556
+ const item = itemMap.get(name);
4557
+ if (item) result.push(item);
4558
+ }
4559
+ return result;
4560
+ }
4561
+ function getBubbleFormatItems(itemMap) {
4562
+ return Array.from(itemMap.values()).filter((item) => item.group === "format").sort((a, b) => (b.priority ?? 100) - (a.priority ?? 100));
4563
+ }
4564
+ function detectBubbleContext(selection, ctxs) {
4565
+ if ("$anchorCell" in selection) return null;
4566
+ if (selection.node) return selection.node.type.name;
4567
+ if (selection.empty) return null;
4568
+ const fromCell = findCellNode(selection.$from);
4569
+ if (fromCell) {
4570
+ const toCell = findCellNode(selection.$to);
4571
+ if (toCell && fromCell !== toCell) return null;
4572
+ return "table";
4573
+ }
4574
+ const fromName = selection.$from.parent.type.name;
4575
+ if (fromName in ctxs) return fromName;
4576
+ if ("text" in ctxs && selection.$from.parent.type.spec.marks !== "") return "text";
4577
+ const toName = selection.$to.parent.type.name;
4578
+ if (toName in ctxs) return toName;
4579
+ if ("text" in ctxs && selection.$to.parent.type.spec.marks !== "") return "text";
4580
+ return null;
4581
+ }
4582
+ function filterBubbleItemsBySchema(editor, contextName, schemaItems) {
4583
+ if (contextName === "text" || contextName === "table") return schemaItems;
4584
+ const schema = editor.state.schema;
4585
+ if (!schema) return schemaItems;
4586
+ const nodeType = schema.nodes[contextName];
4587
+ if (!nodeType) return schemaItems;
4588
+ return schemaItems.filter((item) => {
4589
+ const markName = typeof item.isActive === "string" ? item.isActive : null;
4590
+ if (!markName) return true;
4591
+ const markType = schema.marks[markName];
4592
+ if (!markType) return true;
4593
+ return nodeType.allowsMarkType(markType);
4594
+ });
4595
+ }
4596
+ function isInsideTableCell($pos) {
4597
+ for (let d = $pos.depth; d > 0; d--) {
4598
+ const name = $pos.node(d).type.name;
4599
+ if (name === "tableCell" || name === "tableHeader") return true;
4600
+ }
4601
+ return false;
4602
+ }
4603
+ function findCellNode(pos) {
4604
+ for (let d = pos.depth; d > 0; d--) {
4605
+ const node = pos.node(d);
4606
+ if (node.type.name === "tableCell" || node.type.name === "tableHeader") return node;
4607
+ }
4608
+ return null;
4609
+ }
4610
+ function resolveBubbleMenuItems(options) {
4611
+ return collapseSeparators(pickBubbleMenuItems(options));
4612
+ }
4613
+ function pickBubbleMenuItems({
4614
+ editor,
4615
+ maps,
4616
+ contexts,
4617
+ fallbackItems
4618
+ }) {
4619
+ const selection = editor.state.selection;
4620
+ if (contexts) {
4621
+ const ctx = detectBubbleContext(selection, contexts);
4622
+ if (!ctx) return [];
4623
+ if (ctx in contexts) {
4624
+ const val = contexts[ctx];
4625
+ if (val === null || Array.isArray(val) && val.length === 0) return [];
4626
+ if (val === true) {
4627
+ return filterBubbleItemsBySchema(editor, ctx, getBubbleFormatItems(maps.itemMap));
4628
+ }
4629
+ if (Array.isArray(val)) {
4630
+ const resolved = resolveBubbleNames(val, maps.itemMap, maps.dropdownMap);
4631
+ const buttons = resolved.filter(
4632
+ (item) => item.type !== "separator"
4633
+ );
4634
+ const allowed = new Set(
4635
+ filterBubbleItemsBySchema(editor, ctx, buttons).map((item) => item.name)
4636
+ );
4637
+ return resolved.filter(
4638
+ (item) => item.type === "separator" || allowed.has(item.name)
4639
+ );
4640
+ }
4641
+ }
4642
+ return maps.bubbleDefaults.get(ctx) ?? [];
4643
+ }
4644
+ if (selection.node && maps.bubbleDefaults.has(selection.node.type.name)) {
4645
+ return maps.bubbleDefaults.get(selection.node.type.name) ?? [];
4646
+ }
4647
+ return fallbackItems;
4648
+ }
4649
+ function createBubbleShouldShow(maps, contexts) {
4650
+ if (contexts) {
4651
+ return ({ state }) => {
4652
+ const selection = state.selection;
4653
+ const ctx = detectBubbleContext(selection, contexts);
4654
+ if (!ctx) return false;
4655
+ if (ctx in contexts) {
4656
+ const val = contexts[ctx];
4657
+ if (val === null) return false;
4658
+ return val === true || Array.isArray(val) && val.length > 0;
4659
+ }
4660
+ return maps.bubbleDefaults.has(ctx);
4661
+ };
4662
+ }
4663
+ return ({ state }) => {
4664
+ const selection = state.selection;
4665
+ if (selection.empty) return false;
4666
+ if (selection.node) return maps.bubbleDefaults.has(selection.node.type.name);
4667
+ if (isInsideTableCell(selection.$from)) return false;
4668
+ return selection.$from.parent.type.spec.marks !== "" || selection.$to.parent.type.spec.marks !== "";
4669
+ };
4670
+ }
4671
+
4326
4672
  // src/utils/insertAsListItemChild.ts
4327
4673
  var LIST_ITEM_TYPES3 = /* @__PURE__ */ new Set(["listItem", "taskItem"]);
4328
4674
  var LIST_WRAPPER_TYPES2 = /* @__PURE__ */ new Set(["bulletList", "orderedList", "taskList"]);
@@ -5291,12 +5637,13 @@ var FloatingMenuController = class _FloatingMenuController {
5291
5637
  */
5292
5638
  updateDisabledStates() {
5293
5639
  let changed = false;
5294
- let canProxy = null;
5295
- try {
5296
- canProxy = this.editor.can();
5297
- } catch {
5298
- canProxy = null;
5299
- }
5640
+ const canProxy = (() => {
5641
+ try {
5642
+ return this.editor.can();
5643
+ } catch {
5644
+ return null;
5645
+ }
5646
+ })();
5300
5647
  for (const item of this._flatItems) {
5301
5648
  const was = this._disabledMap.get(item.name) ?? false;
5302
5649
  let now = false;
@@ -7497,7 +7844,7 @@ function linkClickPlugin(options) {
7497
7844
  if (!view.editable) {
7498
7845
  return false;
7499
7846
  }
7500
- let link = null;
7847
+ let link;
7501
7848
  if (event.target instanceof HTMLAnchorElement) {
7502
7849
  link = event.target;
7503
7850
  } else {
@@ -10677,10 +11024,10 @@ function createBubbleMenuPlugin(options) {
10677
11024
  from: 0,
10678
11025
  to: 0
10679
11026
  }),
10680
- apply: (_tr, prevValue, _oldState, newState) => {
11027
+ apply: (tr, prevValue, _oldState, newState) => {
10681
11028
  const { selection } = newState;
10682
11029
  const { from, to } = selection;
10683
- if (from !== prevValue.from || to !== prevValue.to) {
11030
+ if (tr.selectionSet || from !== prevValue.from || to !== prevValue.to) {
10684
11031
  suppressed = false;
10685
11032
  }
10686
11033
  const visible = !suppressed && !selection.empty && editor.isEditable && shouldShow({
@@ -10765,10 +11112,8 @@ function createBubbleMenuPlugin(options) {
10765
11112
  }
10766
11113
  const state = pluginKey.getState(view.state);
10767
11114
  const prevPluginState = pluginKey.getState(prevState);
10768
- if (state?.visible === prevPluginState?.visible && state?.from === prevPluginState?.from && state?.to === prevPluginState?.to && !(state?.visible && view.state.doc !== prevState.doc)) {
10769
- if (!state?.visible && element.hasAttribute("data-show")) {
10770
- hideMenu();
10771
- }
11115
+ const domShown = element.hasAttribute("data-show") || updateTimeout !== null;
11116
+ if (state?.visible === prevPluginState?.visible && state?.from === prevPluginState?.from && state?.to === prevPluginState?.to && !(state?.visible && view.state.doc !== prevState.doc) && domShown === Boolean(state?.visible)) {
10772
11117
  return;
10773
11118
  }
10774
11119
  if (updateTimeout) {
@@ -10895,7 +11240,7 @@ var StarterKit = Extension.create({
10895
11240
  });
10896
11241
 
10897
11242
  // src/index.ts
10898
- var VERSION = "0.15.0";
11243
+ var VERSION = "1.0.1";
10899
11244
 
10900
11245
  Object.defineProperty(exports, "PluginKey", {
10901
11246
  enumerable: true,
@@ -10974,18 +11319,22 @@ exports.UniqueID = UniqueID;
10974
11319
  exports.VERSION = VERSION;
10975
11320
  exports.announce = announce;
10976
11321
  exports.applyInlineStyles = applyInlineStyles;
11322
+ exports.assertSingleProseMirrorCopy = assertSingleProseMirrorCopy;
10977
11323
  exports.autolinkPlugin = autolinkPlugin;
10978
11324
  exports.autolinkPluginKey = autolinkPluginKey;
10979
11325
  exports.blur = blur;
10980
11326
  exports.bubbleMenuPluginKey = bubbleMenuPluginKey;
11327
+ exports.buildBubbleItemMaps = buildBubbleItemMaps;
10981
11328
  exports.buildCommandProps = buildCommandProps;
10982
11329
  exports.builtInCommands = builtInCommands;
10983
11330
  exports.callOrReturn = callOrReturn;
10984
11331
  exports.characterCountPluginKey = characterCountPluginKey;
10985
11332
  exports.clearContent = clearContent;
11333
+ exports.collapseSeparators = collapseSeparators;
10986
11334
  exports.copyThemeClass = copyThemeClass;
10987
11335
  exports.createAccumulatingDispatch = createAccumulatingDispatch;
10988
11336
  exports.createBubbleMenuPlugin = createBubbleMenuPlugin;
11337
+ exports.createBubbleShouldShow = createBubbleShouldShow;
10989
11338
  exports.createCanChecker = createCanChecker;
10990
11339
  exports.createChainBuilder = createChainBuilder;
10991
11340
  exports.createDocument = createDocument;
@@ -10995,6 +11344,8 @@ exports.defaultBubbleContexts = defaultBubbleContexts;
10995
11344
  exports.defaultFloatingMenuShouldShow = defaultFloatingMenuShouldShow;
10996
11345
  exports.defaultIcons = defaultIcons;
10997
11346
  exports.deleteSelection = deleteSelection;
11347
+ exports.detectBubbleContext = detectBubbleContext;
11348
+ exports.filterBubbleItemsBySchema = filterBubbleItemsBySchema;
10998
11349
  exports.findChildren = findChildren;
10999
11350
  exports.findListItemAncestorDepth = findListItemAncestorDepth;
11000
11351
  exports.findParentNode = findParentNode;
@@ -11004,6 +11355,7 @@ exports.focusPluginKey = focusPluginKey;
11004
11355
  exports.generateHTML = generateHTML;
11005
11356
  exports.generateJSON = generateJSON;
11006
11357
  exports.generateText = generateText;
11358
+ exports.getBubbleFormatItems = getBubbleFormatItems;
11007
11359
  exports.getListItemCursorContext = getListItemCursorContext;
11008
11360
  exports.getMarkRange = getMarkRange;
11009
11361
  exports.groupFloatingMenuItems = groupFloatingMenuItems;
@@ -11018,6 +11370,7 @@ exports.invisibleCharsPluginKey = invisibleCharsPluginKey;
11018
11370
  exports.isDocumentEmpty = isDocumentEmpty;
11019
11371
  exports.isInListItemLabel = isInListItemLabel;
11020
11372
  exports.isInsideListItem = isInsideListItem;
11373
+ exports.isInsideTableCell = isInsideTableCell;
11021
11374
  exports.isNodeEmpty = isNodeEmpty;
11022
11375
  exports.isValidUrl = isValidUrl;
11023
11376
  exports.lift = lift;
@@ -11037,7 +11390,10 @@ exports.placeholderPluginKey = placeholderPluginKey;
11037
11390
  exports.positionFloating = positionFloating;
11038
11391
  exports.positionFloatingOnce = positionFloatingOnce;
11039
11392
  exports.refocusEditorAfterCommand = refocusEditorAfterCommand;
11393
+ exports.registerProseMirrorCopy = registerProseMirrorCopy;
11040
11394
  exports.resetAttributes = resetAttributes;
11395
+ exports.resolveBubbleMenuItems = resolveBubbleMenuItems;
11396
+ exports.resolveBubbleNames = resolveBubbleNames;
11041
11397
  exports.selectAll = selectAll;
11042
11398
  exports.selectNodeBackward = selectNodeBackward;
11043
11399
  exports.selectionDecorationPluginKey = selectionDecorationPluginKey;
@@ -11057,6 +11413,7 @@ exports.uniqueIDPluginKey = uniqueIDPluginKey;
11057
11413
  exports.unsetAllMarks = unsetAllMarks;
11058
11414
  exports.unsetMark = unsetMark;
11059
11415
  exports.updateAttributes = updateAttributes;
11416
+ exports.warnOnDuplicateProseMirrorCopy = warnOnDuplicateProseMirrorCopy;
11060
11417
  exports.wrapIn = wrapIn;
11061
11418
  exports.wrappingInputRule = wrappingInputRule;
11062
11419
  exports.writeToClipboard = writeToClipboard;