@threadlabs/looma 0.29.15 → 0.29.16

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,5 +1,5 @@
1
1
  import { n as TABLE_CELL_BACKGROUND_PRESETS } from "./table-backgrounds.js";
2
- import { a as assignLoomaHeadingIds, c as loomaContainerMarker, d as normalizeLoomaChipTone, f as parseLoomaAttributes, h as splitLoomaFrontmatter, i as applyLoomaCodeNotation, l as loomaHeadingSlug, m as readLoomaAttributes, n as LOOMA_CHIP_TONES, o as formatLoomaAttributes, p as parseLoomaFenceInfo, r as LOOMA_CONTAINER_NAMES, s as loomaCodeFence, u as normalizeLoomaCalloutTone } from "./markdown.js";
2
+ import { _ as parseLoomaAttributes, b as splitLoomaFrontmatter, c as LOOMA_CONTAINER_NAMES, d as formatLoomaAttributes, f as loomaCodeFence, g as normalizeLoomaChipTone, h as normalizeLoomaCalloutTone, i as referenceDisplay, l as applyLoomaCodeNotation, m as loomaHeadingSlug, n as LoomaReferencePresentation, p as loomaContainerMarker, r as getReferenceDisplay, s as LOOMA_CHIP_TONES, t as LoomaReference, u as assignLoomaHeadingIds, v as parseLoomaFenceInfo, y as readLoomaAttributes } from "./reference-display.js";
3
3
  import { Extension, Mark, Node as Node$1, callOrReturn, canInsertNode, combineTransactionSteps, escapeForRegEx, findChildren, findChildrenInRange, findParentNodeClosestToPos, getAttributes, getChangedRanges, getExtensionField, getMarksBetween, getText, getTextSerializersFromSchema, isNodeEmpty, isNodeSelection, markInputRule, markPasteRule, mergeAttributes, nodeInputRule, textInputRule, textblockTypeInputRule, wrappingInputRule } from "@tiptap/core";
4
4
  import { NodeSelection, Plugin, PluginKey, Selection, TextSelection } from "@tiptap/pm/state";
5
5
  import { Decoration, DecorationSet } from "@tiptap/pm/view";
@@ -614,17 +614,19 @@ function normalizeTableOfContentsConfig(attributes) {
614
614
  * @contract Depth is a maximum heading level. Relative nesting follows existing
615
615
  * ancestors rather than inserting artificial entries for skipped levels.
616
616
  */
617
- function getTableOfContentsEntries(doc, depth = 3) {
617
+ function getTableOfContentsEntries(doc, depth = 3, references = {}) {
618
618
  const entries = [];
619
619
  const ancestors = [];
620
620
  doc.descendants((node, position) => {
621
621
  if (node.type.name === "loomaTableOfContents") return false;
622
- if (node.type.name !== "heading" || node.attrs.level > depth || !node.textContent.trim()) return;
622
+ if (node.type.name !== "heading" || node.attrs.level > depth) return;
623
+ const text = node.textBetween(0, node.content.size, "", (leaf) => leaf.type.name === "loomaReference" || leaf.type.name === "mention" ? `${leaf.type.name === "mention" ? "@" : ""}${referenceDisplay(leaf, references).label}` : String(leaf.attrs.label ?? "")).trim();
624
+ if (!text) return;
623
625
  const level = Number(node.attrs.level);
624
626
  while (ancestors.length && ancestors[ancestors.length - 1] >= level) ancestors.pop();
625
627
  entries.push({
626
628
  id: String(node.attrs.id ?? ""),
627
- text: node.textContent.trim(),
629
+ text,
628
630
  level,
629
631
  depth: ancestors.length,
630
632
  position
@@ -712,8 +714,8 @@ var LoomaHeadingAnchors = Extension.create({
712
714
  return [new Plugin({ appendTransaction: (transactions, oldState, state) => transactions.some((transaction) => transaction.docChanged) ? anchorTransaction(state, oldState.doc) : null })];
713
715
  }
714
716
  });
715
- function contentsHtml(doc, config) {
716
- const branches = tableOfContentsTree(getTableOfContentsEntries(doc, config.depth));
717
+ function contentsHtml(doc, config, references = {}) {
718
+ const branches = tableOfContentsTree(getTableOfContentsEntries(doc, config.depth, references));
717
719
  const list = (items) => [
718
720
  config.format === "plain" ? "div" : config.format === "numbered" ? "ol" : "ul",
719
721
  { role: "list" },
@@ -755,7 +757,7 @@ function contentsHtml(doc, config) {
755
757
  async function navigateTableOfContents(editor, id) {
756
758
  const target = [...editor.view.dom.querySelectorAll("h1[id], h2[id], h3[id]")].find((heading) => heading.id === id);
757
759
  if (!target) return;
758
- const entry = getTableOfContentsEntries(editor.state.doc).find((heading) => heading.id === id);
760
+ const entry = getTableOfContentsEntries(editor.state.doc, 3, getReferenceDisplay(editor)).find((heading) => heading.id === id);
759
761
  if (entry) await revealLoomaExpandAt(editor, entry.position + 1);
760
762
  if (!target.isConnected || editor.isDestroyed) return;
761
763
  target.scrollIntoView({
@@ -836,7 +838,7 @@ var LoomaTableOfContents = Node$1.create({
836
838
  nav.append(DOMSerializer.fromSchema(editor.schema).serializeFragment(current.content));
837
839
  dom.replaceChildren(nav);
838
840
  } else {
839
- const { dom: content } = DOMSerializer.renderSpec(document, contentsHtml(editor.state.doc, normalizeTableOfContentsConfig(current.attrs)));
841
+ const { dom: content } = DOMSerializer.renderSpec(document, contentsHtml(editor.state.doc, normalizeTableOfContentsConfig(current.attrs), getReferenceDisplay(editor)));
840
842
  dom.replaceChildren(content);
841
843
  }
842
844
  };
@@ -6764,10 +6766,36 @@ var EMPTY_STATE$1 = {
6764
6766
  * than document data.
6765
6767
  */
6766
6768
  var LoomaMentionSuggestionPluginKey = new PluginKey("loomaMentionSuggestion");
6767
- var LoomaMentionNode = Mention.extend({ addAttributes() {
6768
- const { mentionSuggestionChar: _trigger, ...durableAttributes } = this.parent?.() ?? {};
6769
- return durableAttributes;
6770
- } });
6769
+ var LoomaMentionNode = Mention.extend({
6770
+ renderText(props) {
6771
+ const { node } = props;
6772
+ const label = this.editor && getReferenceDisplay(this.editor)[`mention:${node.attrs.id}`]?.label;
6773
+ return this.parent({
6774
+ ...props,
6775
+ node: typeof label === "string" ? node.type.create({
6776
+ ...node.attrs,
6777
+ label
6778
+ }, node.content, node.marks) : node
6779
+ });
6780
+ },
6781
+ renderHTML({ node, HTMLAttributes }) {
6782
+ const label = this.editor && getReferenceDisplay(this.editor)[`mention:${node.attrs.id}`]?.label;
6783
+ return this.parent({
6784
+ node: typeof label === "string" ? node.type.create({
6785
+ ...node.attrs,
6786
+ label
6787
+ }, node.content, node.marks) : node,
6788
+ HTMLAttributes: typeof label === "string" ? {
6789
+ ...HTMLAttributes,
6790
+ "data-label": label
6791
+ } : HTMLAttributes
6792
+ });
6793
+ },
6794
+ addAttributes() {
6795
+ const { mentionSuggestionChar: _trigger, ...durableAttributes } = this.parent?.() ?? {};
6796
+ return durableAttributes;
6797
+ }
6798
+ });
6771
6799
  async function resolveMentionItems(source, query, limit) {
6772
6800
  try {
6773
6801
  return filterLoomaMentionItems(typeof source === "function" ? await source(query, { limit }) : source ?? [], query, limit);
@@ -9934,6 +9962,11 @@ var NODES = {
9934
9962
  mention: (state, node) => {
9935
9963
  const id = String(node.attrs.id ?? "");
9936
9964
  state.write(`:mention[${label(String(node.attrs.label ?? id))}]${formatLoomaAttributes({ id }, { hashId: false })}`);
9965
+ },
9966
+ loomaReference: (state, node) => {
9967
+ const wrapper = document.createElement("span");
9968
+ wrapper.append(DOMSerializer.fromSchema(node.type.schema).serializeNode(node));
9969
+ state.write(wrapper.innerHTML);
9937
9970
  }
9938
9971
  };
9939
9972
  var MARKS = {
@@ -10775,6 +10808,11 @@ function getDefaultEditorExtensions(options = {}) {
10775
10808
  Document,
10776
10809
  Paragraph,
10777
10810
  Text$1,
10811
+ LoomaReference,
10812
+ LoomaReferencePresentation.configure({
10813
+ references: options.references ?? {},
10814
+ mentions: options.references !== void 0
10815
+ }),
10778
10816
  Bold,
10779
10817
  Italic,
10780
10818
  Strike,
@@ -1,3 +1,5 @@
1
+ import { Extension, Node, mergeAttributes } from "@tiptap/core";
2
+ import { NodeSelection, Plugin, PluginKey } from "@tiptap/pm/state";
1
3
  //#region src/markdown/index.ts
2
4
  /**
3
5
  * Callout tones a document stores, in the order the editor offers them. The Callout component also
@@ -239,4 +241,208 @@ function loomaCodeFence(code) {
239
241
  return "`".repeat(Math.max(3, longest + 1));
240
242
  }
241
243
  //#endregion
242
- export { assignLoomaHeadingIds as a, loomaContainerMarker as c, normalizeLoomaChipTone as d, parseLoomaAttributes as f, splitLoomaFrontmatter as h, applyLoomaCodeNotation as i, loomaHeadingSlug as l, readLoomaAttributes as m, LOOMA_CHIP_TONES as n, formatLoomaAttributes as o, parseLoomaFenceInfo as p, LOOMA_CONTAINER_NAMES as r, loomaCodeFence as s, LOOMA_CALLOUT_TONES as t, normalizeLoomaCalloutTone as u };
244
+ //#region src/editor/content.ts
245
+ /**
246
+ * A link destination the editor may store: a site-relative path, fragment, or query, or an
247
+ * absolute http, https, mailto, or tel URL.
248
+ *
249
+ * @failure Returns null for empty input, control characters, other schemes, and unparseable URLs.
250
+ */
251
+ function validLinkHref(value) {
252
+ const href = value.trim();
253
+ if (!href || /[\u0000-\u001f\u007f]/.test(href)) return null;
254
+ if (/^(\/|#|\?)/.test(href)) return href;
255
+ try {
256
+ const url = new URL(href);
257
+ return [
258
+ "http:",
259
+ "https:",
260
+ "mailto:",
261
+ "tel:"
262
+ ].includes(url.protocol) ? href : null;
263
+ } catch {
264
+ return null;
265
+ }
266
+ }
267
+ //#endregion
268
+ //#region src/editor/reference-display.ts
269
+ var referenceDisplayKey = new PluginKey("loomaReferenceDisplay");
270
+ var referenceDisplayMeta = "loomaReferenceDisplay";
271
+ /**
272
+ * Replaces presentation without editing the document or adding an undo/history entry.
273
+ * @lifecycle Values last until the next replacement or editor destruction; hosts own persistence.
274
+ */
275
+ function setReferenceDisplay(editor, references) {
276
+ if (editor.isDestroyed) return;
277
+ editor.storage.loomaReferencePresentation?.enableMentions?.();
278
+ editor.view.dispatch(editor.state.tr.setMeta(referenceDisplayMeta, references).setMeta("addToHistory", false));
279
+ }
280
+ /**
281
+ * The current replaceable presentation; absent entries use the saved fallback.
282
+ * @ownership The returned map belongs to the host; replace it rather than mutating it.
283
+ */
284
+ function getReferenceDisplay(editor) {
285
+ return editor.storage.loomaReferencePresentation?.references ?? {};
286
+ }
287
+ /**
288
+ * Reads current host presentation, with a durable insertion-time fallback for offline copies.
289
+ * @failure Unsafe cached destinations fall back to the saved safe URL or no destination.
290
+ */
291
+ function referenceDisplay(node, references = {}) {
292
+ const kind = node.type.name === "mention" ? "mention" : node.attrs.kind;
293
+ const display = references[`${kind}:${node.attrs.id}`];
294
+ const label = typeof display?.label === "string" ? display.label : typeof node.attrs.label === "string" ? node.attrs.label : String(node.attrs.id ?? "Reference");
295
+ const href = typeof display?.href === "string" ? validLinkHref(display.href) : null;
296
+ return {
297
+ label,
298
+ ...kind === "link" ? { href: href ?? (typeof node.attrs.href === "string" ? validLinkHref(node.attrs.href) : null) ?? void 0 } : {}
299
+ };
300
+ }
301
+ /**
302
+ * An automatically labelled link; custom wording remains an ordinary text link.
303
+ * @contract Identity and insertion-time fallback stay in JSON; cached presentation stays outside it.
304
+ */
305
+ var LoomaReference = Node.create({
306
+ name: "loomaReference",
307
+ inline: true,
308
+ group: "inline",
309
+ atom: true,
310
+ addAttributes: () => ({
311
+ id: {
312
+ default: null,
313
+ renderHTML: () => ({})
314
+ },
315
+ kind: {
316
+ default: "link",
317
+ renderHTML: () => ({})
318
+ },
319
+ href: {
320
+ default: null,
321
+ renderHTML: () => ({})
322
+ },
323
+ label: {
324
+ default: null,
325
+ renderHTML: () => ({})
326
+ },
327
+ target: {
328
+ default: "_self",
329
+ renderHTML: () => ({})
330
+ }
331
+ }),
332
+ parseHTML: () => [{
333
+ tag: "a[data-looma-reference]",
334
+ priority: 1001,
335
+ getAttrs: (element) => ({
336
+ id: element.getAttribute("data-looma-reference"),
337
+ kind: "link",
338
+ href: element.getAttribute("href"),
339
+ label: element.textContent,
340
+ target: element.getAttribute("target") ?? "_self"
341
+ })
342
+ }],
343
+ renderHTML({ node, HTMLAttributes }) {
344
+ const display = referenceDisplay(node, this.editor ? getReferenceDisplay(this.editor) : {});
345
+ return [
346
+ "a",
347
+ mergeAttributes(HTMLAttributes, {
348
+ "data-looma-reference": node.attrs.id,
349
+ href: display.href,
350
+ target: node.attrs.target === "_blank" ? "_blank" : "_self",
351
+ rel: "noopener noreferrer"
352
+ }),
353
+ display.label
354
+ ];
355
+ },
356
+ renderText({ node }) {
357
+ return referenceDisplay(node, this.editor ? getReferenceDisplay(this.editor) : {}).label;
358
+ }
359
+ });
360
+ /**
361
+ * Installs metadata-backed node views for references and existing stable-id mentions.
362
+ * @lifecycle Node views register until destroyed, including across editor reattachment.
363
+ */
364
+ var LoomaReferencePresentation = Extension.create({
365
+ name: "loomaReferencePresentation",
366
+ addOptions: () => ({
367
+ references: {},
368
+ mentions: false
369
+ }),
370
+ addStorage() {
371
+ return { references: this.options.references };
372
+ },
373
+ addProseMirrorPlugins() {
374
+ const initial = this.options.references;
375
+ const refresh = /* @__PURE__ */ new Set();
376
+ const nodeView = (node, view, getPosition) => {
377
+ let current = node;
378
+ const mention = node.type.name === "mention";
379
+ const dom = document.createElement(mention ? "span" : "a");
380
+ if (mention) {
381
+ dom.className = "looma-mention";
382
+ dom.setAttribute("data-type", "mention");
383
+ } else dom.addEventListener("click", (event) => {
384
+ if (!(event instanceof MouseEvent) || !view.editable || event.metaKey || event.ctrlKey) return;
385
+ const position = getPosition();
386
+ if (position === void 0) return;
387
+ event.preventDefault();
388
+ view.dispatch(view.state.tr.setSelection(NodeSelection.create(view.state.doc, position)));
389
+ view.focus();
390
+ });
391
+ const render = () => {
392
+ const display = referenceDisplay(current, referenceDisplayKey.getState(view.state));
393
+ dom.textContent = `${mention ? "@" : ""}${display.label}`;
394
+ if (mention) {
395
+ dom.setAttribute("data-id", current.attrs.id ?? "");
396
+ dom.setAttribute("data-label", display.label);
397
+ }
398
+ if (!mention) {
399
+ dom.setAttribute("data-looma-reference", current.attrs.id ?? "");
400
+ if (display.href) dom.setAttribute("href", display.href);
401
+ else dom.removeAttribute("href");
402
+ dom.setAttribute("target", current.attrs.target === "_blank" ? "_blank" : "_self");
403
+ dom.setAttribute("rel", "noopener noreferrer");
404
+ }
405
+ };
406
+ refresh.add(render);
407
+ render();
408
+ return {
409
+ dom,
410
+ update(next) {
411
+ if (next.type !== current.type) return false;
412
+ current = next;
413
+ render();
414
+ return true;
415
+ },
416
+ destroy: () => {
417
+ refresh.delete(render);
418
+ }
419
+ };
420
+ };
421
+ const nodeViews = {
422
+ loomaReference: nodeView,
423
+ ...this.options.mentions && this.editor.schema.nodes.mention ? { mention: nodeView } : {}
424
+ };
425
+ this.storage.enableMentions = () => {
426
+ if (nodeViews.mention || !this.editor.schema.nodes.mention) return;
427
+ nodeViews.mention = nodeView;
428
+ this.editor.view.setProps({ nodeViews: { ...this.editor.view.props.nodeViews } });
429
+ };
430
+ return [new Plugin({
431
+ key: referenceDisplayKey,
432
+ state: {
433
+ init: () => initial,
434
+ apply: (transaction, previous) => {
435
+ const next = transaction.getMeta(referenceDisplayMeta) ?? previous;
436
+ this.storage.references = next;
437
+ return next;
438
+ }
439
+ },
440
+ props: { nodeViews },
441
+ view: () => ({ update: (view, previous) => {
442
+ if (referenceDisplayKey.getState(view.state) !== referenceDisplayKey.getState(previous)) refresh.forEach((render) => render());
443
+ } })
444
+ })];
445
+ }
446
+ });
447
+ //#endregion
448
+ export { parseLoomaAttributes as _, setReferenceDisplay as a, splitLoomaFrontmatter as b, LOOMA_CONTAINER_NAMES as c, formatLoomaAttributes as d, loomaCodeFence as f, normalizeLoomaChipTone as g, normalizeLoomaCalloutTone as h, referenceDisplay as i, applyLoomaCodeNotation as l, loomaHeadingSlug as m, LoomaReferencePresentation as n, LOOMA_CALLOUT_TONES as o, loomaContainerMarker as p, getReferenceDisplay as r, LOOMA_CHIP_TONES as s, LoomaReference as t, assignLoomaHeadingIds as u, parseLoomaFenceInfo as v, readLoomaAttributes as y };
@@ -1,5 +1,10 @@
1
- import type { JSONContent } from "@tiptap/core";
1
+ import type { Editor, JSONContent } from "@tiptap/core";
2
2
  import { type ConversionOptions, type Document } from "expressmark";
3
+ import { type LoomaReferenceMap } from "./reference-display.js";
4
+ /** Conversion-time display snapshot, independent from the authored editor document. */
5
+ export interface EditorMarkdownConversionOptions extends ConversionOptions {
6
+ references?: LoomaReferenceMap;
7
+ }
3
8
  /**
4
9
  * A conversion cannot faithfully represent the named input at this document location.
5
10
  * @failure Callers retain the original document and can report the unsupported feature and path.
@@ -13,7 +18,7 @@ export declare class EditorMarkdownConversionError extends Error {
13
18
  * Maps an editor's saved JSON to the independent Expressive Markdown AST without DOM access.
14
19
  * @contract Unsupported nodes, marks and durable attributes throw with their location; input is never mutated.
15
20
  */
16
- export declare function editorDocumentToExpressmark(document: JSONContent, options?: ConversionOptions): Document;
21
+ export declare function editorDocumentToExpressmark(document: JSONContent, options?: EditorMarkdownConversionOptions): Document;
17
22
  /**
18
23
  * Maps supported Expressive Markdown content into the Looma editor's saved JSON shape.
19
24
  * @contract The editor schema cannot represent arbitrary format extensions; unsupported meanings fail explicitly.
@@ -23,7 +28,12 @@ export declare function expressmarkToEditorDocument(document: Document): JSONCon
23
28
  * Writes an editor document using Expressmark's shared parser/writer and safe HTML policy.
24
29
  * @failure Unsupported editor data raises EditorMarkdownConversionError before any input is changed.
25
30
  */
26
- export declare function editorDocumentToMarkdown(document: JSONContent, options?: ConversionOptions): string;
31
+ export declare function editorDocumentToMarkdown(document: JSONContent, options?: EditorMarkdownConversionOptions): string;
32
+ /**
33
+ * Serializes the current editor, including its current display cache and concrete TOC entries.
34
+ * @failure Unsupported content throws EditorMarkdownConversionError without changing the document.
35
+ */
36
+ export declare function editorToMarkdown(editor: Editor, options?: ConversionOptions): string;
27
37
  /**
28
38
  * Parses Expressive Markdown into editable JSON for the supported editor schema.
29
39
  * @failure Unsupported content or root metadata stops conversion instead of being silently flattened.
@@ -1,4 +1,4 @@
1
- import { l as loomaHeadingSlug, n as LOOMA_CHIP_TONES, t as LOOMA_CALLOUT_TONES } from "./chunks/markdown.js";
1
+ import { m as loomaHeadingSlug, o as LOOMA_CALLOUT_TONES, r as getReferenceDisplay, s as LOOMA_CHIP_TONES } from "./chunks/reference-display.js";
2
2
  import { parse, stringify } from "expressmark";
3
3
  //#region src/editor/expressmark.ts
4
4
  /**
@@ -179,6 +179,11 @@ function legacyCodePresentation(code, path) {
179
179
  return result;
180
180
  }
181
181
  var REVERSE_MARKS = Object.fromEntries(Object.entries(MARKS).map(([left, right]) => [right, left]));
182
+ var displayText = (node, references) => {
183
+ if (node.type === "loomaReference") return references[`link:${node.attrs?.id}`]?.label ?? string(node.attrs?.label, string(node.attrs?.id));
184
+ if (node.type === "mention") return `@${references[`mention:${node.attrs?.id}`]?.label ?? string(node.attrs?.label, string(node.attrs?.id))}`;
185
+ return node.text ?? (node.content ?? []).map((child) => displayText(child, references)).join("");
186
+ };
182
187
  function tocBody(headings, depth, ordered) {
183
188
  const roots = [];
184
189
  const stack = [];
@@ -225,6 +230,7 @@ function toNode(node, path, context) {
225
230
  if (![
226
231
  "text",
227
232
  "mention",
233
+ "loomaReference",
228
234
  "loomaChip",
229
235
  "hardBreak"
230
236
  ].includes(node.type ?? "")) fail(path, "marks on a block node");
@@ -281,6 +287,29 @@ function toBareNode(node, path, context) {
281
287
  };
282
288
  };
283
289
  switch (node.type) {
290
+ case "loomaReference": {
291
+ fields(node, [
292
+ "id",
293
+ "kind",
294
+ "href",
295
+ "label",
296
+ "target"
297
+ ], path);
298
+ if (!attrs.id || attrs.kind !== "link") fail(path, "invalid link reference identity");
299
+ const display = context.references[`link:${attrs.id}`];
300
+ const href = display?.href ?? string(attrs.href);
301
+ safeNavigation(href, false, path);
302
+ return {
303
+ type: "link",
304
+ url: href,
305
+ value: display?.label ?? string(attrs.label, string(attrs.id)),
306
+ attributes: {
307
+ ref: string(attrs.id),
308
+ target: string(attrs.target, "_self"),
309
+ rel: "noopener noreferrer"
310
+ }
311
+ };
312
+ }
284
313
  case "text":
285
314
  fields(node, [], path);
286
315
  return {
@@ -355,7 +384,7 @@ function toBareNode(node, path, context) {
355
384
  return {
356
385
  type: "inline",
357
386
  subType: "mention",
358
- value: string(attrs.label, string(attrs.id)),
387
+ value: context.references[`mention:${attrs.id}`]?.label ?? string(attrs.label, string(attrs.id)),
359
388
  ...attrs.id ? { ref: string(attrs.id) } : {}
360
389
  };
361
390
  case "loomaChip":
@@ -523,7 +552,7 @@ function editorDocumentToExpressmark(document, options = {}) {
523
552
  if (!insideToc && node.type === "loomaTableOfContents" && !node.content?.length) automaticToc = true;
524
553
  if (!insideToc && node.type === "heading") headings.push({
525
554
  node,
526
- text: (node.content ?? []).map((child) => child.text ?? "").join(""),
555
+ text: displayText(node, options.references ?? {}),
527
556
  id: string(node.attrs?.id),
528
557
  level: Number(node.attrs?.level ?? 1)
529
558
  });
@@ -553,7 +582,8 @@ function editorDocumentToExpressmark(document, options = {}) {
553
582
  }
554
583
  const context = {
555
584
  headings,
556
- headingIds
585
+ headingIds,
586
+ references: options.references ?? {}
557
587
  };
558
588
  return {
559
589
  type: "document",
@@ -601,8 +631,23 @@ function fromNode(node, path, marks = []) {
601
631
  check(node.type === "link" ? [
602
632
  "target",
603
633
  "rel",
604
- "class"
634
+ "class",
635
+ "ref"
605
636
  ] : []);
637
+ if (node.type === "link" && attrs.ref !== void 0) {
638
+ if (typeof attrs.ref !== "string" || !attrs.ref || node.title || attrs.class || content(node).some((child) => child.type !== "text" || child.attributes || child.id)) fail(path, "formatted or invalid link reference");
639
+ return [{
640
+ type: "loomaReference",
641
+ attrs: {
642
+ id: attrs.ref,
643
+ kind: "link",
644
+ href: node.url ?? "",
645
+ label: text(node),
646
+ target: attrs.target ?? "_self"
647
+ },
648
+ ...marks.length ? { marks } : {}
649
+ }];
650
+ }
606
651
  const mark = { type: REVERSE_MARKS[node.type] };
607
652
  if (node.type === "link") mark.attrs = {
608
653
  href: node.url ?? "",
@@ -932,6 +977,16 @@ function editorDocumentToMarkdown(document, options = {}) {
932
977
  return stringify(editorDocumentToExpressmark(document, options), options);
933
978
  }
934
979
  /**
980
+ * Serializes the current editor, including its current display cache and concrete TOC entries.
981
+ * @failure Unsupported content throws EditorMarkdownConversionError without changing the document.
982
+ */
983
+ function editorToMarkdown(editor, options = {}) {
984
+ return editorDocumentToMarkdown(editor.getJSON(), {
985
+ ...options,
986
+ references: getReferenceDisplay(editor)
987
+ });
988
+ }
989
+ /**
935
990
  * Parses Expressive Markdown into editable JSON for the supported editor schema.
936
991
  * @failure Unsupported content or root metadata stops conversion instead of being silently flattened.
937
992
  */
@@ -939,4 +994,4 @@ function markdownToEditorDocument(markdown, options = {}) {
939
994
  return expressmarkToEditorDocument(parse(markdown, options));
940
995
  }
941
996
  //#endregion
942
- export { EditorMarkdownConversionError, editorDocumentToExpressmark, editorDocumentToMarkdown, expressmarkToEditorDocument, markdownToEditorDocument };
997
+ export { EditorMarkdownConversionError, editorDocumentToExpressmark, editorDocumentToMarkdown, editorToMarkdown, expressmarkToEditorDocument, markdownToEditorDocument };
@@ -15,6 +15,7 @@ export { LoomaTab, LoomaTabs } from "./tabs.js";
15
15
  export { LoomaTableOfContents, getTableOfContentsEntries, normalizeTableOfContentsConfig, type LoomaTableOfContentsConfig, type LoomaTableOfContentsEntry, type LoomaTableOfContentsFormat, } from "./table-of-contents.js";
16
16
  export { getDefaultEditorExtensions, getLoomaTableExtensions, LoomaTableKit, type DefaultEditorExtensionsOptions, type LoomaCodeLanguages, } from "./preset.js";
17
17
  export { LoomaSmartPaste } from "./smart-paste.js";
18
+ export { LoomaReference, LoomaReferencePresentation, setReferenceDisplay, getReferenceDisplay, referenceDisplay, type LoomaReferenceDisplay, type LoomaReferenceMap } from "../reference-display.js";
18
19
  export { loomaMarkdownFromDoc, loomaMarkdownToHtml } from "./markdown.js";
19
20
  export { getActiveTableUiState, handleTableAction, handleTableOverlayAction, insertTableAtRange, normalizeActiveTableColumnWidths, shouldShowTextFormattingToolbar, type ActiveTableUiState, type InsertTableAtRangeOptions, type TableActionCapabilities, } from "./table-commands.js";
20
21
  export { LoomaTable, getActiveTableCellAlignment, getActiveTableCellBackground, LoomaTableCell, LoomaTableHeader, setActiveTableCellAlignment, setActiveTableCellBackground, TABLE_CELL_BACKGROUND_PRESETS, type TableCellAlignment, type TableCellBackground, } from "./table-formatting.js";
@@ -1,4 +1,4 @@
1
1
  import { n as TABLE_CELL_BACKGROUND_PRESETS } from "../chunks/table-backgrounds.js";
2
2
  import { A as filterLoomaMentionItems, B as LoomaFigcaption, C as loomaMarkdownToHtml, D as createLoomaMentionExtension, E as LoomaMentionSuggestionPluginKey, F as LoomaTableOfContents, G as LoomaChip, H as LoomaExpand, I as getTableOfContentsEntries, J as LOOMA_ACTIVE_BLOCK_BLUR_GRACE_MS, K as normalizeLoomaChipColor, L as normalizeTableOfContentsConfig, M as LoomaImage, N as normalizeImageDimension, O as DEFAULT_MENTION_RESULT_LIMIT, P as normalizeImagePlacement, Q as LoomaActiveBlockPluginKey, R as LoomaTab, S as loomaMarkdownFromDoc, T as siteRelativeHref, U as revealLoomaExpandAt, V as LoomaFigure, W as LOOMA_CHIP_COLORS, X as LOOMA_ACTIVE_BLOCK_TYPING_IDLE_MS, Y as LOOMA_ACTIVE_BLOCK_CLASS, Z as LoomaActiveBlock, _ as getActiveTableCellAlignment, a as getActiveTableUiState, b as setActiveTableCellBackground, c as insertTableAtRange, d as LoomaTableKit, f as getDefaultEditorExtensions, g as LoomaTableHeader, h as LoomaTableCell, i as getDefaultSlashCommands, j as normalizeMentionResultLimit, k as MAX_MENTION_RESULT_LIMIT, l as normalizeActiveTableColumnWidths, m as LoomaTable, n as createLoomaSlashCommandExtension, o as handleTableAction, p as getLoomaTableExtensions, q as LoomaCallout, r as filterLoomaSlashCommands, s as handleTableOverlayAction, t as LoomaSlashCommand, u as shouldShowTextFormattingToolbar, v as getActiveTableCellBackground, w as createSiteRelativeLinks, x as LoomaSmartPaste, y as setActiveTableCellAlignment, z as LoomaTabs } from "../chunks/extensions.js";
3
- import { t as LOOMA_CALLOUT_TONES } from "../chunks/markdown.js";
4
- export { DEFAULT_MENTION_RESULT_LIMIT, LOOMA_ACTIVE_BLOCK_BLUR_GRACE_MS, LOOMA_ACTIVE_BLOCK_CLASS, LOOMA_ACTIVE_BLOCK_TYPING_IDLE_MS, LOOMA_CALLOUT_TONES, LOOMA_CHIP_COLORS, LoomaActiveBlock, LoomaActiveBlockPluginKey, LoomaCallout, LoomaChip, LoomaExpand, LoomaFigcaption, LoomaFigure, LoomaImage, LoomaMentionSuggestionPluginKey, LoomaSlashCommand, LoomaSmartPaste, LoomaTab, LoomaTable, LoomaTableCell, LoomaTableHeader, LoomaTableKit, LoomaTableOfContents, LoomaTabs, MAX_MENTION_RESULT_LIMIT, TABLE_CELL_BACKGROUND_PRESETS, createLoomaMentionExtension, createLoomaSlashCommandExtension, createSiteRelativeLinks, filterLoomaMentionItems, filterLoomaSlashCommands, getActiveTableCellAlignment, getActiveTableCellBackground, getActiveTableUiState, getDefaultEditorExtensions, getDefaultSlashCommands, getLoomaTableExtensions, getTableOfContentsEntries, handleTableAction, handleTableOverlayAction, insertTableAtRange, loomaMarkdownFromDoc, loomaMarkdownToHtml, normalizeActiveTableColumnWidths, normalizeImageDimension, normalizeImagePlacement, normalizeLoomaChipColor, normalizeMentionResultLimit, normalizeTableOfContentsConfig, revealLoomaExpandAt, setActiveTableCellAlignment, setActiveTableCellBackground, shouldShowTextFormattingToolbar, siteRelativeHref };
3
+ import { a as setReferenceDisplay, i as referenceDisplay, n as LoomaReferencePresentation, o as LOOMA_CALLOUT_TONES, r as getReferenceDisplay, t as LoomaReference } from "../chunks/reference-display.js";
4
+ export { DEFAULT_MENTION_RESULT_LIMIT, LOOMA_ACTIVE_BLOCK_BLUR_GRACE_MS, LOOMA_ACTIVE_BLOCK_CLASS, LOOMA_ACTIVE_BLOCK_TYPING_IDLE_MS, LOOMA_CALLOUT_TONES, LOOMA_CHIP_COLORS, LoomaActiveBlock, LoomaActiveBlockPluginKey, LoomaCallout, LoomaChip, LoomaExpand, LoomaFigcaption, LoomaFigure, LoomaImage, LoomaMentionSuggestionPluginKey, LoomaReference, LoomaReferencePresentation, LoomaSlashCommand, LoomaSmartPaste, LoomaTab, LoomaTable, LoomaTableCell, LoomaTableHeader, LoomaTableKit, LoomaTableOfContents, LoomaTabs, MAX_MENTION_RESULT_LIMIT, TABLE_CELL_BACKGROUND_PRESETS, createLoomaMentionExtension, createLoomaSlashCommandExtension, createSiteRelativeLinks, filterLoomaMentionItems, filterLoomaSlashCommands, getActiveTableCellAlignment, getActiveTableCellBackground, getActiveTableUiState, getDefaultEditorExtensions, getDefaultSlashCommands, getLoomaTableExtensions, getReferenceDisplay, getTableOfContentsEntries, handleTableAction, handleTableOverlayAction, insertTableAtRange, loomaMarkdownFromDoc, loomaMarkdownToHtml, normalizeActiveTableColumnWidths, normalizeImageDimension, normalizeImagePlacement, normalizeLoomaChipColor, normalizeMentionResultLimit, normalizeTableOfContentsConfig, referenceDisplay, revealLoomaExpandAt, setActiveTableCellAlignment, setActiveTableCellBackground, setReferenceDisplay, shouldShowTextFormattingToolbar, siteRelativeHref };
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { type AnyExtension, type NodeViewRenderer } from "@tiptap/core";
6
6
  import { createLowlight } from "lowlight";
7
+ import { type LoomaReferenceMap } from "../reference-display.js";
7
8
  /** Grammars supplied by the host; importing a language remains the host's choice. */
8
9
  export type LoomaCodeLanguages = NonNullable<Parameters<typeof createLowlight>[0]>;
9
10
  /**
@@ -12,6 +13,8 @@ export type LoomaCodeLanguages = NonNullable<Parameters<typeof createLowlight>[0
12
13
  * rather than relying on undocumented mutation of the returned extensions.
13
14
  */
14
15
  export interface DefaultEditorExtensionsOptions {
16
+ /** Replaceable display data; updating it must use setReferenceDisplay, never setContent. */
17
+ references?: LoomaReferenceMap;
15
18
  /** Placeholder shown only for empty paragraphs, not every empty node type. */
16
19
  placeholder?: string;
17
20
  /** Passed to Tiptap Link; defaults false to keep editing clicks in the editor. */
@@ -1,5 +1,6 @@
1
1
  import { Node, type NodeViewRenderer } from "@tiptap/core";
2
2
  import { type Node as ProseMirrorNode } from "@tiptap/pm/model";
3
+ import { type LoomaReferenceMap } from "../reference-display.js";
3
4
  /** Formatting affects presentation only; heading order remains document order. */
4
5
  export type LoomaTableOfContentsFormat = "plain" | "bulleted" | "numbered";
5
6
  /** Persisted TOC choices; entries themselves are always derived from headings. */
@@ -26,7 +27,7 @@ export declare function normalizeTableOfContentsConfig(attributes: Record<string
26
27
  * @contract Depth is a maximum heading level. Relative nesting follows existing
27
28
  * ancestors rather than inserting artificial entries for skipped levels.
28
29
  */
29
- export declare function getTableOfContentsEntries(doc: ProseMirrorNode, depth?: number): LoomaTableOfContentsEntry[];
30
+ export declare function getTableOfContentsEntries(doc: ProseMirrorNode, depth?: number, references?: LoomaReferenceMap): LoomaTableOfContentsEntry[];
30
31
  interface Branch {
31
32
  entry: LoomaTableOfContentsEntry;
32
33
  children: Branch[];
package/editor/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { n as TABLE_CELL_BACKGROUND_PRESETS, t as TABLE_CELL_BACKGROUND_OPTIONS } from "./chunks/table-backgrounds.js";
2
2
  import { n as resolveTableCellAt, t as measureTableOverlayGeometry } from "./chunks/ui.js";
3
3
  import { A as filterLoomaMentionItems, B as LoomaFigcaption, C as loomaMarkdownToHtml, D as createLoomaMentionExtension, E as LoomaMentionSuggestionPluginKey, F as LoomaTableOfContents, G as LoomaChip, H as LoomaExpand, I as getTableOfContentsEntries, J as LOOMA_ACTIVE_BLOCK_BLUR_GRACE_MS, K as normalizeLoomaChipColor, L as normalizeTableOfContentsConfig, M as LoomaImage, N as normalizeImageDimension, O as DEFAULT_MENTION_RESULT_LIMIT, P as normalizeImagePlacement, Q as LoomaActiveBlockPluginKey, R as LoomaTab, S as loomaMarkdownFromDoc, T as siteRelativeHref, U as revealLoomaExpandAt, V as LoomaFigure, W as LOOMA_CHIP_COLORS, X as LOOMA_ACTIVE_BLOCK_TYPING_IDLE_MS, Y as LOOMA_ACTIVE_BLOCK_CLASS, Z as LoomaActiveBlock, _ as getActiveTableCellAlignment, a as getActiveTableUiState, b as setActiveTableCellBackground, c as insertTableAtRange, d as LoomaTableKit, f as getDefaultEditorExtensions, g as LoomaTableHeader, h as LoomaTableCell, i as getDefaultSlashCommands, j as normalizeMentionResultLimit, k as MAX_MENTION_RESULT_LIMIT, l as normalizeActiveTableColumnWidths, m as LoomaTable, n as createLoomaSlashCommandExtension, o as handleTableAction, p as getLoomaTableExtensions, q as LoomaCallout, r as filterLoomaSlashCommands, s as handleTableOverlayAction, t as LoomaSlashCommand, u as shouldShowTextFormattingToolbar, v as getActiveTableCellBackground, w as createSiteRelativeLinks, x as LoomaSmartPaste, y as setActiveTableCellAlignment, z as LoomaTabs } from "./chunks/extensions.js";
4
- import { t as LOOMA_CALLOUT_TONES } from "./chunks/markdown.js";
4
+ import { a as setReferenceDisplay, i as referenceDisplay, n as LoomaReferencePresentation, o as LOOMA_CALLOUT_TONES, r as getReferenceDisplay, t as LoomaReference } from "./chunks/reference-display.js";
5
5
  import { AlignCenter, AlignLeft, AlignRight, ArrowLeft, Bell, Bold, BookUser, Braces, Calculator, Check, ChevronDown, ChevronLeft, ChevronRight, CircleCheck, CircleDot, CircleQuestionMark, CircleX, CodeXml, Columns3, Compass, CreditCard, Ellipsis, Eraser, ExternalLink, FileText, Folder, GripHorizontal, GripVertical, Heading1, Heading2, Heading3, Highlighter, History, Image, Info, Italic, LayoutDashboard, Lightbulb, Link, List, ListOrdered, ListTodo, LoaderCircle, Merge, MessageSquareWarning, Minus, NotebookPen, PaintBucket, PanelBottom, PanelLeft, PanelRight, PanelTop, Pencil, Pilcrow, Plus, Quote, Receipt, Redo2, Rows3, Search, Settings, Split, Strikethrough, Table2, Tag, Trash2, TriangleAlert, Truck, Underline, Undo2, Unlink, UserRound, Users, X } from "lucide";
6
6
  //#region src/editor/icons.ts
7
7
  /** The opinionated Lucide set used by Looma's shipped interaction surfaces. */
@@ -124,4 +124,4 @@ function getVisualViewportRect(owner = window) {
124
124
  };
125
125
  }
126
126
  //#endregion
127
- export { DEFAULT_MENTION_RESULT_LIMIT, LOOMA_ACTIVE_BLOCK_BLUR_GRACE_MS, LOOMA_ACTIVE_BLOCK_CLASS, LOOMA_ACTIVE_BLOCK_TYPING_IDLE_MS, LOOMA_CALLOUT_TONES, LOOMA_CHIP_COLORS, LOOMA_ICONS, LoomaActiveBlock, LoomaActiveBlockPluginKey, LoomaCallout, LoomaChip, LoomaExpand, LoomaFigcaption, LoomaFigure, LoomaImage, LoomaMentionSuggestionPluginKey, LoomaSlashCommand, LoomaSmartPaste, LoomaTab, LoomaTable, LoomaTableCell, LoomaTableHeader, LoomaTableKit, LoomaTableOfContents, LoomaTabs, MAX_MENTION_RESULT_LIMIT, TABLE_CELL_BACKGROUND_OPTIONS, TABLE_CELL_BACKGROUND_PRESETS, createLoomaMentionExtension, createLoomaSlashCommandExtension, createSiteRelativeLinks, filterLoomaMentionItems, filterLoomaSlashCommands, getActiveTableCellAlignment, getActiveTableCellBackground, getActiveTableUiState, getDefaultEditorExtensions, getDefaultSlashCommands, getLoomaTableExtensions, getTableOfContentsEntries, getVisualViewportRect, handleTableAction, handleTableOverlayAction, insertTableAtRange, loomaIconMarkup, loomaMarkdownFromDoc, loomaMarkdownToHtml, measureTableOverlayGeometry, normalizeActiveTableColumnWidths, normalizeImageDimension, normalizeImagePlacement, normalizeLoomaChipColor, normalizeMentionResultLimit, normalizeTableOfContentsConfig, resolveTableCellAt, revealLoomaExpandAt, setActiveTableCellAlignment, setActiveTableCellBackground, shouldShowTextFormattingToolbar, siteRelativeHref };
127
+ export { DEFAULT_MENTION_RESULT_LIMIT, LOOMA_ACTIVE_BLOCK_BLUR_GRACE_MS, LOOMA_ACTIVE_BLOCK_CLASS, LOOMA_ACTIVE_BLOCK_TYPING_IDLE_MS, LOOMA_CALLOUT_TONES, LOOMA_CHIP_COLORS, LOOMA_ICONS, LoomaActiveBlock, LoomaActiveBlockPluginKey, LoomaCallout, LoomaChip, LoomaExpand, LoomaFigcaption, LoomaFigure, LoomaImage, LoomaMentionSuggestionPluginKey, LoomaReference, LoomaReferencePresentation, LoomaSlashCommand, LoomaSmartPaste, LoomaTab, LoomaTable, LoomaTableCell, LoomaTableHeader, LoomaTableKit, LoomaTableOfContents, LoomaTabs, MAX_MENTION_RESULT_LIMIT, TABLE_CELL_BACKGROUND_OPTIONS, TABLE_CELL_BACKGROUND_PRESETS, createLoomaMentionExtension, createLoomaSlashCommandExtension, createSiteRelativeLinks, filterLoomaMentionItems, filterLoomaSlashCommands, getActiveTableCellAlignment, getActiveTableCellBackground, getActiveTableUiState, getDefaultEditorExtensions, getDefaultSlashCommands, getLoomaTableExtensions, getReferenceDisplay, getTableOfContentsEntries, getVisualViewportRect, handleTableAction, handleTableOverlayAction, insertTableAtRange, loomaIconMarkup, loomaMarkdownFromDoc, loomaMarkdownToHtml, measureTableOverlayGeometry, normalizeActiveTableColumnWidths, normalizeImageDimension, normalizeImagePlacement, normalizeLoomaChipColor, normalizeMentionResultLimit, normalizeTableOfContentsConfig, referenceDisplay, resolveTableCellAt, revealLoomaExpandAt, setActiveTableCellAlignment, setActiveTableCellBackground, setReferenceDisplay, shouldShowTextFormattingToolbar, siteRelativeHref };
@@ -1,4 +1,7 @@
1
1
  import type { Editor } from "@tiptap/core";
2
+ /** Whether the current selection identifies an automatically labelled link. */
3
+ export declare function selectedLinkReference(editor: Editor): import("@tiptap/pm/model").Node | null;
4
+ export declare const isLinkActive: (editor: Editor) => boolean;
2
5
  /** A destination supplied by the host application, such as a page or record. */
3
6
  export interface LoomaLinkTarget {
4
7
  id: string;
@@ -47,6 +50,8 @@ export interface LoomaLinkEditingOptions {
47
50
  search: () => LinkSearch | null | undefined;
48
51
  resolve: () => LinkResolve | null | undefined;
49
52
  baseUrl: () => string;
53
+ /** Enables automatically labelled references for newly inserted host targets. */
54
+ references?: () => boolean;
50
55
  }
51
56
  /**
52
57
  * The editor's link form as a state machine: what the form holds, how it applies to the
@@ -0,0 +1,37 @@
1
+ import { Extension, Node, type Editor } from "@tiptap/core";
2
+ import type { Node as ProseMirrorNode } from "@tiptap/pm/model";
3
+ /** Replaceable host-owned presentation, keyed by `link:<id>` or `mention:<id>`. */
4
+ export interface LoomaReferenceDisplay {
5
+ readonly label: string;
6
+ readonly href?: string;
7
+ }
8
+ /** Display values indexed by stable target kind and identity, outside authored document data. */
9
+ export type LoomaReferenceMap = Readonly<Record<string, LoomaReferenceDisplay>>;
10
+ /**
11
+ * Replaces presentation without editing the document or adding an undo/history entry.
12
+ * @lifecycle Values last until the next replacement or editor destruction; hosts own persistence.
13
+ */
14
+ export declare function setReferenceDisplay(editor: Editor, references: LoomaReferenceMap): void;
15
+ /**
16
+ * The current replaceable presentation; absent entries use the saved fallback.
17
+ * @ownership The returned map belongs to the host; replace it rather than mutating it.
18
+ */
19
+ export declare function getReferenceDisplay(editor: Editor): LoomaReferenceMap;
20
+ /**
21
+ * Reads current host presentation, with a durable insertion-time fallback for offline copies.
22
+ * @failure Unsafe cached destinations fall back to the saved safe URL or no destination.
23
+ */
24
+ export declare function referenceDisplay(node: ProseMirrorNode, references?: LoomaReferenceMap): LoomaReferenceDisplay;
25
+ /**
26
+ * An automatically labelled link; custom wording remains an ordinary text link.
27
+ * @contract Identity and insertion-time fallback stay in JSON; cached presentation stays outside it.
28
+ */
29
+ export declare const LoomaReference: Node<any, any>;
30
+ /**
31
+ * Installs metadata-backed node views for references and existing stable-id mentions.
32
+ * @lifecycle Node views register until destroyed, including across editor reattachment.
33
+ */
34
+ export declare const LoomaReferencePresentation: Extension<{
35
+ references: LoomaReferenceMap;
36
+ mentions: boolean;
37
+ }, any>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threadlabs/looma",
3
- "version": "0.29.15",
3
+ "version": "0.29.16",
4
4
  "type": "module",
5
5
  "description": "Looma components as HTML, Vue, and plain DOM, with design tokens.",
6
6
  "license": "MIT",
@@ -8,6 +8,7 @@ import { type LoomaImageUploadDetail } from "../../editor/image-uploads.js";
8
8
  import { type LoomaResponder } from "../../editor/requests.js";
9
9
  import { type LoomaEditorRangeAction, type LoomaEditorRangeActionDetail, type LoomaEditorSelectionAction, type LoomaEditorSelectionActionDetail } from "../../editor/host-actions.js";
10
10
  import { type LoomaLinkTarget } from "../../editor/link-editing.js";
11
+ import { type LoomaReferenceMap } from "../../editor/reference-display.js";
11
12
  export type { LoomaImageUploadDetail } from "../../editor/image-uploads.js";
12
13
  export type { LoomaRequestInit, LoomaResponder } from "../../editor/requests.js";
13
14
  /**
@@ -104,6 +105,11 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
104
105
  text?: string;
105
106
  };
106
107
  };
108
+ /** Cached display values; replace this map to refresh labels without editing content. */
109
+ references: {
110
+ type: PropType<LoomaReferenceMap | undefined>;
111
+ default: undefined;
112
+ };
107
113
  editable: {
108
114
  type: BooleanConstructor;
109
115
  default: boolean;
@@ -249,6 +255,11 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
249
255
  text?: string;
250
256
  };
251
257
  };
258
+ /** Cached display values; replace this map to refresh labels without editing content. */
259
+ references: {
260
+ type: PropType<LoomaReferenceMap | undefined>;
261
+ default: undefined;
262
+ };
252
263
  editable: {
253
264
  type: BooleanConstructor;
254
265
  default: boolean;
@@ -384,6 +395,7 @@ export declare const LoomaEditor: import("vue").DefineComponent<import("vue").Ex
384
395
  linkBaseUrl: string;
385
396
  disableHighlight: boolean;
386
397
  codeLanguages: Readonly<Record<string, import("highlight.js").LanguageFn>> | null;
398
+ references: Readonly<Record<string, import("@threadlabs/looma/editor").LoomaReferenceDisplay>> | undefined;
387
399
  toolbarOpen: boolean;
388
400
  slashCommands: LoomaEditorSlashCommands | undefined;
389
401
  mentionQuery: LoomaMentionQuery | undefined;
@@ -1832,7 +1832,185 @@ function siteRelativeHref(href, baseUrl) {
1832
1832
  }
1833
1833
  }
1834
1834
  //#endregion
1835
+ //#region src/editor/reference-display.ts
1836
+ var referenceDisplayKey = new PluginKey("loomaReferenceDisplay");
1837
+ var referenceDisplayMeta = "loomaReferenceDisplay";
1838
+ /**
1839
+ * Replaces presentation without editing the document or adding an undo/history entry.
1840
+ * @lifecycle Values last until the next replacement or editor destruction; hosts own persistence.
1841
+ */
1842
+ function setReferenceDisplay(editor, references) {
1843
+ if (editor.isDestroyed) return;
1844
+ editor.storage.loomaReferencePresentation?.enableMentions?.();
1845
+ editor.view.dispatch(editor.state.tr.setMeta(referenceDisplayMeta, references).setMeta("addToHistory", false));
1846
+ }
1847
+ /**
1848
+ * The current replaceable presentation; absent entries use the saved fallback.
1849
+ * @ownership The returned map belongs to the host; replace it rather than mutating it.
1850
+ */
1851
+ function getReferenceDisplay(editor) {
1852
+ return editor.storage.loomaReferencePresentation?.references ?? {};
1853
+ }
1854
+ /**
1855
+ * Reads current host presentation, with a durable insertion-time fallback for offline copies.
1856
+ * @failure Unsafe cached destinations fall back to the saved safe URL or no destination.
1857
+ */
1858
+ function referenceDisplay(node, references = {}) {
1859
+ const kind = node.type.name === "mention" ? "mention" : node.attrs.kind;
1860
+ const display = references[`${kind}:${node.attrs.id}`];
1861
+ const label = typeof display?.label === "string" ? display.label : typeof node.attrs.label === "string" ? node.attrs.label : String(node.attrs.id ?? "Reference");
1862
+ const href = typeof display?.href === "string" ? validLinkHref(display.href) : null;
1863
+ return {
1864
+ label,
1865
+ ...kind === "link" ? { href: href ?? (typeof node.attrs.href === "string" ? validLinkHref(node.attrs.href) : null) ?? void 0 } : {}
1866
+ };
1867
+ }
1868
+ Node$1.create({
1869
+ name: "loomaReference",
1870
+ inline: true,
1871
+ group: "inline",
1872
+ atom: true,
1873
+ addAttributes: () => ({
1874
+ id: {
1875
+ default: null,
1876
+ renderHTML: () => ({})
1877
+ },
1878
+ kind: {
1879
+ default: "link",
1880
+ renderHTML: () => ({})
1881
+ },
1882
+ href: {
1883
+ default: null,
1884
+ renderHTML: () => ({})
1885
+ },
1886
+ label: {
1887
+ default: null,
1888
+ renderHTML: () => ({})
1889
+ },
1890
+ target: {
1891
+ default: "_self",
1892
+ renderHTML: () => ({})
1893
+ }
1894
+ }),
1895
+ parseHTML: () => [{
1896
+ tag: "a[data-looma-reference]",
1897
+ priority: 1001,
1898
+ getAttrs: (element) => ({
1899
+ id: element.getAttribute("data-looma-reference"),
1900
+ kind: "link",
1901
+ href: element.getAttribute("href"),
1902
+ label: element.textContent,
1903
+ target: element.getAttribute("target") ?? "_self"
1904
+ })
1905
+ }],
1906
+ renderHTML({ node, HTMLAttributes }) {
1907
+ const display = referenceDisplay(node, this.editor ? getReferenceDisplay(this.editor) : {});
1908
+ return [
1909
+ "a",
1910
+ mergeAttributes(HTMLAttributes, {
1911
+ "data-looma-reference": node.attrs.id,
1912
+ href: display.href,
1913
+ target: node.attrs.target === "_blank" ? "_blank" : "_self",
1914
+ rel: "noopener noreferrer"
1915
+ }),
1916
+ display.label
1917
+ ];
1918
+ },
1919
+ renderText({ node }) {
1920
+ return referenceDisplay(node, this.editor ? getReferenceDisplay(this.editor) : {}).label;
1921
+ }
1922
+ });
1923
+ Extension.create({
1924
+ name: "loomaReferencePresentation",
1925
+ addOptions: () => ({
1926
+ references: {},
1927
+ mentions: false
1928
+ }),
1929
+ addStorage() {
1930
+ return { references: this.options.references };
1931
+ },
1932
+ addProseMirrorPlugins() {
1933
+ const initial = this.options.references;
1934
+ const refresh = /* @__PURE__ */ new Set();
1935
+ const nodeView = (node, view, getPosition) => {
1936
+ let current = node;
1937
+ const mention = node.type.name === "mention";
1938
+ const dom = document.createElement(mention ? "span" : "a");
1939
+ if (mention) {
1940
+ dom.className = "looma-mention";
1941
+ dom.setAttribute("data-type", "mention");
1942
+ } else dom.addEventListener("click", (event) => {
1943
+ if (!(event instanceof MouseEvent) || !view.editable || event.metaKey || event.ctrlKey) return;
1944
+ const position = getPosition();
1945
+ if (position === void 0) return;
1946
+ event.preventDefault();
1947
+ view.dispatch(view.state.tr.setSelection(NodeSelection.create(view.state.doc, position)));
1948
+ view.focus();
1949
+ });
1950
+ const render = () => {
1951
+ const display = referenceDisplay(current, referenceDisplayKey.getState(view.state));
1952
+ dom.textContent = `${mention ? "@" : ""}${display.label}`;
1953
+ if (mention) {
1954
+ dom.setAttribute("data-id", current.attrs.id ?? "");
1955
+ dom.setAttribute("data-label", display.label);
1956
+ }
1957
+ if (!mention) {
1958
+ dom.setAttribute("data-looma-reference", current.attrs.id ?? "");
1959
+ if (display.href) dom.setAttribute("href", display.href);
1960
+ else dom.removeAttribute("href");
1961
+ dom.setAttribute("target", current.attrs.target === "_blank" ? "_blank" : "_self");
1962
+ dom.setAttribute("rel", "noopener noreferrer");
1963
+ }
1964
+ };
1965
+ refresh.add(render);
1966
+ render();
1967
+ return {
1968
+ dom,
1969
+ update(next) {
1970
+ if (next.type !== current.type) return false;
1971
+ current = next;
1972
+ render();
1973
+ return true;
1974
+ },
1975
+ destroy: () => {
1976
+ refresh.delete(render);
1977
+ }
1978
+ };
1979
+ };
1980
+ const nodeViews = {
1981
+ loomaReference: nodeView,
1982
+ ...this.options.mentions && this.editor.schema.nodes.mention ? { mention: nodeView } : {}
1983
+ };
1984
+ this.storage.enableMentions = () => {
1985
+ if (nodeViews.mention || !this.editor.schema.nodes.mention) return;
1986
+ nodeViews.mention = nodeView;
1987
+ this.editor.view.setProps({ nodeViews: { ...this.editor.view.props.nodeViews } });
1988
+ };
1989
+ return [new Plugin({
1990
+ key: referenceDisplayKey,
1991
+ state: {
1992
+ init: () => initial,
1993
+ apply: (transaction, previous) => {
1994
+ const next = transaction.getMeta(referenceDisplayMeta) ?? previous;
1995
+ this.storage.references = next;
1996
+ return next;
1997
+ }
1998
+ },
1999
+ props: { nodeViews },
2000
+ view: () => ({ update: (view, previous) => {
2001
+ if (referenceDisplayKey.getState(view.state) !== referenceDisplayKey.getState(previous)) refresh.forEach((render) => render());
2002
+ } })
2003
+ })];
2004
+ }
2005
+ });
2006
+ //#endregion
1835
2007
  //#region src/editor/link-editing.ts
2008
+ /** Whether the current selection identifies an automatically labelled link. */
2009
+ function selectedLinkReference(editor) {
2010
+ const selection = editor.state.selection;
2011
+ return selection instanceof NodeSelection && selection.node.type.name === "loomaReference" ? selection.node : null;
2012
+ }
2013
+ var isLinkActive = (editor) => editor.isActive("link") || Boolean(selectedLinkReference(editor));
1836
2014
  /** The status line above search results: progress, a hint, or an empty result. */
1837
2015
  function linkFormStatus(state) {
1838
2016
  if (state.searching) return "Searching…";
@@ -1864,6 +2042,7 @@ function createLinkEditing(options) {
1864
2042
  let resolveSequence = 0;
1865
2043
  let currentHref = null;
1866
2044
  let destroyed = false;
2045
+ let automaticText = false;
1867
2046
  const set = (patch) => {
1868
2047
  const previous = state;
1869
2048
  state = {
@@ -1907,13 +2086,27 @@ function createLinkEditing(options) {
1907
2086
  }
1908
2087
  try {
1909
2088
  const target = await resolve(href);
1910
- cache.set(href, target);
1911
- if (sequence === resolveSequence && !destroyed) set({ current: target });
2089
+ if (sequence === resolveSequence && !destroyed) {
2090
+ cache.set(href, target);
2091
+ set({ current: target });
2092
+ }
1912
2093
  } catch {}
1913
2094
  };
1914
2095
  const onTransaction = () => {
1915
- const href = editor.isActive("link") ? String(editor.getAttributes("link").href ?? "") : null;
1916
- if (href !== currentHref) {
2096
+ const reference = selectedLinkReference(editor);
2097
+ const display = reference ? referenceDisplay(reference, getReferenceDisplay(editor)) : null;
2098
+ const href = display?.href ?? (editor.isActive("link") ? String(editor.getAttributes("link").href ?? "") : null);
2099
+ if (reference && display && href) {
2100
+ resolveSequence += 1;
2101
+ const target = {
2102
+ id: String(reference.attrs.id),
2103
+ label: display.label,
2104
+ href
2105
+ };
2106
+ cache.set(href, target);
2107
+ if (state.current?.id !== target.id || state.current.label !== target.label || state.current.href !== href) set({ current: target });
2108
+ currentHref = href;
2109
+ } else if (href !== currentHref) {
1917
2110
  currentHref = href;
1918
2111
  resolveCurrent(href);
1919
2112
  }
@@ -1941,6 +2134,30 @@ function createLinkEditing(options) {
1941
2134
  from: range.from,
1942
2135
  to: range.to
1943
2136
  });
2137
+ if (automaticText && state.mode === "target" && state.selected && (options.references?.() || selectedLinkReference(editor))) {
2138
+ const target = state.selected;
2139
+ const previous = editor.state.doc.nodeAt(range.from);
2140
+ const sameTarget = previous?.type.name === "loomaReference" && previous.attrs.id === target.id;
2141
+ chain.insertContent({
2142
+ type: "loomaReference",
2143
+ attrs: {
2144
+ id: target.id,
2145
+ kind: "link",
2146
+ href: sameTarget ? previous.attrs.href : attrs.href,
2147
+ label: sameTarget ? previous.attrs.label : target.label,
2148
+ target: attrs.target
2149
+ }
2150
+ }).setNodeSelection(range.from).run();
2151
+ set({
2152
+ range: {
2153
+ from: range.from,
2154
+ to: range.from + 1,
2155
+ existing: true
2156
+ },
2157
+ error: ""
2158
+ });
2159
+ return true;
2160
+ }
1944
2161
  if (state.textEditable) {
1945
2162
  const text = state.text.trim();
1946
2163
  if (!text) return fail("Enter link text.");
@@ -1988,10 +2205,17 @@ function createLinkEditing(options) {
1988
2205
  },
1989
2206
  open(from, to, surface) {
1990
2207
  const empty = from === to;
1991
- const existing = editor.isActive("link");
1992
- const attrs = existing ? editor.getAttributes("link") : {};
2208
+ const reference = selectedLinkReference(editor);
2209
+ const existing = isLinkActive(editor);
2210
+ const attrs = reference?.attrs ?? (existing ? editor.getAttributes("link") : {});
1993
2211
  const href = typeof attrs.href === "string" ? attrs.href : "";
1994
- const selected = cache.get(href) ?? null;
2212
+ const display = reference ? referenceDisplay(reference, getReferenceDisplay(editor)) : null;
2213
+ const selected = reference ? {
2214
+ id: String(reference.attrs.id),
2215
+ label: display.label,
2216
+ href: display.href ?? href
2217
+ } : cache.get(href) ?? null;
2218
+ automaticText = Boolean(reference) || empty && !existing;
1995
2219
  set({
1996
2220
  surface,
1997
2221
  range: {
@@ -1999,17 +2223,17 @@ function createLinkEditing(options) {
1999
2223
  to,
2000
2224
  existing
2001
2225
  },
2002
- textEditable: empty && !existing,
2226
+ textEditable: Boolean(reference) || empty && !existing,
2003
2227
  href,
2004
2228
  newTab: attrs.target !== "_self",
2005
- text: empty && !existing ? "" : editor.state.doc.textBetween(from, to),
2229
+ text: display?.label ?? (empty && !existing ? "" : editor.state.doc.textBetween(from, to)),
2006
2230
  error: "",
2007
2231
  selected,
2008
- mode: options.search() && (!existing || selected) ? "target" : "url",
2232
+ mode: reference || options.search() && (!existing || selected) ? "target" : "url",
2009
2233
  query: selected?.label ?? href
2010
2234
  });
2011
2235
  const resolve = options.resolve();
2012
- if (existing && resolve && !cache.has(href)) resolve(href).then((target) => {
2236
+ if (existing && !reference && resolve && !cache.has(href)) resolve(href).then((target) => {
2013
2237
  cache.set(href, target);
2014
2238
  if (!destroyed && state.href === href && target) set({
2015
2239
  selected: target,
@@ -2019,7 +2243,7 @@ function createLinkEditing(options) {
2019
2243
  }).catch(() => {});
2020
2244
  },
2021
2245
  setDestination(value) {
2022
- if (options.search()) {
2246
+ if (options.search() || selectedLinkReference(editor)) {
2023
2247
  const trimmed = value.trim();
2024
2248
  const url = /^(https?:\/\/|mailto:|tel:)/i.test(trimmed) || Boolean(state.range?.existing && /^(\/|\.\.?\/|#|\?)/.test(trimmed));
2025
2249
  set({
@@ -2046,6 +2270,13 @@ function createLinkEditing(options) {
2046
2270
  },
2047
2271
  choose(target) {
2048
2272
  cache.set(target.href, target);
2273
+ if (options.references?.() || selectedLinkReference(editor)) setReferenceDisplay(editor, {
2274
+ ...getReferenceDisplay(editor),
2275
+ [`link:${target.id}`]: {
2276
+ label: target.label,
2277
+ href: target.href
2278
+ }
2279
+ });
2049
2280
  set({
2050
2281
  selected: target,
2051
2282
  href: target.href,
@@ -2062,6 +2293,7 @@ function createLinkEditing(options) {
2062
2293
  apply();
2063
2294
  },
2064
2295
  setText(text) {
2296
+ automaticText = false;
2065
2297
  set({
2066
2298
  text,
2067
2299
  error: ""
@@ -2073,6 +2305,15 @@ function createLinkEditing(options) {
2073
2305
  if (state.surface !== "none") set({ surface: "none" });
2074
2306
  },
2075
2307
  remove() {
2308
+ const reference = selectedLinkReference(editor);
2309
+ if (reference) {
2310
+ editor.chain().focus().insertContent({
2311
+ type: "text",
2312
+ text: referenceDisplay(reference, getReferenceDisplay(editor)).label
2313
+ }).run();
2314
+ set({ surface: "none" });
2315
+ return;
2316
+ }
2076
2317
  if (!editor.isActive("link")) return;
2077
2318
  editor.chain().focus().extendMarkRange("link").unsetLink().run();
2078
2319
  if (state.surface === "context") set({ surface: "none" });
@@ -10252,17 +10493,19 @@ function normalizeTableOfContentsConfig$1(attributes) {
10252
10493
  * @contract Depth is a maximum heading level. Relative nesting follows existing
10253
10494
  * ancestors rather than inserting artificial entries for skipped levels.
10254
10495
  */
10255
- function getTableOfContentsEntries$1(doc, depth = 3) {
10496
+ function getTableOfContentsEntries$1(doc, depth = 3, references = {}) {
10256
10497
  const entries = [];
10257
10498
  const ancestors = [];
10258
10499
  doc.descendants((node, position) => {
10259
10500
  if (node.type.name === "loomaTableOfContents") return false;
10260
- if (node.type.name !== "heading" || node.attrs.level > depth || !node.textContent.trim()) return;
10501
+ if (node.type.name !== "heading" || node.attrs.level > depth) return;
10502
+ const text = node.textBetween(0, node.content.size, "", (leaf) => leaf.type.name === "loomaReference" || leaf.type.name === "mention" ? `${leaf.type.name === "mention" ? "@" : ""}${referenceDisplay(leaf, references).label}` : String(leaf.attrs.label ?? "")).trim();
10503
+ if (!text) return;
10261
10504
  const level = Number(node.attrs.level);
10262
10505
  while (ancestors.length && ancestors[ancestors.length - 1] >= level) ancestors.pop();
10263
10506
  entries.push({
10264
10507
  id: String(node.attrs.id ?? ""),
10265
- text: node.textContent.trim(),
10508
+ text,
10266
10509
  level,
10267
10510
  depth: ancestors.length,
10268
10511
  position
@@ -10350,8 +10593,8 @@ var LoomaHeadingAnchors = Extension.create({
10350
10593
  return [new Plugin({ appendTransaction: (transactions, oldState, state) => transactions.some((transaction) => transaction.docChanged) ? anchorTransaction(state, oldState.doc) : null })];
10351
10594
  }
10352
10595
  });
10353
- function contentsHtml(doc, config) {
10354
- const branches = tableOfContentsTree(getTableOfContentsEntries$1(doc, config.depth));
10596
+ function contentsHtml(doc, config, references = {}) {
10597
+ const branches = tableOfContentsTree(getTableOfContentsEntries$1(doc, config.depth, references));
10355
10598
  const list = (items) => [
10356
10599
  config.format === "plain" ? "div" : config.format === "numbered" ? "ol" : "ul",
10357
10600
  { role: "list" },
@@ -10393,7 +10636,7 @@ function contentsHtml(doc, config) {
10393
10636
  async function navigateTableOfContents(editor, id) {
10394
10637
  const target = [...editor.view.dom.querySelectorAll("h1[id], h2[id], h3[id]")].find((heading) => heading.id === id);
10395
10638
  if (!target) return;
10396
- const entry = getTableOfContentsEntries$1(editor.state.doc).find((heading) => heading.id === id);
10639
+ const entry = getTableOfContentsEntries$1(editor.state.doc, 3, getReferenceDisplay(editor)).find((heading) => heading.id === id);
10397
10640
  if (entry) await revealLoomaExpandAt(editor, entry.position + 1);
10398
10641
  if (!target.isConnected || editor.isDestroyed) return;
10399
10642
  target.scrollIntoView({
@@ -10469,7 +10712,7 @@ Node$1.create({
10469
10712
  nav.append(DOMSerializer.fromSchema(editor.schema).serializeFragment(current.content));
10470
10713
  dom.replaceChildren(nav);
10471
10714
  } else {
10472
- const { dom: content } = DOMSerializer.renderSpec(document, contentsHtml(editor.state.doc, normalizeTableOfContentsConfig$1(current.attrs)));
10715
+ const { dom: content } = DOMSerializer.renderSpec(document, contentsHtml(editor.state.doc, normalizeTableOfContentsConfig$1(current.attrs), getReferenceDisplay(editor)));
10473
10716
  dom.replaceChildren(content);
10474
10717
  }
10475
10718
  };
@@ -10577,7 +10820,7 @@ var tableOfContentsNodeView = ({ editor, node, getPos }) => {
10577
10820
  }
10578
10821
  return;
10579
10822
  }
10580
- const found = getTableOfContentsEntries(editor.state.doc, depth);
10823
+ const found = getTableOfContentsEntries(editor.state.doc, depth, getReferenceDisplay(editor));
10581
10824
  const signature = JSON.stringify([format, found.map(({ id: entryId, text, level }) => [
10582
10825
  entryId,
10583
10826
  text,
@@ -13568,6 +13811,11 @@ var LoomaEditor = defineComponent({
13568
13811
  type: Object,
13569
13812
  default: () => ({ ...EMPTY_DOCUMENT })
13570
13813
  },
13814
+ /** Cached display values; replace this map to refresh labels without editing content. */
13815
+ references: {
13816
+ type: Object,
13817
+ default: void 0
13818
+ },
13571
13819
  editable: {
13572
13820
  type: Boolean,
13573
13821
  default: true
@@ -13941,6 +14189,7 @@ var LoomaEditor = defineComponent({
13941
14189
  session = createEditorSession({
13942
14190
  extensions: [
13943
14191
  ...getDefaultEditorExtensions({
14192
+ references: props.references,
13944
14193
  placeholder: props.placeholder,
13945
14194
  linkBaseUrl: props.linkBaseUrl,
13946
14195
  mention: consumerMention ? false : mentionExtension,
@@ -13983,7 +14232,7 @@ var LoomaEditor = defineComponent({
13983
14232
  if (view.isDestroyed) return;
13984
14233
  editorStateVersion.value += 1;
13985
14234
  const instance = editor.value;
13986
- if (instance?.isFocused && instance.state.selection.empty && instance.isActive("link") && linkState.value.surface !== "popover") {
14235
+ if (instance?.isFocused && (instance.state.selection.empty || selectedLinkReference(instance)) && isLinkActive(instance) && linkState.value.surface !== "popover") {
13987
14236
  linkPopup?.setAnchor(() => link.getBoundingClientRect());
13988
14237
  linkPopup?.show();
13989
14238
  }
@@ -14022,7 +14271,8 @@ var LoomaEditor = defineComponent({
14022
14271
  editor: session.editor,
14023
14272
  search: () => answers("link-search") ? async (query) => await askLatest("link-search", { query }) ?? [] : null,
14024
14273
  resolve: () => async (href) => await askLatest("link-resolve", { href }),
14025
- baseUrl: () => props.linkBaseUrl
14274
+ baseUrl: () => props.linkBaseUrl,
14275
+ references: () => props.references !== void 0
14026
14276
  });
14027
14277
  engine.subscribe(() => {
14028
14278
  linkState.value = engine.state;
@@ -14219,6 +14469,9 @@ var LoomaEditor = defineComponent({
14219
14469
  const wasFocused = instance.isFocused;
14220
14470
  if (session.setContent(value, lastFocusedSelection) && wasFocused) rememberSelection(instance);
14221
14471
  }, { deep: true });
14472
+ watch(() => props.references, (value) => {
14473
+ if (editor.value) setReferenceDisplay(editor.value, value ?? {});
14474
+ }, { deep: true });
14222
14475
  const closeTableUi = () => {
14223
14476
  Object.assign(tableUi, LOOMA_TABLE_CONTROLS_CLOSED);
14224
14477
  tableUi.menuOpen = false;
@@ -14435,7 +14688,7 @@ var LoomaEditor = defineComponent({
14435
14688
  editor: instance,
14436
14689
  element,
14437
14690
  pluginKey: "looma-link-context-menu",
14438
- shouldShow: ({ editor: menuEditor, from, to }) => from === to && menuEditor.isActive("link") && linkState.value.surface !== "popover" && !formatting?.dismissed(menuEditor.state.selection) && Boolean(menuEditor.isFocused || linkState.value.surface === "context" || linkPopup?.element.contains(document.activeElement)) && !(props.toolbarMode === "contextual" && floatingFormattingMode() && formattingActive()),
14691
+ shouldShow: ({ editor: menuEditor, from, to }) => (from === to || Boolean(selectedLinkReference(menuEditor))) && isLinkActive(menuEditor) && linkState.value.surface !== "popover" && !formatting?.dismissed(menuEditor.state.selection) && Boolean(menuEditor.isFocused || linkState.value.surface === "context" || linkPopup?.element.contains(document.activeElement)) && !(props.toolbarMode === "contextual" && floatingFormattingMode() && formattingActive()),
14439
14692
  maxWidth: () => Math.min(480, window.innerWidth - 24),
14440
14693
  related: editorRoot,
14441
14694
  onDismiss: (reason) => reason === "escape" ? dismissFormatting() : linkPopup?.hide()
@@ -14800,8 +15053,8 @@ var LoomaEditor = defineComponent({
14800
15053
  size: "sm",
14801
15054
  variant: "ghost",
14802
15055
  "aria-expanded": linkState.value.surface === "popover" ? "true" : "false",
14803
- "aria-pressed": instance.isActive("link") ? "true" : "false",
14804
- "data-active": instance.isActive("link") ? "true" : "false",
15056
+ "aria-pressed": isLinkActive(instance) ? "true" : "false",
15057
+ "data-active": isLinkActive(instance) ? "true" : "false",
14805
15058
  onPointerenter: () => showTool(linkTriggerId, "Link", false),
14806
15059
  onPointerleave: hideTool,
14807
15060
  onPointerdown: (event) => {
@@ -14983,7 +15236,7 @@ var LoomaEditor = defineComponent({
14983
15236
  ref: formattingSurfaceRef,
14984
15237
  class: "looma-editor__floating-shell looma-editor__formatting-shell",
14985
15238
  hidden: true
14986
- }, [props.toolbarMode === "contextual" ? h(Stack, { gap: "xs" }, () => [renderToolbar(instance, true), (linkState.value.surface === "context" || instance.state.selection.empty && instance.isActive("link")) && linkState.value.surface !== "popover" && formattingActive() ? renderLinkContextCard(instance) : null]) : renderToolbar(instance, true, props.toolbarMode === "popover")])]) : null,
15239
+ }, [props.toolbarMode === "contextual" ? h(Stack, { gap: "xs" }, () => [renderToolbar(instance, true), (linkState.value.surface === "context" || (instance.state.selection.empty || selectedLinkReference(instance)) && isLinkActive(instance)) && linkState.value.surface !== "popover" && formattingActive() ? renderLinkContextCard(instance) : null]) : renderToolbar(instance, true, props.toolbarMode === "popover")])]) : null,
14987
15240
  instance && props.editable ? h("div", { key: "looma-link-host" }, [h("div", {
14988
15241
  ref: linkSurfaceRef,
14989
15242
  class: "looma-editor__floating-shell looma-editor__link-shell",