@threadlabs/looma 0.29.15 → 0.29.17

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.17",
4
4
  "type": "module",
5
5
  "description": "Looma components as HTML, Vue, and plain DOM, with design tokens.",
6
6
  "license": "MIT",