@refrakt-md/runes 0.8.4 → 0.9.0

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.
Files changed (63) hide show
  1. package/dist/config.d.ts +2 -1
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/config.js +387 -23
  4. package/dist/config.js.map +1 -1
  5. package/dist/fence-escape.d.ts +19 -0
  6. package/dist/fence-escape.d.ts.map +1 -0
  7. package/dist/fence-escape.js +53 -0
  8. package/dist/fence-escape.js.map +1 -0
  9. package/dist/index.d.ts +7 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +94 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/lib/index.d.ts +2 -0
  14. package/dist/lib/index.d.ts.map +1 -1
  15. package/dist/lib/index.js +1 -0
  16. package/dist/lib/index.js.map +1 -1
  17. package/dist/nodes.d.ts.map +1 -1
  18. package/dist/nodes.js +2 -1
  19. package/dist/nodes.js.map +1 -1
  20. package/dist/registry.d.ts +8 -0
  21. package/dist/registry.d.ts.map +1 -1
  22. package/dist/registry.js +8 -0
  23. package/dist/registry.js.map +1 -1
  24. package/dist/rune.d.ts +7 -0
  25. package/dist/rune.d.ts.map +1 -1
  26. package/dist/rune.js +2 -0
  27. package/dist/rune.js.map +1 -1
  28. package/dist/sandbox-sources.d.ts +34 -0
  29. package/dist/sandbox-sources.d.ts.map +1 -0
  30. package/dist/sandbox-sources.js +204 -0
  31. package/dist/sandbox-sources.js.map +1 -0
  32. package/dist/schema/blog.d.ts +16 -0
  33. package/dist/schema/blog.d.ts.map +1 -0
  34. package/dist/schema/blog.js +21 -0
  35. package/dist/schema/blog.js.map +1 -0
  36. package/dist/schema/juxtapose.d.ts +7 -0
  37. package/dist/schema/juxtapose.d.ts.map +1 -0
  38. package/dist/schema/juxtapose.js +11 -0
  39. package/dist/schema/juxtapose.js.map +1 -0
  40. package/dist/schema/xref.d.ts +5 -0
  41. package/dist/schema/xref.d.ts.map +1 -0
  42. package/dist/schema/xref.js +7 -0
  43. package/dist/schema/xref.js.map +1 -0
  44. package/dist/tags/blog.d.ts +3 -0
  45. package/dist/tags/blog.d.ts.map +1 -0
  46. package/dist/tags/blog.js +67 -0
  47. package/dist/tags/blog.js.map +1 -0
  48. package/dist/tags/common.d.ts +27 -0
  49. package/dist/tags/common.d.ts.map +1 -1
  50. package/dist/tags/common.js +53 -0
  51. package/dist/tags/common.js.map +1 -1
  52. package/dist/tags/juxtapose.d.ts +3 -0
  53. package/dist/tags/juxtapose.d.ts.map +1 -0
  54. package/dist/tags/juxtapose.js +71 -0
  55. package/dist/tags/juxtapose.js.map +1 -0
  56. package/dist/tags/sandbox.d.ts.map +1 -1
  57. package/dist/tags/sandbox.js +56 -9
  58. package/dist/tags/sandbox.js.map +1 -1
  59. package/dist/tags/xref.d.ts +18 -0
  60. package/dist/tags/xref.d.ts.map +1 -0
  61. package/dist/tags/xref.js +54 -0
  62. package/dist/tags/xref.js.map +1 -0
  63. package/package.json +3 -3
package/dist/config.d.ts CHANGED
@@ -14,7 +14,8 @@ export interface PageTreeNode {
14
14
  /**
15
15
  * Core cross-page pipeline hooks.
16
16
  * Run for every site, before any community package hooks.
17
- * Registers page and heading entities, aggregates the page tree and breadcrumb paths.
17
+ * Registers page and heading entities, aggregates the page tree and breadcrumb paths,
18
+ * and resolves blog post listings.
18
19
  */
19
20
  export declare const corePipelineHooks: PackagePipelineHooks;
20
21
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAA+B,MAAM,uBAAuB,CAAC;AAEtF,OAAO,KAAK,EAAE,oBAAoB,EAAoE,MAAM,mBAAmB,CAAC;AA+EhI;gFACgF;AAChF,eAAO,MAAM,UAAU,EAAE,WAsmBxB,CAAC;AAEF,sGAAsG;AACtG,eAAO,MAAM,UAAU,aAAa,CAAC;AAIrC,gEAAgE;AAChE,MAAM,WAAW,YAAY;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,YAAY,EAAE,CAAC;CACzB;AAsPD;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,EAAE,oBAoG/B,CAAC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAA+B,MAAM,uBAAuB,CAAC;AAEtF,OAAO,KAAK,EAAE,oBAAoB,EAAoE,MAAM,mBAAmB,CAAC;AAgFhI;gFACgF;AAChF,eAAO,MAAM,UAAU,EAAE,WAqrBxB,CAAC;AAEF,sGAAsG;AACtG,eAAO,MAAM,UAAU,aAAa,CAAC;AAIrC,gEAAgE;AAChE,MAAM,WAAW,YAAY;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,YAAY,EAAE,CAAC;CACzB;AA4kBD;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,EAAE,oBA8H/B,CAAC"}
package/dist/config.js CHANGED
@@ -5,6 +5,7 @@ import { createComponentRenderable } from './lib/index.js';
5
5
  import { schema } from './registry.js';
6
6
  import { BREADCRUMB_AUTO_SENTINEL } from './tags/breadcrumb.js';
7
7
  import { NAV_AUTO_SENTINEL } from './tags/nav.js';
8
+ import { XREF_RUNE_MARKER } from './tags/xref.js';
8
9
  // ─── Budget postTransform helpers ───
9
10
  const BUDGET_CURRENCY_SYMBOLS = {
10
11
  USD: '$', EUR: '€', GBP: '£', JPY: '¥', CNY: '¥',
@@ -68,7 +69,7 @@ function readPropText(node, prop) {
68
69
  }
69
70
  /** autoLabel entries shared by all PageSection-based runes */
70
71
  const pageSectionAutoLabel = {
71
- header: 'header', // <header> wrapper element
72
+ header: 'preamble', // <header> wrapper element → data-name="preamble"
72
73
  eyebrow: 'eyebrow', // property="eyebrow"
73
74
  headline: 'headline', // property="headline"
74
75
  blurb: 'blurb', // property="blurb"
@@ -82,11 +83,12 @@ export const coreConfig = {
82
83
  icons: {},
83
84
  runes: {
84
85
  // ─── Simple runes (block name only, engine adds BEM classes) ───
85
- Accordion: { block: 'accordion', autoLabel: pageSectionAutoLabel, editHints: { headline: 'inline', eyebrow: 'inline', blurb: 'inline' } },
86
- AccordionItem: { block: 'accordion-item', parent: 'Accordion', autoLabel: { name: 'header' }, editHints: { header: 'inline', body: 'none' } },
87
- Details: { block: 'details', autoLabel: { summary: 'summary' }, editHints: { summary: 'inline', body: 'none' } },
86
+ Accordion: { block: 'accordion', defaultDensity: 'full', sections: { preamble: 'preamble', headline: 'title', blurb: 'description' }, autoLabel: pageSectionAutoLabel, editHints: { headline: 'inline', eyebrow: 'inline', blurb: 'inline' } },
87
+ AccordionItem: { block: 'accordion-item', parent: 'Accordion', rootAttributes: { 'data-state': 'closed' }, autoLabel: { name: 'header' }, editHints: { header: 'inline', body: 'none' } },
88
+ Details: { block: 'details', defaultDensity: 'compact', sections: { summary: 'title' }, autoLabel: { summary: 'summary' }, editHints: { summary: 'inline', body: 'none' } },
88
89
  Grid: {
89
90
  block: 'grid',
91
+ defaultDensity: 'full',
90
92
  modifiers: {
91
93
  mode: { source: 'meta', default: 'columns' },
92
94
  collapse: { source: 'meta', noBemClass: true },
@@ -108,6 +110,7 @@ export const coreConfig = {
108
110
  },
109
111
  CodeGroup: {
110
112
  block: 'codegroup',
113
+ defaultDensity: 'compact',
111
114
  modifiers: { title: { source: 'meta' }, overflow: { source: 'meta', default: 'scroll' } },
112
115
  structure: {
113
116
  topbar: {
@@ -120,12 +123,14 @@ export const coreConfig = {
120
123
  ],
121
124
  },
122
125
  },
126
+ sections: { topbar: 'header', title: 'title' },
123
127
  editHints: { panel: 'code', title: 'none' },
124
128
  },
125
129
  PageSection: { block: 'page-section' },
126
130
  TableOfContents: { block: 'toc' },
127
131
  Embed: {
128
132
  block: 'embed',
133
+ defaultDensity: 'compact',
129
134
  editHints: { fallback: 'none' },
130
135
  postTransform(node) {
131
136
  const block = node.attributes.class?.split(' ')[0] || 'rf-embed';
@@ -166,10 +171,27 @@ export const coreConfig = {
166
171
  };
167
172
  },
168
173
  },
169
- Breadcrumb: { block: 'breadcrumb', editHints: { items: 'none' } },
174
+ Breadcrumb: { block: 'breadcrumb', defaultDensity: 'minimal', sections: { items: 'body' }, editHints: { items: 'none' } },
170
175
  BreadcrumbItem: { block: 'breadcrumb-item', parent: 'Breadcrumb' },
176
+ Blog: {
177
+ block: 'blog',
178
+ defaultDensity: 'full',
179
+ sections: { preamble: 'preamble', headline: 'title', blurb: 'description', content: 'body' },
180
+ contentWrapper: { tag: 'div', ref: 'content' },
181
+ modifiers: {
182
+ layout: { source: 'meta', default: 'list' },
183
+ sort: { source: 'meta', default: 'date-desc', noBemClass: true },
184
+ filter: { source: 'meta', noBemClass: true },
185
+ limit: { source: 'meta', noBemClass: true },
186
+ folder: { source: 'meta', noBemClass: true },
187
+ },
188
+ autoLabel: pageSectionAutoLabel,
189
+ editHints: { headline: 'inline', blurb: 'inline' },
190
+ },
171
191
  Budget: {
172
192
  block: 'budget',
193
+ defaultDensity: 'full',
194
+ sections: { header: 'header', title: 'title', footer: 'footer' },
173
195
  editHints: { title: 'none', meta: 'none', 'meta-item': 'none' },
174
196
  modifiers: {
175
197
  title: { source: 'meta' },
@@ -189,9 +211,9 @@ export const coreConfig = {
189
211
  {
190
212
  tag: 'div', ref: 'meta',
191
213
  children: [
192
- { tag: 'span', ref: 'meta-item', metaText: 'currency', condition: 'currency' },
193
- { tag: 'span', ref: 'meta-item', metaText: 'travelers', textPrefix: 'Travelers: ', condition: 'travelers' },
194
- { tag: 'span', ref: 'meta-item', metaText: 'duration', textPrefix: 'Duration: ', condition: 'duration' },
214
+ { tag: 'span', ref: 'meta-item', metaText: 'currency', condition: 'currency', metaType: 'category', metaRank: 'primary' },
215
+ { tag: 'span', ref: 'meta-item', metaText: 'travelers', label: 'Travelers:', condition: 'travelers', metaType: 'quantity', metaRank: 'primary' },
216
+ { tag: 'span', ref: 'meta-item', metaText: 'duration', label: 'Duration:', condition: 'duration', metaType: 'temporal', metaRank: 'secondary' },
195
217
  ],
196
218
  },
197
219
  ],
@@ -269,8 +291,10 @@ export const coreConfig = {
269
291
  // ─── Runes with modifier meta tags ───
270
292
  Hint: {
271
293
  block: 'hint',
294
+ defaultDensity: 'compact',
272
295
  modifiers: { hintType: { source: 'meta', default: 'note' } },
273
296
  contextModifiers: { 'hero': 'in-hero', 'feature': 'in-feature' },
297
+ sections: { header: 'header' },
274
298
  editHints: { icon: 'none', title: 'none' },
275
299
  structure: {
276
300
  header: {
@@ -284,14 +308,17 @@ export const coreConfig = {
284
308
  },
285
309
  Figure: {
286
310
  block: 'figure',
311
+ defaultDensity: 'compact',
287
312
  modifiers: {
288
313
  size: { source: 'meta', default: 'default' },
289
314
  align: { source: 'meta', default: 'center' },
290
315
  },
316
+ sections: { caption: 'description' },
291
317
  editHints: { caption: 'inline' },
292
318
  },
293
319
  Gallery: {
294
320
  block: 'gallery',
321
+ defaultDensity: 'full',
295
322
  modifiers: {
296
323
  layout: { source: 'meta', default: 'grid' },
297
324
  lightbox: { source: 'meta', default: 'true', noBemClass: true },
@@ -306,15 +333,18 @@ export const coreConfig = {
306
333
  },
307
334
  Sidenote: {
308
335
  block: 'sidenote',
336
+ defaultDensity: 'minimal',
309
337
  modifiers: { variant: { source: 'meta', default: 'sidenote' } },
338
+ sections: { body: 'body' },
310
339
  editHints: { body: 'inline' },
311
340
  },
312
341
  Compare: {
313
342
  block: 'compare',
343
+ defaultDensity: 'full',
314
344
  modifiers: { layout: { source: 'meta', default: 'side-by-side' } },
315
345
  editHints: { panels: 'none' },
316
346
  },
317
- Conversation: { block: 'conversation', editHints: { messages: 'none' } },
347
+ Conversation: { block: 'conversation', defaultDensity: 'compact', editHints: { messages: 'none' } },
318
348
  ConversationMessage: {
319
349
  block: 'conversation-message',
320
350
  parent: 'Conversation',
@@ -323,12 +353,15 @@ export const coreConfig = {
323
353
  },
324
354
  Annotate: {
325
355
  block: 'annotate',
356
+ defaultDensity: 'full',
326
357
  modifiers: { variant: { source: 'meta', default: 'margin' } },
358
+ sections: { body: 'body' },
327
359
  editHints: { body: 'none', notes: 'none' },
328
360
  },
329
361
  AnnotateNote: { block: 'annotate-note', parent: 'Annotate', editHints: { body: 'inline' } },
330
362
  Nav: {
331
363
  block: 'nav',
364
+ defaultDensity: 'compact',
332
365
  postTransform(node) {
333
366
  return { ...node, name: 'rf-nav' };
334
367
  },
@@ -364,11 +397,13 @@ export const coreConfig = {
364
397
  },
365
398
  Diff: {
366
399
  block: 'diff',
400
+ defaultDensity: 'compact',
367
401
  modifiers: { mode: { source: 'meta', default: 'unified' } },
368
402
  editHints: { line: 'none', 'gutter-num': 'none', 'gutter-prefix': 'none', 'line-content': 'none' },
369
403
  },
370
404
  Chart: {
371
405
  block: 'chart',
406
+ defaultDensity: 'compact',
372
407
  editHints: { data: 'none' },
373
408
  postTransform(node) {
374
409
  const block = node.attributes.class?.split(' ')[0] || 'rf-chart';
@@ -482,33 +517,42 @@ export const coreConfig = {
482
517
  // ─── Text formatting & layout runes ───
483
518
  PullQuote: {
484
519
  block: 'pullquote',
520
+ defaultDensity: 'compact',
485
521
  modifiers: {
486
522
  align: { source: 'meta', default: 'center' },
487
523
  variant: { source: 'meta', default: 'default' },
488
524
  },
525
+ sections: { body: 'body' },
489
526
  editHints: { body: 'inline' },
490
527
  },
491
528
  TextBlock: {
492
529
  block: 'textblock',
530
+ defaultDensity: 'full',
493
531
  modifiers: {
494
532
  dropcap: { source: 'meta' },
495
533
  columns: { source: 'meta' },
496
534
  lead: { source: 'meta' },
497
535
  align: { source: 'meta', default: 'left' },
498
536
  },
537
+ sections: { body: 'body' },
499
538
  editHints: { body: 'none' },
500
539
  },
501
540
  MediaText: {
502
541
  block: 'mediatext',
542
+ defaultDensity: 'full',
503
543
  modifiers: {
504
544
  align: { source: 'meta', default: 'left' },
505
545
  ratio: { source: 'meta', default: '1:1' },
506
546
  wrap: { source: 'meta' },
507
547
  },
548
+ sections: { body: 'body', media: 'media' },
549
+ mediaSlots: { media: 'cover' },
508
550
  editHints: { media: 'image', body: 'none' },
509
551
  },
510
552
  Showcase: {
511
553
  block: 'showcase',
554
+ defaultDensity: 'compact',
555
+ sections: { viewport: 'body' },
512
556
  modifiers: {
513
557
  shadow: { source: 'meta', default: 'none' },
514
558
  bleed: { source: 'meta', default: 'none' },
@@ -534,11 +578,13 @@ export const coreConfig = {
534
578
  },
535
579
  },
536
580
  // ─── Interactive runes (still get BEM classes, components add behavior) ───
537
- TabGroup: { block: 'tabs', autoLabel: pageSectionAutoLabel, editHints: { headline: 'inline', eyebrow: 'inline', blurb: 'inline' } },
538
- Tab: { block: 'tab', parent: 'TabGroup', editHints: { name: 'inline' } },
539
- TabPanel: { block: 'tab-panel', parent: 'TabGroup' },
581
+ TabGroup: { block: 'tabs', defaultDensity: 'full', sections: { preamble: 'preamble', headline: 'title', blurb: 'description' }, autoLabel: pageSectionAutoLabel, editHints: { headline: 'inline', eyebrow: 'inline', blurb: 'inline' } },
582
+ Tab: { block: 'tab', parent: 'TabGroup', rootAttributes: { 'data-state': 'inactive' }, editHints: { name: 'inline' } },
583
+ TabPanel: { block: 'tab-panel', parent: 'TabGroup', rootAttributes: { 'data-state': 'inactive' } },
540
584
  DataTable: {
541
585
  block: 'datatable',
586
+ defaultDensity: 'compact',
587
+ sections: { table: 'body' },
542
588
  modifiers: {
543
589
  searchable: { source: 'meta', default: 'false' },
544
590
  sortable: { source: 'meta' },
@@ -549,6 +595,8 @@ export const coreConfig = {
549
595
  },
550
596
  Form: {
551
597
  block: 'form',
598
+ defaultDensity: 'full',
599
+ sections: { body: 'body' },
552
600
  modifiers: {
553
601
  variant: { source: 'meta', default: 'stacked' },
554
602
  action: { source: 'meta' },
@@ -568,15 +616,34 @@ export const coreConfig = {
568
616
  },
569
617
  Reveal: {
570
618
  block: 'reveal',
619
+ defaultDensity: 'full',
571
620
  modifiers: {
572
621
  mode: { source: 'meta', default: 'click' },
573
622
  },
623
+ sections: { preamble: 'preamble', headline: 'title', blurb: 'description' },
574
624
  autoLabel: pageSectionAutoLabel,
575
625
  editHints: { headline: 'inline', eyebrow: 'inline', blurb: 'inline', steps: 'none' },
576
626
  },
577
- RevealStep: { block: 'reveal-step', parent: 'Reveal', editHints: { body: 'none' } },
627
+ RevealStep: { block: 'reveal-step', parent: 'Reveal', rootAttributes: { 'data-state': 'closed' }, editHints: { body: 'none' } },
628
+ Juxtapose: {
629
+ block: 'juxtapose',
630
+ defaultDensity: 'compact',
631
+ modifiers: {
632
+ variant: { source: 'meta', default: 'slider' },
633
+ orientation: { source: 'meta', default: 'vertical', noBemClass: true },
634
+ position: { source: 'meta', default: '50', noBemClass: true },
635
+ duration: { source: 'meta', default: '1000', noBemClass: true },
636
+ },
637
+ styles: {
638
+ position: '--jx-position',
639
+ duration: '--jx-duration',
640
+ },
641
+ editHints: { panels: 'none' },
642
+ },
643
+ JuxtaposePanel: { block: 'juxtapose-panel', parent: 'Juxtapose', rootAttributes: { 'data-state': 'inactive' }, editHints: { body: 'none' } },
578
644
  Diagram: {
579
645
  block: 'diagram',
646
+ defaultDensity: 'compact',
580
647
  editHints: { source: 'code' },
581
648
  postTransform(node) {
582
649
  const block = node.attributes.class?.split(' ')[0] || 'rf-diagram';
@@ -605,8 +672,12 @@ export const coreConfig = {
605
672
  };
606
673
  },
607
674
  },
675
+ Tint: { block: 'tint', parent: '*' },
676
+ Bg: { block: 'bg', parent: '*' },
677
+ Region: { block: 'region', parent: 'Layout' },
608
678
  Sandbox: {
609
679
  block: 'sandbox',
680
+ defaultDensity: 'compact',
610
681
  editHints: { source: 'code' },
611
682
  postTransform(node) {
612
683
  // Read meta values
@@ -616,14 +687,23 @@ export const coreConfig = {
616
687
  const label = readMeta(node, 'label') || '';
617
688
  const height = readMeta(node, 'height') || 'auto';
618
689
  const designTokens = readMeta(node, 'design-tokens') || '';
619
- // Keep non-meta children (fallback pre, source panels)
620
- const fallbackChildren = node.children.filter(child => {
621
- if (!isTag(child))
622
- return true;
623
- if (child.name === 'meta')
624
- return false;
625
- return true;
626
- });
690
+ // Keep non-meta children (fallback pre) and extract source panels
691
+ const fallbackChildren = [];
692
+ const sourcePanelOrigins = [];
693
+ for (const child of node.children) {
694
+ if (!isTag(child)) {
695
+ fallbackChildren.push(child);
696
+ continue;
697
+ }
698
+ if (child.name === 'meta') {
699
+ // Collect origin data from source panels
700
+ if (child.attributes?.['data-field'] === 'source-panel' && child.attributes?.['data-origin']) {
701
+ sourcePanelOrigins.push(`${child.attributes['data-label'] || ''}\t${child.attributes['data-origin']}`);
702
+ }
703
+ continue;
704
+ }
705
+ fallbackChildren.push(child);
706
+ }
627
707
  // Add hidden content div for web component
628
708
  const children = [
629
709
  ...fallbackChildren,
@@ -640,6 +720,7 @@ export const coreConfig = {
640
720
  ...(label ? { 'data-label': label } : {}),
641
721
  'data-height': height,
642
722
  ...(designTokens ? { 'data-design-tokens': designTokens } : {}),
723
+ ...(sourcePanelOrigins.length > 0 ? { 'data-source-origins': sourcePanelOrigins.join('\n') } : {}),
643
724
  },
644
725
  children,
645
726
  };
@@ -821,10 +902,282 @@ function buildAutoNav(pageUrl, pagesByUrl, ctx) {
821
902
  children: [itemsList],
822
903
  });
823
904
  }
905
+ function walkBlogTags(node, fn) {
906
+ if (Tag.isTag(node)) {
907
+ fn(node);
908
+ for (const child of node.children)
909
+ walkBlogTags(child, fn);
910
+ }
911
+ else if (Array.isArray(node)) {
912
+ node.forEach(n => walkBlogTags(n, fn));
913
+ }
914
+ }
915
+ function mapBlogTags(node, fn) {
916
+ if (Tag.isTag(node)) {
917
+ const mapped = fn(node);
918
+ if (mapped !== node)
919
+ return mapped;
920
+ const newChildren = node.children.map(c => mapBlogTags(c, fn));
921
+ const changed = newChildren.some((c, i) => c !== node.children[i]);
922
+ return changed ? new Tag(node.name, node.attributes, newChildren) : node;
923
+ }
924
+ if (Array.isArray(node))
925
+ return node.map(n => mapBlogTags(n, fn));
926
+ return node;
927
+ }
928
+ /** Normalise folder path for prefix matching: ensure leading slash and trailing slash */
929
+ function normaliseFolderPath(folder) {
930
+ let f = folder.trim();
931
+ if (!f.startsWith('/'))
932
+ f = '/' + f;
933
+ if (!f.endsWith('/'))
934
+ f += '/';
935
+ return f;
936
+ }
937
+ /** Check if a page URL is a direct child of the given folder */
938
+ function isInFolder(pageUrl, folder) {
939
+ if (!pageUrl.startsWith(folder))
940
+ return false;
941
+ const rest = pageUrl.slice(folder.length);
942
+ const segments = rest.replace(/\/$/, '').split('/').filter(Boolean);
943
+ return segments.length === 1;
944
+ }
945
+ /** Parse a simple filter expression like "tag:javascript" into field/value pairs */
946
+ function parseBlogFilter(filter) {
947
+ if (!filter || !filter.trim())
948
+ return [];
949
+ return filter.split(',').map(part => {
950
+ const colonIdx = part.indexOf(':');
951
+ if (colonIdx === -1)
952
+ return { field: part.trim(), value: '' };
953
+ return {
954
+ field: part.slice(0, colonIdx).trim(),
955
+ value: part.slice(colonIdx + 1).trim(),
956
+ };
957
+ });
958
+ }
959
+ /** Check if a post's frontmatter matches all filter conditions */
960
+ function matchesBlogFilter(post, filters) {
961
+ for (const { field, value } of filters) {
962
+ const fmValue = post.frontmatter[field];
963
+ if (value === '') {
964
+ if (fmValue === undefined || fmValue === null)
965
+ return false;
966
+ }
967
+ else if (Array.isArray(fmValue)) {
968
+ if (!fmValue.some(v => String(v).toLowerCase() === value.toLowerCase()))
969
+ return false;
970
+ }
971
+ else {
972
+ if (String(fmValue ?? '').toLowerCase() !== value.toLowerCase())
973
+ return false;
974
+ }
975
+ }
976
+ return true;
977
+ }
978
+ /** Sort blog posts by the specified order */
979
+ function sortBlogPosts(posts, sort) {
980
+ const sorted = [...posts];
981
+ switch (sort) {
982
+ case 'date-asc':
983
+ sorted.sort((a, b) => (a.date || '').localeCompare(b.date || ''));
984
+ break;
985
+ case 'title-asc':
986
+ sorted.sort((a, b) => a.title.localeCompare(b.title));
987
+ break;
988
+ case 'title-desc':
989
+ sorted.sort((a, b) => b.title.localeCompare(a.title));
990
+ break;
991
+ case 'date-desc':
992
+ default:
993
+ sorted.sort((a, b) => (b.date || '').localeCompare(a.date || ''));
994
+ break;
995
+ }
996
+ return sorted;
997
+ }
998
+ /** Build an <article> Tag for a single blog post entry */
999
+ function createBlogPostTag(post) {
1000
+ const titleTag = new Tag('h3', {}, [
1001
+ new Tag('a', { href: post.url }, [post.title]),
1002
+ ]);
1003
+ const children = [titleTag];
1004
+ if (post.date) {
1005
+ children.push(new Tag('time', { datetime: post.date }, [post.date]));
1006
+ }
1007
+ if (post.description) {
1008
+ children.push(new Tag('p', {}, [post.description]));
1009
+ }
1010
+ return new Tag('article', { 'data-name': 'post' }, children);
1011
+ }
1012
+ /** Resolve blog runes in a renderable tree by injecting matching posts */
1013
+ function resolveBlogPosts(renderable, allPosts, ctx, pageUrl) {
1014
+ let modified = false;
1015
+ const result = mapBlogTags(renderable, (tag) => {
1016
+ if (tag.attributes['data-rune'] !== 'blog')
1017
+ return tag;
1018
+ const folderMeta = tag.children.find((c) => Tag.isTag(c) && c.attributes['data-field'] === 'folder');
1019
+ const sortMeta = tag.children.find((c) => Tag.isTag(c) && c.attributes['data-field'] === 'sort');
1020
+ const filterMeta = tag.children.find((c) => Tag.isTag(c) && c.attributes['data-field'] === 'filter');
1021
+ const limitMeta = tag.children.find((c) => Tag.isTag(c) && c.attributes['data-field'] === 'limit');
1022
+ const folder = Tag.isTag(folderMeta) ? folderMeta.attributes.content : '';
1023
+ const sort = Tag.isTag(sortMeta) ? sortMeta.attributes.content : 'date-desc';
1024
+ const filterStr = Tag.isTag(filterMeta) ? filterMeta.attributes.content : '';
1025
+ const limitStr = Tag.isTag(limitMeta) ? limitMeta.attributes.content : '';
1026
+ const limit = limitStr ? parseInt(limitStr, 10) : undefined;
1027
+ if (!folder) {
1028
+ ctx.warn('Blog rune missing folder attribute', pageUrl);
1029
+ return tag;
1030
+ }
1031
+ const normalised = normaliseFolderPath(folder);
1032
+ const filters = parseBlogFilter(filterStr);
1033
+ let posts = allPosts.filter(post => {
1034
+ if (post.draft)
1035
+ return false;
1036
+ if (!isInFolder(post.url, normalised))
1037
+ return false;
1038
+ if (filters.length > 0 && !matchesBlogFilter(post, filters))
1039
+ return false;
1040
+ return true;
1041
+ });
1042
+ posts = sortBlogPosts(posts, sort);
1043
+ if (limit && limit > 0) {
1044
+ posts = posts.slice(0, limit);
1045
+ }
1046
+ const postsContainer = tag.children.find((c) => Tag.isTag(c) && c.attributes['data-name'] === 'posts');
1047
+ if (!Tag.isTag(postsContainer))
1048
+ return tag;
1049
+ const postTags = posts.map(createBlogPostTag);
1050
+ modified = true;
1051
+ const newPostsContainer = new Tag(postsContainer.name, postsContainer.attributes, postTags);
1052
+ const newChildren = tag.children.map((c) => c === postsContainer ? newPostsContainer : c);
1053
+ return new Tag(tag.name, tag.attributes, newChildren);
1054
+ });
1055
+ return modified ? result : renderable;
1056
+ }
1057
+ // ─── Xref resolution helpers ───
1058
+ /**
1059
+ * Find an entity by exact ID across all types in the registry.
1060
+ * If typeHint is provided, only search that type.
1061
+ */
1062
+ function findEntityById(registry, id, typeHint) {
1063
+ const types = typeHint ? [typeHint] : registry.getTypes();
1064
+ for (const type of types) {
1065
+ const entity = registry.getById(type, id);
1066
+ if (entity)
1067
+ return { entity, ambiguous: false };
1068
+ }
1069
+ return undefined;
1070
+ }
1071
+ /**
1072
+ * Find entities by name/title match (case-insensitive) across all types.
1073
+ * If typeHint is provided, only search that type.
1074
+ */
1075
+ function findEntitiesByName(registry, name, typeHint) {
1076
+ const nameLower = name.toLowerCase();
1077
+ const types = typeHint ? [typeHint] : registry.getTypes();
1078
+ const matches = [];
1079
+ for (const type of types) {
1080
+ for (const entity of registry.getAll(type)) {
1081
+ const entityName = entity.data.name ?? '';
1082
+ const entityTitle = entity.data.title ?? '';
1083
+ if (entityName.toLowerCase() === nameLower || entityTitle.toLowerCase() === nameLower) {
1084
+ matches.push(entity);
1085
+ }
1086
+ }
1087
+ }
1088
+ return matches;
1089
+ }
1090
+ /** Resolve an entity's URL for use as an href */
1091
+ function resolveEntityHref(entity) {
1092
+ const baseUrl = entity.data.url || entity.sourceUrl;
1093
+ const headingId = entity.data.headingId;
1094
+ if (headingId)
1095
+ return `${baseUrl}#${headingId}`;
1096
+ return baseUrl;
1097
+ }
1098
+ /** Walk a Markdoc renderable tree, resolving any xref placeholders */
1099
+ function resolveXrefs(renderable, pageUrl, registry, ctx) {
1100
+ if (!Tag.isTag(renderable)) {
1101
+ if (Array.isArray(renderable)) {
1102
+ const newChildren = renderable.map(c => resolveXrefs(c, pageUrl, registry, ctx));
1103
+ if (newChildren.every((c, i) => c === renderable[i]))
1104
+ return renderable;
1105
+ return newChildren;
1106
+ }
1107
+ return renderable;
1108
+ }
1109
+ const tag = renderable;
1110
+ // Check if this is an xref placeholder
1111
+ if (tag.attributes?.['data-rune'] === XREF_RUNE_MARKER) {
1112
+ const id = tag.attributes['data-xref-id'];
1113
+ const label = tag.attributes['data-xref-label'];
1114
+ const typeHint = tag.attributes['data-xref-type'];
1115
+ // Try exact ID match first
1116
+ const idMatch = findEntityById(registry, id, typeHint);
1117
+ if (idMatch) {
1118
+ const entity = idMatch.entity;
1119
+ const href = resolveEntityHref(entity);
1120
+ const text = label || entity.data.title || entity.data.name || entity.data.text || id;
1121
+ if (entity.sourceUrl === pageUrl) {
1122
+ ctx.info(`xref "${id}" on ${pageUrl} — references itself`, pageUrl);
1123
+ }
1124
+ return new Tag('a', {
1125
+ class: `rf-xref rf-xref--${entity.type}`,
1126
+ href,
1127
+ 'data-entity-type': entity.type,
1128
+ 'data-entity-id': entity.id,
1129
+ }, [text]);
1130
+ }
1131
+ // Try name/title match
1132
+ const nameMatches = findEntitiesByName(registry, id, typeHint);
1133
+ if (nameMatches.length === 1) {
1134
+ const entity = nameMatches[0];
1135
+ const href = resolveEntityHref(entity);
1136
+ const text = label || entity.data.title || entity.data.name || entity.data.text || id;
1137
+ if (entity.sourceUrl === pageUrl) {
1138
+ ctx.info(`xref "${id}" on ${pageUrl} — references itself`, pageUrl);
1139
+ }
1140
+ return new Tag('a', {
1141
+ class: `rf-xref rf-xref--${entity.type}`,
1142
+ href,
1143
+ 'data-entity-type': entity.type,
1144
+ 'data-entity-id': entity.id,
1145
+ }, [text]);
1146
+ }
1147
+ if (nameMatches.length > 1) {
1148
+ const matchList = nameMatches
1149
+ .map(e => `${e.type} "${e.data.title || e.data.name || e.id}" on ${e.sourceUrl}`)
1150
+ .join(', ');
1151
+ ctx.warn(`xref "${id}" on ${pageUrl} — matches ${nameMatches.length} entities (${matchList}). Add type hint to disambiguate.`, pageUrl);
1152
+ // Use first match
1153
+ const entity = nameMatches[0];
1154
+ const href = resolveEntityHref(entity);
1155
+ const text = label || entity.data.title || entity.data.name || entity.data.text || id;
1156
+ return new Tag('a', {
1157
+ class: `rf-xref rf-xref--${entity.type}`,
1158
+ href,
1159
+ 'data-entity-type': entity.type,
1160
+ 'data-entity-id': entity.id,
1161
+ }, [text]);
1162
+ }
1163
+ // No match — unresolved
1164
+ ctx.warn(`xref "${id}" on ${pageUrl} — entity not found`, pageUrl);
1165
+ return new Tag('span', {
1166
+ class: 'rf-xref rf-xref--unresolved',
1167
+ 'data-entity-id': id,
1168
+ }, [label || id]);
1169
+ }
1170
+ // Recurse into children
1171
+ const newChildren = (tag.children ?? []).map((c) => resolveXrefs(c, pageUrl, registry, ctx));
1172
+ if (newChildren.every((c, i) => c === tag.children[i]))
1173
+ return tag;
1174
+ return { ...tag, children: newChildren };
1175
+ }
824
1176
  /**
825
1177
  * Core cross-page pipeline hooks.
826
1178
  * Run for every site, before any community package hooks.
827
- * Registers page and heading entities, aggregates the page tree and breadcrumb paths.
1179
+ * Registers page and heading entities, aggregates the page tree and breadcrumb paths,
1180
+ * and resolves blog post listings.
828
1181
  */
829
1182
  export const corePipelineHooks = {
830
1183
  register(pages, registry, ctx) {
@@ -879,7 +1232,16 @@ export const corePipelineHooks = {
879
1232
  for (const h of registry.getAll('heading')) {
880
1233
  headingIndex.set(h.id, h.data);
881
1234
  }
882
- return { pageTree, breadcrumbPaths, pagesByUrl, headingIndex };
1235
+ // Blog: collect all pages as potential blog posts
1236
+ const allPosts = pageEntities.map(e => ({
1237
+ title: e.data.title || '',
1238
+ url: e.data.url || e.id,
1239
+ date: e.data.date || '',
1240
+ description: e.data.description || '',
1241
+ draft: e.data.draft || false,
1242
+ frontmatter: e.data,
1243
+ }));
1244
+ return { pageTree, breadcrumbPaths, pagesByUrl, headingIndex, allPosts, registry };
883
1245
  },
884
1246
  postProcess(page, aggregated, ctx) {
885
1247
  const coreData = aggregated['__core__'];
@@ -887,6 +1249,8 @@ export const corePipelineHooks = {
887
1249
  return page;
888
1250
  let renderable = resolveAutoBreadcrumbs(page.renderable, page.url, coreData.breadcrumbPaths, coreData.pagesByUrl, ctx);
889
1251
  renderable = resolveAutoNavs(renderable, page.url, coreData.pagesByUrl, ctx);
1252
+ renderable = resolveBlogPosts(renderable, coreData.allPosts, ctx, page.url);
1253
+ renderable = resolveXrefs(renderable, page.url, coreData.registry, ctx);
890
1254
  if (renderable === page.renderable)
891
1255
  return page;
892
1256
  return { ...page, renderable };