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