@threadlabs/looma 0.28.14 → 0.28.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.
@@ -8,8 +8,8 @@ import { dropCursor } from "@tiptap/pm/dropcursor";
8
8
  import { gapCursor } from "@tiptap/pm/gapcursor";
9
9
  import { createLowlight } from "lowlight";
10
10
  import { Mapping } from "@tiptap/pm/transform";
11
- import MarkdownIt from "markdown-it";
12
11
  import { MarkdownSerializer, defaultMarkdownSerializer } from "@tiptap/pm/markdown";
12
+ import MarkdownIt from "markdown-it";
13
13
  import { CellSelection, TableMap, addColumn, addColumnAfter, addColumnBefore, addRow, addRowAfter, addRowBefore, cellAround, columnResizing, deleteCellSelection, deleteColumn, deleteRow, deleteTable, fixTables, goToNextCell, mergeCells, moveTableColumn, moveTableRow, setCellAttr, splitCell, tableEditing, toggleHeader, toggleHeaderCell } from "@tiptap/pm/tables";
14
14
  //#region src/editor/extensions/active-block.ts
15
15
  /**
@@ -127,10 +127,10 @@ var LoomaActiveBlock = Extension.create({
127
127
  }
128
128
  });
129
129
  //#endregion
130
- //#region src/editor/extensions/callout.ts
130
+ //#region src/markdown/index.ts
131
131
  /**
132
- * Canonical tone values that may be persisted in a Looma callout node: the shared block
133
- * vocabulary of Callout, Markdown `:::` containers and GitHub alerts.
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
134
  */
135
135
  var LOOMA_CALLOUT_TONES = [
136
136
  "note",
@@ -140,16 +140,182 @@ var LOOMA_CALLOUT_TONES = [
140
140
  "warning",
141
141
  "danger"
142
142
  ];
143
- var TONE_ALIASES = {
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 = {
144
145
  caution: "danger",
145
146
  neutral: "note"
146
147
  };
147
- /** Maps a tone name, alias, or unknown value onto a persisted tone. */
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
+ */
148
152
  function normalizeLoomaCalloutTone(value) {
149
153
  if (typeof value !== "string") return "note";
150
154
  const tone = value.toLowerCase();
151
- return LOOMA_CALLOUT_TONES.includes(tone) ? tone : TONE_ALIASES[tone] ?? "note";
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
+ * The anchor a heading's text derives: NFC, lowercase, keeping letters, numbers and combining
246
+ * marks, with every other run of characters one hyphen.
247
+ * @failure Text with no letters or numbers, such as only emoji, derives `heading`.
248
+ */
249
+ function loomaHeadingSlug(text) {
250
+ return text.normalize("NFC").toLowerCase().replace(/[^\p{L}\p{N}\p{M}]+/gu, "-").replace(/^-+|-+$/g, "") || "heading";
251
+ }
252
+ /**
253
+ * Gives every heading of a document its anchor, in document order.
254
+ * @contract A custom ID is kept unless an earlier heading already has it. Any other heading takes
255
+ * its slug, or the slug's first free suffix (`-1`, `-2`…), never one a later custom ID reserves.
256
+ */
257
+ function assignLoomaHeadingIds(headings) {
258
+ const reserved = new Set(headings.map(({ id }) => id).filter((id) => Boolean(id)));
259
+ const used = /* @__PURE__ */ new Set();
260
+ return headings.map(({ text, id: custom }) => {
261
+ let id = custom || loomaHeadingSlug(text);
262
+ if (custom ? used.has(id) : used.has(id) || reserved.has(id)) {
263
+ const stem = id;
264
+ let suffix = 1;
265
+ while (used.has(`${stem}-${suffix}`) || reserved.has(`${stem}-${suffix}`)) suffix++;
266
+ id = `${stem}-${suffix}`;
267
+ }
268
+ used.add(id);
269
+ return id;
270
+ });
271
+ }
272
+ /** Reads a fence's info string: ```` ```ts title="a.ts" {1,3-5} :line-numbers ````. */
273
+ function parseLoomaFenceInfo(info) {
274
+ const source = info.trim();
275
+ const language = /^[^\s={:]+(?=[\s{:]|$)/.exec(source)?.[0];
276
+ const meta = (language ? source.slice(language.length) : source).trim();
277
+ const title = /(?:^|\s)title="([^"]*)"/.exec(meta)?.[1];
278
+ const rest = meta.replace(/(?:^|\s)title="[^"]*"/, "");
279
+ const lines = /* @__PURE__ */ new Set();
280
+ for (const range of /\{([\d\s,-]+)\}/.exec(rest)?.[1].split(",") ?? []) {
281
+ const [from = NaN, to = from] = range.split("-").map((part) => Number.parseInt(part, 10));
282
+ for (let line = from; line <= to; line++) lines.add(line);
283
+ }
284
+ const numbers = /:(no-)?line-numbers(?![\w-])/.exec(rest);
285
+ return {
286
+ ...language ? { language } : {},
287
+ meta,
288
+ ...title !== void 0 ? { title } : {},
289
+ lines: [...lines].sort((a, b) => a - b),
290
+ ...numbers ? { lineNumbers: numbers[1] === void 0 } : {}
291
+ };
292
+ }
293
+ /**
294
+ * Splits YAML front matter from the start of a document.
295
+ * @contract Front matter opens with a `---` line at the very start, holds at least one `key: value`
296
+ * line, and closes at a `---` or `...` line. `source` is the block as written, delimiters included.
297
+ * @failure Returns null when the document has none.
298
+ */
299
+ function splitLoomaFrontmatter(document) {
300
+ const match = /^---[ \t]*\r?\n([\s\S]*?)\r?\n(?:---|\.\.\.)[ \t]*(?:\r?\n|$)/.exec(document.slice(0, 8192));
301
+ if (!match || !/^[A-Za-z][\w-]*:[ \t]*\S/m.test(match[1])) return null;
302
+ return {
303
+ source: match[0].trimEnd(),
304
+ yaml: match[1],
305
+ body: document.slice(match[0].length)
306
+ };
307
+ }
308
+ /** The colons a container opens and closes with, given how deeply containers nest inside it. */
309
+ function loomaContainerMarker(nestedDepth) {
310
+ return ":".repeat(3 + Math.max(0, nestedDepth));
311
+ }
312
+ /** A backtick fence longer than any run of backticks in the code, and at least three long. */
313
+ function loomaCodeFence(code) {
314
+ const longest = Math.max(0, ...(code.match(/`+/g) ?? []).map((run) => run.length));
315
+ return "`".repeat(Math.max(3, longest + 1));
152
316
  }
317
+ //#endregion
318
+ //#region src/editor/extensions/callout.ts
153
319
  /**
154
320
  * A durable block container for informational editor content, saved as Looma's Callout.
155
321
  *
@@ -196,19 +362,12 @@ var LoomaCallout = Node$1.create({
196
362
  });
197
363
  //#endregion
198
364
  //#region src/editor/extensions/chip.ts
199
- /** The six palette values a chip may persist; other values fall back to neutral. */
200
- var LOOMA_CHIP_COLORS = [
201
- "neutral",
202
- "blue",
203
- "green",
204
- "yellow",
205
- "red",
206
- "purple"
207
- ];
365
+ /** The six palette values a chip may persist, Looma Markdown's chip tones; other values fall back to neutral. */
366
+ var LOOMA_CHIP_COLORS = LOOMA_CHIP_TONES;
208
367
  var CHIP_PLACEHOLDER = "Set a label";
209
368
  /** Keep imported or programmatic colors within the supported palette. */
210
369
  function normalizeLoomaChipColor(value) {
211
- return LOOMA_CHIP_COLORS.includes(value) ? value : "neutral";
370
+ return normalizeLoomaChipTone(value);
212
371
  }
213
372
  /**
214
373
  * An inline, atomic label. The editor's popover edits its attributes; its text
@@ -9165,47 +9324,96 @@ function createDOMPurify() {
9165
9324
  }
9166
9325
  var purify = createDOMPurify();
9167
9326
  //#endregion
9168
- //#region src/editor/extensions/markdown.ts
9169
- var CONTAINER_NAMES = /* @__PURE__ */ new Set([
9170
- "note",
9171
- "tip",
9172
- "info",
9173
- "important",
9174
- "warning",
9175
- "danger",
9176
- "caution",
9177
- "details"
9178
- ]);
9179
- var CONTAINER_OPEN = /^(:{3,})[ \t]*([A-Za-z]+)(?:[ \t]+(.*?))?[ \t]*$/;
9327
+ //#region src/markdown/markdown-it.ts
9328
+ var CONTAINER_OPEN = /^(:{3,})[ \t]*([A-Za-z][\w-]*)(?:[ \t]+(.*?))?[ \t]*$/;
9180
9329
  var CONTAINER_CLOSE = /^:{3,}[ \t]*$/;
9330
+ var FENCE_OPEN = /^(`{3,}|~{3,})/;
9331
+ var TAB_LABEL = /^==[ \t]+(\S.*?)[ \t]*$/;
9181
9332
  var OPEN_FLAG = /[ \t]*\{open\}$/;
9182
9333
  var ALERT = /^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\][ \t]*(?:\n|$)/i;
9334
+ var LEAF = /^::([A-Za-z][\w-]*)(\{.*\})?[ \t]*$/;
9335
+ var TASK = /^\[([ xX])\][ \t]+/;
9336
+ var TOC_FORMATS = /* @__PURE__ */ new Set([
9337
+ "plain",
9338
+ "bulleted",
9339
+ "numbered"
9340
+ ]);
9341
+ var PLACEMENTS = /* @__PURE__ */ new Set([
9342
+ "block",
9343
+ "center",
9344
+ "wrap-left",
9345
+ "wrap-right"
9346
+ ]);
9347
+ var opensContainer = (text) => {
9348
+ const match = CONTAINER_OPEN.exec(text);
9349
+ return match && LOOMA_CONTAINER_NAMES.has(match[2].toLowerCase()) ? match : null;
9350
+ };
9183
9351
  function lineText(state, line) {
9184
9352
  return state.src.slice(state.bMarks[line] + state.tShift[line], state.eMarks[line]);
9185
9353
  }
9354
+ function frontmatter(state, startLine, _endLine, silent) {
9355
+ if (startLine !== 0 || state.blkIndent !== 0) return false;
9356
+ const split = splitLoomaFrontmatter(state.src);
9357
+ if (!split) return false;
9358
+ if (silent) return true;
9359
+ const lines = split.source.split("\n").length;
9360
+ const token = state.push("looma_frontmatter", "", 0);
9361
+ Object.assign(token, {
9362
+ content: split.yaml,
9363
+ block: true,
9364
+ hidden: true,
9365
+ map: [0, lines]
9366
+ });
9367
+ state.env.frontmatter = split.yaml;
9368
+ state.line = lines;
9369
+ return true;
9370
+ }
9186
9371
  function container(state, startLine, endLine, silent) {
9187
9372
  if (state.sCount[startLine] - state.blkIndent >= 4) return false;
9188
- const match = CONTAINER_OPEN.exec(lineText(state, startLine));
9189
- if (!match || !CONTAINER_NAMES.has(match[2].toLowerCase())) return false;
9373
+ const match = opensContainer(lineText(state, startLine));
9374
+ if (!match) return false;
9190
9375
  if (silent) return true;
9376
+ const name = match[2].toLowerCase();
9377
+ const grouped = name === "tabs" || name === "code-group";
9378
+ const labels = [];
9191
9379
  let depth = 1;
9380
+ let fence = null;
9192
9381
  let next = startLine + 1;
9193
9382
  for (; next < endLine; next++) {
9194
- if (state.sCount[next] < state.blkIndent) break;
9195
9383
  const text = lineText(state, next);
9384
+ if (text && state.sCount[next] < state.blkIndent) break;
9385
+ if (fence) {
9386
+ const close = FENCE_OPEN.exec(text);
9387
+ if (close && close[1][0] === fence.char && close[1].length >= fence.length && !text.slice(close[1].length).trim()) fence = null;
9388
+ continue;
9389
+ }
9390
+ const opener = FENCE_OPEN.exec(text);
9391
+ if (opener) {
9392
+ fence = {
9393
+ char: opener[1][0],
9394
+ length: opener[1].length
9395
+ };
9396
+ continue;
9397
+ }
9196
9398
  if (CONTAINER_CLOSE.test(text)) {
9197
9399
  if (--depth === 0) break;
9198
- } else if (CONTAINER_OPEN.test(text) && CONTAINER_NAMES.has(CONTAINER_OPEN.exec(text)[2].toLowerCase())) depth++;
9400
+ } else if (opensContainer(text)) depth++;
9401
+ else if (grouped && depth === 1) {
9402
+ const label = TAB_LABEL.exec(text);
9403
+ if (label) labels.push({
9404
+ line: next,
9405
+ label: label[1]
9406
+ });
9407
+ }
9199
9408
  }
9200
- const name = match[2].toLowerCase();
9201
9409
  let title = (match[3] ?? "").trim();
9202
- const open = OPEN_FLAG.test(title);
9203
- title = title.replace(OPEN_FLAG, "");
9410
+ const open = name === "details" && OPEN_FLAG.test(title);
9411
+ if (name === "details") title = title.replace(OPEN_FLAG, "");
9204
9412
  const parentType = state.parentType;
9205
9413
  const lineMax = state.lineMax;
9206
9414
  state.parentType = "container";
9207
9415
  state.lineMax = next;
9208
- const opening = state.push("looma_container_open", "", 1);
9416
+ const opening = state.push("looma_container_open", "div", 1);
9209
9417
  Object.assign(opening, {
9210
9418
  info: name,
9211
9419
  meta: {
@@ -9216,10 +9424,30 @@ function container(state, startLine, endLine, silent) {
9216
9424
  map: [startLine, next],
9217
9425
  markup: match[1]
9218
9426
  });
9219
- state.md.block.tokenize(state, startLine + 1, next);
9220
- const closing = state.push("looma_container_close", "", -1);
9427
+ const panel = (label, from, to) => {
9428
+ state.push("looma_tab_open", "section", 1).meta = { label };
9429
+ state.md.block.tokenize(state, from, to);
9430
+ state.push("looma_tab_close", "section", -1);
9431
+ };
9432
+ if (labels.length) {
9433
+ state.md.block.tokenize(state, startLine + 1, labels[0].line);
9434
+ labels.forEach(({ line, label }, index) => panel(label, line + 1, labels[index + 1]?.line ?? next));
9435
+ } else {
9436
+ const first = state.tokens.length;
9437
+ state.md.block.tokenize(state, startLine + 1, next);
9438
+ if (name === "code-group") for (let index = state.tokens.length - 1; index >= first; index--) {
9439
+ const token = state.tokens[index];
9440
+ if (token.type !== "fence" || token.level !== opening.level + 1) continue;
9441
+ const { title: fenceTitle, language } = parseLoomaFenceInfo(token.info);
9442
+ const tabOpen = new state.Token("looma_tab_open", "section", 1);
9443
+ tabOpen.meta = { label: fenceTitle ?? language ?? "Code" };
9444
+ state.tokens.splice(index, 1, tabOpen, token, new state.Token("looma_tab_close", "section", -1));
9445
+ }
9446
+ }
9447
+ const closing = state.push("looma_container_close", "div", -1);
9221
9448
  Object.assign(closing, {
9222
9449
  info: name,
9450
+ meta: { title },
9223
9451
  block: true,
9224
9452
  markup: match[1]
9225
9453
  });
@@ -9228,6 +9456,27 @@ function container(state, startLine, endLine, silent) {
9228
9456
  state.line = Math.min(next + 1, endLine);
9229
9457
  return true;
9230
9458
  }
9459
+ function leaf(state, startLine, _endLine, silent) {
9460
+ if (state.sCount[startLine] - state.blkIndent >= 4) return false;
9461
+ const match = LEAF.exec(lineText(state, startLine));
9462
+ if (!match || match[1].toLowerCase() !== "toc") return false;
9463
+ const attributes = match[2] ? parseLoomaAttributes(match[2]) : {};
9464
+ if (!attributes) return false;
9465
+ if (silent) return true;
9466
+ const format = typeof attributes.format === "string" && TOC_FORMATS.has(attributes.format) ? attributes.format : "plain";
9467
+ const depth = attributes.depth === "1" || attributes.depth === "2" ? Number(attributes.depth) : 3;
9468
+ const token = state.push("looma_toc", "nav", 0);
9469
+ Object.assign(token, {
9470
+ block: true,
9471
+ map: [startLine, startLine + 1],
9472
+ meta: {
9473
+ format,
9474
+ depth
9475
+ }
9476
+ });
9477
+ state.line = startLine + 1;
9478
+ return true;
9479
+ }
9231
9480
  function alerts(state) {
9232
9481
  const tokens = state.tokens;
9233
9482
  for (let index = 0; index < tokens.length; index++) {
@@ -9246,36 +9495,239 @@ function alerts(state) {
9246
9495
  }
9247
9496
  }
9248
9497
  }
9249
- function withSharedBlocks(md) {
9498
+ function taskLists(state) {
9499
+ const tokens = state.tokens;
9500
+ for (let index = 0; index < tokens.length; index++) {
9501
+ if (tokens[index].type !== "bullet_list_open") continue;
9502
+ const level = tokens[index].level;
9503
+ const items = [];
9504
+ let isTaskList = true;
9505
+ for (let cursor = index + 1; cursor < tokens.length; cursor++) {
9506
+ const token = tokens[cursor];
9507
+ if (token.type === "bullet_list_close" && token.level === level) break;
9508
+ if (token.type !== "list_item_open" || token.level !== level + 1) continue;
9509
+ const inline = tokens[cursor + 2];
9510
+ const match = tokens[cursor + 1]?.type === "paragraph_open" && inline?.type === "inline" ? TASK.exec(inline.content) : null;
9511
+ if (!match) {
9512
+ isTaskList = false;
9513
+ break;
9514
+ }
9515
+ items.push({
9516
+ item: token,
9517
+ inline,
9518
+ checked: match[1] !== " "
9519
+ });
9520
+ }
9521
+ if (!isTaskList || !items.length) continue;
9522
+ tokens[index].attrSet("data-type", "taskList");
9523
+ for (const { item, inline, checked } of items) {
9524
+ inline.content = inline.content.replace(TASK, "");
9525
+ item.attrSet("data-type", "taskItem");
9526
+ item.attrSet("data-checked", String(checked));
9527
+ item.meta = { checked };
9528
+ }
9529
+ }
9530
+ }
9531
+ function directive(state, silent) {
9532
+ const { src, pos } = state;
9533
+ if (src.charCodeAt(pos) !== 58) return false;
9534
+ const name = /^:(chip|mention)\[/.exec(src.slice(pos, pos + 10))?.[1];
9535
+ if (!name) return false;
9536
+ let label = "";
9537
+ let index = pos + name.length + 2;
9538
+ for (; index < state.posMax && src[index] !== "]"; index++) {
9539
+ if (src[index] === "\\" && index + 1 < state.posMax) index++;
9540
+ label += src[index];
9541
+ }
9542
+ if (src[index] !== "]") return false;
9543
+ const read = src[index + 1] === "{" ? readLoomaAttributes(src, index + 1) : {
9544
+ attributes: {},
9545
+ end: index + 1
9546
+ };
9547
+ if (!read || read.end > state.posMax) return false;
9548
+ if (name === "mention" && typeof read.attributes.id !== "string") return false;
9549
+ if (!silent) {
9550
+ const token = state.push(`looma_${name}`, "span", 0);
9551
+ token.meta = {
9552
+ label,
9553
+ attributes: read.attributes
9554
+ };
9555
+ }
9556
+ state.pos = read.end;
9557
+ return true;
9558
+ }
9559
+ function inlineText(token) {
9560
+ return (token?.children ?? []).map((child) => child.type === "image" ? inlineText(child) : child.content).join("");
9561
+ }
9562
+ function attributes(state) {
9563
+ const headings = [];
9564
+ const tokens = state.tokens;
9565
+ for (let index = 0; index < tokens.length; index++) {
9566
+ const token = tokens[index];
9567
+ if (token.type !== "inline" || !token.children) continue;
9568
+ const children = token.children;
9569
+ for (let child = children.length - 3; child >= 0; child--) {
9570
+ const text = children[child + 1];
9571
+ if (children[child].type !== "link_open" || children[child].markup !== "linkify" || /^(?:[a-z][\w+.-]*:|www\.)/i.test(text.content)) continue;
9572
+ children.splice(child, 3, Object.assign(new state.Token("text", "", 0), { content: text.content }));
9573
+ }
9574
+ if (tokens[index - 1]?.type === "heading_open") {
9575
+ const last = children.at(-1);
9576
+ const marker = last?.type === "text" ? /[ \t]+(\{[^{}]*\})$/.exec(last.content) : null;
9577
+ const parsed = marker && parseLoomaAttributes(marker[1]);
9578
+ let id;
9579
+ if (last && marker && parsed && typeof parsed.id === "string") {
9580
+ id = parsed.id;
9581
+ last.content = last.content.slice(0, marker.index);
9582
+ }
9583
+ headings.push({
9584
+ open: tokens[index - 1],
9585
+ text: inlineText(token).trim(),
9586
+ id
9587
+ });
9588
+ }
9589
+ for (let child = 0; child < children.length; child++) {
9590
+ const next = children[child + 1];
9591
+ if (children[child].type !== "image" || next?.type !== "text" || !next.content.startsWith("{")) continue;
9592
+ const read = readLoomaAttributes(next.content);
9593
+ if (!read) continue;
9594
+ applyImageAttributes(children[child], read.attributes);
9595
+ next.content = next.content.slice(read.end);
9596
+ if (!next.content) children.splice(child + 1, 1);
9597
+ }
9598
+ if (tokens[index - 1]?.type === "paragraph_open" && children.length === 1 && children[0].type === "image") {
9599
+ tokens[index - 1].hidden = true;
9600
+ tokens[index + 1].hidden = true;
9601
+ }
9602
+ }
9603
+ const ids = assignLoomaHeadingIds(headings);
9604
+ headings.forEach(({ open }, index) => open.attrSet("id", ids[index]));
9605
+ }
9606
+ function applyImageAttributes(image, attributes) {
9607
+ for (const key of ["width", "height"]) {
9608
+ const value = Number(attributes[key]);
9609
+ if (Number.isSafeInteger(value) && value > 0) image.attrSet(key, String(value));
9610
+ }
9611
+ const placement = attributes.placement;
9612
+ if (typeof placement === "string" && PLACEMENTS.has(placement) && placement !== "block") image.attrSet("data-placement", placement);
9613
+ if (attributes.responsive === true) image.attrSet("data-looma-responsive", "");
9614
+ }
9615
+ function tableOfContents(tokens, format, depth, escape) {
9616
+ const roots = [];
9617
+ const ancestors = [];
9618
+ tokens.forEach((token, index) => {
9619
+ if (token.type !== "heading_open") return;
9620
+ const level = Number(token.tag.slice(1));
9621
+ const text = inlineText(tokens[index + 1]).trim();
9622
+ if (level > depth || !text) return;
9623
+ while (ancestors.length && ancestors[ancestors.length - 1].level >= level) ancestors.pop();
9624
+ const entry = {
9625
+ id: token.attrGet("id") ?? "",
9626
+ text,
9627
+ children: []
9628
+ };
9629
+ (ancestors.at(-1)?.entry.children ?? roots).push(entry);
9630
+ ancestors.push({
9631
+ level,
9632
+ entry
9633
+ });
9634
+ });
9635
+ const [list, item] = format === "plain" ? ["div", "div"] : [format === "numbered" ? "ol" : "ul", "li"];
9636
+ const render = (entries) => `<${list} role="list">${entries.map(({ id, text, children }) => `<${item} role="listitem"><a href="#${escape(id)}">${escape(text)}</a>${children.length ? render(children) : ""}</${item}>`).join("")}</${list}>`;
9637
+ return `<nav data-looma-toc="" data-format="${format}" data-depth="${depth}" aria-label="Table of contents"><strong>Table of contents</strong>${roots.length ? render(roots) : ""}</nav>\n`;
9638
+ }
9639
+ /**
9640
+ * Adds Looma Markdown to a markdown-it instance: containers, alerts, task lists, the table of
9641
+ * contents, chips, mentions, brace attributes, heading anchors, code meta and front matter.
9642
+ * @contract Renders the canonical HTML, which is the HTML LoomaEditor saves. It is not sanitized.
9643
+ * Linkify and raw HTML are instance options; `createLoomaMarkdownIt` turns both on.
9644
+ */
9645
+ function loomaMarkdown$1(md) {
9250
9646
  const escape = md.utils.escapeHtml;
9647
+ md.block.ruler.before("table", "looma_frontmatter", frontmatter);
9251
9648
  md.block.ruler.before("fence", "looma_container", container, { alt: [
9252
9649
  "paragraph",
9253
9650
  "reference",
9254
9651
  "blockquote",
9255
9652
  "list"
9256
9653
  ] });
9654
+ md.block.ruler.before("paragraph", "looma_leaf", leaf, { alt: [
9655
+ "paragraph",
9656
+ "reference",
9657
+ "blockquote",
9658
+ "list"
9659
+ ] });
9660
+ md.inline.ruler.before("link", "looma_directive", directive);
9661
+ md.core.ruler.after("block", "looma_task_lists", taskLists);
9257
9662
  md.core.ruler.before("inline", "looma_alerts", alerts);
9258
- md.renderer.rules.looma_container_open = (tokens, index) => {
9663
+ md.core.ruler.before("text_join", "looma_attributes", attributes);
9664
+ const rules = md.renderer.rules;
9665
+ rules.looma_frontmatter = () => "";
9666
+ rules.looma_container_open = (tokens, index) => {
9259
9667
  const { info, meta } = tokens[index];
9260
9668
  if (info === "details") return `<details${meta.open ? " open" : ""}><summary>${escape(meta.title || "Details")}</summary>\n`;
9669
+ if (info === "figure") return "<figure>\n";
9670
+ if (info === "tabs" || info === "code-group") return `<ui-tabs${info === "code-group" ? " class=\"code-group\"" : ""} group="${escape(meta.title || info)}">\n`;
9261
9671
  return `<ui-callout tone="${normalizeLoomaCalloutTone(info)}"${meta.title ? ` heading="${escape(meta.title)}"` : ""}>\n`;
9262
9672
  };
9263
- md.renderer.rules.looma_container_close = (tokens, index) => tokens[index].info === "details" ? "</details>\n" : "</ui-callout>\n";
9264
- return md;
9673
+ rules.looma_container_close = (tokens, index, _options, env) => {
9674
+ const { info } = tokens[index];
9675
+ if (info === "details") return "</details>\n";
9676
+ if (info === "figure") {
9677
+ const title = tokens[index].meta.title;
9678
+ return `${title ? `<figcaption>${md.renderInline(title, env)}</figcaption>\n` : ""}</figure>\n`;
9679
+ }
9680
+ if (info === "tabs" || info === "code-group") return "</ui-tabs>\n";
9681
+ return "</ui-callout>\n";
9682
+ };
9683
+ rules.looma_tab_open = (tokens, index) => `<section aria-label="${escape(tokens[index].meta.label)}">\n`;
9684
+ rules.looma_tab_close = () => "</section>\n";
9685
+ rules.looma_toc = (tokens, index) => tableOfContents(tokens, tokens[index].meta.format, tokens[index].meta.depth, escape);
9686
+ rules.looma_chip = (tokens, index) => {
9687
+ const { label, attributes } = tokens[index].meta;
9688
+ const tone = normalizeLoomaChipTone(attributes.tone);
9689
+ return `<span data-looma-chip="" data-label="${escape(label)}" data-color="${tone}"${label ? "" : " data-placeholder=\"\""}>${escape(label)}</span>`;
9690
+ };
9691
+ rules.looma_mention = (tokens, index) => {
9692
+ const { label, attributes } = tokens[index].meta;
9693
+ const id = String(attributes.id);
9694
+ return `<span data-type="mention" class="looma-mention" data-id="${escape(id)}" data-label="${escape(label || id)}">@${escape(label || id)}</span>`;
9695
+ };
9696
+ const listItem = rules.list_item_open ?? ((tokens, index, options, _env, self) => self.renderToken(tokens, index, options));
9697
+ rules.list_item_open = (tokens, index, options, env, self) => {
9698
+ const html = listItem(tokens, index, options, env, self);
9699
+ const task = tokens[index].meta;
9700
+ return task && typeof task.checked === "boolean" ? `${html}<input type="checkbox" disabled=""${task.checked ? " checked=\"\"" : ""}>` : html;
9701
+ };
9702
+ const code = (content, info) => {
9703
+ const { language, meta } = parseLoomaFenceInfo(info);
9704
+ return `<pre><code${language ? ` class="language-${escape(language)}"` : ""}${meta ? ` data-meta="${escape(meta)}"` : ""}>${escape(content.replace(/\n$/, ""))}</code></pre>\n`;
9705
+ };
9706
+ rules.fence = (tokens, index) => code(tokens[index].content, tokens[index].info);
9707
+ rules.code_block = (tokens, index) => code(tokens[index].content, "");
9265
9708
  }
9266
- /** The markdown-it instance LoomaEditor imports Markdown with, shared block vocabulary included. */
9267
- var loomaMarkdown = withSharedBlocks(new MarkdownIt({
9268
- html: true,
9269
- linkify: false,
9270
- typographer: false
9271
- }));
9272
9709
  /**
9273
- * Renders Markdown, including the shared block vocabulary, as HTML the editor parses.
9710
+ * A markdown-it instance for Looma Markdown: raw HTML and GFM autolinks on, with `loomaMarkdown`.
9711
+ * @contract Each call returns a new instance that callers may extend.
9712
+ */
9713
+ function createLoomaMarkdownIt() {
9714
+ return new MarkdownIt({
9715
+ html: true,
9716
+ linkify: true,
9717
+ typographer: false
9718
+ }).use(loomaMarkdown$1);
9719
+ }
9720
+ //#endregion
9721
+ //#region src/editor/extensions/markdown.ts
9722
+ /** The markdown-it instance LoomaEditor reads Looma Markdown with. */
9723
+ var loomaMarkdown = createLoomaMarkdownIt();
9724
+ /**
9725
+ * Renders Looma Markdown as the HTML the editor parses.
9274
9726
  * @contract The result is not sanitized; pass it to the editor's `setContent`, whose schema
9275
- * keeps only content it knows.
9727
+ * keeps only content it knows. Front matter renders nothing.
9276
9728
  */
9277
9729
  function loomaMarkdownToHtml(source) {
9278
- return loomaMarkdown.render(source).replace(/\n<\/code><\/pre>/g, "</code></pre>");
9730
+ return loomaMarkdown.render(source);
9279
9731
  }
9280
9732
  var containerDepth = (node) => {
9281
9733
  let depth = 0;
@@ -9286,10 +9738,11 @@ var containerDepth = (node) => {
9286
9738
  return depth;
9287
9739
  };
9288
9740
  function writeContainer(state, node, opening) {
9289
- const marker = ":".repeat(3 + containerDepth(node));
9741
+ const marker = loomaContainerMarker(containerDepth(node));
9290
9742
  state.write(`${marker} ${opening}`.trimEnd());
9291
9743
  state.ensureNewLine();
9292
9744
  state.renderContent(node);
9745
+ state.flushClose(1);
9293
9746
  state.write(marker);
9294
9747
  state.closeBlock(node);
9295
9748
  }
@@ -9300,7 +9753,7 @@ var NODES = {
9300
9753
  blockquote: defaults.blockquote,
9301
9754
  heading: defaults.heading,
9302
9755
  horizontalRule: defaults.horizontal_rule,
9303
- bulletList: defaults.bullet_list,
9756
+ bulletList: (state, node) => state.renderList(node, " ", () => "- "),
9304
9757
  orderedList: (state, node) => {
9305
9758
  const start = Number(node.attrs.start ?? 1);
9306
9759
  state.renderList(node, " ", (index) => `${start + index}. `);
@@ -9308,8 +9761,7 @@ var NODES = {
9308
9761
  listItem: defaults.list_item,
9309
9762
  hardBreak: defaults.hard_break,
9310
9763
  codeBlock: (state, node) => {
9311
- const longest = Math.max(0, ...(node.textContent.match(/`+/g) ?? []).map((run) => run.length));
9312
- const fence = "`".repeat(Math.max(3, longest + 1));
9764
+ const fence = loomaCodeFence(node.textContent);
9313
9765
  state.write(`${fence}${node.attrs.language ?? ""}\n`);
9314
9766
  state.text(node.textContent, false);
9315
9767
  state.ensureNewLine();
@@ -9342,8 +9794,8 @@ var MARKS = {
9342
9794
  }
9343
9795
  };
9344
9796
  /**
9345
- * Writes an editor document as Markdown, with callouts and expand sections in the shared
9346
- * `:::` syntax that `loomaMarkdownToHtml` and Markupress read.
9797
+ * Writes an editor document as Looma Markdown, with callouts and expand sections as `:::`
9798
+ * containers that `loomaMarkdownToHtml` and Markupress read.
9347
9799
  * @contract Content Markdown has no syntax for (tables, task lists, images, chips, mentions,
9348
9800
  * the table of contents) is written as its saved HTML, which Markdown carries unchanged.
9349
9801
  * Marks without a Markdown form other than underline and highlight are dropped.
@@ -9391,12 +9843,14 @@ function looksLikeMarkdown(value) {
9391
9843
  "blockquote_open",
9392
9844
  "fence",
9393
9845
  "table_open",
9394
- "looma_container_open"
9846
+ "looma_container_open",
9847
+ "looma_toc"
9395
9848
  ].includes(token.type)) return true;
9396
- return token.type === "inline" && Boolean(token.children?.some((child) => [
9397
- "link_open",
9849
+ return token.type === "inline" && Boolean(token.children?.some((child) => child.type === "link_open" && child.markup !== "linkify" || [
9398
9850
  "image",
9399
- "strong_open"
9851
+ "strong_open",
9852
+ "looma_chip",
9853
+ "looma_mention"
9400
9854
  ].includes(child.type)));
9401
9855
  });
9402
9856
  }
@@ -9411,7 +9865,7 @@ function hasRichClipboardStructure(value) {
9411
9865
  if (body.querySelector("[data-pm-slice]") && !body.querySelector("pre")) return true;
9412
9866
  return body.querySelectorAll("p").length > 1;
9413
9867
  }
9414
- function parseHtmlSlice(schema, value) {
9868
+ function parseHtmlSlice(schema, value, fromHtml = false) {
9415
9869
  const parsed = new globalThis.DOMParser().parseFromString(value, "text/html");
9416
9870
  if (parsed.body.style.display === "none") return Slice.empty;
9417
9871
  for (const element of parsed.body.querySelectorAll("[style]")) if (element.style.display === "none") element.remove();
@@ -9422,7 +9876,7 @@ function parseHtmlSlice(schema, value) {
9422
9876
  if (/\s$/.test(child.textContent) || /^\s/.test(next.textContent)) continue;
9423
9877
  wrapper.insertBefore(parsed.createTextNode(" "), next);
9424
9878
  }
9425
- for (const pre of parsed.body.querySelectorAll("pre")) {
9879
+ for (const pre of fromHtml ? parsed.body.querySelectorAll("pre") : []) {
9426
9880
  let last = pre;
9427
9881
  while (last.lastChild) last = last.lastChild;
9428
9882
  if (last.nodeType === Node.TEXT_NODE) last.textContent = last.textContent?.replace(/\r?\n$/, "") ?? "";
@@ -9446,21 +9900,13 @@ function parseHtmlSlice(schema, value) {
9446
9900
  if (!container.textContent?.trim() && !container.querySelector("img, hr")) return Slice.empty;
9447
9901
  return DOMParser.fromSchema(schema).parseSlice(container, { preserveWhitespace: false });
9448
9902
  }
9449
- function markdownFrontmatter(value) {
9450
- const match = /^---[ \t]*\r?\n([\s\S]*?)\r?\n(?:---|\.\.\.)[ \t]*(?:\r?\n|$)/.exec(value.slice(0, 8192));
9451
- if (!match || !/^[A-Za-z][\w-]*:[ \t]*\S/m.test(match[1])) return null;
9452
- return {
9453
- source: match[0].trimEnd(),
9454
- body: value.slice(match[0].length)
9455
- };
9456
- }
9457
9903
  function escapeHtmlText(value) {
9458
9904
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
9459
9905
  }
9460
9906
  function documentSlice(schema, plainText) {
9461
- const frontmatter = markdownFrontmatter(plainText);
9907
+ const frontmatter = splitLoomaFrontmatter(plainText);
9462
9908
  if (frontmatter || looksLikeMarkdown(plainText)) return parseHtmlSlice(schema, frontmatter ? `<pre><code class="language-yaml">${escapeHtmlText(frontmatter.source)}</code></pre>${loomaMarkdown.render(frontmatter.body)}` : loomaMarkdown.render(plainText));
9463
- if (looksLikeDocumentHtml(plainText)) return parseHtmlSlice(schema, plainText);
9909
+ if (looksLikeDocumentHtml(plainText)) return parseHtmlSlice(schema, plainText, true);
9464
9910
  return null;
9465
9911
  }
9466
9912
  /**
@@ -11076,4 +11522,4 @@ function createLoomaSlashCommandExtension(options = {}) {
11076
11522
  });
11077
11523
  }
11078
11524
  //#endregion
11079
- export { filterLoomaMentionItems as A, LOOMA_CHIP_COLORS as B, loomaMarkdownToHtml as C, createLoomaMentionExtension as D, LoomaMentionSuggestionPluginKey as E, LoomaTableOfContents as F, LOOMA_ACTIVE_BLOCK_BLUR_GRACE_MS as G, normalizeLoomaChipColor as H, getTableOfContentsEntries as I, LoomaActiveBlock as J, LOOMA_ACTIVE_BLOCK_CLASS as K, normalizeTableOfContentsConfig as L, LoomaImage as M, normalizeImageDimension as N, DEFAULT_MENTION_RESULT_LIMIT as O, normalizeImagePlacement as P, LoomaExpand as R, loomaMarkdownFromDoc as S, siteRelativeHref as T, LOOMA_CALLOUT_TONES as U, LoomaChip as V, LoomaCallout as W, LoomaActiveBlockPluginKey as Y, 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, LOOMA_ACTIVE_BLOCK_TYPING_IDLE_MS 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, revealLoomaExpandAt as z };
11525
+ export { filterLoomaMentionItems as A, LOOMA_CHIP_COLORS as B, loomaMarkdownToHtml as C, createLoomaMentionExtension as D, LoomaMentionSuggestionPluginKey as E, LoomaTableOfContents as F, LOOMA_ACTIVE_BLOCK_BLUR_GRACE_MS as G, normalizeLoomaChipColor as H, getTableOfContentsEntries as I, LoomaActiveBlock as J, LOOMA_ACTIVE_BLOCK_CLASS as K, normalizeTableOfContentsConfig as L, LoomaImage as M, normalizeImageDimension as N, DEFAULT_MENTION_RESULT_LIMIT as O, normalizeImagePlacement as P, LoomaExpand as R, loomaMarkdownFromDoc as S, siteRelativeHref as T, LoomaCallout as U, LoomaChip as V, LOOMA_CALLOUT_TONES as W, LoomaActiveBlockPluginKey as Y, 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, LOOMA_ACTIVE_BLOCK_TYPING_IDLE_MS 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, revealLoomaExpandAt as z };