@threadlabs/looma 0.29.14 → 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.
- package/editor/chunks/extensions.js +56 -13
- package/editor/chunks/{markdown.js → reference-display.js} +207 -1
- package/editor/expressmark.d.ts +13 -3
- package/editor/expressmark.js +65 -10
- package/editor/extensions/index.d.ts +1 -0
- package/editor/extensions/index.js +2 -2
- package/editor/extensions/preset.d.ts +3 -0
- package/editor/extensions/table-of-contents.d.ts +2 -1
- package/editor/index.js +2 -2
- package/editor/link-editing.d.ts +5 -0
- package/editor/reference-display.d.ts +37 -0
- package/package.json +1 -1
- package/vue/editor/LoomaEditor.d.ts +12 -0
- package/vue/editor/index.js +285 -27
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as TABLE_CELL_BACKGROUND_PRESETS } from "./table-backgrounds.js";
|
|
2
|
-
import {
|
|
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,16 +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
|
-
if (node.type.name
|
|
621
|
+
if (node.type.name === "loomaTableOfContents") return false;
|
|
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;
|
|
622
625
|
const level = Number(node.attrs.level);
|
|
623
626
|
while (ancestors.length && ancestors[ancestors.length - 1] >= level) ancestors.pop();
|
|
624
627
|
entries.push({
|
|
625
628
|
id: String(node.attrs.id ?? ""),
|
|
626
|
-
text
|
|
629
|
+
text,
|
|
627
630
|
level,
|
|
628
631
|
depth: ancestors.length,
|
|
629
632
|
position
|
|
@@ -661,7 +664,10 @@ function anchorTransaction(state, previous) {
|
|
|
661
664
|
let hasToc = false;
|
|
662
665
|
const headings = [];
|
|
663
666
|
state.doc.descendants((node, position) => {
|
|
664
|
-
if (node.type.name === "loomaTableOfContents")
|
|
667
|
+
if (node.type.name === "loomaTableOfContents") {
|
|
668
|
+
hasToc = true;
|
|
669
|
+
return false;
|
|
670
|
+
}
|
|
665
671
|
if (node.type.name === "heading") headings.push({
|
|
666
672
|
node,
|
|
667
673
|
position
|
|
@@ -670,6 +676,7 @@ function anchorTransaction(state, previous) {
|
|
|
670
676
|
if (!headings.length) return null;
|
|
671
677
|
const before = /* @__PURE__ */ new Map();
|
|
672
678
|
previous?.descendants((node) => {
|
|
679
|
+
if (node.type.name === "loomaTableOfContents") return false;
|
|
673
680
|
if (node.type.name === "heading" && typeof node.attrs.id === "string") before.set(node.attrs.id, node.textContent.trim());
|
|
674
681
|
});
|
|
675
682
|
const custom = (node) => {
|
|
@@ -707,8 +714,8 @@ var LoomaHeadingAnchors = Extension.create({
|
|
|
707
714
|
return [new Plugin({ appendTransaction: (transactions, oldState, state) => transactions.some((transaction) => transaction.docChanged) ? anchorTransaction(state, oldState.doc) : null })];
|
|
708
715
|
}
|
|
709
716
|
});
|
|
710
|
-
function contentsHtml(doc, config) {
|
|
711
|
-
const branches = tableOfContentsTree(getTableOfContentsEntries(doc, config.depth));
|
|
717
|
+
function contentsHtml(doc, config, references = {}) {
|
|
718
|
+
const branches = tableOfContentsTree(getTableOfContentsEntries(doc, config.depth, references));
|
|
712
719
|
const list = (items) => [
|
|
713
720
|
config.format === "plain" ? "div" : config.format === "numbered" ? "ol" : "ul",
|
|
714
721
|
{ role: "list" },
|
|
@@ -750,7 +757,7 @@ function contentsHtml(doc, config) {
|
|
|
750
757
|
async function navigateTableOfContents(editor, id) {
|
|
751
758
|
const target = [...editor.view.dom.querySelectorAll("h1[id], h2[id], h3[id]")].find((heading) => heading.id === id);
|
|
752
759
|
if (!target) return;
|
|
753
|
-
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);
|
|
754
761
|
if (entry) await revealLoomaExpandAt(editor, entry.position + 1);
|
|
755
762
|
if (!target.isConnected || editor.isDestroyed) return;
|
|
756
763
|
target.scrollIntoView({
|
|
@@ -831,7 +838,7 @@ var LoomaTableOfContents = Node$1.create({
|
|
|
831
838
|
nav.append(DOMSerializer.fromSchema(editor.schema).serializeFragment(current.content));
|
|
832
839
|
dom.replaceChildren(nav);
|
|
833
840
|
} else {
|
|
834
|
-
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)));
|
|
835
842
|
dom.replaceChildren(content);
|
|
836
843
|
}
|
|
837
844
|
};
|
|
@@ -6759,10 +6766,36 @@ var EMPTY_STATE$1 = {
|
|
|
6759
6766
|
* than document data.
|
|
6760
6767
|
*/
|
|
6761
6768
|
var LoomaMentionSuggestionPluginKey = new PluginKey("loomaMentionSuggestion");
|
|
6762
|
-
var LoomaMentionNode = Mention.extend({
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
|
|
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
|
+
});
|
|
6766
6799
|
async function resolveMentionItems(source, query, limit) {
|
|
6767
6800
|
try {
|
|
6768
6801
|
return filterLoomaMentionItems(typeof source === "function" ? await source(query, { limit }) : source ?? [], query, limit);
|
|
@@ -9929,6 +9962,11 @@ var NODES = {
|
|
|
9929
9962
|
mention: (state, node) => {
|
|
9930
9963
|
const id = String(node.attrs.id ?? "");
|
|
9931
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);
|
|
9932
9970
|
}
|
|
9933
9971
|
};
|
|
9934
9972
|
var MARKS = {
|
|
@@ -10770,6 +10808,11 @@ function getDefaultEditorExtensions(options = {}) {
|
|
|
10770
10808
|
Document,
|
|
10771
10809
|
Paragraph,
|
|
10772
10810
|
Text$1,
|
|
10811
|
+
LoomaReference,
|
|
10812
|
+
LoomaReferencePresentation.configure({
|
|
10813
|
+
references: options.references ?? {},
|
|
10814
|
+
mentions: options.references !== void 0
|
|
10815
|
+
}),
|
|
10773
10816
|
Bold,
|
|
10774
10817
|
Italic,
|
|
10775
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
|
-
|
|
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 };
|
package/editor/expressmark.d.ts
CHANGED
|
@@ -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?:
|
|
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?:
|
|
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.
|
package/editor/expressmark.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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":
|
|
@@ -486,7 +515,7 @@ function editorDocumentToExpressmark(document, options = {}) {
|
|
|
486
515
|
let automaticToc = false;
|
|
487
516
|
const colors = /* @__PURE__ */ new Set();
|
|
488
517
|
const urls = /* @__PURE__ */ new Set();
|
|
489
|
-
const visit = (node) => {
|
|
518
|
+
const visit = (node, insideToc = false) => {
|
|
490
519
|
const attrs = node.attrs ?? {};
|
|
491
520
|
if (node.type === "tableCell" || node.type === "tableHeader") {
|
|
492
521
|
if (attrs.backgroundColor != null && !colors.has(attrs.backgroundColor)) {
|
|
@@ -520,14 +549,14 @@ function editorDocumentToExpressmark(document, options = {}) {
|
|
|
520
549
|
urls.add(key);
|
|
521
550
|
}
|
|
522
551
|
}
|
|
523
|
-
if (node.type === "loomaTableOfContents" && !node.content?.length) automaticToc = true;
|
|
524
|
-
if (node.type === "heading") headings.push({
|
|
552
|
+
if (!insideToc && node.type === "loomaTableOfContents" && !node.content?.length) automaticToc = true;
|
|
553
|
+
if (!insideToc && node.type === "heading") headings.push({
|
|
525
554
|
node,
|
|
526
|
-
text: (node
|
|
555
|
+
text: displayText(node, options.references ?? {}),
|
|
527
556
|
id: string(node.attrs?.id),
|
|
528
557
|
level: Number(node.attrs?.level ?? 1)
|
|
529
558
|
});
|
|
530
|
-
for (const child of node.content ?? []) visit(child);
|
|
559
|
+
for (const child of node.content ?? []) visit(child, insideToc || node.type === "loomaTableOfContents");
|
|
531
560
|
};
|
|
532
561
|
visit(document);
|
|
533
562
|
const headingIds = /* @__PURE__ */ new WeakMap();
|
|
@@ -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 {
|
|
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 {
|
|
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 };
|
package/editor/link-editing.d.ts
CHANGED
|
@@ -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
|
@@ -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;
|
package/vue/editor/index.js
CHANGED
|
@@ -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
|
-
|
|
1911
|
-
|
|
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
|
|
1916
|
-
|
|
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
|
|
1992
|
-
const
|
|
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
|
|
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,16 +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
|
-
if (node.type.name
|
|
10500
|
+
if (node.type.name === "loomaTableOfContents") return false;
|
|
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;
|
|
10260
10504
|
const level = Number(node.attrs.level);
|
|
10261
10505
|
while (ancestors.length && ancestors[ancestors.length - 1] >= level) ancestors.pop();
|
|
10262
10506
|
entries.push({
|
|
10263
10507
|
id: String(node.attrs.id ?? ""),
|
|
10264
|
-
text
|
|
10508
|
+
text,
|
|
10265
10509
|
level,
|
|
10266
10510
|
depth: ancestors.length,
|
|
10267
10511
|
position
|
|
@@ -10299,7 +10543,10 @@ function anchorTransaction(state, previous) {
|
|
|
10299
10543
|
let hasToc = false;
|
|
10300
10544
|
const headings = [];
|
|
10301
10545
|
state.doc.descendants((node, position) => {
|
|
10302
|
-
if (node.type.name === "loomaTableOfContents")
|
|
10546
|
+
if (node.type.name === "loomaTableOfContents") {
|
|
10547
|
+
hasToc = true;
|
|
10548
|
+
return false;
|
|
10549
|
+
}
|
|
10303
10550
|
if (node.type.name === "heading") headings.push({
|
|
10304
10551
|
node,
|
|
10305
10552
|
position
|
|
@@ -10308,6 +10555,7 @@ function anchorTransaction(state, previous) {
|
|
|
10308
10555
|
if (!headings.length) return null;
|
|
10309
10556
|
const before = /* @__PURE__ */ new Map();
|
|
10310
10557
|
previous?.descendants((node) => {
|
|
10558
|
+
if (node.type.name === "loomaTableOfContents") return false;
|
|
10311
10559
|
if (node.type.name === "heading" && typeof node.attrs.id === "string") before.set(node.attrs.id, node.textContent.trim());
|
|
10312
10560
|
});
|
|
10313
10561
|
const custom = (node) => {
|
|
@@ -10345,8 +10593,8 @@ var LoomaHeadingAnchors = Extension.create({
|
|
|
10345
10593
|
return [new Plugin({ appendTransaction: (transactions, oldState, state) => transactions.some((transaction) => transaction.docChanged) ? anchorTransaction(state, oldState.doc) : null })];
|
|
10346
10594
|
}
|
|
10347
10595
|
});
|
|
10348
|
-
function contentsHtml(doc, config) {
|
|
10349
|
-
const branches = tableOfContentsTree(getTableOfContentsEntries$1(doc, config.depth));
|
|
10596
|
+
function contentsHtml(doc, config, references = {}) {
|
|
10597
|
+
const branches = tableOfContentsTree(getTableOfContentsEntries$1(doc, config.depth, references));
|
|
10350
10598
|
const list = (items) => [
|
|
10351
10599
|
config.format === "plain" ? "div" : config.format === "numbered" ? "ol" : "ul",
|
|
10352
10600
|
{ role: "list" },
|
|
@@ -10388,7 +10636,7 @@ function contentsHtml(doc, config) {
|
|
|
10388
10636
|
async function navigateTableOfContents(editor, id) {
|
|
10389
10637
|
const target = [...editor.view.dom.querySelectorAll("h1[id], h2[id], h3[id]")].find((heading) => heading.id === id);
|
|
10390
10638
|
if (!target) return;
|
|
10391
|
-
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);
|
|
10392
10640
|
if (entry) await revealLoomaExpandAt(editor, entry.position + 1);
|
|
10393
10641
|
if (!target.isConnected || editor.isDestroyed) return;
|
|
10394
10642
|
target.scrollIntoView({
|
|
@@ -10464,7 +10712,7 @@ Node$1.create({
|
|
|
10464
10712
|
nav.append(DOMSerializer.fromSchema(editor.schema).serializeFragment(current.content));
|
|
10465
10713
|
dom.replaceChildren(nav);
|
|
10466
10714
|
} else {
|
|
10467
|
-
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)));
|
|
10468
10716
|
dom.replaceChildren(content);
|
|
10469
10717
|
}
|
|
10470
10718
|
};
|
|
@@ -10572,7 +10820,7 @@ var tableOfContentsNodeView = ({ editor, node, getPos }) => {
|
|
|
10572
10820
|
}
|
|
10573
10821
|
return;
|
|
10574
10822
|
}
|
|
10575
|
-
const found = getTableOfContentsEntries(editor.state.doc, depth);
|
|
10823
|
+
const found = getTableOfContentsEntries(editor.state.doc, depth, getReferenceDisplay(editor));
|
|
10576
10824
|
const signature = JSON.stringify([format, found.map(({ id: entryId, text, level }) => [
|
|
10577
10825
|
entryId,
|
|
10578
10826
|
text,
|
|
@@ -13563,6 +13811,11 @@ var LoomaEditor = defineComponent({
|
|
|
13563
13811
|
type: Object,
|
|
13564
13812
|
default: () => ({ ...EMPTY_DOCUMENT })
|
|
13565
13813
|
},
|
|
13814
|
+
/** Cached display values; replace this map to refresh labels without editing content. */
|
|
13815
|
+
references: {
|
|
13816
|
+
type: Object,
|
|
13817
|
+
default: void 0
|
|
13818
|
+
},
|
|
13566
13819
|
editable: {
|
|
13567
13820
|
type: Boolean,
|
|
13568
13821
|
default: true
|
|
@@ -13936,6 +14189,7 @@ var LoomaEditor = defineComponent({
|
|
|
13936
14189
|
session = createEditorSession({
|
|
13937
14190
|
extensions: [
|
|
13938
14191
|
...getDefaultEditorExtensions({
|
|
14192
|
+
references: props.references,
|
|
13939
14193
|
placeholder: props.placeholder,
|
|
13940
14194
|
linkBaseUrl: props.linkBaseUrl,
|
|
13941
14195
|
mention: consumerMention ? false : mentionExtension,
|
|
@@ -13978,7 +14232,7 @@ var LoomaEditor = defineComponent({
|
|
|
13978
14232
|
if (view.isDestroyed) return;
|
|
13979
14233
|
editorStateVersion.value += 1;
|
|
13980
14234
|
const instance = editor.value;
|
|
13981
|
-
if (instance?.isFocused && instance.state.selection.empty && instance
|
|
14235
|
+
if (instance?.isFocused && (instance.state.selection.empty || selectedLinkReference(instance)) && isLinkActive(instance) && linkState.value.surface !== "popover") {
|
|
13982
14236
|
linkPopup?.setAnchor(() => link.getBoundingClientRect());
|
|
13983
14237
|
linkPopup?.show();
|
|
13984
14238
|
}
|
|
@@ -14017,7 +14271,8 @@ var LoomaEditor = defineComponent({
|
|
|
14017
14271
|
editor: session.editor,
|
|
14018
14272
|
search: () => answers("link-search") ? async (query) => await askLatest("link-search", { query }) ?? [] : null,
|
|
14019
14273
|
resolve: () => async (href) => await askLatest("link-resolve", { href }),
|
|
14020
|
-
baseUrl: () => props.linkBaseUrl
|
|
14274
|
+
baseUrl: () => props.linkBaseUrl,
|
|
14275
|
+
references: () => props.references !== void 0
|
|
14021
14276
|
});
|
|
14022
14277
|
engine.subscribe(() => {
|
|
14023
14278
|
linkState.value = engine.state;
|
|
@@ -14214,6 +14469,9 @@ var LoomaEditor = defineComponent({
|
|
|
14214
14469
|
const wasFocused = instance.isFocused;
|
|
14215
14470
|
if (session.setContent(value, lastFocusedSelection) && wasFocused) rememberSelection(instance);
|
|
14216
14471
|
}, { deep: true });
|
|
14472
|
+
watch(() => props.references, (value) => {
|
|
14473
|
+
if (editor.value) setReferenceDisplay(editor.value, value ?? {});
|
|
14474
|
+
}, { deep: true });
|
|
14217
14475
|
const closeTableUi = () => {
|
|
14218
14476
|
Object.assign(tableUi, LOOMA_TABLE_CONTROLS_CLOSED);
|
|
14219
14477
|
tableUi.menuOpen = false;
|
|
@@ -14430,7 +14688,7 @@ var LoomaEditor = defineComponent({
|
|
|
14430
14688
|
editor: instance,
|
|
14431
14689
|
element,
|
|
14432
14690
|
pluginKey: "looma-link-context-menu",
|
|
14433
|
-
shouldShow: ({ editor: menuEditor, from, to }) => from === to && menuEditor
|
|
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()),
|
|
14434
14692
|
maxWidth: () => Math.min(480, window.innerWidth - 24),
|
|
14435
14693
|
related: editorRoot,
|
|
14436
14694
|
onDismiss: (reason) => reason === "escape" ? dismissFormatting() : linkPopup?.hide()
|
|
@@ -14795,8 +15053,8 @@ var LoomaEditor = defineComponent({
|
|
|
14795
15053
|
size: "sm",
|
|
14796
15054
|
variant: "ghost",
|
|
14797
15055
|
"aria-expanded": linkState.value.surface === "popover" ? "true" : "false",
|
|
14798
|
-
"aria-pressed": instance
|
|
14799
|
-
"data-active": instance
|
|
15056
|
+
"aria-pressed": isLinkActive(instance) ? "true" : "false",
|
|
15057
|
+
"data-active": isLinkActive(instance) ? "true" : "false",
|
|
14800
15058
|
onPointerenter: () => showTool(linkTriggerId, "Link", false),
|
|
14801
15059
|
onPointerleave: hideTool,
|
|
14802
15060
|
onPointerdown: (event) => {
|
|
@@ -14978,7 +15236,7 @@ var LoomaEditor = defineComponent({
|
|
|
14978
15236
|
ref: formattingSurfaceRef,
|
|
14979
15237
|
class: "looma-editor__floating-shell looma-editor__formatting-shell",
|
|
14980
15238
|
hidden: true
|
|
14981
|
-
}, [props.toolbarMode === "contextual" ? h(Stack, { gap: "xs" }, () => [renderToolbar(instance, true), (linkState.value.surface === "context" || instance.state.selection.empty && instance
|
|
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,
|
|
14982
15240
|
instance && props.editable ? h("div", { key: "looma-link-host" }, [h("div", {
|
|
14983
15241
|
ref: linkSurfaceRef,
|
|
14984
15242
|
class: "looma-editor__floating-shell looma-editor__link-shell",
|