@refrakt-md/runes 0.11.3 → 0.14.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.
- package/dist/config.d.ts +38 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +605 -2
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -3
- package/dist/index.js.map +1 -1
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +1 -0
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/sanitize.d.ts +28 -0
- package/dist/lib/sanitize.d.ts.map +1 -0
- package/dist/lib/sanitize.js +65 -0
- package/dist/lib/sanitize.js.map +1 -0
- package/dist/{packages.d.ts → plugins.d.ts} +29 -29
- package/dist/plugins.d.ts.map +1 -0
- package/dist/{packages.js → plugins.js} +45 -68
- package/dist/plugins.js.map +1 -0
- package/dist/reference.d.ts +8 -8
- package/dist/reference.d.ts.map +1 -1
- package/dist/reference.js +12 -12
- package/dist/reference.js.map +1 -1
- package/dist/tags/nav.d.ts +2 -0
- package/dist/tags/nav.d.ts.map +1 -1
- package/dist/tags/nav.js +45 -9
- package/dist/tags/nav.js.map +1 -1
- package/dist/tags/pagination.d.ts +5 -0
- package/dist/tags/pagination.d.ts.map +1 -0
- package/dist/tags/pagination.js +62 -0
- package/dist/tags/pagination.js.map +1 -0
- package/dist/tags/sandbox.d.ts.map +1 -1
- package/dist/tags/sandbox.js +23 -5
- package/dist/tags/sandbox.js.map +1 -1
- package/package.json +3 -3
- package/dist/packages.d.ts.map +0 -1
- package/dist/packages.js.map +0 -1
package/dist/config.js
CHANGED
|
@@ -3,7 +3,8 @@ import Markdoc from '@markdoc/markdoc';
|
|
|
3
3
|
const { Tag } = Markdoc;
|
|
4
4
|
import { createComponentRenderable } from './lib/index.js';
|
|
5
5
|
import { BREADCRUMB_AUTO_SENTINEL } from './tags/breadcrumb.js';
|
|
6
|
-
import { NAV_AUTO_SENTINEL } from './tags/nav.js';
|
|
6
|
+
import { NAV_AUTO_SENTINEL, NAV_COLLAPSED_AUTO } from './tags/nav.js';
|
|
7
|
+
import { PAGINATION_AUTO_SENTINEL } from './tags/pagination.js';
|
|
7
8
|
import { resolveXrefs } from './xref-resolve.js';
|
|
8
9
|
// ─── Budget postTransform helpers ───
|
|
9
10
|
const BUDGET_CURRENCY_SYMBOLS = {
|
|
@@ -358,11 +359,21 @@ export const coreConfig = {
|
|
|
358
359
|
Nav: {
|
|
359
360
|
block: 'nav',
|
|
360
361
|
defaultDensity: 'compact',
|
|
362
|
+
modifiers: {
|
|
363
|
+
layout: { source: 'attribute' },
|
|
364
|
+
},
|
|
361
365
|
postTransform(node) {
|
|
362
366
|
return { ...node, name: 'rf-nav' };
|
|
363
367
|
},
|
|
364
368
|
},
|
|
365
369
|
NavGroup: { block: 'nav-group', parent: 'Nav' },
|
|
370
|
+
Pagination: {
|
|
371
|
+
block: 'pagination',
|
|
372
|
+
defaultDensity: 'compact',
|
|
373
|
+
modifiers: {
|
|
374
|
+
scope: { source: 'meta', default: 'siblings', noBemClass: true },
|
|
375
|
+
},
|
|
376
|
+
},
|
|
366
377
|
NavItem: {
|
|
367
378
|
block: 'nav-item',
|
|
368
379
|
parent: 'Nav',
|
|
@@ -689,6 +700,9 @@ export const coreConfig = {
|
|
|
689
700
|
const label = readMeta(node, 'label') || '';
|
|
690
701
|
const height = readMeta(node, 'height') || 'auto';
|
|
691
702
|
const designTokens = readMeta(node, 'design-tokens') || '';
|
|
703
|
+
const securityMode = readMeta(node, 'security-mode') || 'trusted';
|
|
704
|
+
const allowJs = readMeta(node, 'allow-js') || 'true';
|
|
705
|
+
const sandboxOrigin = readMeta(node, 'sandbox-origin') || '';
|
|
692
706
|
// Keep non-meta children (fallback pre) and extract source panels
|
|
693
707
|
const fallbackChildren = [];
|
|
694
708
|
const sourcePanelOrigins = [];
|
|
@@ -729,6 +743,9 @@ export const coreConfig = {
|
|
|
729
743
|
'data-height': height,
|
|
730
744
|
...(designTokens ? { 'data-design-tokens': designTokens } : {}),
|
|
731
745
|
...(sourcePanelOrigins.length > 0 ? { 'data-source-origins': sourcePanelOrigins.join('\n') } : {}),
|
|
746
|
+
'data-security-mode': securityMode,
|
|
747
|
+
'data-allow-js': allowJs,
|
|
748
|
+
...(sandboxOrigin ? { 'data-sandbox-origin': sandboxOrigin } : {}),
|
|
732
749
|
},
|
|
733
750
|
children,
|
|
734
751
|
};
|
|
@@ -910,6 +927,559 @@ function buildAutoNav(pageUrl, pagesByUrl, ctx) {
|
|
|
910
927
|
children: [itemsList],
|
|
911
928
|
});
|
|
912
929
|
}
|
|
930
|
+
// ─── Collapsible nav auto-open ───
|
|
931
|
+
function sharedPrefixLength(a, b) {
|
|
932
|
+
let i = 0;
|
|
933
|
+
while (i < a.length && i < b.length && a[i] === b[i])
|
|
934
|
+
i++;
|
|
935
|
+
return i;
|
|
936
|
+
}
|
|
937
|
+
/** Resolve a slug-only nav item to a page URL using the pagesByUrl registry.
|
|
938
|
+
* Mirrors the runtime resolution in @refrakt-md/behaviors RfNav. */
|
|
939
|
+
function resolveSlugToUrl(slug, pagesByUrl, currentUrl) {
|
|
940
|
+
const candidates = Array.from(pagesByUrl.values()).filter(p => p.url.endsWith('/' + slug) || p.url === '/' + slug);
|
|
941
|
+
if (candidates.length === 0)
|
|
942
|
+
return null;
|
|
943
|
+
if (candidates.length === 1)
|
|
944
|
+
return candidates[0].url;
|
|
945
|
+
return candidates
|
|
946
|
+
.slice()
|
|
947
|
+
.sort((a, b) => sharedPrefixLength(b.url, currentUrl) - sharedPrefixLength(a.url, currentUrl))[0]
|
|
948
|
+
.url;
|
|
949
|
+
}
|
|
950
|
+
/** Read a NavItem's slug from either `data-slug` (post-engine) or a nested
|
|
951
|
+
* `<span data-field="slug">` (pre-engine, e.g. layout regions). */
|
|
952
|
+
function readNavItemSlug(item) {
|
|
953
|
+
const direct = item.attributes?.['data-slug'];
|
|
954
|
+
if (direct)
|
|
955
|
+
return String(direct);
|
|
956
|
+
const findSpan = (node) => {
|
|
957
|
+
if (!Tag.isTag(node)) {
|
|
958
|
+
if (Array.isArray(node)) {
|
|
959
|
+
for (const c of node) {
|
|
960
|
+
const f = findSpan(c);
|
|
961
|
+
if (f)
|
|
962
|
+
return f;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
return null;
|
|
966
|
+
}
|
|
967
|
+
const t = node;
|
|
968
|
+
if (t.name === 'span' && t.attributes?.['data-field'] === 'slug') {
|
|
969
|
+
const parts = [];
|
|
970
|
+
for (const c of t.children ?? []) {
|
|
971
|
+
if (typeof c === 'string')
|
|
972
|
+
parts.push(c);
|
|
973
|
+
}
|
|
974
|
+
return parts.join('').trim() || null;
|
|
975
|
+
}
|
|
976
|
+
for (const c of t.children ?? []) {
|
|
977
|
+
const f = findSpan(c);
|
|
978
|
+
if (f)
|
|
979
|
+
return f;
|
|
980
|
+
}
|
|
981
|
+
return null;
|
|
982
|
+
};
|
|
983
|
+
return findSpan(item);
|
|
984
|
+
}
|
|
985
|
+
/** Collect URLs covered by a NavGroup's items: explicit hrefs + resolved slugs. */
|
|
986
|
+
function collectGroupItemUrls(group, pagesByUrl, currentUrl) {
|
|
987
|
+
const urls = [];
|
|
988
|
+
const walk = (node) => {
|
|
989
|
+
if (!Tag.isTag(node)) {
|
|
990
|
+
if (Array.isArray(node))
|
|
991
|
+
node.forEach(walk);
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
const t = node;
|
|
995
|
+
if (t.attributes?.['data-rune'] === 'nav-item') {
|
|
996
|
+
const href = findNavItemHref(t);
|
|
997
|
+
if (href) {
|
|
998
|
+
urls.push(href);
|
|
999
|
+
}
|
|
1000
|
+
else {
|
|
1001
|
+
const slug = readNavItemSlug(t);
|
|
1002
|
+
if (slug) {
|
|
1003
|
+
const resolved = resolveSlugToUrl(slug, pagesByUrl, currentUrl);
|
|
1004
|
+
if (resolved)
|
|
1005
|
+
urls.push(resolved);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
(t.children ?? []).forEach(walk);
|
|
1010
|
+
};
|
|
1011
|
+
(group.children ?? []).forEach(walk);
|
|
1012
|
+
return urls;
|
|
1013
|
+
}
|
|
1014
|
+
function extractGroupTitle(group) {
|
|
1015
|
+
const findText = (node) => {
|
|
1016
|
+
if (typeof node === 'string')
|
|
1017
|
+
return node;
|
|
1018
|
+
if (!Tag.isTag(node)) {
|
|
1019
|
+
if (Array.isArray(node)) {
|
|
1020
|
+
for (const c of node) {
|
|
1021
|
+
const found = findText(c);
|
|
1022
|
+
if (found)
|
|
1023
|
+
return found;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
return null;
|
|
1027
|
+
}
|
|
1028
|
+
const t = node;
|
|
1029
|
+
if (t.attributes?.['data-field'] === 'title') {
|
|
1030
|
+
const parts = [];
|
|
1031
|
+
for (const c of t.children ?? []) {
|
|
1032
|
+
if (typeof c === 'string')
|
|
1033
|
+
parts.push(c);
|
|
1034
|
+
}
|
|
1035
|
+
if (parts.length > 0)
|
|
1036
|
+
return parts.join('').trim();
|
|
1037
|
+
}
|
|
1038
|
+
for (const c of t.children ?? []) {
|
|
1039
|
+
const found = findText(c);
|
|
1040
|
+
if (found)
|
|
1041
|
+
return found;
|
|
1042
|
+
}
|
|
1043
|
+
return null;
|
|
1044
|
+
};
|
|
1045
|
+
return findText(group) ?? '';
|
|
1046
|
+
}
|
|
1047
|
+
function urlMatchesGroup(currentUrl, itemUrls) {
|
|
1048
|
+
for (const url of itemUrls) {
|
|
1049
|
+
if (!url)
|
|
1050
|
+
continue;
|
|
1051
|
+
if (currentUrl === url)
|
|
1052
|
+
return true;
|
|
1053
|
+
const prefix = url.endsWith('/') ? url : url + '/';
|
|
1054
|
+
if (currentUrl.startsWith(prefix))
|
|
1055
|
+
return true;
|
|
1056
|
+
}
|
|
1057
|
+
return false;
|
|
1058
|
+
}
|
|
1059
|
+
/** Walk a renderable tree, resolving data-collapsed="auto" on NavGroup tags
|
|
1060
|
+
* inside collapsible Nav containers. */
|
|
1061
|
+
function resolveCollapsibleNavs(renderable, pageUrl, pagesByUrl) {
|
|
1062
|
+
if (!Tag.isTag(renderable)) {
|
|
1063
|
+
if (Array.isArray(renderable)) {
|
|
1064
|
+
const newChildren = renderable.map(c => resolveCollapsibleNavs(c, pageUrl, pagesByUrl));
|
|
1065
|
+
if (newChildren.every((c, i) => c === renderable[i]))
|
|
1066
|
+
return renderable;
|
|
1067
|
+
return newChildren;
|
|
1068
|
+
}
|
|
1069
|
+
return renderable;
|
|
1070
|
+
}
|
|
1071
|
+
const tag = renderable;
|
|
1072
|
+
if (tag.attributes?.['data-rune'] === 'nav' &&
|
|
1073
|
+
tag.attributes?.['data-collapsible'] === 'true') {
|
|
1074
|
+
const defaultOpenRaw = String(tag.attributes?.['data-default-open'] ?? '');
|
|
1075
|
+
const defaultOpen = defaultOpenRaw
|
|
1076
|
+
? defaultOpenRaw.split(',').map(s => s.trim()).filter(Boolean)
|
|
1077
|
+
: [];
|
|
1078
|
+
const groups = [];
|
|
1079
|
+
const findGroups = (node) => {
|
|
1080
|
+
if (!Tag.isTag(node))
|
|
1081
|
+
return;
|
|
1082
|
+
const t = node;
|
|
1083
|
+
if (t.attributes?.['data-rune'] === 'nav-group') {
|
|
1084
|
+
groups.push(t);
|
|
1085
|
+
}
|
|
1086
|
+
(t.children ?? []).forEach(findGroups);
|
|
1087
|
+
};
|
|
1088
|
+
(tag.children ?? []).forEach(findGroups);
|
|
1089
|
+
for (const group of groups) {
|
|
1090
|
+
if (group.attributes?.['data-collapsed'] !== NAV_COLLAPSED_AUTO)
|
|
1091
|
+
continue;
|
|
1092
|
+
const title = extractGroupTitle(group);
|
|
1093
|
+
const inDefault = title && defaultOpen.includes(title);
|
|
1094
|
+
const itemUrls = collectGroupItemUrls(group, pagesByUrl, pageUrl);
|
|
1095
|
+
const matches = urlMatchesGroup(pageUrl, itemUrls);
|
|
1096
|
+
group.attributes['data-collapsed'] = (inDefault || matches) ? 'false' : 'true';
|
|
1097
|
+
}
|
|
1098
|
+
const newChildren = (tag.children ?? []).map((c) => resolveCollapsibleNavs(c, pageUrl, pagesByUrl));
|
|
1099
|
+
if (newChildren.every((c, i) => c === tag.children[i]))
|
|
1100
|
+
return tag;
|
|
1101
|
+
return { ...tag, children: newChildren };
|
|
1102
|
+
}
|
|
1103
|
+
const newChildren = (tag.children ?? []).map((c) => resolveCollapsibleNavs(c, pageUrl, pagesByUrl));
|
|
1104
|
+
if (newChildren.every((c, i) => c === tag.children[i]))
|
|
1105
|
+
return tag;
|
|
1106
|
+
return { ...tag, children: newChildren };
|
|
1107
|
+
}
|
|
1108
|
+
function enrichNavItemAsCard(item, pageMeta, itemUrl) {
|
|
1109
|
+
const titleText = pageMeta?.title ?? itemUrl ?? '';
|
|
1110
|
+
const description = pageMeta?.description;
|
|
1111
|
+
const icon = pageMeta?.icon;
|
|
1112
|
+
const href = pageMeta?.url ?? itemUrl ?? '';
|
|
1113
|
+
const linkChildren = [];
|
|
1114
|
+
if (icon) {
|
|
1115
|
+
linkChildren.push(new Tag('rf-icon', { name: icon, 'data-name': 'icon', class: 'rf-nav-item__icon' }, []));
|
|
1116
|
+
}
|
|
1117
|
+
linkChildren.push(new Tag('span', { 'data-name': 'title', class: 'rf-nav-item__title' }, [titleText]));
|
|
1118
|
+
if (description) {
|
|
1119
|
+
linkChildren.push(new Tag('span', { 'data-name': 'description', class: 'rf-nav-item__description' }, [description]));
|
|
1120
|
+
}
|
|
1121
|
+
const link = href
|
|
1122
|
+
? new Tag('a', { href, class: 'rf-nav-item__link' }, linkChildren)
|
|
1123
|
+
: new Tag('span', { class: 'rf-nav-item__link' }, linkChildren);
|
|
1124
|
+
return { ...item, children: [link] };
|
|
1125
|
+
}
|
|
1126
|
+
function findNavItemHref(item) {
|
|
1127
|
+
const walk = (node) => {
|
|
1128
|
+
if (!Tag.isTag(node)) {
|
|
1129
|
+
if (Array.isArray(node)) {
|
|
1130
|
+
for (const c of node) {
|
|
1131
|
+
const found = walk(c);
|
|
1132
|
+
if (found)
|
|
1133
|
+
return found;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
return null;
|
|
1137
|
+
}
|
|
1138
|
+
const t = node;
|
|
1139
|
+
if (t.name === 'a' && t.attributes?.href)
|
|
1140
|
+
return String(t.attributes.href);
|
|
1141
|
+
for (const c of t.children ?? []) {
|
|
1142
|
+
const found = walk(c);
|
|
1143
|
+
if (found)
|
|
1144
|
+
return found;
|
|
1145
|
+
}
|
|
1146
|
+
return null;
|
|
1147
|
+
};
|
|
1148
|
+
return walk(item);
|
|
1149
|
+
}
|
|
1150
|
+
function getNavItemUrl(item, pagesByUrl, currentUrl) {
|
|
1151
|
+
// Prefer an explicit <a href> — covers `[Label](/url)` items whose link text
|
|
1152
|
+
// is wrapped in a slug span by the nav item transform.
|
|
1153
|
+
const href = findNavItemHref(item);
|
|
1154
|
+
if (href) {
|
|
1155
|
+
const isExternal = /^[a-z]+:\/\//i.test(href);
|
|
1156
|
+
return { url: href, isExternal };
|
|
1157
|
+
}
|
|
1158
|
+
const slug = readNavItemSlug(item);
|
|
1159
|
+
if (slug) {
|
|
1160
|
+
const resolved = resolveSlugToUrl(slug, pagesByUrl, currentUrl);
|
|
1161
|
+
return resolved ? { url: resolved, isExternal: false } : null;
|
|
1162
|
+
}
|
|
1163
|
+
return null;
|
|
1164
|
+
}
|
|
1165
|
+
function resolveCardsNavs(renderable, pageUrl, pagesByUrl) {
|
|
1166
|
+
if (!Tag.isTag(renderable)) {
|
|
1167
|
+
if (Array.isArray(renderable)) {
|
|
1168
|
+
const newChildren = renderable.map(c => resolveCardsNavs(c, pageUrl, pagesByUrl));
|
|
1169
|
+
if (newChildren.every((c, i) => c === renderable[i]))
|
|
1170
|
+
return renderable;
|
|
1171
|
+
return newChildren;
|
|
1172
|
+
}
|
|
1173
|
+
return renderable;
|
|
1174
|
+
}
|
|
1175
|
+
const tag = renderable;
|
|
1176
|
+
if (tag.attributes?.['data-rune'] === 'nav' &&
|
|
1177
|
+
(tag.attributes?.['data-layout'] === 'cards' || tag.attributes?.['layout'] === 'cards')) {
|
|
1178
|
+
const enrichItem = (node) => {
|
|
1179
|
+
if (!Tag.isTag(node)) {
|
|
1180
|
+
if (Array.isArray(node))
|
|
1181
|
+
return node.map(enrichItem);
|
|
1182
|
+
return node;
|
|
1183
|
+
}
|
|
1184
|
+
const t = node;
|
|
1185
|
+
if (t.attributes?.['data-rune'] === 'nav-item') {
|
|
1186
|
+
const ref = getNavItemUrl(t, pagesByUrl, pageUrl);
|
|
1187
|
+
if (!ref)
|
|
1188
|
+
return t;
|
|
1189
|
+
if (ref.isExternal) {
|
|
1190
|
+
// External link — title only, no enrichment
|
|
1191
|
+
return enrichNavItemAsCard(t, null, ref.url);
|
|
1192
|
+
}
|
|
1193
|
+
const page = pagesByUrl.get(ref.url);
|
|
1194
|
+
const meta = page
|
|
1195
|
+
? { url: page.url, title: page.title, description: page.description, icon: page.icon }
|
|
1196
|
+
: null;
|
|
1197
|
+
return enrichNavItemAsCard(t, meta, ref.url);
|
|
1198
|
+
}
|
|
1199
|
+
const newChildren = (t.children ?? []).map(enrichItem);
|
|
1200
|
+
if (newChildren.every((c, i) => c === t.children[i]))
|
|
1201
|
+
return t;
|
|
1202
|
+
return { ...t, children: newChildren };
|
|
1203
|
+
};
|
|
1204
|
+
const newChildren = (tag.children ?? []).map(enrichItem);
|
|
1205
|
+
return { ...tag, children: newChildren };
|
|
1206
|
+
}
|
|
1207
|
+
const newChildren = (tag.children ?? []).map((c) => resolveCardsNavs(c, pageUrl, pagesByUrl));
|
|
1208
|
+
if (newChildren.every((c, i) => c === tag.children[i]))
|
|
1209
|
+
return tag;
|
|
1210
|
+
return { ...tag, children: newChildren };
|
|
1211
|
+
}
|
|
1212
|
+
/** Walk a renderable tree collecting all nav-rune tags and their item URLs in order.
|
|
1213
|
+
* Skips navs with non-sequential layouts (menubar / columns / cards) — those
|
|
1214
|
+
* aren't reading sequences. */
|
|
1215
|
+
function collectNavSequences(renderable, pagesByUrl, currentUrl) {
|
|
1216
|
+
const sequences = [];
|
|
1217
|
+
const walk = (node) => {
|
|
1218
|
+
if (!Tag.isTag(node)) {
|
|
1219
|
+
if (Array.isArray(node))
|
|
1220
|
+
node.forEach(walk);
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
const t = node;
|
|
1224
|
+
if (t.attributes?.['data-rune'] === 'nav') {
|
|
1225
|
+
const layout = t.attributes?.['layout'] ?? t.attributes?.['data-layout'];
|
|
1226
|
+
if (layout && layout !== 'vertical') {
|
|
1227
|
+
// Non-sequential layouts (menubar / columns / cards) — skip but recurse
|
|
1228
|
+
(t.children ?? []).forEach(walk);
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
// Each NavGroup is its own reading sequence. Top-level items (before
|
|
1232
|
+
// the first group) are cross-section links and not a reading order,
|
|
1233
|
+
// so we don't include them in any sequence.
|
|
1234
|
+
const groups = [];
|
|
1235
|
+
const findGroupsLocal = (n) => {
|
|
1236
|
+
if (!Tag.isTag(n)) {
|
|
1237
|
+
if (Array.isArray(n))
|
|
1238
|
+
n.forEach(findGroupsLocal);
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
const node = n;
|
|
1242
|
+
if (node.attributes?.['data-rune'] === 'nav-group') {
|
|
1243
|
+
groups.push(node);
|
|
1244
|
+
}
|
|
1245
|
+
(node.children ?? []).forEach(findGroupsLocal);
|
|
1246
|
+
};
|
|
1247
|
+
(t.children ?? []).forEach(findGroupsLocal);
|
|
1248
|
+
if (groups.length === 0) {
|
|
1249
|
+
// Flat nav (no headings) — collect all items as one sequence
|
|
1250
|
+
const items = [];
|
|
1251
|
+
const collectFlat = (n) => {
|
|
1252
|
+
if (!Tag.isTag(n)) {
|
|
1253
|
+
if (Array.isArray(n))
|
|
1254
|
+
n.forEach(collectFlat);
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
const node = n;
|
|
1258
|
+
if (node.attributes?.['data-rune'] === 'nav-item') {
|
|
1259
|
+
const href = findNavItemHref(node);
|
|
1260
|
+
if (href) {
|
|
1261
|
+
if (!/^[a-z]+:\/\//i.test(href))
|
|
1262
|
+
items.push(href);
|
|
1263
|
+
}
|
|
1264
|
+
else {
|
|
1265
|
+
const slug = readNavItemSlug(node);
|
|
1266
|
+
if (slug) {
|
|
1267
|
+
const resolved = resolveSlugToUrl(slug, pagesByUrl, currentUrl);
|
|
1268
|
+
if (resolved)
|
|
1269
|
+
items.push(resolved);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
(node.children ?? []).forEach(collectFlat);
|
|
1274
|
+
};
|
|
1275
|
+
(t.children ?? []).forEach(collectFlat);
|
|
1276
|
+
if (items.length > 0)
|
|
1277
|
+
sequences.push(items);
|
|
1278
|
+
}
|
|
1279
|
+
else {
|
|
1280
|
+
for (const group of groups) {
|
|
1281
|
+
const items = [];
|
|
1282
|
+
const collectGroup = (n) => {
|
|
1283
|
+
if (!Tag.isTag(n)) {
|
|
1284
|
+
if (Array.isArray(n))
|
|
1285
|
+
n.forEach(collectGroup);
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
const node = n;
|
|
1289
|
+
if (node.attributes?.['data-rune'] === 'nav-item') {
|
|
1290
|
+
const href = findNavItemHref(node);
|
|
1291
|
+
if (href) {
|
|
1292
|
+
if (!/^[a-z]+:\/\//i.test(href))
|
|
1293
|
+
items.push(href);
|
|
1294
|
+
}
|
|
1295
|
+
else {
|
|
1296
|
+
const slug = readNavItemSlug(node);
|
|
1297
|
+
if (slug) {
|
|
1298
|
+
const resolved = resolveSlugToUrl(slug, pagesByUrl, currentUrl);
|
|
1299
|
+
if (resolved)
|
|
1300
|
+
items.push(resolved);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
(node.children ?? []).forEach(collectGroup);
|
|
1305
|
+
};
|
|
1306
|
+
(group.children ?? []).forEach(collectGroup);
|
|
1307
|
+
if (items.length > 0)
|
|
1308
|
+
sequences.push(items);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
(t.children ?? []).forEach(walk);
|
|
1313
|
+
};
|
|
1314
|
+
walk(renderable);
|
|
1315
|
+
return sequences;
|
|
1316
|
+
}
|
|
1317
|
+
/** Get siblings: pages sharing the same parentUrl, optionally widened to the whole section. */
|
|
1318
|
+
function getSiblingPages(currentUrl, pagesByUrl, scope) {
|
|
1319
|
+
const current = pagesByUrl.get(currentUrl);
|
|
1320
|
+
if (!current)
|
|
1321
|
+
return [];
|
|
1322
|
+
const pages = Array.from(pagesByUrl.values());
|
|
1323
|
+
let candidates;
|
|
1324
|
+
if (scope === 'section') {
|
|
1325
|
+
// Find the top-level section URL: walk up from current to a page whose parent is '/' or itself.
|
|
1326
|
+
let sectionRoot = currentUrl;
|
|
1327
|
+
let cursor = current;
|
|
1328
|
+
while (cursor && cursor.parentUrl && cursor.parentUrl !== '/') {
|
|
1329
|
+
const parent = pagesByUrl.get(cursor.parentUrl);
|
|
1330
|
+
if (!parent)
|
|
1331
|
+
break;
|
|
1332
|
+
sectionRoot = parent.url;
|
|
1333
|
+
cursor = parent;
|
|
1334
|
+
}
|
|
1335
|
+
const prefix = sectionRoot.endsWith('/') ? sectionRoot : sectionRoot + '/';
|
|
1336
|
+
candidates = pages.filter(p => p.url === sectionRoot || p.url.startsWith(prefix));
|
|
1337
|
+
}
|
|
1338
|
+
else {
|
|
1339
|
+
candidates = pages.filter(p => p.parentUrl === current.parentUrl);
|
|
1340
|
+
}
|
|
1341
|
+
// Sort by frontmatter order (asc, missing → end), tie-break by URL.
|
|
1342
|
+
candidates.sort((a, b) => {
|
|
1343
|
+
const ao = a.order ?? Number.POSITIVE_INFINITY;
|
|
1344
|
+
const bo = b.order ?? Number.POSITIVE_INFINITY;
|
|
1345
|
+
if (ao !== bo)
|
|
1346
|
+
return ao - bo;
|
|
1347
|
+
return a.url.localeCompare(b.url);
|
|
1348
|
+
});
|
|
1349
|
+
return candidates.map(p => ({ url: p.url, title: p.title }));
|
|
1350
|
+
}
|
|
1351
|
+
function pickPrevNextFromSequence(sequence, currentUrl, pagesByUrl) {
|
|
1352
|
+
const idx = sequence.indexOf(currentUrl);
|
|
1353
|
+
if (idx === -1)
|
|
1354
|
+
return { prev: null, next: null };
|
|
1355
|
+
const prevUrl = idx > 0 ? sequence[idx - 1] : null;
|
|
1356
|
+
const nextUrl = idx < sequence.length - 1 ? sequence[idx + 1] : null;
|
|
1357
|
+
const lookup = (u) => {
|
|
1358
|
+
if (!u)
|
|
1359
|
+
return null;
|
|
1360
|
+
const page = pagesByUrl.get(u);
|
|
1361
|
+
return page ? { url: page.url, title: page.title } : { url: u, title: u };
|
|
1362
|
+
};
|
|
1363
|
+
return { prev: lookup(prevUrl), next: lookup(nextUrl) };
|
|
1364
|
+
}
|
|
1365
|
+
function buildPaginationLink(target, direction, label) {
|
|
1366
|
+
const marker = direction === 'prev' ? '←' : '→';
|
|
1367
|
+
const linkText = label ?? target.title;
|
|
1368
|
+
const linkChildren = direction === 'prev'
|
|
1369
|
+
? [
|
|
1370
|
+
new Tag('span', { 'data-name': 'marker' }, [marker]),
|
|
1371
|
+
new Tag('span', { 'data-name': 'label' }, [linkText]),
|
|
1372
|
+
]
|
|
1373
|
+
: [
|
|
1374
|
+
new Tag('span', { 'data-name': 'label' }, [linkText]),
|
|
1375
|
+
new Tag('span', { 'data-name': 'marker' }, [marker]),
|
|
1376
|
+
];
|
|
1377
|
+
return new Tag('a', {
|
|
1378
|
+
href: target.url,
|
|
1379
|
+
'data-direction': direction,
|
|
1380
|
+
'data-name': direction,
|
|
1381
|
+
}, linkChildren);
|
|
1382
|
+
}
|
|
1383
|
+
function resolveAutoPagination(renderable, pageUrl, pagesByUrl, rootRenderable) {
|
|
1384
|
+
if (!Tag.isTag(renderable)) {
|
|
1385
|
+
if (Array.isArray(renderable)) {
|
|
1386
|
+
const newChildren = renderable.map(c => resolveAutoPagination(c, pageUrl, pagesByUrl, rootRenderable));
|
|
1387
|
+
if (newChildren.every((c, i) => c === renderable[i]))
|
|
1388
|
+
return renderable;
|
|
1389
|
+
return newChildren;
|
|
1390
|
+
}
|
|
1391
|
+
return renderable;
|
|
1392
|
+
}
|
|
1393
|
+
const tag = renderable;
|
|
1394
|
+
if (tag.attributes?.['data-rune'] === 'pagination') {
|
|
1395
|
+
const hasSentinel = (tag.children ?? []).some((c) => Tag.isTag(c) && c.attributes?.['data-field'] === PAGINATION_AUTO_SENTINEL);
|
|
1396
|
+
if (hasSentinel) {
|
|
1397
|
+
const scopeMeta = (tag.children ?? []).find((c) => Tag.isTag(c) && c.attributes?.['data-field'] === 'scope');
|
|
1398
|
+
const scopeValue = scopeMeta
|
|
1399
|
+
? String(scopeMeta.attributes?.content ?? '')
|
|
1400
|
+
: String(tag.attributes?.['data-scope'] ?? '');
|
|
1401
|
+
const scope = (scopeValue === 'section' ? 'section' : 'siblings');
|
|
1402
|
+
const prevLabelMeta = (tag.children ?? []).find((c) => Tag.isTag(c) && c.attributes?.['data-field'] === 'prev-label');
|
|
1403
|
+
const nextLabelMeta = (tag.children ?? []).find((c) => Tag.isTag(c) && c.attributes?.['data-field'] === 'next-label');
|
|
1404
|
+
const prevLabel = prevLabelMeta ? String(prevLabelMeta.attributes.content ?? '') : undefined;
|
|
1405
|
+
const nextLabel = nextLabelMeta ? String(nextLabelMeta.attributes.content ?? '') : undefined;
|
|
1406
|
+
// Skip when current page is a section index (has children)
|
|
1407
|
+
const hasChildren = Array.from(pagesByUrl.values()).some(p => p.parentUrl === pageUrl && p.url !== pageUrl);
|
|
1408
|
+
if (hasChildren) {
|
|
1409
|
+
return { ...tag, children: [] };
|
|
1410
|
+
}
|
|
1411
|
+
// 1. Explicit nav order from the layout cascade
|
|
1412
|
+
let sequence = null;
|
|
1413
|
+
const sequences = collectNavSequences(rootRenderable, pagesByUrl, pageUrl);
|
|
1414
|
+
for (const seq of sequences) {
|
|
1415
|
+
if (seq.includes(pageUrl)) {
|
|
1416
|
+
sequence = seq;
|
|
1417
|
+
break;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
let prev = null;
|
|
1421
|
+
let next = null;
|
|
1422
|
+
if (sequence) {
|
|
1423
|
+
({ prev, next } = pickPrevNextFromSequence(sequence, pageUrl, pagesByUrl));
|
|
1424
|
+
}
|
|
1425
|
+
else {
|
|
1426
|
+
// 2. Sibling pages by frontmatter order → directory order
|
|
1427
|
+
const siblings = getSiblingPages(pageUrl, pagesByUrl, scope);
|
|
1428
|
+
const idx = siblings.findIndex(s => s.url === pageUrl);
|
|
1429
|
+
if (idx !== -1) {
|
|
1430
|
+
prev = idx > 0 ? siblings[idx - 1] : null;
|
|
1431
|
+
next = idx < siblings.length - 1 ? siblings[idx + 1] : null;
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
const links = [];
|
|
1435
|
+
if (prev)
|
|
1436
|
+
links.push(buildPaginationLink(prev, 'prev', prevLabel));
|
|
1437
|
+
if (next)
|
|
1438
|
+
links.push(buildPaginationLink(next, 'next', nextLabel));
|
|
1439
|
+
// Preserve any non-sentinel/non-label meta children (none expected, but defensive)
|
|
1440
|
+
const consumed = new Set([PAGINATION_AUTO_SENTINEL, 'prev-label', 'next-label', 'scope']);
|
|
1441
|
+
const remaining = (tag.children ?? []).filter((c) => {
|
|
1442
|
+
if (!Tag.isTag(c))
|
|
1443
|
+
return true;
|
|
1444
|
+
if (c.name !== 'meta')
|
|
1445
|
+
return true;
|
|
1446
|
+
const field = c.attributes?.['data-field'];
|
|
1447
|
+
return !consumed.has(field);
|
|
1448
|
+
});
|
|
1449
|
+
return { ...tag, children: [...remaining, ...links] };
|
|
1450
|
+
}
|
|
1451
|
+
// Explicit prev/next mode — resolve any __slug:foo hrefs through pagesByUrl
|
|
1452
|
+
const newChildren = (tag.children ?? []).map((c) => {
|
|
1453
|
+
if (!Tag.isTag(c))
|
|
1454
|
+
return c;
|
|
1455
|
+
if (c.name === 'a' && typeof c.attributes?.href === 'string' && c.attributes.href.startsWith('__slug:')) {
|
|
1456
|
+
const slug = c.attributes.href.slice('__slug:'.length);
|
|
1457
|
+
const resolvedUrl = resolveSlugToUrl(slug, pagesByUrl, pageUrl);
|
|
1458
|
+
if (resolvedUrl) {
|
|
1459
|
+
const page = pagesByUrl.get(resolvedUrl);
|
|
1460
|
+
const newLabel = page?.title ?? slug;
|
|
1461
|
+
// Replace label text if user didn't override (label child has same text as slug)
|
|
1462
|
+
const newChildren = (c.children ?? []).map((child) => {
|
|
1463
|
+
if (Tag.isTag(child) && child.attributes?.['data-name'] === 'label') {
|
|
1464
|
+
const currentLabel = (child.children ?? []).filter((x) => typeof x === 'string').join('');
|
|
1465
|
+
if (currentLabel === slug) {
|
|
1466
|
+
return { ...child, children: [newLabel] };
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
return child;
|
|
1470
|
+
});
|
|
1471
|
+
return { ...c, attributes: { ...c.attributes, href: resolvedUrl }, children: newChildren };
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
return c;
|
|
1475
|
+
});
|
|
1476
|
+
return { ...tag, children: newChildren };
|
|
1477
|
+
}
|
|
1478
|
+
const newChildren = (tag.children ?? []).map((c) => resolveAutoPagination(c, pageUrl, pagesByUrl, rootRenderable));
|
|
1479
|
+
if (newChildren.every((c, i) => c === tag.children[i]))
|
|
1480
|
+
return tag;
|
|
1481
|
+
return { ...tag, children: newChildren };
|
|
1482
|
+
}
|
|
913
1483
|
function walkBlogTags(node, fn) {
|
|
914
1484
|
if (Tag.isTag(node)) {
|
|
915
1485
|
fn(node);
|
|
@@ -1062,9 +1632,35 @@ function resolveBlogPosts(renderable, allPosts, ctx, pageUrl) {
|
|
|
1062
1632
|
});
|
|
1063
1633
|
return modified ? result : renderable;
|
|
1064
1634
|
}
|
|
1635
|
+
/**
|
|
1636
|
+
* Apply core auto-resolutions (breadcrumb, nav, collapsible, cards, pagination,
|
|
1637
|
+
* blog, xref) to an arbitrary renderable tree using the same aggregated data
|
|
1638
|
+
* the pipeline produces.
|
|
1639
|
+
*
|
|
1640
|
+
* Used by callers that need to resolve sentinels in renderables outside the
|
|
1641
|
+
* per-page pipeline — most notably layout regions, which are parsed once but
|
|
1642
|
+
* need per-page URL context for auto-open and auto-pagination.
|
|
1643
|
+
*/
|
|
1644
|
+
export function resolveCoreSentinels(renderable, pageUrl, coreData, ctx,
|
|
1645
|
+
/** Extra trees (e.g. layout regions + page content) to scan when looking
|
|
1646
|
+
* for nav sequences during auto-pagination. Required when calling against
|
|
1647
|
+
* a layout region where the sidebar nav lives in a different region. */
|
|
1648
|
+
navSearchScope) {
|
|
1649
|
+
let result = resolveAutoBreadcrumbs(renderable, pageUrl, coreData.breadcrumbPaths, coreData.pagesByUrl, ctx);
|
|
1650
|
+
result = resolveAutoNavs(result, pageUrl, coreData.pagesByUrl, ctx);
|
|
1651
|
+
result = resolveCollapsibleNavs(result, pageUrl, coreData.pagesByUrl);
|
|
1652
|
+
result = resolveCardsNavs(result, pageUrl, coreData.pagesByUrl);
|
|
1653
|
+
const searchRoot = navSearchScope && navSearchScope.length > 0
|
|
1654
|
+
? [result, ...navSearchScope]
|
|
1655
|
+
: result;
|
|
1656
|
+
result = resolveAutoPagination(result, pageUrl, coreData.pagesByUrl, searchRoot);
|
|
1657
|
+
result = resolveBlogPosts(result, coreData.allPosts, ctx, pageUrl);
|
|
1658
|
+
result = resolveXrefs(result, pageUrl, coreData.registry, ctx);
|
|
1659
|
+
return result;
|
|
1660
|
+
}
|
|
1065
1661
|
/**
|
|
1066
1662
|
* Core cross-page pipeline hooks.
|
|
1067
|
-
* Run for every site, before any
|
|
1663
|
+
* Run for every site, before any plugin hooks.
|
|
1068
1664
|
* Registers page and heading entities, aggregates the page tree and breadcrumb paths,
|
|
1069
1665
|
* and resolves blog post listings.
|
|
1070
1666
|
*/
|
|
@@ -1088,6 +1684,7 @@ export const corePipelineHooks = {
|
|
|
1088
1684
|
description: page.frontmatter.description,
|
|
1089
1685
|
date: page.frontmatter.date,
|
|
1090
1686
|
order: page.frontmatter.order,
|
|
1687
|
+
icon: page.frontmatter.icon,
|
|
1091
1688
|
},
|
|
1092
1689
|
});
|
|
1093
1690
|
for (const h of page.headings) {
|
|
@@ -1111,6 +1708,9 @@ export const corePipelineHooks = {
|
|
|
1111
1708
|
url: e.data.url,
|
|
1112
1709
|
title: e.data.title,
|
|
1113
1710
|
parentUrl: e.data.parentUrl,
|
|
1711
|
+
description: e.data.description,
|
|
1712
|
+
icon: e.data.icon,
|
|
1713
|
+
order: e.data.order,
|
|
1114
1714
|
}));
|
|
1115
1715
|
const pageTree = buildPageTree(pages);
|
|
1116
1716
|
const breadcrumbPaths = buildBreadcrumbPaths(pages);
|
|
@@ -1138,6 +1738,9 @@ export const corePipelineHooks = {
|
|
|
1138
1738
|
return page;
|
|
1139
1739
|
let renderable = resolveAutoBreadcrumbs(page.renderable, page.url, coreData.breadcrumbPaths, coreData.pagesByUrl, ctx);
|
|
1140
1740
|
renderable = resolveAutoNavs(renderable, page.url, coreData.pagesByUrl, ctx);
|
|
1741
|
+
renderable = resolveCollapsibleNavs(renderable, page.url, coreData.pagesByUrl);
|
|
1742
|
+
renderable = resolveCardsNavs(renderable, page.url, coreData.pagesByUrl);
|
|
1743
|
+
renderable = resolveAutoPagination(renderable, page.url, coreData.pagesByUrl, renderable);
|
|
1141
1744
|
renderable = resolveBlogPosts(renderable, coreData.allPosts, ctx, page.url);
|
|
1142
1745
|
renderable = resolveXrefs(renderable, page.url, coreData.registry, ctx);
|
|
1143
1746
|
if (renderable === page.renderable)
|