@refrakt-md/runes 0.8.5 → 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 (42) hide show
  1. package/dist/config.d.ts.map +1 -1
  2. package/dist/config.js +193 -23
  3. package/dist/config.js.map +1 -1
  4. package/dist/fence-escape.d.ts +19 -0
  5. package/dist/fence-escape.d.ts.map +1 -0
  6. package/dist/fence-escape.js +53 -0
  7. package/dist/fence-escape.js.map +1 -0
  8. package/dist/index.d.ts +5 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +11 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/lib/index.d.ts +2 -0
  13. package/dist/lib/index.d.ts.map +1 -1
  14. package/dist/lib/index.js +1 -0
  15. package/dist/lib/index.js.map +1 -1
  16. package/dist/nodes.d.ts.map +1 -1
  17. package/dist/nodes.js +2 -1
  18. package/dist/nodes.js.map +1 -1
  19. package/dist/registry.d.ts +2 -0
  20. package/dist/registry.d.ts.map +1 -1
  21. package/dist/registry.js +2 -0
  22. package/dist/registry.js.map +1 -1
  23. package/dist/sandbox-sources.d.ts +34 -0
  24. package/dist/sandbox-sources.d.ts.map +1 -0
  25. package/dist/sandbox-sources.js +204 -0
  26. package/dist/sandbox-sources.js.map +1 -0
  27. package/dist/schema/xref.d.ts +5 -0
  28. package/dist/schema/xref.d.ts.map +1 -0
  29. package/dist/schema/xref.js +7 -0
  30. package/dist/schema/xref.js.map +1 -0
  31. package/dist/tags/common.d.ts +27 -0
  32. package/dist/tags/common.d.ts.map +1 -1
  33. package/dist/tags/common.js +53 -0
  34. package/dist/tags/common.js.map +1 -1
  35. package/dist/tags/sandbox.d.ts.map +1 -1
  36. package/dist/tags/sandbox.js +56 -9
  37. package/dist/tags/sandbox.js.map +1 -1
  38. package/dist/tags/xref.d.ts +18 -0
  39. package/dist/tags/xref.d.ts.map +1 -0
  40. package/dist/tags/xref.js +54 -0
  41. package/dist/tags/xref.js.map +1 -0
  42. package/package.json +3 -3
@@ -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,WAqoBxB,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;AAgbD;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,EAAE,oBAsH/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,12 @@ 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' },
171
176
  Blog: {
172
177
  block: 'blog',
178
+ defaultDensity: 'full',
179
+ sections: { preamble: 'preamble', headline: 'title', blurb: 'description', content: 'body' },
173
180
  contentWrapper: { tag: 'div', ref: 'content' },
174
181
  modifiers: {
175
182
  layout: { source: 'meta', default: 'list' },
@@ -183,6 +190,8 @@ export const coreConfig = {
183
190
  },
184
191
  Budget: {
185
192
  block: 'budget',
193
+ defaultDensity: 'full',
194
+ sections: { header: 'header', title: 'title', footer: 'footer' },
186
195
  editHints: { title: 'none', meta: 'none', 'meta-item': 'none' },
187
196
  modifiers: {
188
197
  title: { source: 'meta' },
@@ -202,9 +211,9 @@ export const coreConfig = {
202
211
  {
203
212
  tag: 'div', ref: 'meta',
204
213
  children: [
205
- { tag: 'span', ref: 'meta-item', metaText: 'currency', condition: 'currency' },
206
- { tag: 'span', ref: 'meta-item', metaText: 'travelers', textPrefix: 'Travelers: ', condition: 'travelers' },
207
- { 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' },
208
217
  ],
209
218
  },
210
219
  ],
@@ -282,8 +291,10 @@ export const coreConfig = {
282
291
  // ─── Runes with modifier meta tags ───
283
292
  Hint: {
284
293
  block: 'hint',
294
+ defaultDensity: 'compact',
285
295
  modifiers: { hintType: { source: 'meta', default: 'note' } },
286
296
  contextModifiers: { 'hero': 'in-hero', 'feature': 'in-feature' },
297
+ sections: { header: 'header' },
287
298
  editHints: { icon: 'none', title: 'none' },
288
299
  structure: {
289
300
  header: {
@@ -297,14 +308,17 @@ export const coreConfig = {
297
308
  },
298
309
  Figure: {
299
310
  block: 'figure',
311
+ defaultDensity: 'compact',
300
312
  modifiers: {
301
313
  size: { source: 'meta', default: 'default' },
302
314
  align: { source: 'meta', default: 'center' },
303
315
  },
316
+ sections: { caption: 'description' },
304
317
  editHints: { caption: 'inline' },
305
318
  },
306
319
  Gallery: {
307
320
  block: 'gallery',
321
+ defaultDensity: 'full',
308
322
  modifiers: {
309
323
  layout: { source: 'meta', default: 'grid' },
310
324
  lightbox: { source: 'meta', default: 'true', noBemClass: true },
@@ -319,15 +333,18 @@ export const coreConfig = {
319
333
  },
320
334
  Sidenote: {
321
335
  block: 'sidenote',
336
+ defaultDensity: 'minimal',
322
337
  modifiers: { variant: { source: 'meta', default: 'sidenote' } },
338
+ sections: { body: 'body' },
323
339
  editHints: { body: 'inline' },
324
340
  },
325
341
  Compare: {
326
342
  block: 'compare',
343
+ defaultDensity: 'full',
327
344
  modifiers: { layout: { source: 'meta', default: 'side-by-side' } },
328
345
  editHints: { panels: 'none' },
329
346
  },
330
- Conversation: { block: 'conversation', editHints: { messages: 'none' } },
347
+ Conversation: { block: 'conversation', defaultDensity: 'compact', editHints: { messages: 'none' } },
331
348
  ConversationMessage: {
332
349
  block: 'conversation-message',
333
350
  parent: 'Conversation',
@@ -336,12 +353,15 @@ export const coreConfig = {
336
353
  },
337
354
  Annotate: {
338
355
  block: 'annotate',
356
+ defaultDensity: 'full',
339
357
  modifiers: { variant: { source: 'meta', default: 'margin' } },
358
+ sections: { body: 'body' },
340
359
  editHints: { body: 'none', notes: 'none' },
341
360
  },
342
361
  AnnotateNote: { block: 'annotate-note', parent: 'Annotate', editHints: { body: 'inline' } },
343
362
  Nav: {
344
363
  block: 'nav',
364
+ defaultDensity: 'compact',
345
365
  postTransform(node) {
346
366
  return { ...node, name: 'rf-nav' };
347
367
  },
@@ -377,11 +397,13 @@ export const coreConfig = {
377
397
  },
378
398
  Diff: {
379
399
  block: 'diff',
400
+ defaultDensity: 'compact',
380
401
  modifiers: { mode: { source: 'meta', default: 'unified' } },
381
402
  editHints: { line: 'none', 'gutter-num': 'none', 'gutter-prefix': 'none', 'line-content': 'none' },
382
403
  },
383
404
  Chart: {
384
405
  block: 'chart',
406
+ defaultDensity: 'compact',
385
407
  editHints: { data: 'none' },
386
408
  postTransform(node) {
387
409
  const block = node.attributes.class?.split(' ')[0] || 'rf-chart';
@@ -495,33 +517,42 @@ export const coreConfig = {
495
517
  // ─── Text formatting & layout runes ───
496
518
  PullQuote: {
497
519
  block: 'pullquote',
520
+ defaultDensity: 'compact',
498
521
  modifiers: {
499
522
  align: { source: 'meta', default: 'center' },
500
523
  variant: { source: 'meta', default: 'default' },
501
524
  },
525
+ sections: { body: 'body' },
502
526
  editHints: { body: 'inline' },
503
527
  },
504
528
  TextBlock: {
505
529
  block: 'textblock',
530
+ defaultDensity: 'full',
506
531
  modifiers: {
507
532
  dropcap: { source: 'meta' },
508
533
  columns: { source: 'meta' },
509
534
  lead: { source: 'meta' },
510
535
  align: { source: 'meta', default: 'left' },
511
536
  },
537
+ sections: { body: 'body' },
512
538
  editHints: { body: 'none' },
513
539
  },
514
540
  MediaText: {
515
541
  block: 'mediatext',
542
+ defaultDensity: 'full',
516
543
  modifiers: {
517
544
  align: { source: 'meta', default: 'left' },
518
545
  ratio: { source: 'meta', default: '1:1' },
519
546
  wrap: { source: 'meta' },
520
547
  },
548
+ sections: { body: 'body', media: 'media' },
549
+ mediaSlots: { media: 'cover' },
521
550
  editHints: { media: 'image', body: 'none' },
522
551
  },
523
552
  Showcase: {
524
553
  block: 'showcase',
554
+ defaultDensity: 'compact',
555
+ sections: { viewport: 'body' },
525
556
  modifiers: {
526
557
  shadow: { source: 'meta', default: 'none' },
527
558
  bleed: { source: 'meta', default: 'none' },
@@ -547,11 +578,13 @@ export const coreConfig = {
547
578
  },
548
579
  },
549
580
  // ─── Interactive runes (still get BEM classes, components add behavior) ───
550
- TabGroup: { block: 'tabs', autoLabel: pageSectionAutoLabel, editHints: { headline: 'inline', eyebrow: 'inline', blurb: 'inline' } },
551
- Tab: { block: 'tab', parent: 'TabGroup', editHints: { name: 'inline' } },
552
- 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' } },
553
584
  DataTable: {
554
585
  block: 'datatable',
586
+ defaultDensity: 'compact',
587
+ sections: { table: 'body' },
555
588
  modifiers: {
556
589
  searchable: { source: 'meta', default: 'false' },
557
590
  sortable: { source: 'meta' },
@@ -562,6 +595,8 @@ export const coreConfig = {
562
595
  },
563
596
  Form: {
564
597
  block: 'form',
598
+ defaultDensity: 'full',
599
+ sections: { body: 'body' },
565
600
  modifiers: {
566
601
  variant: { source: 'meta', default: 'stacked' },
567
602
  action: { source: 'meta' },
@@ -581,15 +616,18 @@ export const coreConfig = {
581
616
  },
582
617
  Reveal: {
583
618
  block: 'reveal',
619
+ defaultDensity: 'full',
584
620
  modifiers: {
585
621
  mode: { source: 'meta', default: 'click' },
586
622
  },
623
+ sections: { preamble: 'preamble', headline: 'title', blurb: 'description' },
587
624
  autoLabel: pageSectionAutoLabel,
588
625
  editHints: { headline: 'inline', eyebrow: 'inline', blurb: 'inline', steps: 'none' },
589
626
  },
590
- RevealStep: { block: 'reveal-step', parent: 'Reveal', editHints: { body: 'none' } },
627
+ RevealStep: { block: 'reveal-step', parent: 'Reveal', rootAttributes: { 'data-state': 'closed' }, editHints: { body: 'none' } },
591
628
  Juxtapose: {
592
629
  block: 'juxtapose',
630
+ defaultDensity: 'compact',
593
631
  modifiers: {
594
632
  variant: { source: 'meta', default: 'slider' },
595
633
  orientation: { source: 'meta', default: 'vertical', noBemClass: true },
@@ -602,9 +640,10 @@ export const coreConfig = {
602
640
  },
603
641
  editHints: { panels: 'none' },
604
642
  },
605
- JuxtaposePanel: { block: 'juxtapose-panel', parent: 'Juxtapose', editHints: { body: 'none' } },
643
+ JuxtaposePanel: { block: 'juxtapose-panel', parent: 'Juxtapose', rootAttributes: { 'data-state': 'inactive' }, editHints: { body: 'none' } },
606
644
  Diagram: {
607
645
  block: 'diagram',
646
+ defaultDensity: 'compact',
608
647
  editHints: { source: 'code' },
609
648
  postTransform(node) {
610
649
  const block = node.attributes.class?.split(' ')[0] || 'rf-diagram';
@@ -638,6 +677,7 @@ export const coreConfig = {
638
677
  Region: { block: 'region', parent: 'Layout' },
639
678
  Sandbox: {
640
679
  block: 'sandbox',
680
+ defaultDensity: 'compact',
641
681
  editHints: { source: 'code' },
642
682
  postTransform(node) {
643
683
  // Read meta values
@@ -647,14 +687,23 @@ export const coreConfig = {
647
687
  const label = readMeta(node, 'label') || '';
648
688
  const height = readMeta(node, 'height') || 'auto';
649
689
  const designTokens = readMeta(node, 'design-tokens') || '';
650
- // Keep non-meta children (fallback pre, source panels)
651
- const fallbackChildren = node.children.filter(child => {
652
- if (!isTag(child))
653
- return true;
654
- if (child.name === 'meta')
655
- return false;
656
- return true;
657
- });
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
+ }
658
707
  // Add hidden content div for web component
659
708
  const children = [
660
709
  ...fallbackChildren,
@@ -671,6 +720,7 @@ export const coreConfig = {
671
720
  ...(label ? { 'data-label': label } : {}),
672
721
  'data-height': height,
673
722
  ...(designTokens ? { 'data-design-tokens': designTokens } : {}),
723
+ ...(sourcePanelOrigins.length > 0 ? { 'data-source-origins': sourcePanelOrigins.join('\n') } : {}),
674
724
  },
675
725
  children,
676
726
  };
@@ -1004,6 +1054,125 @@ function resolveBlogPosts(renderable, allPosts, ctx, pageUrl) {
1004
1054
  });
1005
1055
  return modified ? result : renderable;
1006
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
+ }
1007
1176
  /**
1008
1177
  * Core cross-page pipeline hooks.
1009
1178
  * Run for every site, before any community package hooks.
@@ -1072,7 +1241,7 @@ export const corePipelineHooks = {
1072
1241
  draft: e.data.draft || false,
1073
1242
  frontmatter: e.data,
1074
1243
  }));
1075
- return { pageTree, breadcrumbPaths, pagesByUrl, headingIndex, allPosts };
1244
+ return { pageTree, breadcrumbPaths, pagesByUrl, headingIndex, allPosts, registry };
1076
1245
  },
1077
1246
  postProcess(page, aggregated, ctx) {
1078
1247
  const coreData = aggregated['__core__'];
@@ -1081,6 +1250,7 @@ export const corePipelineHooks = {
1081
1250
  let renderable = resolveAutoBreadcrumbs(page.renderable, page.url, coreData.breadcrumbPaths, coreData.pagesByUrl, ctx);
1082
1251
  renderable = resolveAutoNavs(renderable, page.url, coreData.pagesByUrl, ctx);
1083
1252
  renderable = resolveBlogPosts(renderable, coreData.allPosts, ctx, page.url);
1253
+ renderable = resolveXrefs(renderable, page.url, coreData.registry, ctx);
1084
1254
  if (renderable === page.renderable)
1085
1255
  return page;
1086
1256
  return { ...page, renderable };