@threadlabs/looma 0.29.12 → 0.29.13

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,4 +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
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";
3
4
  import { NodeSelection, Plugin, PluginKey, Selection, TextSelection } from "@tiptap/pm/state";
4
5
  import { Decoration, DecorationSet } from "@tiptap/pm/view";
@@ -127,247 +128,6 @@ var LoomaActiveBlock = Extension.create({
127
128
  }
128
129
  });
129
130
  //#endregion
130
- //#region src/markdown/index.ts
131
- /**
132
- * Callout tones a document stores, in the order the editor offers them. The Callout component also
133
- * takes `neutral` and `success`, which Markdown and the editor store as `note`.
134
- */
135
- var LOOMA_CALLOUT_TONES = [
136
- "note",
137
- "tip",
138
- "info",
139
- "important",
140
- "warning",
141
- "danger"
142
- ];
143
- /** Names that read as a stored tone: `caution` is `danger`, and `neutral` is the same muted aside as `note`. */
144
- var LOOMA_CALLOUT_TONE_ALIASES = {
145
- caution: "danger",
146
- neutral: "note"
147
- };
148
- /**
149
- * Maps a tone name or alias, in any case, onto a stored tone.
150
- * @failure Unknown and non-string values become `note`.
151
- */
152
- function normalizeLoomaCalloutTone(value) {
153
- if (typeof value !== "string") return "note";
154
- const tone = value.toLowerCase();
155
- return LOOMA_CALLOUT_TONES.includes(tone) ? tone : LOOMA_CALLOUT_TONE_ALIASES[tone] ?? "note";
156
- }
157
- /** The hues chips and swatches take as their tone, written `:chip[Label]{tone=green}`. */
158
- var LOOMA_CHIP_TONES = [
159
- "neutral",
160
- "blue",
161
- "green",
162
- "yellow",
163
- "red",
164
- "purple"
165
- ];
166
- /**
167
- * Keeps a chip tone within the palette.
168
- * @failure Unknown values become `neutral`.
169
- */
170
- function normalizeLoomaChipTone(value) {
171
- return LOOMA_CHIP_TONES.includes(value) ? value : "neutral";
172
- }
173
- /**
174
- * Names that open a `:::` container, matched without regard to case. A container with any other
175
- * name is ordinary text.
176
- */
177
- var LOOMA_CONTAINER_NAMES = /* @__PURE__ */ new Set([
178
- ...LOOMA_CALLOUT_TONES,
179
- "caution",
180
- "details",
181
- "figure",
182
- "tabs",
183
- "code-group"
184
- ]);
185
- var ATTRIBUTE_KEY = /[A-Za-z][\w-]*/y;
186
- var ATTRIBUTE_ID = /#([\w-]+)/y;
187
- var ATTRIBUTE_VALUE = /[^\s"'=<>`{}]+/y;
188
- function sticky(pattern, source, index) {
189
- pattern.lastIndex = index;
190
- return pattern.exec(source);
191
- }
192
- /**
193
- * Reads a brace attribute list starting at `source[start]`, which must be `{`.
194
- * @contract Returns the attributes and the index just past the closing brace.
195
- * @failure Returns null for anything that is not a complete, well-formed list, so the text stays literal.
196
- */
197
- function readLoomaAttributes(source, start = 0) {
198
- if (source[start] !== "{") return null;
199
- const attributes = {};
200
- let index = start + 1;
201
- for (;;) {
202
- while (source[index] === " " || source[index] === " ") index++;
203
- if (source[index] === "}") return {
204
- attributes,
205
- end: index + 1
206
- };
207
- if (index > start + 1 && !/\s/.test(source[index - 1])) return null;
208
- const id = sticky(ATTRIBUTE_ID, source, index);
209
- if (id) {
210
- attributes.id = id[1];
211
- index += id[0].length;
212
- continue;
213
- }
214
- const key = sticky(ATTRIBUTE_KEY, source, index);
215
- if (!key) return null;
216
- index += key[0].length;
217
- if (source[index] !== "=") {
218
- attributes[key[0]] = true;
219
- continue;
220
- }
221
- index++;
222
- const quote = source[index];
223
- if (quote === "\"" || quote === "'") {
224
- const close = source.indexOf(quote, index + 1);
225
- if (close < 0) return null;
226
- attributes[key[0]] = source.slice(index + 1, close);
227
- index = close + 1;
228
- continue;
229
- }
230
- const value = sticky(ATTRIBUTE_VALUE, source, index);
231
- if (!value) return null;
232
- attributes[key[0]] = value[0];
233
- index += value[0].length;
234
- }
235
- }
236
- /**
237
- * Reads a whole string as one brace attribute list.
238
- * @failure Returns null unless the string is exactly one well-formed list.
239
- */
240
- function parseLoomaAttributes(source) {
241
- const read = readLoomaAttributes(source);
242
- return read && read.end === source.length ? read.attributes : null;
243
- }
244
- /**
245
- * Writes attributes as a brace list, `id` first as `#id`, then the rest in order.
246
- * @contract `true` writes a bare key; values are quoted only when they need it. `false`, null and
247
- * undefined values are left out, and an empty set writes an empty string. An ID that `#` cannot
248
- * carry, or any ID when `hashId` is false, is written `id=…`.
249
- */
250
- function formatLoomaAttributes(attributes, { hashId = true } = {}) {
251
- const items = [];
252
- const { id, ...rest } = attributes;
253
- const hashed = hashId && typeof id === "string" && /^[\w-]+$/.test(id);
254
- if (hashed) items.push(`#${id}`);
255
- for (const [key, value] of Object.entries(hashed ? rest : attributes)) if (value === true) items.push(key);
256
- else if (value === false || value === null || value === void 0) continue;
257
- else {
258
- const text = String(value);
259
- items.push(/^[^\s"'=<>`{}]+$/.test(text) ? `${key}=${text}` : text.includes("\"") ? `${key}='${text}'` : `${key}="${text}"`);
260
- }
261
- return items.length ? `{${items.join(" ")}}` : "";
262
- }
263
- /**
264
- * The anchor a heading's text derives: NFC, lowercase, keeping letters, numbers and combining
265
- * marks, with every other run of characters one hyphen.
266
- * @failure Text with no letters or numbers, such as only emoji, derives `heading`.
267
- */
268
- function loomaHeadingSlug(text) {
269
- return text.normalize("NFC").toLowerCase().replace(/[^\p{L}\p{N}\p{M}]+/gu, "-").replace(/^-+|-+$/g, "") || "heading";
270
- }
271
- /**
272
- * Gives every heading of a document its anchor, in document order.
273
- * @contract A custom ID is kept unless an earlier heading already has it. Any other heading takes
274
- * its slug, or the slug's first free suffix (`-1`, `-2`…), never one a later custom ID reserves.
275
- */
276
- function assignLoomaHeadingIds(headings) {
277
- const reserved = new Set(headings.map(({ id }) => id).filter((id) => Boolean(id)));
278
- const used = /* @__PURE__ */ new Set();
279
- return headings.map(({ text, id: custom }) => {
280
- let id = custom || loomaHeadingSlug(text);
281
- if (custom ? used.has(id) : used.has(id) || reserved.has(id)) {
282
- const stem = id;
283
- let suffix = 1;
284
- while (used.has(`${stem}-${suffix}`) || reserved.has(`${stem}-${suffix}`)) suffix++;
285
- id = `${stem}-${suffix}`;
286
- }
287
- used.add(id);
288
- return id;
289
- });
290
- }
291
- /** Reads a fence's info string: ```` ```ts title="a.ts" {1,3-5} :line-numbers ````. */
292
- function parseLoomaFenceInfo(info) {
293
- const source = info.trim();
294
- const language = /^[^\s={:]+(?=[\s{:]|$)/.exec(source)?.[0];
295
- const meta = (language ? source.slice(language.length) : source).trim();
296
- const title = /(?:^|\s)title="([^"]*)"/.exec(meta)?.[1];
297
- const rest = meta.replace(/(?:^|\s)title="[^"]*"/, "");
298
- const lines = /* @__PURE__ */ new Set();
299
- for (const range of /\{([\d\s,-]+)\}/.exec(rest)?.[1].split(",") ?? []) {
300
- const [from = NaN, to = from] = range.split("-").map((part) => Number.parseInt(part, 10));
301
- for (let line = from; line <= to; line++) lines.add(line);
302
- }
303
- const numbers = /:(no-)?line-numbers(?![\w-])/.exec(rest);
304
- return {
305
- ...language ? { language } : {},
306
- meta,
307
- ...title !== void 0 ? { title } : {},
308
- lines: [...lines].sort((a, b) => a - b),
309
- ...numbers ? { lineNumbers: numbers[1] === void 0 } : {}
310
- };
311
- }
312
- var MAPPING_KEY = /^(?:[^\s#:'"\-?,[\]{}&*!|>%@`][^:\n]*?|"[^"\n]*"|'[^'\n]*')[ \t]*:(?:[ \t]|$)/;
313
- var NOTATION = /[ \t]*(?:\/\/|#|--|<!--|\{?\/\*)[ \t]*\[!code highlight(?::(\d+))?\][ \t]*(?:-->|\*\/\}?)?[ \t]*$/;
314
- var ESCAPED_NOTATION = /\[\\(\\*!code highlight)/g;
315
- /**
316
- * Applies code notation: a line ending in a `[!code highlight]` comment is highlighted and loses
317
- * the comment, `[!code highlight:3]` covers three lines, and a marker alone on its line is removed
318
- * with the line and applies to the lines after it. A backslash between `[` and `!` escapes a
319
- * marker, `[\!code highlight]`, which then shows as text with one backslash fewer and highlights
320
- * nothing.
321
- * @contract Returns the code a reader sees and its highlighted line numbers, from 1, ascending.
322
- * ponytail: matched on source lines, so a string that ends in the marker counts too.
323
- */
324
- function applyLoomaCodeNotation(code) {
325
- const kept = [];
326
- const lines = /* @__PURE__ */ new Set();
327
- for (const line of code.split("\n")) {
328
- const match = NOTATION.exec(line);
329
- if (!match) {
330
- kept.push(line);
331
- continue;
332
- }
333
- const rest = line.slice(0, match.index);
334
- const own = rest.trim() !== "";
335
- if (own) kept.push(rest);
336
- for (let count = 0; count < Number(match[1] ?? 1); count++) lines.add(kept.length + (own ? 0 : 1) + count);
337
- }
338
- return {
339
- code: kept.join("\n").replace(ESCAPED_NOTATION, "[$1"),
340
- lines: [...lines].sort((a, b) => a - b)
341
- };
342
- }
343
- /**
344
- * Splits YAML front matter from the start of a document.
345
- * @contract Front matter opens with a `---` line at the very start and closes at a `---` or `...`
346
- * line, and what lies between is a YAML mapping: its first line that is not blank or a comment
347
- * starts with a key, whose value may be a scalar, a list or a mapping. `source` is the block as
348
- * written, delimiters included.
349
- * @failure Returns null when the document has none.
350
- */
351
- function splitLoomaFrontmatter(document) {
352
- const match = /^---[ \t]*\r?\n([\s\S]*?)\r?\n(?:---|\.\.\.)[ \t]*(?:\r?\n|$)/.exec(document.slice(0, 8192));
353
- const first = match?.[1].split(/\r?\n/).find((line) => line.trim() && !line.trimStart().startsWith("#"));
354
- if (!match || !first || !MAPPING_KEY.test(first)) return null;
355
- return {
356
- source: match[0].trimEnd(),
357
- yaml: match[1],
358
- body: document.slice(match[0].length)
359
- };
360
- }
361
- /** The colons a container opens and closes with, given how deeply containers nest inside it. */
362
- function loomaContainerMarker(nestedDepth) {
363
- return ":".repeat(3 + Math.max(0, nestedDepth));
364
- }
365
- /** A backtick fence longer than any run of backticks in the code, and at least three long. */
366
- function loomaCodeFence(code) {
367
- const longest = Math.max(0, ...(code.match(/`+/g) ?? []).map((run) => run.length));
368
- return "`".repeat(Math.max(3, longest + 1));
369
- }
370
- //#endregion
371
131
  //#region src/editor/extensions/callout.ts
372
132
  /**
373
133
  * A durable block container for informational editor content, saved as Looma's Callout.
@@ -735,6 +495,24 @@ var LoomaTabs = Node$1.create({
735
495
  parseHTML: (element) => element.getAttribute("group") ?? "",
736
496
  renderHTML: (attributes) => attributes.group ? { group: attributes.group } : {}
737
497
  },
498
+ defaultValue: {
499
+ default: null,
500
+ parseHTML: (element) => element.getAttribute("data-default-value"),
501
+ renderHTML: (attributes) => attributes.defaultValue === null ? {} : {
502
+ "data-default-value": attributes.defaultValue,
503
+ value: attributes.defaultValue
504
+ }
505
+ },
506
+ queryString: {
507
+ default: null,
508
+ parseHTML: (element) => element.getAttribute("data-query-string"),
509
+ renderHTML: (attributes) => attributes.queryString === null ? {} : { "data-query-string": attributes.queryString }
510
+ },
511
+ lazy: {
512
+ default: null,
513
+ parseHTML: (element) => element.hasAttribute("data-lazy") ? element.getAttribute("data-lazy") !== "false" : null,
514
+ renderHTML: (attributes) => attributes.lazy === null ? {} : { "data-lazy": String(attributes.lazy) }
515
+ },
738
516
  codeGroup: {
739
517
  default: false,
740
518
  parseHTML: (element) => element.classList.contains("code-group"),
@@ -773,11 +551,18 @@ var LoomaTab = Node$1.create({
773
551
  defining: true,
774
552
  isolating: true,
775
553
  addAttributes() {
776
- return { label: {
777
- default: "",
778
- parseHTML: (element) => element.getAttribute("aria-label") ?? "",
779
- renderHTML: (attributes) => ({ "aria-label": attributes.label })
780
- } };
554
+ return {
555
+ value: {
556
+ default: null,
557
+ parseHTML: (element) => element.getAttribute("data-value"),
558
+ renderHTML: (attributes) => attributes.value === null ? {} : { "data-value": attributes.value }
559
+ },
560
+ label: {
561
+ default: "",
562
+ parseHTML: (element) => element.getAttribute("aria-label") ?? "",
563
+ renderHTML: (attributes) => ({ "aria-label": attributes.label })
564
+ }
565
+ };
781
566
  },
782
567
  parseHTML() {
783
568
  return [{ tag: "ui-tabs > section" }];
@@ -975,14 +760,18 @@ async function navigateTableOfContents(editor, id) {
975
760
  target.tabIndex = -1;
976
761
  target.focus({ preventScroll: true });
977
762
  }
763
+ function importAuthoredNavigation(element, schema) {
764
+ return DOMParser.fromSchema(schema).parse(element).content;
765
+ }
978
766
  /**
979
- * Automatic whole-document navigation with durable configuration only.
767
+ * Automatic navigation, or an explicitly authored navigation body imported from a document.
980
768
  * @ownership The schema owns configuration/anchors; node views derive entries.
981
769
  * @lifecycle Headless views subscribe to transactions and unsubscribe on destroy.
982
770
  */
983
771
  var LoomaTableOfContents = Node$1.create({
984
772
  name: "loomaTableOfContents",
985
773
  group: "block",
774
+ content: "block*",
986
775
  atom: true,
987
776
  selectable: true,
988
777
  isolating: true,
@@ -1002,8 +791,25 @@ var LoomaTableOfContents = Node$1.create({
1002
791
  }
1003
792
  };
1004
793
  },
1005
- parseHTML: () => [{ tag: "nav[data-looma-toc]" }],
794
+ parseHTML: () => [{
795
+ tag: "nav[data-looma-toc]",
796
+ getContent: (element, schema) => {
797
+ if (!(element instanceof HTMLElement) || !element.hasAttribute("data-authored-body")) return Fragment.empty;
798
+ return importAuthoredNavigation(element, schema);
799
+ }
800
+ }],
1006
801
  renderHTML({ node }) {
802
+ if (node.childCount) return [
803
+ "nav",
804
+ {
805
+ "data-looma-toc": "",
806
+ "data-authored-body": "",
807
+ "data-format": node.attrs.format,
808
+ "data-depth": node.attrs.depth,
809
+ "aria-label": "Table of contents"
810
+ },
811
+ 0
812
+ ];
1007
813
  return contentsHtml(this.editor?.view?.state.doc ?? node, normalizeTableOfContentsConfig(node.attrs));
1008
814
  },
1009
815
  addCommands() {
@@ -1019,8 +825,15 @@ var LoomaTableOfContents = Node$1.create({
1019
825
  dom.contentEditable = "false";
1020
826
  let current = node;
1021
827
  const render = () => {
1022
- const { dom: content } = DOMSerializer.renderSpec(document, contentsHtml(editor.state.doc, normalizeTableOfContentsConfig(current.attrs)));
1023
- dom.replaceChildren(content);
828
+ if (current.childCount) {
829
+ const nav = document.createElement("nav");
830
+ nav.setAttribute("aria-label", "Table of contents");
831
+ nav.append(DOMSerializer.fromSchema(editor.schema).serializeFragment(current.content));
832
+ dom.replaceChildren(nav);
833
+ } else {
834
+ const { dom: content } = DOMSerializer.renderSpec(document, contentsHtml(editor.state.doc, normalizeTableOfContentsConfig(current.attrs)));
835
+ dom.replaceChildren(content);
836
+ }
1024
837
  };
1025
838
  dom.addEventListener("click", (event) => {
1026
839
  const anchor = event.target.closest("a");
@@ -10176,6 +9989,22 @@ function loomaMarkdownFromDoc(doc) {
10176
9989
  const custom = customHeadingIds(doc);
10177
9990
  let heading = 0;
10178
9991
  const nodes = {
9992
+ loomaTableOfContents: (state, node, parent, index) => {
9993
+ if (node.childCount) {
9994
+ state.write(html(node));
9995
+ state.closeBlock(node);
9996
+ } else NODES.loomaTableOfContents(state, node, parent, index);
9997
+ },
9998
+ loomaTabs: (state, node, parent, index) => {
9999
+ if ([
10000
+ node.attrs.defaultValue,
10001
+ node.attrs.queryString,
10002
+ node.attrs.lazy
10003
+ ].some((value) => value !== null && value !== void 0) || Array.from({ length: node.childCount }, (_, index) => node.child(index)).some((tab) => tab.attrs.value !== null && tab.attrs.value !== void 0)) {
10004
+ state.write(html(node));
10005
+ state.closeBlock(node);
10006
+ } else NODES.loomaTabs(state, node, parent, index);
10007
+ },
10179
10008
  heading: (state, node) => {
10180
10009
  state.write(`${"#".repeat(node.attrs.level)} `);
10181
10010
  state.renderInline(node, false);
@@ -11955,4 +11784,4 @@ function createLoomaSlashCommandExtension(options = {}) {
11955
11784
  });
11956
11785
  }
11957
11786
  //#endregion
11958
- export { LoomaActiveBlockPluginKey as $, filterLoomaMentionItems as A, LoomaFigcaption as B, loomaMarkdownToHtml as C, createLoomaMentionExtension as D, LoomaMentionSuggestionPluginKey as E, LoomaTableOfContents as F, LoomaChip as G, LoomaExpand as H, getTableOfContentsEntries as I, LOOMA_CALLOUT_TONES as J, normalizeLoomaChipColor as K, normalizeTableOfContentsConfig as L, LoomaImage as M, normalizeImageDimension as N, DEFAULT_MENTION_RESULT_LIMIT as O, normalizeImagePlacement as P, LoomaActiveBlock as Q, LoomaTab as R, loomaMarkdownFromDoc as S, siteRelativeHref as T, revealLoomaExpandAt as U, LoomaFigure as V, LOOMA_CHIP_COLORS as W, LOOMA_ACTIVE_BLOCK_CLASS as X, LOOMA_ACTIVE_BLOCK_BLUR_GRACE_MS as Y, LOOMA_ACTIVE_BLOCK_TYPING_IDLE_MS as Z, getActiveTableCellAlignment as _, getActiveTableUiState as a, setActiveTableCellBackground as b, insertTableAtRange as c, LoomaTableKit as d, getDefaultEditorExtensions as f, LoomaTableHeader as g, LoomaTableCell as h, getDefaultSlashCommands as i, normalizeMentionResultLimit as j, MAX_MENTION_RESULT_LIMIT as k, normalizeActiveTableColumnWidths as l, LoomaTable as m, createLoomaSlashCommandExtension as n, handleTableAction as o, getLoomaTableExtensions as p, LoomaCallout as q, filterLoomaSlashCommands as r, handleTableOverlayAction as s, LoomaSlashCommand as t, shouldShowTextFormattingToolbar as u, getActiveTableCellBackground as v, createSiteRelativeLinks as w, LoomaSmartPaste as x, setActiveTableCellAlignment as y, LoomaTabs as z };
11787
+ export { filterLoomaMentionItems as A, LoomaFigcaption as B, loomaMarkdownToHtml as C, createLoomaMentionExtension as D, LoomaMentionSuggestionPluginKey as E, LoomaTableOfContents as F, LoomaChip as G, LoomaExpand as H, getTableOfContentsEntries as I, LOOMA_ACTIVE_BLOCK_BLUR_GRACE_MS as J, normalizeLoomaChipColor as K, normalizeTableOfContentsConfig as L, LoomaImage as M, normalizeImageDimension as N, DEFAULT_MENTION_RESULT_LIMIT as O, normalizeImagePlacement as P, LoomaActiveBlockPluginKey as Q, LoomaTab as R, loomaMarkdownFromDoc as S, siteRelativeHref as T, revealLoomaExpandAt as U, LoomaFigure as V, LOOMA_CHIP_COLORS as W, LOOMA_ACTIVE_BLOCK_TYPING_IDLE_MS as X, LOOMA_ACTIVE_BLOCK_CLASS as Y, LoomaActiveBlock as Z, getActiveTableCellAlignment as _, getActiveTableUiState as a, setActiveTableCellBackground as b, insertTableAtRange as c, LoomaTableKit as d, getDefaultEditorExtensions as f, LoomaTableHeader as g, LoomaTableCell as h, getDefaultSlashCommands as i, normalizeMentionResultLimit as j, MAX_MENTION_RESULT_LIMIT as k, normalizeActiveTableColumnWidths as l, LoomaTable as m, createLoomaSlashCommandExtension as n, handleTableAction as o, getLoomaTableExtensions as p, LoomaCallout as q, filterLoomaSlashCommands as r, handleTableOverlayAction as s, LoomaSlashCommand as t, shouldShowTextFormattingToolbar as u, getActiveTableCellBackground as v, createSiteRelativeLinks as w, LoomaSmartPaste as x, setActiveTableCellAlignment as y, LoomaTabs as z };
@@ -0,0 +1,242 @@
1
+ //#region src/markdown/index.ts
2
+ /**
3
+ * Callout tones a document stores, in the order the editor offers them. The Callout component also
4
+ * takes `neutral` and `success`, which Markdown and the editor store as `note`.
5
+ */
6
+ var LOOMA_CALLOUT_TONES = [
7
+ "note",
8
+ "tip",
9
+ "info",
10
+ "important",
11
+ "warning",
12
+ "danger"
13
+ ];
14
+ /** Names that read as a stored tone: `caution` is `danger`, and `neutral` is the same muted aside as `note`. */
15
+ var LOOMA_CALLOUT_TONE_ALIASES = {
16
+ caution: "danger",
17
+ neutral: "note"
18
+ };
19
+ /**
20
+ * Maps a tone name or alias, in any case, onto a stored tone.
21
+ * @failure Unknown and non-string values become `note`.
22
+ */
23
+ function normalizeLoomaCalloutTone(value) {
24
+ if (typeof value !== "string") return "note";
25
+ const tone = value.toLowerCase();
26
+ return LOOMA_CALLOUT_TONES.includes(tone) ? tone : LOOMA_CALLOUT_TONE_ALIASES[tone] ?? "note";
27
+ }
28
+ /** The hues chips and swatches take as their tone, written `:chip[Label]{tone=green}`. */
29
+ var LOOMA_CHIP_TONES = [
30
+ "neutral",
31
+ "blue",
32
+ "green",
33
+ "yellow",
34
+ "red",
35
+ "purple"
36
+ ];
37
+ /**
38
+ * Keeps a chip tone within the palette.
39
+ * @failure Unknown values become `neutral`.
40
+ */
41
+ function normalizeLoomaChipTone(value) {
42
+ return LOOMA_CHIP_TONES.includes(value) ? value : "neutral";
43
+ }
44
+ /**
45
+ * Names that open a `:::` container, matched without regard to case. A container with any other
46
+ * name is ordinary text.
47
+ */
48
+ var LOOMA_CONTAINER_NAMES = /* @__PURE__ */ new Set([
49
+ ...LOOMA_CALLOUT_TONES,
50
+ "caution",
51
+ "details",
52
+ "figure",
53
+ "tabs",
54
+ "code-group"
55
+ ]);
56
+ var ATTRIBUTE_KEY = /[A-Za-z][\w-]*/y;
57
+ var ATTRIBUTE_ID = /#([\w-]+)/y;
58
+ var ATTRIBUTE_VALUE = /[^\s"'=<>`{}]+/y;
59
+ function sticky(pattern, source, index) {
60
+ pattern.lastIndex = index;
61
+ return pattern.exec(source);
62
+ }
63
+ /**
64
+ * Reads a brace attribute list starting at `source[start]`, which must be `{`.
65
+ * @contract Returns the attributes and the index just past the closing brace.
66
+ * @failure Returns null for anything that is not a complete, well-formed list, so the text stays literal.
67
+ */
68
+ function readLoomaAttributes(source, start = 0) {
69
+ if (source[start] !== "{") return null;
70
+ const attributes = {};
71
+ let index = start + 1;
72
+ for (;;) {
73
+ while (source[index] === " " || source[index] === " ") index++;
74
+ if (source[index] === "}") return {
75
+ attributes,
76
+ end: index + 1
77
+ };
78
+ if (index > start + 1 && !/\s/.test(source[index - 1])) return null;
79
+ const id = sticky(ATTRIBUTE_ID, source, index);
80
+ if (id) {
81
+ attributes.id = id[1];
82
+ index += id[0].length;
83
+ continue;
84
+ }
85
+ const key = sticky(ATTRIBUTE_KEY, source, index);
86
+ if (!key) return null;
87
+ index += key[0].length;
88
+ if (source[index] !== "=") {
89
+ attributes[key[0]] = true;
90
+ continue;
91
+ }
92
+ index++;
93
+ const quote = source[index];
94
+ if (quote === "\"" || quote === "'") {
95
+ const close = source.indexOf(quote, index + 1);
96
+ if (close < 0) return null;
97
+ attributes[key[0]] = source.slice(index + 1, close);
98
+ index = close + 1;
99
+ continue;
100
+ }
101
+ const value = sticky(ATTRIBUTE_VALUE, source, index);
102
+ if (!value) return null;
103
+ attributes[key[0]] = value[0];
104
+ index += value[0].length;
105
+ }
106
+ }
107
+ /**
108
+ * Reads a whole string as one brace attribute list.
109
+ * @failure Returns null unless the string is exactly one well-formed list.
110
+ */
111
+ function parseLoomaAttributes(source) {
112
+ const read = readLoomaAttributes(source);
113
+ return read && read.end === source.length ? read.attributes : null;
114
+ }
115
+ /**
116
+ * Writes attributes as a brace list, `id` first as `#id`, then the rest in order.
117
+ * @contract `true` writes a bare key; values are quoted only when they need it. `false`, null and
118
+ * undefined values are left out, and an empty set writes an empty string. An ID that `#` cannot
119
+ * carry, or any ID when `hashId` is false, is written `id=…`.
120
+ */
121
+ function formatLoomaAttributes(attributes, { hashId = true } = {}) {
122
+ const items = [];
123
+ const { id, ...rest } = attributes;
124
+ const hashed = hashId && typeof id === "string" && /^[\w-]+$/.test(id);
125
+ if (hashed) items.push(`#${id}`);
126
+ for (const [key, value] of Object.entries(hashed ? rest : attributes)) if (value === true) items.push(key);
127
+ else if (value === false || value === null || value === void 0) continue;
128
+ else {
129
+ const text = String(value);
130
+ items.push(/^[^\s"'=<>`{}]+$/.test(text) ? `${key}=${text}` : text.includes("\"") ? `${key}='${text}'` : `${key}="${text}"`);
131
+ }
132
+ return items.length ? `{${items.join(" ")}}` : "";
133
+ }
134
+ /**
135
+ * The anchor a heading's text derives: NFC, lowercase, keeping letters, numbers and combining
136
+ * marks, with every other run of characters one hyphen.
137
+ * @failure Text with no letters or numbers, such as only emoji, derives `heading`.
138
+ */
139
+ function loomaHeadingSlug(text) {
140
+ return text.normalize("NFC").toLowerCase().replace(/[^\p{L}\p{N}\p{M}]+/gu, "-").replace(/^-+|-+$/g, "") || "heading";
141
+ }
142
+ /**
143
+ * Gives every heading of a document its anchor, in document order.
144
+ * @contract A custom ID is kept unless an earlier heading already has it. Any other heading takes
145
+ * its slug, or the slug's first free suffix (`-1`, `-2`…), never one a later custom ID reserves.
146
+ */
147
+ function assignLoomaHeadingIds(headings) {
148
+ const reserved = new Set(headings.map(({ id }) => id).filter((id) => Boolean(id)));
149
+ const used = /* @__PURE__ */ new Set();
150
+ return headings.map(({ text, id: custom }) => {
151
+ let id = custom || loomaHeadingSlug(text);
152
+ if (custom ? used.has(id) : used.has(id) || reserved.has(id)) {
153
+ const stem = id;
154
+ let suffix = 1;
155
+ while (used.has(`${stem}-${suffix}`) || reserved.has(`${stem}-${suffix}`)) suffix++;
156
+ id = `${stem}-${suffix}`;
157
+ }
158
+ used.add(id);
159
+ return id;
160
+ });
161
+ }
162
+ /** Reads a fence's info string: ```` ```ts title="a.ts" {1,3-5} :line-numbers ````. */
163
+ function parseLoomaFenceInfo(info) {
164
+ const source = info.trim();
165
+ const language = /^[^\s={:]+(?=[\s{:]|$)/.exec(source)?.[0];
166
+ const meta = (language ? source.slice(language.length) : source).trim();
167
+ const title = /(?:^|\s)title="([^"]*)"/.exec(meta)?.[1];
168
+ const rest = meta.replace(/(?:^|\s)title="[^"]*"/, "");
169
+ const lines = /* @__PURE__ */ new Set();
170
+ for (const range of /\{([\d\s,-]+)\}/.exec(rest)?.[1].split(",") ?? []) {
171
+ const [from = NaN, to = from] = range.split("-").map((part) => Number.parseInt(part, 10));
172
+ for (let line = from; line <= to; line++) lines.add(line);
173
+ }
174
+ const numbers = /:(no-)?line-numbers(?![\w-])/.exec(rest);
175
+ return {
176
+ ...language ? { language } : {},
177
+ meta,
178
+ ...title !== void 0 ? { title } : {},
179
+ lines: [...lines].sort((a, b) => a - b),
180
+ ...numbers ? { lineNumbers: numbers[1] === void 0 } : {}
181
+ };
182
+ }
183
+ var MAPPING_KEY = /^(?:[^\s#:'"\-?,[\]{}&*!|>%@`][^:\n]*?|"[^"\n]*"|'[^'\n]*')[ \t]*:(?:[ \t]|$)/;
184
+ var NOTATION = /[ \t]*(?:\/\/|#|--|<!--|\{?\/\*)[ \t]*\[!code highlight(?::(\d+))?\][ \t]*(?:-->|\*\/\}?)?[ \t]*$/;
185
+ var ESCAPED_NOTATION = /\[\\(\\*!code highlight)/g;
186
+ /**
187
+ * Applies code notation: a line ending in a `[!code highlight]` comment is highlighted and loses
188
+ * the comment, `[!code highlight:3]` covers three lines, and a marker alone on its line is removed
189
+ * with the line and applies to the lines after it. A backslash between `[` and `!` escapes a
190
+ * marker, `[\!code highlight]`, which then shows as text with one backslash fewer and highlights
191
+ * nothing.
192
+ * @contract Returns the code a reader sees and its highlighted line numbers, from 1, ascending.
193
+ * ponytail: matched on source lines, so a string that ends in the marker counts too.
194
+ */
195
+ function applyLoomaCodeNotation(code) {
196
+ const kept = [];
197
+ const lines = /* @__PURE__ */ new Set();
198
+ for (const line of code.split("\n")) {
199
+ const match = NOTATION.exec(line);
200
+ if (!match) {
201
+ kept.push(line);
202
+ continue;
203
+ }
204
+ const rest = line.slice(0, match.index);
205
+ const own = rest.trim() !== "";
206
+ if (own) kept.push(rest);
207
+ for (let count = 0; count < Number(match[1] ?? 1); count++) lines.add(kept.length + (own ? 0 : 1) + count);
208
+ }
209
+ return {
210
+ code: kept.join("\n").replace(ESCAPED_NOTATION, "[$1"),
211
+ lines: [...lines].sort((a, b) => a - b)
212
+ };
213
+ }
214
+ /**
215
+ * Splits YAML front matter from the start of a document.
216
+ * @contract Front matter opens with a `---` line at the very start and closes at a `---` or `...`
217
+ * line, and what lies between is a YAML mapping: its first line that is not blank or a comment
218
+ * starts with a key, whose value may be a scalar, a list or a mapping. `source` is the block as
219
+ * written, delimiters included.
220
+ * @failure Returns null when the document has none.
221
+ */
222
+ function splitLoomaFrontmatter(document) {
223
+ const match = /^---[ \t]*\r?\n([\s\S]*?)\r?\n(?:---|\.\.\.)[ \t]*(?:\r?\n|$)/.exec(document.slice(0, 8192));
224
+ const first = match?.[1].split(/\r?\n/).find((line) => line.trim() && !line.trimStart().startsWith("#"));
225
+ if (!match || !first || !MAPPING_KEY.test(first)) return null;
226
+ return {
227
+ source: match[0].trimEnd(),
228
+ yaml: match[1],
229
+ body: document.slice(match[0].length)
230
+ };
231
+ }
232
+ /** The colons a container opens and closes with, given how deeply containers nest inside it. */
233
+ function loomaContainerMarker(nestedDepth) {
234
+ return ":".repeat(3 + Math.max(0, nestedDepth));
235
+ }
236
+ /** A backtick fence longer than any run of backticks in the code, and at least three long. */
237
+ function loomaCodeFence(code) {
238
+ const longest = Math.max(0, ...(code.match(/`+/g) ?? []).map((run) => run.length));
239
+ return "`".repeat(Math.max(3, longest + 1));
240
+ }
241
+ //#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 };
@@ -0,0 +1,31 @@
1
+ import type { JSONContent } from "@tiptap/core";
2
+ import { type ConversionOptions, type Document } from "expressmark";
3
+ /**
4
+ * A conversion cannot faithfully represent the named input at this document location.
5
+ * @failure Callers retain the original document and can report the unsupported feature and path.
6
+ */
7
+ export declare class EditorMarkdownConversionError extends Error {
8
+ readonly path: string;
9
+ readonly feature: string;
10
+ constructor(path: string, feature: string);
11
+ }
12
+ /**
13
+ * Maps an editor's saved JSON to the independent Expressive Markdown AST without DOM access.
14
+ * @contract Unsupported nodes, marks and durable attributes throw with their location; input is never mutated.
15
+ */
16
+ export declare function editorDocumentToExpressmark(document: JSONContent, options?: ConversionOptions): Document;
17
+ /**
18
+ * Maps supported Expressive Markdown content into the Looma editor's saved JSON shape.
19
+ * @contract The editor schema cannot represent arbitrary format extensions; unsupported meanings fail explicitly.
20
+ */
21
+ export declare function expressmarkToEditorDocument(document: Document): JSONContent;
22
+ /**
23
+ * Writes an editor document using Expressmark's shared parser/writer and safe HTML policy.
24
+ * @failure Unsupported editor data raises EditorMarkdownConversionError before any input is changed.
25
+ */
26
+ export declare function editorDocumentToMarkdown(document: JSONContent, options?: ConversionOptions): string;
27
+ /**
28
+ * Parses Expressive Markdown into editable JSON for the supported editor schema.
29
+ * @failure Unsupported content or root metadata stops conversion instead of being silently flattened.
30
+ */
31
+ export declare function markdownToEditorDocument(markdown: string, options?: ConversionOptions): JSONContent;