@threadlabs/looma 0.28.0 → 0.28.2

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.
@@ -0,0 +1,25 @@
1
+ import type { Editor } from "@tiptap/core";
2
+ import { createLowlight } from "lowlight";
3
+ import type { LoomaCodeLanguages } from "./extensions/preset.js";
4
+ /** Code-block highlighting for one editor: its lowlight registry, picker languages, and grammar loading. */
5
+ export interface LoomaCodeHighlighting {
6
+ /** The registry the code-block extension highlights with. */
7
+ readonly lowlight: ReturnType<typeof createLowlight>;
8
+ /** Languages the picker offers, in display order. */
9
+ readonly languageNames: readonly string[];
10
+ /**
11
+ * Loads the grammar a code block needs, then re-highlights the document.
12
+ *
13
+ * @contract Does nothing when the host supplied its own languages; each grammar loads once.
14
+ * @failure A failed import is forgotten so a later call retries it.
15
+ */
16
+ loadForCodeBlock(editor: Editor, language: unknown, value: string): void;
17
+ }
18
+ /**
19
+ * Highlighting from the host's `codeLanguages`, or, when that is null, from the built-in catalog,
20
+ * whose grammars load on demand and whose automatic detection uses a common subset.
21
+ *
22
+ * @lifecycle `onGrammarsLoaded` runs after each lazy load, before the document is re-highlighted,
23
+ * because the lowlight registry is mutable rather than reactive.
24
+ */
25
+ export declare function createCodeHighlighting(codeLanguages: LoomaCodeLanguages | null, onGrammarsLoaded: () => void): LoomaCodeHighlighting;
@@ -0,0 +1,10 @@
1
+ import type { JSONContent } from "@tiptap/core";
2
+ /** Whether a controlled document already matches the editor, so applying it would only reset the selection. */
3
+ export declare function sameDocument(left: JSONContent, right: JSONContent): boolean;
4
+ /**
5
+ * A link destination the editor may store: a site-relative path, fragment, or query, or an
6
+ * absolute http, https, mailto, or tel URL.
7
+ *
8
+ * @failure Returns null for empty input, control characters, other schemes, and unparseable URLs.
9
+ */
10
+ export declare function validLinkHref(value: string): string | null;
@@ -1,5 +1,5 @@
1
1
  import { Extension, type Editor, type JSONContent } from "@tiptap/core";
2
- export { normalizeImageDimension } from "../../editor/extensions/image.js";
2
+ export { normalizeImageDimension } from "./extensions/image.js";
3
3
  /** What caused an overlay to open or close. */
4
4
  type OverlayTrigger = "keyboard" | "pointer" | "programmatic";
5
5
  /**
@@ -0,0 +1,8 @@
1
+ import type { Editor } from "@tiptap/core";
2
+ import type { SlashMenuAnchorRect } from "./slash-menu.js";
3
+ /** A plain copy of a client rectangle, the anchor shape the slash and mention menus accept as a prop. */
4
+ export declare function menuAnchorRect(rect: DOMRect | null): SlashMenuAnchorRect | null;
5
+ /** The rendered table that holds the selection's start. */
6
+ export declare function selectedTableElement(editor: Editor): HTMLTableElement | null;
7
+ /** The rendered cell that holds the selection's start. */
8
+ export declare function selectedTableCellElement(editor: Editor): HTMLTableCellElement | null;
@@ -0,0 +1,39 @@
1
+ import { Editor, type EditorOptions, type JSONContent } from "@tiptap/core";
2
+ import { type Selection, type SelectionBookmark } from "@tiptap/pm/state";
3
+ /** A selection remembered so a controlled document replacement can put it back. */
4
+ export interface LoomaSelectionSnapshot {
5
+ bookmark: SelectionBookmark;
6
+ from: number;
7
+ to: number;
8
+ text: boolean;
9
+ }
10
+ export declare function selectionSnapshot(selection: Selection): LoomaSelectionSnapshot;
11
+ /**
12
+ * One editor's Tiptap instance, independent of any component framework.
13
+ *
14
+ * @ownership The session owns the `Editor` from creation until `destroy()`. A host renders an
15
+ * element for the document and hands it to `attach()`; it never creates or destroys the editor.
16
+ * @lifecycle The editor, its document, and its undo history survive `attach()` to a new element,
17
+ * so a host can remount its view without losing state.
18
+ */
19
+ export interface LoomaEditorSession<E extends Editor = Editor> {
20
+ readonly editor: E;
21
+ /** Moves the editor's view into `element` (idempotent). */
22
+ attach(element: HTMLElement): void;
23
+ /**
24
+ * Applies a controlled document.
25
+ *
26
+ * @contract Returns false, and leaves the editor untouched, when the document already matches.
27
+ * A focused editor keeps `selection` (or its current selection) in the same transaction.
28
+ * @failure A selection the new document cannot hold becomes the closest valid text range.
29
+ */
30
+ setContent(content: JSONContent, selection?: LoomaSelectionSnapshot | null): boolean;
31
+ /** Calls `listener` after every transaction; returns the unsubscribe function. */
32
+ subscribe(listener: () => void): () => void;
33
+ destroy(): void;
34
+ }
35
+ /**
36
+ * Creates the session's editor from `options`. A framework layer may pass its own `Editor`
37
+ * subclass, such as `@tiptap/vue-3`'s reactive one, so the editor it exposes keeps that behavior.
38
+ */
39
+ export declare function createEditorSession<E extends Editor = Editor>(options: Partial<EditorOptions>, EditorClass?: new (options: Partial<EditorOptions>) => E): LoomaEditorSession<E>;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Shows a key binding in ProseMirror notation ("Mod-Shift-8") the way the reader's platform writes
3
+ * it: modifier symbols in Apple's order on Apple devices (⇧⌘8), and Ctrl+Shift+8 elsewhere.
4
+ */
5
+ export declare function formatEditorShortcut(keys: string, apple: boolean): string;
6
+ export declare const isApplePlatform: () => boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threadlabs/looma",
3
- "version": "0.28.0",
3
+ "version": "0.28.2",
4
4
  "type": "module",
5
5
  "description": "Looma components as HTML, Vue, and plain DOM, with design tokens.",
6
6
  "license": "MIT",
@@ -3,7 +3,7 @@ import "./looma-editor.css";
3
3
  import { type AnyExtension, type Editor, type JSONContent } from "@tiptap/core";
4
4
  import { type PropType, type VNode } from "vue";
5
5
  import { type LoomaSlashCommand, type LoomaMentionItem, type LoomaMentionProvider, type LoomaCodeLanguages } from "@threadlabs/looma/editor";
6
- import { type LoomaImageActivationDetail, type LoomaImageAttributeResolver, type LoomaImageDescriptor, type LoomaImageRenditionErrorDetail } from "./image-delivery.js";
6
+ import { type LoomaImageActivationDetail, type LoomaImageAttributeResolver, type LoomaImageDescriptor, type LoomaImageRenditionErrorDetail } from "../../editor/image-delivery.js";
7
7
  /**
8
8
  * Host upload result normalized into the editor's durable image descriptor.
9
9
  * `url` becomes the stored `src`; optional dimensions preserve layout before
@@ -368,10 +368,10 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
368
368
  onImageActivate?: ((_detail: LoomaImageActivationDetail) => any) | undefined;
369
369
  onImageRenditionError?: ((_detail: LoomaImageRenditionErrorDetail) => any) | undefined;
370
370
  }>, {
371
- label: string;
372
- placeholder: string;
373
371
  extensions: AnyExtension[];
374
372
  editable: boolean;
373
+ label: string;
374
+ placeholder: string;
375
375
  linkBaseUrl: string;
376
376
  disableHighlight: boolean;
377
377
  codeLanguages: Readonly<Record<string, import("highlight.js").LanguageFn>> | null;
@@ -62,10 +62,10 @@ export declare const LoomaCompactEditor: import("vue").DefineComponent<import("v
62
62
  "onUpdate:modelValue"?: ((_value: JSONContent) => any) | undefined;
63
63
  onReady?: ((_editor: Editor) => any) | undefined;
64
64
  }>, {
65
- label: string;
66
- id: string;
67
65
  extensions: AnyExtension[];
68
66
  editable: boolean;
67
+ label: string;
68
+ id: string;
69
69
  modelValue: JSONContent;
70
70
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
71
71
  /** Renders basic compact JSON as escaped Vue nodes, including during SSR. */
@@ -1,6 +1,6 @@
1
1
  import { defineComponent, h, onBeforeUnmount, onMounted, shallowRef, watch } from "vue";
2
2
  import { Extension, Mark, Node, markInputRule, markPasteRule, mergeAttributes } from "@tiptap/core";
3
- import { Editor, EditorContent } from "@tiptap/vue-3";
3
+ import { Editor as Editor$1, EditorContent } from "@tiptap/vue-3";
4
4
  import { closeHistory, history, redo, undo } from "@tiptap/pm/history";
5
5
  import { Plugin } from "@tiptap/pm/state";
6
6
  //#region ../../node_modules/.pnpm/@tiptap+extension-document@2.27.2_@tiptap+core@2.27.2_@tiptap+pm@2.27.2_/node_modules/@tiptap/extension-document/dist/index.js
@@ -498,7 +498,7 @@ var LoomaCompactEditor = defineComponent({
498
498
  setup(props, { attrs, emit, expose }) {
499
499
  const editor = shallowRef();
500
500
  onMounted(() => {
501
- editor.value = new Editor({
501
+ editor.value = new Editor$1({
502
502
  extensions: [...getCompactEditorExtensions(), ...props.extensions],
503
503
  content: props.modelValue,
504
504
  editable: props.editable,
@@ -1,4 +1,4 @@
1
1
  export * from "@threadlabs/looma/editor";
2
2
  export * from "./primitives.js";
3
3
  export { LoomaEditor, type LoomaEditorToolbarMode, type LoomaEditorSelectionAction, type LoomaEditorSelectionActionDetail, type LoomaEditorRangeAction, type LoomaEditorRangeActionDetail, type LoomaEditorSlashCommands, type LoomaLinkTarget, type LoomaLinkSearch, type LoomaLinkResolve, type LoomaImageUploader, type LoomaImageUploadResult, } from "./LoomaEditor.js";
4
- export type { LoomaImageActivationDetail, LoomaImageActivationTrigger, LoomaImageAttributeResolver, LoomaImageDescriptor, LoomaImageRenditionErrorDetail, LoomaImageRenditionAttributes, } from "./image-delivery.js";
4
+ export type { LoomaImageActivationDetail, LoomaImageActivationTrigger, LoomaImageAttributeResolver, LoomaImageDescriptor, LoomaImageRenditionErrorDetail, LoomaImageRenditionAttributes, } from "../../editor/image-delivery.js";
@@ -1,12 +1,12 @@
1
1
  import { computed, createApp, defineComponent, h, nextTick, onBeforeUnmount, onMounted, reactive, ref, shallowRef, useId, watch } from "vue";
2
2
  import { DEFAULT_MENTION_RESULT_LIMIT, LOOMA_CHIP_COLORS, LOOMA_ICONS, createLoomaMentionExtension, createLoomaSlashCommandExtension, getActiveTableUiState, getDefaultEditorExtensions, getDefaultSlashCommands, getTableOfContentsEntries, getVisualViewportRect, handleTableAction, handleTableOverlayAction, measureTableOverlayGeometry, normalizeActiveTableColumnWidths, normalizeLoomaChipColor, normalizeTableOfContentsConfig, resolveTableCellAt, siteRelativeHref } from "@threadlabs/looma/editor";
3
3
  import { Button, Callout, Card, Checkbox, Cluster, Combobox, Disclosure, EditorInsertTableGrid, EditorMentionMenu, EditorMentionMenuItem, EditorSlashMenu, EditorSlashMenuGroup, EditorSlashMenuItem, EditorSurface, EditorTableContextMenu, EditorTableOverlay, EditorTableToolbar, EditorToolbar, EditorToolbar as EditorToolbar$1, FormField, Icon, IconButton, Image, Input, InputGroup, List, Menu, MenuItem, Popover, ScrollArea, SearchResultRow, Separator, Stack, Text, Tooltip } from "@threadlabs/looma/vue";
4
- import { Extension, Node as Node$1, getText, getTextSerializersFromSchema, mergeAttributes, nodeInputRule, posToDOMRect } from "@tiptap/core";
5
- import { BubbleMenu, EditorContent, useEditor } from "@tiptap/vue-3";
4
+ import { Editor, Extension, Node as Node$1, getText, getTextSerializersFromSchema, mergeAttributes, nodeInputRule, posToDOMRect } from "@tiptap/core";
5
+ import { BubbleMenu, Editor as Editor$1 } from "@tiptap/vue-3";
6
6
  import { closeHistory } from "@tiptap/pm/history";
7
- import { createLowlight } from "lowlight";
8
7
  import { AllSelection, NodeSelection, Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
9
8
  import { Decoration, DecorationSet } from "@tiptap/pm/view";
9
+ import { createLowlight } from "lowlight";
10
10
  import { DOMParser, DOMSerializer, Fragment as Fragment$1 } from "@tiptap/pm/model";
11
11
  export * from "@threadlabs/looma/editor";
12
12
  //#region src/vue/editor/primitives.ts
@@ -272,7 +272,7 @@ Image$1.extend({ addAttributes() {
272
272
  };
273
273
  } });
274
274
  //#endregion
275
- //#region src/vue/editor/image-delivery.ts
275
+ //#region src/editor/image-delivery.ts
276
276
  function optionalString(value) {
277
277
  return typeof value === "string" && value.length > 0 ? value : void 0;
278
278
  }
@@ -409,7 +409,7 @@ function imageUploadContent(image) {
409
409
  };
410
410
  }
411
411
  //#endregion
412
- //#region src/vue/editor/code-language-catalog.ts
412
+ //#region src/editor/code-language-catalog.ts
413
413
  var codeLanguageCatalog = {
414
414
  "1c": () => import("../chunks/1c.js").then((module) => module.default),
415
415
  "abnf": () => import("../chunks/abnf.js").then((module) => module.default),
@@ -628,6 +628,191 @@ var autoCodeLanguageNames = [
628
628
  "yaml"
629
629
  ];
630
630
  //#endregion
631
+ //#region src/editor/code-highlighting.ts
632
+ /**
633
+ * Highlighting from the host's `codeLanguages`, or, when that is null, from the built-in catalog,
634
+ * whose grammars load on demand and whose automatic detection uses a common subset.
635
+ *
636
+ * @lifecycle `onGrammarsLoaded` runs after each lazy load, before the document is re-highlighted,
637
+ * because the lowlight registry is mutable rather than reactive.
638
+ */
639
+ function createCodeHighlighting(codeLanguages, onGrammarsLoaded) {
640
+ const registry = createLowlight(codeLanguages ?? void 0);
641
+ const lowlight = codeLanguages === null ? {
642
+ ...registry,
643
+ highlightAuto: (value) => registry.highlightAuto(value, { subset: [...autoCodeLanguageNames] })
644
+ } : registry;
645
+ const languageNames = codeLanguages === null ? [...autoCodeLanguageNames, ...Object.keys(codeLanguageCatalog).filter((name) => !autoCodeLanguageNames.includes(name)).sort()] : Object.keys(codeLanguages).sort();
646
+ const grammarLoads = /* @__PURE__ */ new Map();
647
+ let catalogLoad;
648
+ const refresh = (editor) => {
649
+ if (editor.isDestroyed) return;
650
+ onGrammarsLoaded();
651
+ const tr = editor.state.tr;
652
+ editor.state.doc.descendants((node, pos) => {
653
+ if (node.type.name === "codeBlock") tr.setNodeMarkup(pos, void 0, node.attrs);
654
+ });
655
+ if (tr.docChanged) editor.view.dispatch(tr.setMeta("addToHistory", false));
656
+ };
657
+ const loadGrammar = (name) => {
658
+ const existing = grammarLoads.get(name);
659
+ if (existing) return existing;
660
+ const pending = codeLanguageCatalog[name]().then((grammar) => {
661
+ registry.register({ [name]: grammar });
662
+ }).catch((error) => {
663
+ grammarLoads.delete(name);
664
+ throw error;
665
+ });
666
+ grammarLoads.set(name, pending);
667
+ return pending;
668
+ };
669
+ return {
670
+ lowlight,
671
+ languageNames,
672
+ loadForCodeBlock(editor, language, value) {
673
+ if (codeLanguages !== null || editor.isDestroyed) return;
674
+ if (typeof language === "string" && Object.hasOwn(codeLanguageCatalog, language)) {
675
+ if (registry.registered(language) || grammarLoads.has(language)) return;
676
+ loadGrammar(language).then(() => refresh(editor)).catch(() => {});
677
+ } else if (!language && value.trim()) catalogLoad ??= Promise.all(autoCodeLanguageNames.map((name) => loadGrammar(name))).then(() => refresh(editor)).catch(() => {
678
+ catalogLoad = void 0;
679
+ });
680
+ }
681
+ };
682
+ }
683
+ //#endregion
684
+ //#region src/editor/content.ts
685
+ /** Whether a controlled document already matches the editor, so applying it would only reset the selection. */
686
+ function sameDocument(left, right) {
687
+ return JSON.stringify(left) === JSON.stringify(right);
688
+ }
689
+ /**
690
+ * A link destination the editor may store: a site-relative path, fragment, or query, or an
691
+ * absolute http, https, mailto, or tel URL.
692
+ *
693
+ * @failure Returns null for empty input, control characters, other schemes, and unparseable URLs.
694
+ */
695
+ function validLinkHref(value) {
696
+ const href = value.trim();
697
+ if (!href || /[\u0000-\u001f\u007f]/.test(href)) return null;
698
+ if (/^(\/|#|\?)/.test(href)) return href;
699
+ try {
700
+ const url = new URL(href);
701
+ return [
702
+ "http:",
703
+ "https:",
704
+ "mailto:",
705
+ "tel:"
706
+ ].includes(url.protocol) ? href : null;
707
+ } catch {
708
+ return null;
709
+ }
710
+ }
711
+ //#endregion
712
+ //#region src/editor/selection-geometry.ts
713
+ /** A plain copy of a client rectangle, the anchor shape the slash and mention menus accept as a prop. */
714
+ function menuAnchorRect(rect) {
715
+ if (!rect) return null;
716
+ return {
717
+ left: rect.left,
718
+ top: rect.top,
719
+ right: rect.right,
720
+ bottom: rect.bottom,
721
+ x: rect.x,
722
+ y: rect.y,
723
+ width: rect.width,
724
+ height: rect.height
725
+ };
726
+ }
727
+ function selectionElement(editor) {
728
+ const { node } = editor.view.domAtPos(editor.state.selection.from);
729
+ return node instanceof HTMLElement ? node : node.parentElement;
730
+ }
731
+ /** The rendered table that holds the selection's start. */
732
+ function selectedTableElement(editor) {
733
+ return selectionElement(editor)?.closest("table");
734
+ }
735
+ /** The rendered cell that holds the selection's start. */
736
+ function selectedTableCellElement(editor) {
737
+ return selectionElement(editor)?.closest("td, th");
738
+ }
739
+ //#endregion
740
+ //#region src/editor/session.ts
741
+ function selectionSnapshot(selection) {
742
+ return {
743
+ bookmark: selection.getBookmark(),
744
+ from: selection.from,
745
+ to: selection.to,
746
+ text: selection instanceof TextSelection
747
+ };
748
+ }
749
+ /**
750
+ * Creates the session's editor from `options`. A framework layer may pass its own `Editor`
751
+ * subclass, such as `@tiptap/vue-3`'s reactive one, so the editor it exposes keeps that behavior.
752
+ */
753
+ function createEditorSession(options, EditorClass = Editor) {
754
+ const editor = new EditorClass(options);
755
+ return {
756
+ editor,
757
+ attach(element) {
758
+ const current = editor.options.element;
759
+ if (editor.isDestroyed || current === element || !current.firstChild) return;
760
+ element.append(...current.childNodes);
761
+ editor.setOptions({ element });
762
+ editor.createNodeViews();
763
+ },
764
+ setContent(content, selection) {
765
+ if (editor.isDestroyed || sameDocument(editor.getJSON(), content)) return false;
766
+ const previous = selection ?? selectionSnapshot(editor.view.state.selection);
767
+ if (!editor.isFocused) {
768
+ editor.commands.setContent(content, false);
769
+ return true;
770
+ }
771
+ editor.chain().setContent(content, false).command(({ tr }) => {
772
+ try {
773
+ tr.setSelection(previous.text ? TextSelection.create(tr.doc, previous.from, previous.to) : previous.bookmark.resolve(tr.doc));
774
+ } catch {
775
+ const max = tr.doc.content.size;
776
+ const from = Math.max(1, Math.min(previous.from, max));
777
+ const to = Math.max(from, Math.min(previous.to, max));
778
+ tr.setSelection(TextSelection.create(tr.doc, from, to));
779
+ }
780
+ return true;
781
+ }).run();
782
+ editor.view.focus();
783
+ return true;
784
+ },
785
+ subscribe(listener) {
786
+ editor.on("transaction", listener);
787
+ return () => {
788
+ editor.off("transaction", listener);
789
+ };
790
+ },
791
+ destroy() {
792
+ editor.destroy();
793
+ }
794
+ };
795
+ }
796
+ //#endregion
797
+ //#region src/editor/shortcuts.ts
798
+ /**
799
+ * Shows a key binding in ProseMirror notation ("Mod-Shift-8") the way the reader's platform writes
800
+ * it: modifier symbols in Apple's order on Apple devices (⇧⌘8), and Ctrl+Shift+8 elsewhere.
801
+ */
802
+ function formatEditorShortcut(keys, apple) {
803
+ const parts = keys.split("-");
804
+ const key = (parts.pop() ?? "").toUpperCase();
805
+ const has = (name) => parts.includes(name);
806
+ if (apple) return `${has("Ctrl") ? "⌃" : ""}${has("Alt") ? "⌥" : ""}${has("Shift") ? "⇧" : ""}${has("Mod") ? "⌘" : ""}${key}`;
807
+ return [
808
+ has("Mod") || has("Ctrl") ? "Ctrl" : "",
809
+ has("Alt") ? "Alt" : "",
810
+ has("Shift") ? "Shift" : "",
811
+ key
812
+ ].filter(Boolean).join("+");
813
+ }
814
+ var isApplePlatform = () => typeof navigator !== "undefined" && /mac|iphone|ipad|ipod/i.test(navigator.userAgentData?.platform || navigator.platform || navigator.userAgent);
815
+ //#endregion
631
816
  //#region src/editor/extensions/expand.ts
632
817
  /**
633
818
  * Reveals collapsed ancestors before a host navigates to a document position.
@@ -1669,23 +1854,6 @@ var imageNodeView = ({ editor, node, getPos, decorations }) => {
1669
1854
  };
1670
1855
  //#endregion
1671
1856
  //#region src/vue/editor/LoomaEditor.ts
1672
- /**
1673
- * Shows a key binding in ProseMirror notation ("Mod-Shift-8") the way the reader's platform writes
1674
- * it: modifier symbols in Apple's order on Apple devices (⇧⌘8), and Ctrl+Shift+8 elsewhere.
1675
- */
1676
- function formatEditorShortcut(keys, apple) {
1677
- const parts = keys.split("-");
1678
- const key = (parts.pop() ?? "").toUpperCase();
1679
- const has = (name) => parts.includes(name);
1680
- if (apple) return `${has("Ctrl") ? "⌃" : ""}${has("Alt") ? "⌥" : ""}${has("Shift") ? "⇧" : ""}${has("Mod") ? "⌘" : ""}${key}`;
1681
- return [
1682
- has("Mod") || has("Ctrl") ? "Ctrl" : "",
1683
- has("Alt") ? "Alt" : "",
1684
- has("Shift") ? "Shift" : "",
1685
- key
1686
- ].filter(Boolean).join("+");
1687
- }
1688
- var isApplePlatform = () => typeof navigator !== "undefined" && /mac|iphone|ipad|ipod/i.test(navigator.userAgentData?.platform || navigator.platform || navigator.userAgent);
1689
1857
  var EMPTY_DOCUMENT = {
1690
1858
  type: "doc",
1691
1859
  content: []
@@ -1708,38 +1876,6 @@ var EMPTY_CAPABILITIES = {
1708
1876
  canMergeCells: false,
1709
1877
  canSplitCell: false
1710
1878
  };
1711
- function sameDocument(left, right) {
1712
- return JSON.stringify(left) === JSON.stringify(right);
1713
- }
1714
- function validLinkHref(value) {
1715
- const href = value.trim();
1716
- if (!href || /[\u0000-\u001f\u007f]/.test(href)) return null;
1717
- if (/^(\/|#|\?)/.test(href)) return href;
1718
- try {
1719
- const url = new URL(href);
1720
- return [
1721
- "http:",
1722
- "https:",
1723
- "mailto:",
1724
- "tel:"
1725
- ].includes(url.protocol) ? href : null;
1726
- } catch {
1727
- return null;
1728
- }
1729
- }
1730
- function managedMenuAnchorRect(rect) {
1731
- if (!rect) return null;
1732
- return {
1733
- left: rect.left,
1734
- top: rect.top,
1735
- right: rect.right,
1736
- bottom: rect.bottom,
1737
- x: rect.x,
1738
- y: rect.y,
1739
- width: rect.width,
1740
- height: rect.height
1741
- };
1742
- }
1743
1879
  function slashMenuRows(items) {
1744
1880
  const groups = [];
1745
1881
  for (const [index, item] of items.entries()) {
@@ -1760,14 +1896,6 @@ function slashMenuRows(items) {
1760
1896
  label
1761
1897
  }, () => rows)] : rows);
1762
1898
  }
1763
- function selectedTableElement(editor) {
1764
- const { node } = editor.view.domAtPos(editor.state.selection.from);
1765
- return (node instanceof HTMLElement ? node : node.parentElement)?.closest("table");
1766
- }
1767
- function selectedTableCellElement(editor) {
1768
- const { node } = editor.view.domAtPos(editor.state.selection.from);
1769
- return (node instanceof HTMLElement ? node : node.parentElement)?.closest("td, th");
1770
- }
1771
1899
  function loomaIcon(name) {
1772
1900
  return h("svg", {
1773
1901
  class: "looma-icon",
@@ -2135,21 +2263,13 @@ var LoomaEditor = defineComponent({
2135
2263
  let linkPressedSelection = null;
2136
2264
  const rememberSelection = (instance) => {
2137
2265
  if (!instance.isFocused) return;
2138
- const selection = instance.view.state.selection;
2139
- lastFocusedSelection = {
2140
- bookmark: selection.getBookmark(),
2141
- from: selection.from,
2142
- to: selection.to,
2143
- text: selection instanceof TextSelection
2144
- };
2266
+ lastFocusedSelection = selectionSnapshot(instance.view.state.selection);
2145
2267
  };
2146
2268
  const authorHighlight = !props.disableHighlight;
2147
- const codeLowlight = createLowlight(props.codeLanguages ?? void 0);
2148
- const codeHighlightLowlight = props.codeLanguages === null ? {
2149
- ...codeLowlight,
2150
- highlightAuto: (value) => codeLowlight.highlightAuto(value, { subset: [...autoCodeLanguageNames] })
2151
- } : codeLowlight;
2152
- const codeLanguageNames = props.codeLanguages === null ? [...autoCodeLanguageNames, ...Object.keys(codeLanguageCatalog).filter((name) => !autoCodeLanguageNames.includes(name)).sort()] : Object.keys(props.codeLanguages).sort();
2269
+ const codeHighlighting = createCodeHighlighting(props.codeLanguages, () => {
2270
+ codeUi.grammarVersion += 1;
2271
+ });
2272
+ const codeLanguageNames = codeHighlighting.languageNames;
2153
2273
  const codeUi = reactive({
2154
2274
  open: false,
2155
2275
  pos: 0,
@@ -2158,115 +2278,97 @@ var LoomaEditor = defineComponent({
2158
2278
  grammarVersion: 0,
2159
2279
  style: {}
2160
2280
  });
2161
- const grammarLoads = /* @__PURE__ */ new Map();
2162
- let catalogLoad;
2163
- const refreshCodeHighlights = (instance) => {
2164
- if (instance.isDestroyed) return;
2165
- codeUi.grammarVersion += 1;
2166
- const tr = instance.state.tr;
2167
- instance.state.doc.descendants((node, pos) => {
2168
- if (node.type.name === "codeBlock") tr.setNodeMarkup(pos, void 0, node.attrs);
2169
- });
2170
- if (tr.docChanged) instance.view.dispatch(tr.setMeta("addToHistory", false));
2171
- };
2172
- const loadGrammar = (name) => {
2173
- const existing = grammarLoads.get(name);
2174
- if (existing) return existing;
2175
- const pending = codeLanguageCatalog[name]().then((grammar) => {
2176
- codeLowlight.register({ [name]: grammar });
2177
- }).catch((error) => {
2178
- grammarLoads.delete(name);
2179
- throw error;
2180
- });
2181
- grammarLoads.set(name, pending);
2182
- return pending;
2183
- };
2184
- const loadForCodeBlock = (instance, language, value) => {
2185
- if (props.codeLanguages !== null || instance.isDestroyed) return;
2186
- if (typeof language === "string" && Object.hasOwn(codeLanguageCatalog, language)) {
2187
- if (codeLowlight.registered(language) || grammarLoads.has(language)) return;
2188
- loadGrammar(language).then(() => refreshCodeHighlights(instance)).catch(() => {});
2189
- } else if (!language && value.trim()) catalogLoad ??= Promise.all(autoCodeLanguageNames.map((name) => loadGrammar(name))).then(() => refreshCodeHighlights(instance)).catch(() => {
2190
- catalogLoad = void 0;
2191
- });
2192
- };
2193
- const editor = useEditor({
2194
- extensions: [
2195
- ...getDefaultEditorExtensions({
2196
- placeholder: props.placeholder,
2197
- linkBaseUrl: props.linkBaseUrl,
2198
- mention: mentionExtension ?? false,
2199
- disableHighlight: props.disableHighlight,
2200
- codeLanguages: props.codeLanguages ?? void 0,
2201
- codeLowlight: codeHighlightLowlight,
2202
- tableOfContentsNodeView,
2203
- expandNodeView,
2204
- dividerNodeView,
2205
- calloutNodeView,
2206
- imageNodeView
2207
- }),
2208
- imageDelivery.extension,
2209
- slashExtension,
2210
- ...props.extensions
2211
- ],
2212
- content: props.modelValue,
2213
- editable: props.editable,
2214
- editorProps: {
2215
- attributes: {
2216
- role: "textbox",
2217
- "aria-multiline": "true",
2218
- "aria-label": props.label
2219
- },
2220
- handleKeyDown: (_view, event) => onFormattingKeyDown(event),
2221
- handleDOMEvents: { click: (view, event) => {
2222
- if (!(event.target instanceof Element) || event.button !== 0) return false;
2223
- const link = event.target.closest("a[href]");
2224
- if (!link || !view.dom.contains(link)) return false;
2225
- const href = validLinkHref(link.getAttribute("href") ?? "");
2226
- if (!href) return false;
2227
- if (isApplePlatform() ? event.metaKey : event.ctrlKey) {
2228
- event.preventDefault();
2229
- window.open(href, "_blank", "noopener,noreferrer");
2230
- return true;
2231
- }
2232
- dismissedFormattingSelection = null;
2233
- requestAnimationFrame(() => {
2234
- if (view.isDestroyed) return;
2235
- editorStateVersion.value += 1;
2236
- const instance = editor.value;
2237
- if (instance?.isFocused && instance.state.selection.empty && instance.isActive("link") && !linkOpen.value) {
2238
- linkPopup?.setProps({ getReferenceClientRect: () => link.getBoundingClientRect() });
2239
- linkPopup?.show();
2281
+ const loadForCodeBlock = codeHighlighting.loadForCodeBlock;
2282
+ const editor = shallowRef();
2283
+ let session;
2284
+ onMounted(() => {
2285
+ session = createEditorSession({
2286
+ extensions: [
2287
+ ...getDefaultEditorExtensions({
2288
+ placeholder: props.placeholder,
2289
+ linkBaseUrl: props.linkBaseUrl,
2290
+ mention: mentionExtension ?? false,
2291
+ disableHighlight: props.disableHighlight,
2292
+ codeLanguages: props.codeLanguages ?? void 0,
2293
+ codeLowlight: codeHighlighting.lowlight,
2294
+ tableOfContentsNodeView,
2295
+ expandNodeView,
2296
+ dividerNodeView,
2297
+ calloutNodeView,
2298
+ imageNodeView
2299
+ }),
2300
+ imageDelivery.extension,
2301
+ slashExtension,
2302
+ ...props.extensions
2303
+ ],
2304
+ content: props.modelValue,
2305
+ editable: props.editable,
2306
+ editorProps: {
2307
+ attributes: {
2308
+ role: "textbox",
2309
+ "aria-multiline": "true",
2310
+ "aria-label": props.label
2311
+ },
2312
+ handleKeyDown: (_view, event) => onFormattingKeyDown(event),
2313
+ handleDOMEvents: { click: (view, event) => {
2314
+ if (!(event.target instanceof Element) || event.button !== 0) return false;
2315
+ const link = event.target.closest("a[href]");
2316
+ if (!link || !view.dom.contains(link)) return false;
2317
+ const href = validLinkHref(link.getAttribute("href") ?? "");
2318
+ if (!href) return false;
2319
+ if (isApplePlatform() ? event.metaKey : event.ctrlKey) {
2320
+ event.preventDefault();
2321
+ window.open(href, "_blank", "noopener,noreferrer");
2322
+ return true;
2240
2323
  }
2324
+ dismissedFormattingSelection = null;
2325
+ requestAnimationFrame(() => {
2326
+ if (view.isDestroyed) return;
2327
+ editorStateVersion.value += 1;
2328
+ const instance = editor.value;
2329
+ if (instance?.isFocused && instance.state.selection.empty && instance.isActive("link") && !linkOpen.value) {
2330
+ linkPopup?.setProps({ getReferenceClientRect: () => link.getBoundingClientRect() });
2331
+ linkPopup?.show();
2332
+ }
2333
+ });
2334
+ return false;
2335
+ } }
2336
+ },
2337
+ onCreate: ({ editor: instance }) => {
2338
+ emit("ready", instance);
2339
+ instance.state.doc.descendants((node) => {
2340
+ if (node.type.name === "codeBlock") loadForCodeBlock(instance, node.attrs.language, node.textContent);
2241
2341
  });
2242
- return false;
2243
- } }
2244
- },
2245
- onCreate: ({ editor: instance }) => {
2246
- emit("ready", instance);
2247
- instance.state.doc.descendants((node) => {
2248
- if (node.type.name === "codeBlock") loadForCodeBlock(instance, node.attrs.language, node.textContent);
2249
- });
2250
- },
2251
- onFocus: ({ editor: instance }) => rememberSelection(instance),
2252
- onSelectionUpdate: ({ editor: instance }) => {
2253
- rememberSelection(instance);
2254
- if (floatingFormattingMode()) {
2255
- const selection = instance.state.selection;
2256
- if (!dismissedFormattingSelection?.eq(selection)) dismissedFormattingSelection = null;
2257
- if (!heldFormattingSelection?.eq(selection)) heldFormattingSelection = null;
2258
- if (!formattingSelectionActive(instance)) formattingPopup?.hide();
2342
+ },
2343
+ onFocus: ({ editor: instance }) => rememberSelection(instance),
2344
+ onSelectionUpdate: ({ editor: instance }) => {
2345
+ rememberSelection(instance);
2346
+ if (floatingFormattingMode()) {
2347
+ const selection = instance.state.selection;
2348
+ if (!dismissedFormattingSelection?.eq(selection)) dismissedFormattingSelection = null;
2349
+ if (!heldFormattingSelection?.eq(selection)) heldFormattingSelection = null;
2350
+ if (!formattingSelectionActive(instance)) formattingPopup?.hide();
2351
+ }
2352
+ if (linkContextEditing.value && instance.state.selection.from !== linkSelection?.from) linkContextEditing.value = false;
2353
+ },
2354
+ onUpdate: ({ editor: instance }) => {
2355
+ const value = instance.getJSON();
2356
+ emit("update:modelValue", value);
2357
+ emit("update", value);
2259
2358
  }
2260
- if (linkContextEditing.value && instance.state.selection.from !== linkSelection?.from) linkContextEditing.value = false;
2261
- },
2262
- onTransaction: () => {
2359
+ }, Editor$1);
2360
+ session.subscribe(() => {
2263
2361
  editorStateVersion.value += 1;
2264
- },
2265
- onUpdate: ({ editor: instance }) => {
2266
- const value = instance.getJSON();
2267
- emit("update:modelValue", value);
2268
- emit("update", value);
2269
- }
2362
+ });
2363
+ editor.value = session.editor;
2364
+ });
2365
+ const attachDocument = (element) => {
2366
+ if (element instanceof HTMLElement) session?.attach(element);
2367
+ };
2368
+ onBeforeUnmount(() => {
2369
+ const element = session?.editor.options.element;
2370
+ element?.parentNode?.replaceChild(element.cloneNode(true), element);
2371
+ session?.destroy();
2270
2372
  });
2271
2373
  watch(() => {
2272
2374
  editorStateVersion.value;
@@ -2735,31 +2837,9 @@ var LoomaEditor = defineComponent({
2735
2837
  });
2736
2838
  watch(() => props.modelValue, (value) => {
2737
2839
  const instance = editor.value;
2738
- if (!instance || sameDocument(instance.getJSON(), value)) return;
2840
+ if (!instance || !session) return;
2739
2841
  const wasFocused = instance.isFocused;
2740
- const previousSelection = lastFocusedSelection ?? {
2741
- bookmark: instance.view.state.selection.getBookmark(),
2742
- from: instance.view.state.selection.from,
2743
- to: instance.view.state.selection.to,
2744
- text: instance.view.state.selection instanceof TextSelection
2745
- };
2746
- if (!wasFocused) {
2747
- instance.commands.setContent(value, false);
2748
- return;
2749
- }
2750
- instance.chain().setContent(value, false).command(({ tr }) => {
2751
- try {
2752
- tr.setSelection(previousSelection.text ? TextSelection.create(tr.doc, previousSelection.from, previousSelection.to) : previousSelection.bookmark.resolve(tr.doc));
2753
- } catch {
2754
- const max = tr.doc.content.size;
2755
- const from = Math.max(1, Math.min(previousSelection.from, max));
2756
- const to = Math.max(from, Math.min(previousSelection.to, max));
2757
- tr.setSelection(TextSelection.create(tr.doc, from, to));
2758
- }
2759
- return true;
2760
- }).run();
2761
- instance.view.focus();
2762
- rememberSelection(instance);
2842
+ if (session.setContent(value, lastFocusedSelection) && wasFocused) rememberSelection(instance);
2763
2843
  }, { deep: true });
2764
2844
  const closeTableUi = () => {
2765
2845
  tableUi.toolbarOpen = false;
@@ -2823,7 +2903,7 @@ var LoomaEditor = defineComponent({
2823
2903
  };
2824
2904
  const codeLanguageOptions = computed(() => {
2825
2905
  codeUi.grammarVersion;
2826
- const detected = codeUi.open && codeUi.text.trim() ? codeHighlightLowlight.highlightAuto(codeUi.text).data?.language : null;
2906
+ const detected = codeUi.open && codeUi.text.trim() ? codeHighlighting.lowlight.highlightAuto(codeUi.text).data?.language : null;
2827
2907
  return [
2828
2908
  {
2829
2909
  value: "auto",
@@ -3672,7 +3752,7 @@ var LoomaEditor = defineComponent({
3672
3752
  }
3673
3753
  }
3674
3754
  }, { default: () => h(ImageControls, { editor: instance }) }) : null,
3675
- instance ? h(EditorSurface, { density: props.editable ? "comfortable" : props.contentDensity }, () => h(EditorContent, { editor: instance })) : null,
3755
+ instance ? h(EditorSurface, { density: props.editable ? "comfortable" : props.contentDensity }, () => h("div", { ref: attachDocument })) : null,
3676
3756
  codeUi.open ? h("div", {
3677
3757
  ref: codeLanguageRef,
3678
3758
  class: "looma-editor__code-language",
@@ -3836,7 +3916,7 @@ var LoomaEditor = defineComponent({
3836
3916
  open: true,
3837
3917
  query: slash.query,
3838
3918
  selectedIndex: slash.selectedIndex,
3839
- anchorRect: managedMenuAnchorRect(slash.rect),
3919
+ anchorRect: menuAnchorRect(slash.rect),
3840
3920
  onHighlight: (event) => {
3841
3921
  slash.highlight?.(event.detail.index);
3842
3922
  },
@@ -3857,7 +3937,7 @@ var LoomaEditor = defineComponent({
3857
3937
  query: mention.query,
3858
3938
  items: mention.items,
3859
3939
  selectedIndex: mention.selectedIndex,
3860
- anchorRect: managedMenuAnchorRect(mention.rect),
3940
+ anchorRect: menuAnchorRect(mention.rect),
3861
3941
  loading: mention.loading,
3862
3942
  onHighlight: (event) => {
3863
3943
  mention.selectedIndex = event.detail.index;