scavold 0.2.0-rc.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.
Files changed (42) hide show
  1. package/COMPONENTS.md +862 -0
  2. package/FRONTMATTER.md +248 -0
  3. package/LICENSE +21 -0
  4. package/README.md +26 -0
  5. package/components/ScavoldArticle.vue +12 -0
  6. package/components/ScavoldAside.vue +12 -0
  7. package/components/ScavoldBreadcrumb.vue +36 -0
  8. package/components/ScavoldContainer.vue +16 -0
  9. package/components/ScavoldFooter.vue +12 -0
  10. package/components/ScavoldHeader.vue +12 -0
  11. package/components/ScavoldImage.vue +33 -0
  12. package/components/ScavoldLayout.vue +21 -0
  13. package/components/ScavoldLocaleMenu.vue +86 -0
  14. package/components/ScavoldLocaleRedirect.vue +47 -0
  15. package/components/ScavoldMain.vue +12 -0
  16. package/components/ScavoldMenu.vue +82 -0
  17. package/components/ScavoldMenuItems.vue +45 -0
  18. package/components/ScavoldNav.vue +12 -0
  19. package/components/ScavoldSection.vue +12 -0
  20. package/components/ScavoldSimpleRedirect.vue +35 -0
  21. package/components/ScavoldVideo.vue +74 -0
  22. package/composables/hierarchy.ts +391 -0
  23. package/composables/useContainer.js +59 -0
  24. package/composables/useI18n.js +37 -0
  25. package/composables/useRedirect.js +20 -0
  26. package/composables/useVideo.js +89 -0
  27. package/index.d.ts +43 -0
  28. package/l10n/de.json +6 -0
  29. package/l10n/en.json +6 -0
  30. package/lib/config.d.ts +17 -0
  31. package/lib/config.js +396 -0
  32. package/lib/containers.js +128 -0
  33. package/lib/index.d.ts +9 -0
  34. package/lib/index.js +60 -0
  35. package/lib/markdown.js +22 -0
  36. package/lib/media.js +231 -0
  37. package/lib/pages.js +494 -0
  38. package/lib/parser.js +83 -0
  39. package/lib/redirectTarget.js +46 -0
  40. package/lib/sectionManifest.js +200 -0
  41. package/package.json +86 -0
  42. package/scripts/check-csp.js +68 -0
@@ -0,0 +1,45 @@
1
+ <script setup lang="ts">
2
+ import type { Scavold } from "../index";
3
+
4
+ defineProps<{
5
+ items: Scavold.MenuItem[];
6
+ }>();
7
+
8
+ function label( node: Scavold.HierarchyNode ): string {
9
+ if ( node.label ) return node.label;
10
+ if ( node.title ) return node.title;
11
+
12
+ // Fall back to the last meaningful path segment (strip index.md, extension)
13
+ const segment = node.path
14
+ .replace( /\/index\.md$/, "" )
15
+ .replace( /\.md$/, "" )
16
+ .split( "/" )
17
+ .filter( Boolean )
18
+ .pop();
19
+
20
+ return segment ?? node.path;
21
+ }
22
+
23
+ function href( node: Scavold.HierarchyNode ): string {
24
+ return "/" + ( node.url ?? node.path ).replace( /\.md$/, "" ).replace( /\/index$/, "/" );
25
+ }
26
+ </script>
27
+
28
+ <template>
29
+ <ul>
30
+ <li
31
+ v-for="item of items"
32
+ :key="item.node.path"
33
+ :class="{
34
+ active: item.active,
35
+ current: item.current,
36
+ }"
37
+ >
38
+ <a :href="href( item.node )" :aria-current="item.current ? 'page' : undefined">{{ label( item.node ) }}</a>
39
+ <ScavoldMenuItems
40
+ v-if="item.children.length"
41
+ :items="item.children"
42
+ />
43
+ </li>
44
+ </ul>
45
+ </template>
@@ -0,0 +1,12 @@
1
+ <script setup>
2
+ import { useContainer, containerProps } from "../composables/useContainer.js";
3
+
4
+ const props = defineProps( containerProps );
5
+ const { classes, dataAttrs } = useContainer( props );
6
+ </script>
7
+
8
+ <template>
9
+ <nav :class="classes" v-bind="dataAttrs">
10
+ <slot />
11
+ </nav>
12
+ </template>
@@ -0,0 +1,12 @@
1
+ <script setup>
2
+ import { useContainer, containerProps } from "../composables/useContainer.js";
3
+
4
+ const props = defineProps( containerProps );
5
+ const { classes, dataAttrs } = useContainer( props );
6
+ </script>
7
+
8
+ <template>
9
+ <section :class="classes" v-bind="dataAttrs">
10
+ <slot />
11
+ </section>
12
+ </template>
@@ -0,0 +1,35 @@
1
+ <script setup>
2
+ import { useData } from "vitepress";
3
+ import { computed, onMounted } from "vue";
4
+ import { isExternalUrl, servableRedirectTarget } from "../lib/redirectTarget.js";
5
+
6
+ const { frontmatter, page } = useData();
7
+
8
+ // The resolved, server-servable redirect target. External URLs pass through
9
+ // unchanged — they are handled at build time via <meta http-equiv="refresh">
10
+ // injected by transformPageData (direct page loads) and by the capture-phase
11
+ // click interceptor in lib/index.js (SPA navigation). Only internal targets
12
+ // need runtime handling here.
13
+ const target = computed( () => {
14
+ const redirect = frontmatter.value.redirect;
15
+ if ( typeof redirect !== "string" ) return null;
16
+ if ( isExternalUrl( redirect ) ) return null;
17
+ if ( redirect.startsWith( "/" ) ) return servableRedirectTarget( redirect );
18
+ // Relative .md path — resolve against the current page's directory.
19
+ // page.relativePath is used (not location.href) so the computed works in
20
+ // SSR/build context where location is undefined.
21
+ const dir = page.value.relativePath.replace( /\/[^/]*$/, "" );
22
+ const resolved = dir ? `${dir}/${redirect}` : redirect;
23
+ return servableRedirectTarget( "/" + resolved.replace( /\/index\.md$/, "/" ).replace( /\.md$/, "" ) );
24
+ } );
25
+
26
+ onMounted( () => {
27
+ if ( !target.value ) return;
28
+ // Internal redirects: replace the history entry so this page never appears
29
+ // in the browser back-stack. This path is taken on both direct page loads
30
+ // and (as a fallback) SPA navigations not caught by the click interceptor.
31
+ location.replace( target.value );
32
+ } );
33
+ </script>
34
+
35
+ <template><!-- redirect handled in head (external) or onMounted (internal) --></template>
@@ -0,0 +1,74 @@
1
+ <script setup>
2
+ import { useVideo, videoProps } from "../composables/useVideo.js";
3
+
4
+ const props = defineProps( videoProps );
5
+
6
+ const { src, poster, autoplay, loop, muted, preload, overlay, controls, ariaLabel } =
7
+ useVideo( props );
8
+ </script>
9
+
10
+ <template>
11
+ <!-- overlay mode: the video is a background, the slot content sits on top -->
12
+ <div v-if="overlay && src" class="scavold-video scavold-video--overlay">
13
+ <video
14
+ class="scavold-video__media"
15
+ :src="src"
16
+ :poster="poster || undefined"
17
+ :autoplay="autoplay || undefined"
18
+ :loop="loop || undefined"
19
+ :muted="muted || undefined"
20
+ :controls="controls || undefined"
21
+ :preload="preload"
22
+ :aria-label="ariaLabel"
23
+ playsinline
24
+ />
25
+ <div class="scavold-video__overlay">
26
+ <slot />
27
+ </div>
28
+ </div>
29
+
30
+ <!-- default mode: plain inline player; the slot is fallback content -->
31
+ <video
32
+ v-else-if="src"
33
+ class="scavold-video scavold-video__media"
34
+ :src="src"
35
+ :poster="poster || undefined"
36
+ :autoplay="autoplay || undefined"
37
+ :loop="loop || undefined"
38
+ :muted="muted || undefined"
39
+ :preload="preload"
40
+ :aria-label="ariaLabel"
41
+ controls
42
+ playsinline
43
+ >
44
+ <!-- <track kind="captions" src="..." srclang="..." label="..." /> -->
45
+ <slot />
46
+ </video>
47
+ </template>
48
+
49
+ <style scoped>
50
+ /*
51
+ * Functional layout only — visual styling (scrim, colours, alignment, min-height)
52
+ * belongs to the consuming theme. The overlay stacks the video and the content in
53
+ * a single grid cell so the content defines the block's height and the video
54
+ * covers the same area behind it.
55
+ */
56
+ .scavold-video--overlay {
57
+ position: relative;
58
+ display: grid;
59
+ overflow: hidden;
60
+ }
61
+
62
+ .scavold-video--overlay .scavold-video__media {
63
+ grid-area: 1 / 1;
64
+ width: 100%;
65
+ height: 100%;
66
+ object-fit: cover;
67
+ }
68
+
69
+ .scavold-video--overlay .scavold-video__overlay {
70
+ grid-area: 1 / 1;
71
+ position: relative;
72
+ z-index: 1;
73
+ }
74
+ </style>
@@ -0,0 +1,391 @@
1
+ import { useData } from "vitepress";
2
+ import { computed } from "vue";
3
+ import { useL10n } from "@cepharum/vue3-i18n";
4
+ import type { Scavold } from "../index";
5
+
6
+ /**
7
+ *
8
+ */
9
+ export function useHierarchy() {
10
+ const { page } = useData();
11
+
12
+ const reparent = ( node: Scavold.HierarchyNode ) => {
13
+ if ( node?.subs ) {
14
+ for ( const sub of Object.values( node.subs ) ) {
15
+ Object.defineProperty( sub, "parent", { value: node, writable: true } );
16
+
17
+ reparent( sub );
18
+ }
19
+ }
20
+ };
21
+
22
+ const hierarchy = computed( () => {
23
+ // eslint-disable-next-line no-shadow
24
+ const { hierarchy } = page.value as Scavold.AugmentedPageData;
25
+
26
+ reparent( hierarchy );
27
+
28
+ return hierarchy;
29
+ } );
30
+
31
+ const findByPath = ( thread: Scavold.HierarchyNode | undefined, searchedPath: string ): Scavold.HierarchyNode | undefined => {
32
+ if ( !thread ) {
33
+ return undefined;
34
+ }
35
+
36
+ if ( thread.path === searchedPath ) {
37
+ return thread;
38
+ }
39
+
40
+ if ( thread.subs ) {
41
+ for ( const sub of Object.values( thread.subs ) ) {
42
+ const match = findByPath( sub, searchedPath );
43
+
44
+ if ( match != null ) {
45
+ return match;
46
+ }
47
+ }
48
+ }
49
+
50
+ return undefined;
51
+ };
52
+
53
+ const current = computed( () => hierarchy.value ? findByPath( hierarchy.value, page.value.filePath ) : undefined );
54
+
55
+ /**
56
+ * Returns the ancestor of `node` that is exactly `targetDepth` levels below
57
+ * the root, following the path upward from `node`. Returns undefined if
58
+ * `node` is not deep enough or `targetDepth` is 0 (root itself has no
59
+ * listable parent at depth 0).
60
+ */
61
+ const ancestorAtDepth = ( node: Scavold.HierarchyNode | undefined, targetDepth: number ): Scavold.HierarchyNode | undefined => {
62
+ if ( node == null ) {
63
+ return undefined;
64
+ }
65
+
66
+ // Compute depth of this node by walking to root.
67
+ let depth = 0;
68
+ let iter: Scavold.HierarchyNode | undefined = node;
69
+
70
+ while ( iter?.parent ) {
71
+ depth++;
72
+ iter = iter.parent;
73
+ }
74
+
75
+ // targetDepth is the depth of *listed items*, so the parent node to
76
+ // list from is at targetDepth - 1.
77
+ const parentDepth = targetDepth - 1;
78
+
79
+ if ( depth < parentDepth ) {
80
+ return undefined;
81
+ }
82
+
83
+ // Walk up from node until we reach parentDepth.
84
+ iter = node;
85
+
86
+ for ( let d = depth; d > parentDepth && iter?.parent; d-- ) {
87
+ iter = iter.parent;
88
+ }
89
+
90
+ return iter;
91
+ };
92
+
93
+ /**
94
+ * Returns true if `node` should be hidden in the given rendering context.
95
+ * Reads the `hide` frontmatter property:
96
+ * false / absent — visible everywhere
97
+ * true — hidden everywhere
98
+ * "menu" — hidden in menus only
99
+ * "breadcrumb" — hidden in breadcrumbs only
100
+ */
101
+ const isHidden = ( node: Scavold.HierarchyNode, context: "menu" | "breadcrumb" ): boolean => {
102
+ const hide = node.frontmatter?.hide;
103
+
104
+ if ( hide === true ) {
105
+ return true;
106
+ }
107
+
108
+ if ( hide === "menu" || hide === "breadcrumb" ) {
109
+ return hide === context;
110
+ }
111
+
112
+ return false;
113
+ };
114
+
115
+ /**
116
+ * Returns true if `node` is on the path from the root to the current page
117
+ * (i.e. is an ancestor of current, or is current itself).
118
+ */
119
+ const isOnActivePath = ( node: Scavold.HierarchyNode ): boolean => {
120
+ let iter: Scavold.HierarchyNode | undefined = current.value;
121
+
122
+ while ( iter ) {
123
+ if ( iter === node ) {
124
+ return true;
125
+ }
126
+
127
+ iter = iter.parent;
128
+ }
129
+
130
+ return false;
131
+ };
132
+
133
+ /**
134
+ * Collects menu items recursively from a starting node.
135
+ *
136
+ * @param parent the node whose direct children form the listed items
137
+ * @param remaining levels left to descend; -1 means unlimited
138
+ * @param activeOnly only expand children along the active path
139
+ * @param expand expand all nodes with children regardless of active path
140
+ */
141
+ const collectItems = (
142
+ parent: Scavold.HierarchyNode | undefined,
143
+ remaining: number,
144
+ activeOnly: boolean,
145
+ expand: boolean,
146
+ ): Scavold.MenuItem[] => {
147
+ if ( parent?.subs == null ) {
148
+ return [];
149
+ }
150
+
151
+ return Object.values( parent.subs )
152
+ .filter( node => !isHidden( node, "menu" ) )
153
+ .map( node => {
154
+ const shouldDescend = remaining !== 0 && (
155
+ expand ||
156
+ ( activeOnly && isOnActivePath( node ) )
157
+ );
158
+
159
+ return {
160
+ node,
161
+ active: isOnActivePath( node ),
162
+ current: node === current.value,
163
+ children: shouldDescend
164
+ ? collectItems( node, remaining === -1 ? -1 : remaining - 1, activeOnly, expand )
165
+ : [],
166
+ };
167
+ } );
168
+ };
169
+
170
+ /**
171
+ * Collects the ancestor chain from root to the current page as a flat array
172
+ * of MenuItem objects, suitable for breadcrumb rendering.
173
+ *
174
+ * @param includeCurrent include the current page as the last item
175
+ * @param includeRoot include the root node as the first item
176
+ */
177
+ const collectAncestors = (
178
+ includeCurrent: boolean,
179
+ includeRoot: boolean,
180
+ ): Scavold.MenuItem[] => {
181
+ const chain: Scavold.HierarchyNode[] = [];
182
+ let iter: Scavold.HierarchyNode | undefined = current.value;
183
+
184
+ while ( iter ) {
185
+ chain.unshift( iter );
186
+ iter = iter.parent;
187
+ }
188
+
189
+ return chain
190
+ .filter( ( node, index ) => {
191
+ if ( index === 0 && !includeRoot ) {
192
+ return false;
193
+ }
194
+
195
+ if ( node === current.value && !includeCurrent ) {
196
+ return false;
197
+ }
198
+
199
+ if ( isHidden( node, "breadcrumb" ) ) {
200
+ return false;
201
+ }
202
+
203
+ return true;
204
+ } )
205
+ .map( node => ( {
206
+ node,
207
+ active: true,
208
+ current: node === current.value,
209
+ children: [],
210
+ } ) );
211
+ };
212
+
213
+ /**
214
+ * The resolved locale for the current page. Falls back to the browser locale
215
+ * (via @cepharum/vue3-i18n) when no ancestor declares one.
216
+ */
217
+ const currentLocale = computed( () =>
218
+ current.value?.locale ?? useL10n().locale
219
+ );
220
+
221
+ /**
222
+ * Resolves a raw path string to a hierarchy node. Supported placeholders:
223
+ * {locale} — full BCP 47 locale tag (e.g. "de-CH", "en")
224
+ * {lang} — primary language subtag only (e.g. "de", "en")
225
+ * Tries the interpolated path as-is, then with `/index.md`, then with `.md`.
226
+ */
227
+ const resolveByPath = ( rawPath: string ): Scavold.HierarchyNode | undefined => {
228
+ const locale = currentLocale.value ?? "";
229
+ const lang = locale.split( "-" )[0];
230
+ const resolved = rawPath
231
+ .replace( /\{locale\}/g, locale )
232
+ .replace( /\{lang\}/g, lang );
233
+
234
+ return (
235
+ findByPath( hierarchy.value, resolved ) ??
236
+ findByPath( hierarchy.value, resolved.replace( /\/?$/, "/index.md" ) ) ??
237
+ findByPath( hierarchy.value, resolved.replace( /\/?$/, ".md" ) )
238
+ );
239
+ };
240
+
241
+ /**
242
+ * Converts a hierarchy node path to a root-relative href.
243
+ * Prefers node.url (the alias output path) when declared.
244
+ */
245
+ const nodeHref = ( node: Scavold.HierarchyNode ): string =>
246
+ "/" + ( node.url ?? node.path ).replace( /\.md$/, "" ).replace( /\/index$/, "/" );
247
+
248
+ /**
249
+ * Walks the full hierarchy depth-first and collects every node, flattened.
250
+ */
251
+ const allNodes = ( root: Scavold.HierarchyNode | undefined ): Scavold.HierarchyNode[] => {
252
+ const result: Scavold.HierarchyNode[] = [];
253
+
254
+ const walk = ( node: Scavold.HierarchyNode | undefined ) => {
255
+ if ( !node ) {
256
+ return;
257
+ }
258
+
259
+ result.push( node );
260
+
261
+ if ( node.subs ) {
262
+ for ( const sub of Object.values( node.subs ) ) {
263
+ walk( sub );
264
+ }
265
+ }
266
+ };
267
+
268
+ walk( root );
269
+
270
+ return result;
271
+ };
272
+
273
+ /**
274
+ * Returns the topmost page node for a given locale — the shallowest isPage
275
+ * node in the hierarchy that carries that locale.
276
+ */
277
+ const topmostForLocale = ( locale: string ): Scavold.HierarchyNode | undefined => {
278
+ let best: Scavold.HierarchyNode | undefined;
279
+ let bestDepth = Infinity;
280
+
281
+ for ( const node of allNodes( hierarchy.value ) ) {
282
+ if ( !node.isPage || node.locale !== locale ) {
283
+ continue;
284
+ }
285
+
286
+ let depth = 0;
287
+ let iter: Scavold.HierarchyNode | undefined = node;
288
+
289
+ while ( iter?.parent ) {
290
+ depth++;
291
+ iter = iter.parent;
292
+ }
293
+
294
+ if ( depth < bestDepth ) {
295
+ best = node;
296
+ bestDepth = depth;
297
+ }
298
+ }
299
+
300
+ return best;
301
+ };
302
+
303
+ /**
304
+ * Collects available locale links for the current page.
305
+ *
306
+ * @param detection
307
+ * "auto" — try explicit, then inherited, then global; use first non-empty result.
308
+ * "explicit" — only locales declared in the current page's own `translations` frontmatter.
309
+ * "inherited" — walks up the ancestor chain and, for each locale, uses the link declared
310
+ * by the *nearest* ancestor (including the current page itself) that has a
311
+ * `translations` entry for that locale. This means a section index page can
312
+ * declare `translations` once and all its descendants without their own
313
+ * translation link will fall back to the section-level translation — useful
314
+ * when not every page in one locale has a counterpart in another.
315
+ * "global" — any locale present anywhere in the hierarchy; link goes to the topmost
316
+ * page of that locale regardless of any `translations` declarations.
317
+ * @param includeCurrent include the current locale in the result (default false)
318
+ */
319
+ const collectLocaleLinks = (
320
+ detection: Scavold.LocaleDetection,
321
+ includeCurrent = false,
322
+ ): Scavold.LocaleLink[] => {
323
+ if ( detection === "auto" ) {
324
+ for ( const mode of [ "explicit", "inherited", "global" ] as const ) {
325
+ const result = collectLocaleLinks( mode, includeCurrent );
326
+
327
+ if ( result.length > 0 ) {
328
+ return result;
329
+ }
330
+ }
331
+
332
+ return [];
333
+ }
334
+ const locale = currentLocale.value;
335
+ const links = new Map<string, string>(); // locale → href
336
+
337
+ if ( detection === "global" ) {
338
+ // Collect all distinct locales in the hierarchy.
339
+ for ( const node of allNodes( hierarchy.value ) ) {
340
+ if ( node.isPage && node.locale && !links.has( node.locale ) ) {
341
+ const top = topmostForLocale( node.locale );
342
+
343
+ if ( top ) {
344
+ links.set( node.locale, nodeHref( top ) );
345
+ }
346
+ }
347
+ }
348
+ } else {
349
+ // Walk up the ancestor chain; for "explicit" stop after current node.
350
+ let iter: Scavold.HierarchyNode | undefined = current.value;
351
+
352
+ while ( iter ) {
353
+ const translations = iter.frontmatter?.translations as Record<string, string> | undefined;
354
+
355
+ if ( translations ) {
356
+ for ( const [ loc, path ] of Object.entries( translations ) ) {
357
+ if ( !links.has( loc ) ) {
358
+ links.set( loc, "/" + String( path ).replace( /\.md$/, "" ).replace( /\/index$/, "/" ) );
359
+ }
360
+ }
361
+ }
362
+
363
+ if ( detection === "explicit" ) {
364
+ break;
365
+ }
366
+
367
+ iter = iter.parent;
368
+ }
369
+ }
370
+
371
+ return Array.from( links.entries() )
372
+ .filter( ( [ loc ] ) => includeCurrent || loc !== locale )
373
+ .map( ( [ loc, href ] ) => ( {
374
+ locale: loc,
375
+ href,
376
+ current: loc === locale,
377
+ } ) );
378
+ };
379
+
380
+ return {
381
+ hierarchy,
382
+ current,
383
+ currentLocale,
384
+ ancestorAtDepth,
385
+ isOnActivePath,
386
+ collectItems,
387
+ collectAncestors,
388
+ collectLocaleLinks,
389
+ resolveByPath,
390
+ };
391
+ }
@@ -0,0 +1,59 @@
1
+ import { computed } from "vue";
2
+ import { SECTIONING_ELEMENTS } from "../lib/containers.js";
3
+
4
+ /**
5
+ * Shared composable for Scavold container components.
6
+ *
7
+ * Consumes the props emitted by the markdown-it container renderer and exposes
8
+ * derived values ready for binding in templates:
9
+ *
10
+ * - `rootTag` — the HTML element to use as root (sectioning element if
11
+ * the container name matches one, otherwise "div")
12
+ * - `containerName` — the original container name from markdown
13
+ * - `classes` — class string from boolean arguments
14
+ * - `dataAttrs` — object of data-* attribute key/value pairs
15
+ *
16
+ * Custom container components should declare the same props and call this
17
+ * composable to get consistent behaviour without duplicating logic.
18
+ *
19
+ * @param {import('vue').ToRefs<{class: string, [key: string]: any}>} props
20
+ * @returns {{ rootTag, containerName, classes, dataAttrs }}
21
+ */
22
+ export function useContainer( props ) {
23
+ const containerName = computed( () => props.dataContainer ?? "" );
24
+
25
+ const rootTag = computed( () =>
26
+ SECTIONING_ELEMENTS.has( containerName.value ) ? containerName.value : "div"
27
+ );
28
+
29
+ const classes = computed( () => props.class ?? "" );
30
+
31
+ const dataAttrs = computed( () => {
32
+ const attrs = {};
33
+
34
+ for ( const [ key, value ] of Object.entries( props ) ) {
35
+ if ( key.startsWith( "data-" ) ) {
36
+ attrs[key] = value;
37
+ }
38
+ }
39
+
40
+ return attrs;
41
+ } );
42
+
43
+ return { rootTag, containerName, classes, dataAttrs };
44
+ }
45
+
46
+ /**
47
+ * Props definition shared by all Scavold container components. Declare these
48
+ * via `defineProps( containerProps )` in any custom container component.
49
+ */
50
+ export const containerProps = {
51
+ class: {
52
+ type: String,
53
+ default: "",
54
+ },
55
+ dataContainer: {
56
+ type: String,
57
+ default: "",
58
+ },
59
+ };
@@ -0,0 +1,37 @@
1
+ import { useL10n } from "@cepharum/vue3-i18n";
2
+
3
+ /**
4
+ * Registers Scavold's built-in translations into the shared l10n context under
5
+ * the `@scavold` namespace. All keys are accessed as `@scavold.<key>`.
6
+ *
7
+ * Call once from enhanceApp before components mount. The consuming theme's own
8
+ * setLoader() (the default namespace) runs last per library convention, so any
9
+ * `@scavold.*` key can be overridden by including it in the theme's translation
10
+ * files.
11
+ *
12
+ * Safe to call multiple times — useL10n() returns the same shared context.
13
+ */
14
+ export function registerScavoldI18n() {
15
+ useL10n().setNamespaceLoader( "@scavold", locale =>
16
+ import( `../l10n/${locale}.json` ).catch( () =>
17
+ import( "../l10n/en.json" )
18
+ )
19
+ );
20
+ }
21
+
22
+ /**
23
+ * Composable for use inside Scavold components. Returns a reactive `t()`
24
+ * helper that looks up keys in the `@scavold` namespace.
25
+ *
26
+ * const { t } = useScavoldI18n();
27
+ * t("nav.breadcrumb") // resolves "@scavold.nav.breadcrumb"
28
+ *
29
+ * @returns {{ t: (key: string) => import('vue').ComputedRef<string> }}
30
+ */
31
+ export function useScavoldI18n() {
32
+ const l10n = useL10n();
33
+
34
+ const t = key => l10n.translate( `@scavold.${key}` );
35
+
36
+ return { t };
37
+ }
@@ -0,0 +1,20 @@
1
+ import { computed } from "vue";
2
+ import { useData } from "vitepress";
3
+
4
+ /**
5
+ * Provides redirect-related state derived from the current page's frontmatter.
6
+ *
7
+ * @returns {{ isLocaleRedirect: import("vue").ComputedRef<boolean> }}
8
+ */
9
+ export function useRedirect() {
10
+ const { frontmatter } = useData();
11
+
12
+ const isLocaleRedirect = computed( () => {
13
+ const r = frontmatter.value.redirect;
14
+ return r !== null && typeof r === "object" && !Array.isArray( r );
15
+ } );
16
+
17
+ const isSimpleRedirect = computed( () => typeof frontmatter.value.redirect === "string" );
18
+
19
+ return { isLocaleRedirect, isSimpleRedirect };
20
+ }