@threadlabs/looma 0.27.6 → 0.28.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.
@@ -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
- import { Button, 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";
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
4
  import { Extension, Node as Node$1, getText, getTextSerializersFromSchema, mergeAttributes, nodeInputRule, posToDOMRect } from "@tiptap/core";
5
5
  import { BubbleMenu, EditorContent, useEditor } 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,134 @@ 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/shortcuts.ts
741
+ /**
742
+ * Shows a key binding in ProseMirror notation ("Mod-Shift-8") the way the reader's platform writes
743
+ * it: modifier symbols in Apple's order on Apple devices (⇧⌘8), and Ctrl+Shift+8 elsewhere.
744
+ */
745
+ function formatEditorShortcut(keys, apple) {
746
+ const parts = keys.split("-");
747
+ const key = (parts.pop() ?? "").toUpperCase();
748
+ const has = (name) => parts.includes(name);
749
+ if (apple) return `${has("Ctrl") ? "⌃" : ""}${has("Alt") ? "⌥" : ""}${has("Shift") ? "⇧" : ""}${has("Mod") ? "⌘" : ""}${key}`;
750
+ return [
751
+ has("Mod") || has("Ctrl") ? "Ctrl" : "",
752
+ has("Alt") ? "Alt" : "",
753
+ has("Shift") ? "Shift" : "",
754
+ key
755
+ ].filter(Boolean).join("+");
756
+ }
757
+ var isApplePlatform = () => typeof navigator !== "undefined" && /mac|iphone|ipad|ipod/i.test(navigator.userAgentData?.platform || navigator.platform || navigator.userAgent);
758
+ //#endregion
631
759
  //#region src/editor/extensions/expand.ts
632
760
  /**
633
761
  * Reveals collapsed ancestors before a host navigates to a document position.
@@ -1382,6 +1510,42 @@ var dividerNodeView = ({ editor, getPos }) => {
1382
1510
  };
1383
1511
  };
1384
1512
  //#endregion
1513
+ //#region src/vue/editor/callout.ts
1514
+ /**
1515
+ * Draws an editor callout with Looma's Callout, so it looks the same as a callout in any page.
1516
+ * @lifecycle ProseMirror owns the content DOM inside Callout's slot; destroying the view
1517
+ * releases its Vue app.
1518
+ */
1519
+ var calloutNodeView = ({ node }) => {
1520
+ const dom = document.createElement("div");
1521
+ dom.dataset.loomaCallout = "";
1522
+ const current = shallowRef(node);
1523
+ let contentDOM;
1524
+ const app = createApp({ render: () => h(Callout, {
1525
+ tone: current.value.attrs.tone,
1526
+ heading: current.value.attrs.heading || void 0
1527
+ }, () => h("div", {
1528
+ "data-looma-callout-body": "",
1529
+ ref: (element) => {
1530
+ if (element) contentDOM = element;
1531
+ }
1532
+ })) });
1533
+ app.mount(dom);
1534
+ return {
1535
+ dom,
1536
+ contentDOM,
1537
+ update(next) {
1538
+ if (next.type !== current.value.type) return false;
1539
+ current.value = next;
1540
+ return true;
1541
+ },
1542
+ ignoreMutation: (mutation) => mutation.type !== "selection" && !contentDOM.contains(mutation.target),
1543
+ destroy() {
1544
+ app.unmount();
1545
+ }
1546
+ };
1547
+ };
1548
+ //#endregion
1385
1549
  //#region src/vue/editor/image-controls.ts
1386
1550
  var placements = [
1387
1551
  {
@@ -1633,23 +1797,6 @@ var imageNodeView = ({ editor, node, getPos, decorations }) => {
1633
1797
  };
1634
1798
  //#endregion
1635
1799
  //#region src/vue/editor/LoomaEditor.ts
1636
- /**
1637
- * Shows a key binding in ProseMirror notation ("Mod-Shift-8") the way the reader's platform writes
1638
- * it: modifier symbols in Apple's order on Apple devices (⇧⌘8), and Ctrl+Shift+8 elsewhere.
1639
- */
1640
- function formatEditorShortcut(keys, apple) {
1641
- const parts = keys.split("-");
1642
- const key = (parts.pop() ?? "").toUpperCase();
1643
- const has = (name) => parts.includes(name);
1644
- if (apple) return `${has("Ctrl") ? "⌃" : ""}${has("Alt") ? "⌥" : ""}${has("Shift") ? "⇧" : ""}${has("Mod") ? "⌘" : ""}${key}`;
1645
- return [
1646
- has("Mod") || has("Ctrl") ? "Ctrl" : "",
1647
- has("Alt") ? "Alt" : "",
1648
- has("Shift") ? "Shift" : "",
1649
- key
1650
- ].filter(Boolean).join("+");
1651
- }
1652
- var isApplePlatform = () => typeof navigator !== "undefined" && /mac|iphone|ipad|ipod/i.test(navigator.userAgentData?.platform || navigator.platform || navigator.userAgent);
1653
1800
  var EMPTY_DOCUMENT = {
1654
1801
  type: "doc",
1655
1802
  content: []
@@ -1672,38 +1819,6 @@ var EMPTY_CAPABILITIES = {
1672
1819
  canMergeCells: false,
1673
1820
  canSplitCell: false
1674
1821
  };
1675
- function sameDocument(left, right) {
1676
- return JSON.stringify(left) === JSON.stringify(right);
1677
- }
1678
- function validLinkHref(value) {
1679
- const href = value.trim();
1680
- if (!href || /[\u0000-\u001f\u007f]/.test(href)) return null;
1681
- if (/^(\/|#|\?)/.test(href)) return href;
1682
- try {
1683
- const url = new URL(href);
1684
- return [
1685
- "http:",
1686
- "https:",
1687
- "mailto:",
1688
- "tel:"
1689
- ].includes(url.protocol) ? href : null;
1690
- } catch {
1691
- return null;
1692
- }
1693
- }
1694
- function managedMenuAnchorRect(rect) {
1695
- if (!rect) return null;
1696
- return {
1697
- left: rect.left,
1698
- top: rect.top,
1699
- right: rect.right,
1700
- bottom: rect.bottom,
1701
- x: rect.x,
1702
- y: rect.y,
1703
- width: rect.width,
1704
- height: rect.height
1705
- };
1706
- }
1707
1822
  function slashMenuRows(items) {
1708
1823
  const groups = [];
1709
1824
  for (const [index, item] of items.entries()) {
@@ -1724,14 +1839,6 @@ function slashMenuRows(items) {
1724
1839
  label
1725
1840
  }, () => rows)] : rows);
1726
1841
  }
1727
- function selectedTableElement(editor) {
1728
- const { node } = editor.view.domAtPos(editor.state.selection.from);
1729
- return (node instanceof HTMLElement ? node : node.parentElement)?.closest("table");
1730
- }
1731
- function selectedTableCellElement(editor) {
1732
- const { node } = editor.view.domAtPos(editor.state.selection.from);
1733
- return (node instanceof HTMLElement ? node : node.parentElement)?.closest("td, th");
1734
- }
1735
1842
  function loomaIcon(name) {
1736
1843
  return h("svg", {
1737
1844
  class: "looma-icon",
@@ -2108,12 +2215,10 @@ var LoomaEditor = defineComponent({
2108
2215
  };
2109
2216
  };
2110
2217
  const authorHighlight = !props.disableHighlight;
2111
- const codeLowlight = createLowlight(props.codeLanguages ?? void 0);
2112
- const codeHighlightLowlight = props.codeLanguages === null ? {
2113
- ...codeLowlight,
2114
- highlightAuto: (value) => codeLowlight.highlightAuto(value, { subset: [...autoCodeLanguageNames] })
2115
- } : codeLowlight;
2116
- const codeLanguageNames = props.codeLanguages === null ? [...autoCodeLanguageNames, ...Object.keys(codeLanguageCatalog).filter((name) => !autoCodeLanguageNames.includes(name)).sort()] : Object.keys(props.codeLanguages).sort();
2218
+ const codeHighlighting = createCodeHighlighting(props.codeLanguages, () => {
2219
+ codeUi.grammarVersion += 1;
2220
+ });
2221
+ const codeLanguageNames = codeHighlighting.languageNames;
2117
2222
  const codeUi = reactive({
2118
2223
  open: false,
2119
2224
  pos: 0,
@@ -2122,38 +2227,7 @@ var LoomaEditor = defineComponent({
2122
2227
  grammarVersion: 0,
2123
2228
  style: {}
2124
2229
  });
2125
- const grammarLoads = /* @__PURE__ */ new Map();
2126
- let catalogLoad;
2127
- const refreshCodeHighlights = (instance) => {
2128
- if (instance.isDestroyed) return;
2129
- codeUi.grammarVersion += 1;
2130
- const tr = instance.state.tr;
2131
- instance.state.doc.descendants((node, pos) => {
2132
- if (node.type.name === "codeBlock") tr.setNodeMarkup(pos, void 0, node.attrs);
2133
- });
2134
- if (tr.docChanged) instance.view.dispatch(tr.setMeta("addToHistory", false));
2135
- };
2136
- const loadGrammar = (name) => {
2137
- const existing = grammarLoads.get(name);
2138
- if (existing) return existing;
2139
- const pending = codeLanguageCatalog[name]().then((grammar) => {
2140
- codeLowlight.register({ [name]: grammar });
2141
- }).catch((error) => {
2142
- grammarLoads.delete(name);
2143
- throw error;
2144
- });
2145
- grammarLoads.set(name, pending);
2146
- return pending;
2147
- };
2148
- const loadForCodeBlock = (instance, language, value) => {
2149
- if (props.codeLanguages !== null || instance.isDestroyed) return;
2150
- if (typeof language === "string" && Object.hasOwn(codeLanguageCatalog, language)) {
2151
- if (codeLowlight.registered(language) || grammarLoads.has(language)) return;
2152
- loadGrammar(language).then(() => refreshCodeHighlights(instance)).catch(() => {});
2153
- } else if (!language && value.trim()) catalogLoad ??= Promise.all(autoCodeLanguageNames.map((name) => loadGrammar(name))).then(() => refreshCodeHighlights(instance)).catch(() => {
2154
- catalogLoad = void 0;
2155
- });
2156
- };
2230
+ const loadForCodeBlock = codeHighlighting.loadForCodeBlock;
2157
2231
  const editor = useEditor({
2158
2232
  extensions: [
2159
2233
  ...getDefaultEditorExtensions({
@@ -2162,10 +2236,11 @@ var LoomaEditor = defineComponent({
2162
2236
  mention: mentionExtension ?? false,
2163
2237
  disableHighlight: props.disableHighlight,
2164
2238
  codeLanguages: props.codeLanguages ?? void 0,
2165
- codeLowlight: codeHighlightLowlight,
2239
+ codeLowlight: codeHighlighting.lowlight,
2166
2240
  tableOfContentsNodeView,
2167
2241
  expandNodeView,
2168
2242
  dividerNodeView,
2243
+ calloutNodeView,
2169
2244
  imageNodeView
2170
2245
  }),
2171
2246
  imageDelivery.extension,
@@ -2786,7 +2861,7 @@ var LoomaEditor = defineComponent({
2786
2861
  };
2787
2862
  const codeLanguageOptions = computed(() => {
2788
2863
  codeUi.grammarVersion;
2789
- const detected = codeUi.open && codeUi.text.trim() ? codeHighlightLowlight.highlightAuto(codeUi.text).data?.language : null;
2864
+ const detected = codeUi.open && codeUi.text.trim() ? codeHighlighting.lowlight.highlightAuto(codeUi.text).data?.language : null;
2790
2865
  return [
2791
2866
  {
2792
2867
  value: "auto",
@@ -3799,7 +3874,7 @@ var LoomaEditor = defineComponent({
3799
3874
  open: true,
3800
3875
  query: slash.query,
3801
3876
  selectedIndex: slash.selectedIndex,
3802
- anchorRect: managedMenuAnchorRect(slash.rect),
3877
+ anchorRect: menuAnchorRect(slash.rect),
3803
3878
  onHighlight: (event) => {
3804
3879
  slash.highlight?.(event.detail.index);
3805
3880
  },
@@ -3820,7 +3895,7 @@ var LoomaEditor = defineComponent({
3820
3895
  query: mention.query,
3821
3896
  items: mention.items,
3822
3897
  selectedIndex: mention.selectedIndex,
3823
- anchorRect: managedMenuAnchorRect(mention.rect),
3898
+ anchorRect: menuAnchorRect(mention.rect),
3824
3899
  loading: mention.loading,
3825
3900
  onHighlight: (event) => {
3826
3901
  mention.selectedIndex = event.detail.index;