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