@threadlabs/looma 0.30.7 → 0.30.9

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.
@@ -296,6 +296,119 @@ function applyImageAttributes(image, attributes) {
296
296
  if (typeof placement === "string" && PLACEMENTS.has(placement) && placement !== "block") image.attrSet("data-placement", placement);
297
297
  if (attributes.responsive === true) image.attrSet("data-looma-responsive", "");
298
298
  }
299
+ var FOOTNOTE_DEFINITION = /^\[\^([^\s\]]+)\]:/;
300
+ var FOOTNOTE_REFERENCE = /^\[\^([^\s\]]+)\]/;
301
+ function footnoteDefinition(state, startLine, endLine, silent) {
302
+ if (state.sCount[startLine] - state.blkIndent >= 4) return false;
303
+ const start = state.bMarks[startLine] + state.tShift[startLine];
304
+ const match = FOOTNOTE_DEFINITION.exec(state.src.slice(start, state.eMarks[startLine]));
305
+ if (!match) return false;
306
+ if (silent) return true;
307
+ (state.env.footnotes ??= /* @__PURE__ */ new Set()).add(match[1]);
308
+ const open = state.push("looma_footnote_open", "li", 1);
309
+ Object.assign(open, {
310
+ meta: { label: match[1] },
311
+ block: true,
312
+ map: [startLine, startLine]
313
+ });
314
+ const saved = {
315
+ bMark: state.bMarks[startLine],
316
+ tShift: state.tShift[startLine],
317
+ sCount: state.sCount[startLine],
318
+ parentType: state.parentType
319
+ };
320
+ let position = start + match[0].length;
321
+ const contentStart = position;
322
+ let offset = state.sCount[startLine] + match[0].length;
323
+ const initial = offset;
324
+ while (position < state.eMarks[startLine] && (state.src[position] === " " || state.src[position] === " ")) {
325
+ offset += state.src[position] === " " ? 4 - (offset + state.bsCount[startLine]) % 4 : 1;
326
+ position++;
327
+ }
328
+ state.tShift[startLine] = position - contentStart;
329
+ state.sCount[startLine] = offset - initial;
330
+ state.bMarks[startLine] = contentStart;
331
+ state.blkIndent += 4;
332
+ state.parentType = "footnote";
333
+ if (state.sCount[startLine] < state.blkIndent) state.sCount[startLine] += state.blkIndent;
334
+ state.md.block.tokenize(state, startLine, endLine);
335
+ state.parentType = saved.parentType;
336
+ state.blkIndent -= 4;
337
+ state.tShift[startLine] = saved.tShift;
338
+ state.sCount[startLine] = saved.sCount;
339
+ state.bMarks[startLine] = saved.bMark;
340
+ open.map[1] = state.line;
341
+ state.push("looma_footnote_close", "li", -1).block = true;
342
+ return true;
343
+ }
344
+ function footnoteReference(state, silent) {
345
+ if (state.src.charCodeAt(state.pos) !== 91 || state.src.charCodeAt(state.pos + 1) !== 94) return false;
346
+ const match = FOOTNOTE_REFERENCE.exec(state.src.slice(state.pos, state.posMax));
347
+ if (!match || !state.env.footnotes?.has(match[1])) return false;
348
+ if (!silent) state.push("looma_footnote_ref", "sup", 0).meta = { label: match[1] };
349
+ state.pos += match[0].length;
350
+ return true;
351
+ }
352
+ function footnotes(state) {
353
+ const definitions = /* @__PURE__ */ new Map();
354
+ const body = [];
355
+ for (let index = 0; index < state.tokens.length; index++) {
356
+ const token = state.tokens[index];
357
+ if (token.type !== "looma_footnote_open") {
358
+ body.push(token);
359
+ continue;
360
+ }
361
+ let depth = 0, end = index;
362
+ for (; end < state.tokens.length; end++) if (state.tokens[end].type === "looma_footnote_open") depth++;
363
+ else if (state.tokens[end].type === "looma_footnote_close" && --depth === 0) break;
364
+ const label = token.meta.label;
365
+ if (!definitions.has(label)) definitions.set(label, state.tokens.slice(index, end + 1));
366
+ index = end;
367
+ }
368
+ if (!definitions.size) return;
369
+ const order = [];
370
+ const counts = /* @__PURE__ */ new Map();
371
+ const number = (tokens) => {
372
+ for (const token of tokens) for (const child of token.children ?? []) {
373
+ if (child.type !== "looma_footnote_ref") continue;
374
+ const label = child.meta.label;
375
+ if (!counts.has(label)) order.push(label);
376
+ const occurrence = (counts.get(label) ?? 0) + 1;
377
+ counts.set(label, occurrence);
378
+ child.meta = {
379
+ label,
380
+ number: order.indexOf(label) + 1,
381
+ occurrence
382
+ };
383
+ }
384
+ };
385
+ number(body);
386
+ const notes = [];
387
+ for (let index = 0; index < order.length; index++) {
388
+ const label = order[index];
389
+ const tokens = definitions.get(label);
390
+ number(tokens);
391
+ const backrefs = Object.assign(new state.Token("looma_footnote_backrefs", "", 0), { meta: {
392
+ label,
393
+ number: index + 1
394
+ } });
395
+ const last = tokens.length - 2;
396
+ if (tokens[last].type === "paragraph_close") tokens.splice(last, 0, backrefs);
397
+ else tokens.splice(last + 1, 0, new state.Token("paragraph_open", "p", 1), backrefs, new state.Token("paragraph_close", "p", -1));
398
+ notes.push(...tokens);
399
+ }
400
+ if (!notes.length) {
401
+ state.tokens = body;
402
+ return;
403
+ }
404
+ for (const note of notes) if (note.type === "looma_footnote_open") note.meta.count = counts.get(note.meta.label);
405
+ state.tokens = [
406
+ ...body,
407
+ Object.assign(new state.Token("looma_footnotes_open", "section", 1), { block: true }),
408
+ ...notes,
409
+ Object.assign(new state.Token("looma_footnotes_close", "section", -1), { block: true })
410
+ ];
411
+ }
299
412
  function tableOfContents(tokens, format, depth, escape) {
300
413
  const roots = [];
301
414
  const ancestors = [];
@@ -321,7 +434,7 @@ function tableOfContents(tokens, format, depth, escape) {
321
434
  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`;
322
435
  }
323
436
  /**
324
- * Adds Looma Markdown to a markdown-it instance: containers, alerts, task lists, the table of
437
+ * Adds Looma Markdown to a markdown-it instance: containers, alerts, task lists, footnotes, the table of
325
438
  * contents, chips, mentions, brace attributes, heading anchors, code meta and front matter.
326
439
  * @contract Renders the canonical HTML, which is the HTML LoomaEditor saves. It is not sanitized.
327
440
  * Linkify and raw HTML are instance options; `createLoomaMarkdownIt` turns both on.
@@ -329,6 +442,7 @@ function tableOfContents(tokens, format, depth, escape) {
329
442
  function loomaMarkdown(md, { codeNotation = "apply" } = {}) {
330
443
  const escape = md.utils.escapeHtml;
331
444
  md.block.ruler.before("table", "looma_frontmatter", frontmatter);
445
+ md.block.ruler.before("reference", "looma_footnote", footnoteDefinition, { alt: ["paragraph", "reference"] });
332
446
  md.block.ruler.before("fence", "looma_container", container, { alt: [
333
447
  "paragraph",
334
448
  "reference",
@@ -342,9 +456,11 @@ function loomaMarkdown(md, { codeNotation = "apply" } = {}) {
342
456
  "list"
343
457
  ] });
344
458
  md.inline.ruler.before("link", "looma_directive", directive);
459
+ md.inline.ruler.before("link", "looma_footnote_ref", footnoteReference);
345
460
  md.core.ruler.after("block", "looma_task_lists", taskLists);
346
461
  md.core.ruler.before("inline", "looma_alerts", alerts);
347
462
  md.core.ruler.before("text_join", "looma_attributes", attributes);
463
+ md.core.ruler.after("inline", "looma_footnotes", footnotes);
348
464
  const rules = md.renderer.rules;
349
465
  rules.looma_frontmatter = () => "";
350
466
  rules.looma_container_open = (tokens, index) => {
@@ -366,6 +482,22 @@ function loomaMarkdown(md, { codeNotation = "apply" } = {}) {
366
482
  };
367
483
  rules.looma_tab_open = (tokens, index) => `<section aria-label="${escape(tokens[index].meta.label)}">\n`;
368
484
  rules.looma_tab_close = () => "</section>\n";
485
+ rules.looma_footnote_ref = (tokens, index) => {
486
+ const { label, number, occurrence } = tokens[index].meta;
487
+ return `<sup><a href="#fn:${escape(label)}" id="fnref:${escape(label)}${occurrence > 1 ? `:${occurrence}` : ""}" data-footnote-ref="" aria-describedby="footnote:label">${number}</a></sup>`;
488
+ };
489
+ rules.looma_footnote_backrefs = (tokens, index) => {
490
+ const { label, number } = tokens[index].meta;
491
+ const count = tokens.find((token) => token.type === "looma_footnote_open" && token.meta.label === label)?.meta.count ?? 1;
492
+ return Array.from({ length: count }, (_, occurrence) => {
493
+ const suffix = occurrence ? `:${occurrence + 1}` : "";
494
+ return ` <a href="#fnref:${escape(label)}${suffix}" data-footnote-backref="" aria-label="Back to reference ${number}${occurrence ? `-${occurrence + 1}` : ""}">↩${occurrence ? `<sup>${occurrence + 1}</sup>` : ""}</a>`;
495
+ }).join("");
496
+ };
497
+ rules.looma_footnotes_open = () => "<section data-footnotes=\"\" class=\"footnotes\">\n<h2 id=\"footnote:label\" class=\"sr-only\">Footnotes</h2>\n<ol>\n";
498
+ rules.looma_footnotes_close = () => "</ol>\n</section>\n";
499
+ rules.looma_footnote_open = (tokens, index) => `<li id="fn:${escape(tokens[index].meta.label)}">\n`;
500
+ rules.looma_footnote_close = () => "</li>\n";
369
501
  rules.looma_toc = (tokens, index) => tableOfContents(tokens, tokens[index].meta.format, tokens[index].meta.depth, escape);
370
502
  rules.looma_chip = (tokens, index) => {
371
503
  const { label, attributes } = tokens[index].meta;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threadlabs/looma",
3
- "version": "0.30.7",
3
+ "version": "0.30.9",
4
4
  "type": "module",
5
5
  "description": "Looma components as HTML, Vue, and plain DOM, with design tokens.",
6
6
  "license": "MIT",
@@ -111,6 +111,11 @@
111
111
  padding: calc(var(--ui-space-3) + var(--ui-space-1) / 2) var(--ui-space-3) var(--ui-space-3) var(--ui-space-5);
112
112
  }
113
113
 
114
+ /* Alerts have no close target to add height above the title. */
115
+ :scope:where([data-component~="ui-dialog"])[data-ui-dialog-state~="alert"] header {
116
+ padding: var(--ui-space-5) var(--ui-space-5) var(--ui-space-3);
117
+ }
118
+
114
119
  /* :host-qualified so page heading rules that reach in cannot outrank the dialog chrome. */
115
120
  :scope:where([data-component~="ui-dialog"]) .title {
116
121
  flex: 1;
@@ -238,6 +243,8 @@
238
243
 
239
244
  /* Own the layout wrapper; nested component roots are outside these scoped rules. */
240
245
 
246
+ /* Alerts have no close target to add height above the title. */
247
+
241
248
  /* :host-qualified so page heading rules that reach in cannot outrank the dialog chrome. */
242
249
 
243
250
  /* A CSS-drawn X. */
package/vue/UiDialog.vue CHANGED
@@ -363,6 +363,8 @@ useComponentHost(() => import('../components/ui-dialog/ui-dialog.js'), {
363
363
 
364
364
  /* Own the layout wrapper; nested component roots are outside these scoped rules. */
365
365
 
366
+ /* Alerts have no close target to add height above the title. */
367
+
366
368
  /* :host-qualified so page heading rules that reach in cannot outrank the dialog chrome. */
367
369
 
368
370
  /* A CSS-drawn X. */
@@ -460,6 +462,9 @@ useComponentHost(() => import('../components/ui-dialog/ui-dialog.js'), {
460
462
  padding: calc(var(--ui-space-3) + var(--ui-space-1) / 2) var(--ui-space-3) var(--ui-space-3)
461
463
  var(--ui-space-5);
462
464
  }
465
+ :global(:scope[data-ui-dialog-state~="alert"] header:where([data-hn-ui-dialog])) {
466
+ padding: var(--ui-space-5) var(--ui-space-5) var(--ui-space-3);
467
+ }
463
468
  :global(:scope .title:where([data-hn-ui-dialog])) {
464
469
  flex: 1;
465
470
  min-inline-size: 0;
@@ -353,6 +353,6 @@ var UiDialog_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PUR
353
353
  }]]);
354
354
  };
355
355
  }
356
- }), [["__scopeId", "data-v-995d5c18"]]);
356
+ }), [["__scopeId", "data-v-f26be724"]]);
357
357
  //#endregion
358
358
  export { UiDialog_default as t };
@@ -3035,18 +3035,20 @@ dd:where([data-hn-ui-description-item]) {
3035
3035
 
3036
3036
  /* Own the layout wrapper; nested component roots are outside these scoped rules. */
3037
3037
 
3038
+ /* Alerts have no close target to add height above the title. */
3039
+
3038
3040
  /* :host-qualified so page heading rules that reach in cannot outrank the dialog chrome. */
3039
3041
 
3040
3042
  /* A CSS-drawn X. */
3041
3043
 
3042
3044
  /* A touch target, like Button on coarse pointers. */
3043
- @keyframes ui-dialog-enter-995d5c18 {
3045
+ @keyframes ui-dialog-enter-f26be724 {
3044
3046
  from {
3045
3047
  opacity: 0;
3046
3048
  transform: scale(0.96);
3047
3049
  }
3048
3050
  }
3049
- @keyframes ui-dialog-backdrop-enter-995d5c18 {
3051
+ @keyframes ui-dialog-backdrop-enter-f26be724 {
3050
3052
  from {
3051
3053
  background-color: transparent;
3052
3054
  }
@@ -3094,7 +3096,7 @@ from {
3094
3096
  pointer-events: auto;
3095
3097
  opacity: 1;
3096
3098
  transform: scale(1);
3097
- animation: ui-dialog-enter-995d5c18 var(--ui-dialog-duration, var(--ui-motion-base))
3099
+ animation: ui-dialog-enter-f26be724 var(--ui-dialog-duration, var(--ui-motion-base))
3098
3100
  var(--ui-motion-ease);
3099
3101
  }
3100
3102
  :scope[data-ui-dialog-state~="size=sm"]:where([data-hn-ui-dialog]) {
@@ -3124,6 +3126,9 @@ header:where([data-hn-ui-dialog]) {
3124
3126
  padding: calc(var(--ui-space-3) + var(--ui-space-1) / 2) var(--ui-space-3) var(--ui-space-3)
3125
3127
  var(--ui-space-5);
3126
3128
  }
3129
+ :scope[data-ui-dialog-state~="alert"] header:where([data-hn-ui-dialog]) {
3130
+ padding: var(--ui-space-5) var(--ui-space-5) var(--ui-space-3);
3131
+ }
3127
3132
  :scope .title:where([data-hn-ui-dialog]) {
3128
3133
  flex: 1;
3129
3134
  min-inline-size: 0;
@@ -3212,7 +3217,7 @@ footer:empty:where([data-hn-ui-dialog]) {
3212
3217
  }
3213
3218
  :scope[open]:modal:where([data-hn-ui-dialog])::backdrop {
3214
3219
  background-color: var(--ui-overlay-backdrop);
3215
- animation: ui-dialog-backdrop-enter-995d5c18 var(--_ui-dialog-duration, var(--ui-motion-base))
3220
+ animation: ui-dialog-backdrop-enter-f26be724 var(--_ui-dialog-duration, var(--ui-motion-base))
3216
3221
  var(--ui-motion-ease);
3217
3222
  }
3218
3223
  :scope[open][data-ui-backdrop-hidden]:where([data-hn-ui-dialog])::backdrop {