scavold 0.2.0-rc.10 → 0.2.0-rc.11

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/CHANGELOG.md CHANGED
@@ -9,6 +9,16 @@ changes may occur in any release.
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [0.2.0-rc.11] — 2026-10-01
13
+
14
+ ### Added
15
+
16
+ - A container opening a page carries an empty `data-leading` attribute, and the page
17
+ data name it as `leadingContainer` — known before the content renders, so a layout
18
+ can arrange its own parts around a cover at the top of a page, e.g. move its
19
+ breadcrumb below it. Both are information only; nothing changes for a theme that
20
+ does not ask.
21
+
12
22
  ## [0.2.0-rc.10] — 2026-10-01
13
23
 
14
24
  ### Added
@@ -324,7 +334,8 @@ First published release. Version `0.1.0` existed in-tree only.
324
334
  - Only images are processed out of `media_folder`; other file types (video, documents)
325
335
  are never copied into the build output and have to live in the static folder.
326
336
 
327
- [Unreleased]: https://gitlab.com/cratly/scavold/-/compare/v0.2.0-rc.10...main
337
+ [Unreleased]: https://gitlab.com/cratly/scavold/-/compare/v0.2.0-rc.11...main
338
+ [0.2.0-rc.11]: https://gitlab.com/cratly/scavold/-/compare/v0.2.0-rc.10...v0.2.0-rc.11
328
339
  [0.2.0-rc.10]: https://gitlab.com/cratly/scavold/-/compare/v0.2.0-rc.9...v0.2.0-rc.10
329
340
  [0.2.0-rc.9]: https://gitlab.com/cratly/scavold/-/compare/v0.2.0-rc.8...v0.2.0-rc.9
330
341
  [0.2.0-rc.8]: https://gitlab.com/cratly/scavold/-/compare/v0.2.0-rc.7...v0.2.0-rc.8
package/COMPONENTS.md CHANGED
@@ -504,6 +504,71 @@ html { overflow-x: clip; }
504
504
  The overlay is an empty grid cell over the picture; position its content, add a scrim
505
505
  for legibility and restyle `.scavold-cover__toggle` as the design asks.
506
506
 
507
+ #### A cover opening the page
508
+
509
+ Where the theme's own parts go around a cover is the theme's choice; Scavold places
510
+ none of them. It only tells, in two ways, that a page opens with a container:
511
+
512
+ - the container carries an empty `data-leading` attribute;
513
+ - the page data carry `leadingContainer`, the name of that container (`"cover"`), or
514
+ `null` when the page opens with anything else. A layout renders its own parts before
515
+ the page content, so this is known at build time, ahead of the content.
516
+
517
+ A theme that wants its breadcrumb below a cover opening the page, rather than above
518
+ it, combines both. The layout leaves out the breadcrumb on such a page:
519
+
520
+ ```vue
521
+ <!-- Layout.vue -->
522
+ <script setup>
523
+ import { useData } from "vitepress";
524
+ const { page } = useData();
525
+ </script>
526
+
527
+ <template>
528
+ <ScavoldBreadcrumb v-if="page.leadingContainer !== 'cover'" />
529
+ <Content />
530
+ </template>
531
+ ```
532
+
533
+ …and the cover is mapped to a component of the site's own that wraps the built-in one
534
+ and adds the breadcrumb after it when it is the leading one:
535
+
536
+ ```yaml
537
+ # .cratly.config.yaml — the editor keeps offering the cover's own properties
538
+ containers:
539
+ cover:
540
+ component: SiteCover
541
+ ```
542
+
543
+ ```vue
544
+ <!-- .vitepress/theme/components/SiteCover.vue -->
545
+ <script setup>
546
+ import ScavoldCover from "scavold/components/ScavoldCover.vue";
547
+ import ScavoldBreadcrumb from "scavold/components/ScavoldBreadcrumb.vue";
548
+
549
+ defineOptions( { inheritAttrs: false } );
550
+ </script>
551
+
552
+ <template>
553
+ <ScavoldCover v-bind="$attrs"><slot /></ScavoldCover>
554
+ <ScavoldBreadcrumb v-if="$attrs['data-leading'] != null" />
555
+ </template>
556
+ ```
557
+
558
+ ```js
559
+ // .vitepress/theme/index.js — the container renders the component by name
560
+ async function enhanceApp( context ) {
561
+ await scavoldEnhanceApp( context );
562
+ context.app.component( "SiteCover", SiteCover );
563
+ }
564
+ ```
565
+
566
+ Below a cover the breadcrumb sits inside the content column, so the theme's prose
567
+ styles reach its list; keep them out where they differ from the breadcrumb's own.
568
+
569
+ Visual and reading order stay the same this way, which a reordering stylesheet could
570
+ not promise.
571
+
507
572
  ---
508
573
 
509
574
  ### `<ScavoldPageList>`
@@ -1040,6 +1105,12 @@ class names. A flag a component does not declare falls through into the markup a
1040
1105
  empty attribute — `::: section highlight` renders `<section class="highlight"
1041
1106
  data-highlight>`.
1042
1107
 
1108
+ A container that opens the page — its fence is the first thing in the page body —
1109
+ additionally receives an empty `data-leading` attribute. It is information only and
1110
+ changes nothing by itself; a component or a stylesheet can tell from it that this
1111
+ block stands at the very top. The same fact reaches the layout ahead of the content as
1112
+ `page.leadingContainer` (see [A cover opening the page](#a-cover-opening-the-page)).
1113
+
1043
1114
  #### Returns
1044
1115
 
1045
1116
  | Name | Type | Description |
package/index.d.ts CHANGED
@@ -37,6 +37,9 @@ export module Scavold {
37
37
 
38
38
  interface AugmentedPageData extends PageData {
39
39
  hierarchy: HierarchyNode;
40
+
41
+ /** Name of the container the page opens with, e.g. `"cover"`, or null when it opens with anything else. Known before the content renders, so a layout can arrange its own parts around it. */
42
+ leadingContainer: string | null;
40
43
  }
41
44
 
42
45
  interface MenuItem {
package/lib/config.js CHANGED
@@ -8,7 +8,7 @@ import { extractFrontmatterMediaSrcs } from "./excerpt.js";
8
8
  import { DEFAULT_FEED_LIMIT, collectFeedPages, renderRss } from "./feed.js";
9
9
  import { useMedia } from "./media.js";
10
10
  import { patchRenderer } from "./markdown.js";
11
- import { extractContainerMediaSrcs, mediaPropsOf, registerContainers, resolveContainerMap } from "./containers.js";
11
+ import { extractContainerMediaSrcs, leadingContainerOf, mediaPropsOf, registerContainers, resolveContainerMap } from "./containers.js";
12
12
  import { buildSectionManifest, writeSectionManifest } from "./sectionManifest.js";
13
13
  import { isExternalUrl, servableRedirectTarget } from "./redirectTarget.js";
14
14
  import { assertUniquePageKeys } from "./pageKeys.js";
@@ -686,7 +686,21 @@ export async function augmentConfig( rawConfig, options = {} ) {
686
686
  url => media.collectImage( url ),
687
687
  );
688
688
 
689
- return { hierarchy };
689
+ // A layout renders its own parts — a breadcrumb, say — before the page
690
+ // content, so whether the page opens with a container has to be known before
691
+ // that content is rendered. Read from the source rather than the rendered
692
+ // page, which does not exist yet when the layout needs it.
693
+ let leadingContainer = null;
694
+
695
+ try {
696
+ leadingContainer = leadingContainerOf(
697
+ readFileSync( join( sourceFolder( resolvedConfig ), pageData.filePath ), "utf-8" ),
698
+ );
699
+ } catch {
700
+ // a page without a source file of its own, e.g. a generated one
701
+ }
702
+
703
+ return { hierarchy, leadingContainer };
690
704
  },
691
705
  markdown: {
692
706
  config( md ) {
package/lib/containers.js CHANGED
@@ -55,7 +55,7 @@ function resolveTag( name, map ) {
55
55
  * @param {string} containerName the original container name from markdown
56
56
  * @returns {string}
57
57
  */
58
- function renderToken( tag, token, containerName, { mediaProps = {}, resolveMedia, resolveImage } = {} ) {
58
+ function renderToken( tag, token, containerName, { mediaProps = {}, resolveMedia, resolveImage } = {}, leading = false ) {
59
59
  if ( token.nesting !== 1 ) {
60
60
  return `</${tag}>\n`;
61
61
  }
@@ -107,6 +107,13 @@ function renderToken( tag, token, containerName, { mediaProps = {}, resolveMedia
107
107
 
108
108
  attrs.push( `data-container="${containerName}"` );
109
109
 
110
+ // A container opening the page is marked as such, so a theme can tell a cover at
111
+ // the top of a page from one further down. It is information only; see
112
+ // leadingContainerOf() for the page-level counterpart.
113
+ if ( leading ) {
114
+ attrs.push( 'data-leading=""' );
115
+ }
116
+
110
117
  if ( classes ) {
111
118
  attrs.push( `class="${classes}"` );
112
119
  }
@@ -133,7 +140,7 @@ export function registerContainers( md, containerMap, { warn = console.warn, med
133
140
 
134
141
  for ( const [ name, tag ] of Object.entries( containerMap ) ) {
135
142
  md.use( ContainerPlugin, name, {
136
- render: ( tokens, index ) => renderToken( tag, tokens[index], name, renderOptions ),
143
+ render: ( tokens, index ) => renderToken( tag, tokens[index], name, renderOptions, index === 0 ),
137
144
  } );
138
145
  }
139
146
 
@@ -164,11 +171,31 @@ export function registerContainers( md, containerMap, { warn = console.warn, med
164
171
  );
165
172
  }
166
173
 
167
- return renderToken( resolveTag( name, containerMap ), tokens[index], name, renderOptions );
174
+ return renderToken( resolveTag( name, containerMap ), tokens[index], name, renderOptions, index === 0 );
168
175
  },
169
176
  } );
170
177
  }
171
178
 
179
+ /**
180
+ * Names the container a page opens with, i.e. the one whose fence is the first thing
181
+ * in the page body, or null when the page opens with anything else. Follows
182
+ * markdown-it: the front matter does not count, blank lines do not either, and a
183
+ * fence may be indented by up to three spaces.
184
+ *
185
+ * This is the page-level counterpart of the `data-leading` attribute: a layout
186
+ * renders its own parts before the page content, so it has to know in advance.
187
+ *
188
+ * @param {string} source markdown source of one page, front matter included
189
+ * @returns {string|null}
190
+ */
191
+ export function leadingContainerOf( source ) {
192
+ const body = String( source ?? "" ).replace( /^---\r?\n[\s\S]*?\r?\n---[ \t]*(?:\r?\n|$)/, "" );
193
+ const first = body.split( /\r?\n/ ).find( line => line.trim() !== "" ) ?? "";
194
+ const match = /^ {0,3}:{3,}\s*(.*)$/.exec( first );
195
+
196
+ return match ? containerNameOf( match[1] ) || null : null;
197
+ }
198
+
172
199
  /**
173
200
  * Extracts the container name from a container fence's info string, i.e. the first
174
201
  * word after the colons. Returns an empty string when it is not a usable name.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scavold",
3
- "version": "0.2.0-rc.10",
3
+ "version": "0.2.0-rc.11",
4
4
  "type": "module",
5
5
  "description": "VitePress theme framework — a scaffold for building custom VitePress themes with Vue at the core",
6
6
  "keywords": [
@@ -297,6 +297,12 @@ const covers = cover.split( /<div class="scavold-cover[ "]/ ).slice( 1 );
297
297
  const [ coverImage = "", coverVideo = "" ] = covers;
298
298
 
299
299
  expect( covers.length === 2, "renders both covers of the page", `found ${covers.length}` );
300
+ expect( /<div class="scavold-cover"[^>]*\sdata-leading[\s=>]/.test( cover ) && cover.match( /\sdata-leading[\s=>]/g )?.length === 1,
301
+ "marks the cover opening the page, and only that one" );
302
+ expect( cover.includes( '<main data-leading-container="cover">' ),
303
+ "tells the layout before the content renders that the page opens with a cover" );
304
+ expect( !/<main[^>]*data-leading-container/.test( built( "media.html" ) ),
305
+ "tells it nothing on a page that opens with anything else" );
300
306
  // The variants, not just the widest one: a cover is the largest image of a page and
301
307
  // must not cost a phone the full 1920px file.
302
308
  expect( /<source type="image\/webp" srcset="[^"]*wide--320w-[0-9a-f]+\.webp 320w[^"]*wide--1920w-[0-9a-f]+\.webp 1920w"/.test( coverImage ),