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.
- package/COMPONENTS.md +862 -0
- package/FRONTMATTER.md +248 -0
- package/LICENSE +21 -0
- package/README.md +26 -0
- package/components/ScavoldArticle.vue +12 -0
- package/components/ScavoldAside.vue +12 -0
- package/components/ScavoldBreadcrumb.vue +36 -0
- package/components/ScavoldContainer.vue +16 -0
- package/components/ScavoldFooter.vue +12 -0
- package/components/ScavoldHeader.vue +12 -0
- package/components/ScavoldImage.vue +33 -0
- package/components/ScavoldLayout.vue +21 -0
- package/components/ScavoldLocaleMenu.vue +86 -0
- package/components/ScavoldLocaleRedirect.vue +47 -0
- package/components/ScavoldMain.vue +12 -0
- package/components/ScavoldMenu.vue +82 -0
- package/components/ScavoldMenuItems.vue +45 -0
- package/components/ScavoldNav.vue +12 -0
- package/components/ScavoldSection.vue +12 -0
- package/components/ScavoldSimpleRedirect.vue +35 -0
- package/components/ScavoldVideo.vue +74 -0
- package/composables/hierarchy.ts +391 -0
- package/composables/useContainer.js +59 -0
- package/composables/useI18n.js +37 -0
- package/composables/useRedirect.js +20 -0
- package/composables/useVideo.js +89 -0
- package/index.d.ts +43 -0
- package/l10n/de.json +6 -0
- package/l10n/en.json +6 -0
- package/lib/config.d.ts +17 -0
- package/lib/config.js +396 -0
- package/lib/containers.js +128 -0
- package/lib/index.d.ts +9 -0
- package/lib/index.js +60 -0
- package/lib/markdown.js +22 -0
- package/lib/media.js +231 -0
- package/lib/pages.js +494 -0
- package/lib/parser.js +83 -0
- package/lib/redirectTarget.js +46 -0
- package/lib/sectionManifest.js +200 -0
- package/package.json +86 -0
- package/scripts/check-csp.js +68 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { mkdir, writeFile, readFile } from "node:fs/promises";
|
|
3
|
+
import { SECTIONING_ELEMENTS } from "./containers.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Canonical URL of the section-type grammar this manifest conforms to. The
|
|
7
|
+
* grammar is owned by cratly (the cratly.io site), not by Scavold.
|
|
8
|
+
*/
|
|
9
|
+
export const SECTION_SCHEMA_URL = "https://cratly.io/schema/section-types/v0.json";
|
|
10
|
+
|
|
11
|
+
/** Version of the section-type grammar emitted manifests conform to. */
|
|
12
|
+
export const SECTION_SPEC_VERSION = 0;
|
|
13
|
+
|
|
14
|
+
/** Conventional location of the emitted manifest within a website repo. */
|
|
15
|
+
export const SECTION_MANIFEST_DIR = ".cratly";
|
|
16
|
+
export const SECTION_MANIFEST_FILE = "sections.json";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Editor-facing schema for Scavold's built-in section kinds. Emitted into each
|
|
20
|
+
* website's `.cratly/sections.json` so the cratly editor can render the right
|
|
21
|
+
* controls when an author inserts or edits a section.
|
|
22
|
+
*
|
|
23
|
+
* Keep the `video` props in sync with `videoProps` in
|
|
24
|
+
* composables/useVideo.js — that composable defines the runtime (data-*) props;
|
|
25
|
+
* this object describes the same options in the editor's typed vocabulary.
|
|
26
|
+
*/
|
|
27
|
+
export const BUILTIN_SECTIONS = {
|
|
28
|
+
// The seven sectioning wrappers are generic content containers with no
|
|
29
|
+
// named props of their own; authors may still add ad-hoc flags / key-value
|
|
30
|
+
// arguments, which the editor surfaces only when declared in config.
|
|
31
|
+
...Object.fromEntries(
|
|
32
|
+
[ ...SECTIONING_ELEMENTS ].map( name => [ name, {
|
|
33
|
+
label: name[0].toUpperCase() + name.slice( 1 ),
|
|
34
|
+
} ] ),
|
|
35
|
+
),
|
|
36
|
+
|
|
37
|
+
video: {
|
|
38
|
+
label: "Video",
|
|
39
|
+
hint: "Embedded video with playback controls.",
|
|
40
|
+
props: {
|
|
41
|
+
src: { type: "media-file", label: "Video file", required: true },
|
|
42
|
+
poster: { type: "media-file", label: "Poster image" },
|
|
43
|
+
overlay: {
|
|
44
|
+
type: "boolean",
|
|
45
|
+
label: "Background mode",
|
|
46
|
+
hint: "Play the video as a background and lay this container's content on top of it.",
|
|
47
|
+
},
|
|
48
|
+
autoplay: { type: "boolean", label: "Autoplay (muted)", default: false },
|
|
49
|
+
loop: { type: "boolean", label: "Loop" },
|
|
50
|
+
muted: { type: "boolean", label: "Muted" },
|
|
51
|
+
controls: {
|
|
52
|
+
type: "boolean",
|
|
53
|
+
label: "Show controls",
|
|
54
|
+
hint: "Always shown for a normal video; in background mode the controls are hidden unless you enable this.",
|
|
55
|
+
},
|
|
56
|
+
preload: {
|
|
57
|
+
type: "enum",
|
|
58
|
+
label: "Preload",
|
|
59
|
+
values: [ "none", "metadata", "auto" ],
|
|
60
|
+
default: "metadata",
|
|
61
|
+
},
|
|
62
|
+
label: {
|
|
63
|
+
type: "text",
|
|
64
|
+
label: "Accessible label",
|
|
65
|
+
hint: "Announced by screen readers when the surrounding text does not describe the video.",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Normalises a single prop definition from a container declaration. Accepts the
|
|
73
|
+
* shorthand string form (`image: media-file`) and the full object form.
|
|
74
|
+
*
|
|
75
|
+
* @param {string|object} def prop definition from .cratly.config.yaml
|
|
76
|
+
* @returns {object} prop definition for the manifest
|
|
77
|
+
*/
|
|
78
|
+
function normaliseProp( def ) {
|
|
79
|
+
if ( typeof def === "string" ) {
|
|
80
|
+
return { type: def };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { ...def };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Derives the typed prop map for one declared container, merging the three
|
|
88
|
+
* declaration forms. Explicit `props` win; `flags` and `kv` only fill names not
|
|
89
|
+
* already present as typed props.
|
|
90
|
+
*
|
|
91
|
+
* @param {object} decl object form of a container declaration
|
|
92
|
+
* @returns {Object<string,object>}
|
|
93
|
+
*/
|
|
94
|
+
function propsFromDeclaration( decl ) {
|
|
95
|
+
const props = {};
|
|
96
|
+
|
|
97
|
+
if ( decl.props && typeof decl.props === "object" ) {
|
|
98
|
+
for ( const [ name, def ] of Object.entries( decl.props ) ) {
|
|
99
|
+
props[name] = normaliseProp( def );
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// boolean flag sugar: `flags: [ dark, centered ]`
|
|
104
|
+
for ( const flag of decl.flags ?? [] ) {
|
|
105
|
+
props[flag] ??= { type: "boolean" };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// untyped key-value sugar: `kv: [ background, link ]` → text props
|
|
109
|
+
for ( const key of decl.kv ?? [] ) {
|
|
110
|
+
props[key] ??= { type: "text" };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return props;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Builds a section-type manifest object from the resolved set of containers.
|
|
118
|
+
*
|
|
119
|
+
* Starts from Scavold's built-in section kinds, then applies the site's
|
|
120
|
+
* `containers` declarations from .cratly.config.yaml — adding custom sections
|
|
121
|
+
* and overriding labels/hints/props of built-ins.
|
|
122
|
+
*
|
|
123
|
+
* @param {object} [crateContainers] the `containers` block from .cratly.config.yaml
|
|
124
|
+
* @param {object} [options]
|
|
125
|
+
* @param {string} [options.adapterVersion] version reported under `adapter.version`
|
|
126
|
+
* @returns {object} manifest conforming to SECTION_SCHEMA_URL
|
|
127
|
+
*/
|
|
128
|
+
export function buildSectionManifest( crateContainers = {}, { adapterVersion } = {} ) {
|
|
129
|
+
const sections = {};
|
|
130
|
+
|
|
131
|
+
for ( const [ name, def ] of Object.entries( BUILTIN_SECTIONS ) ) {
|
|
132
|
+
sections[name] = structuredClone( def );
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for ( const [ name, value ] of Object.entries( crateContainers ) ) {
|
|
136
|
+
const decl = typeof value === "string" || value == null ? {} : value;
|
|
137
|
+
const entry = sections[name] ?? {};
|
|
138
|
+
|
|
139
|
+
if ( decl.label != null ) {
|
|
140
|
+
entry.label = decl.label;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if ( decl.hint != null ) {
|
|
144
|
+
entry.hint = decl.hint;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const props = propsFromDeclaration( decl );
|
|
148
|
+
if ( Object.keys( props ).length > 0 ) {
|
|
149
|
+
entry.props = { ...( entry.props ?? {} ), ...props };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Editors fall back to the raw container name when no label is given.
|
|
153
|
+
if ( entry.label == null ) {
|
|
154
|
+
entry.label = name;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
sections[name] = entry;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
$schema: SECTION_SCHEMA_URL,
|
|
162
|
+
specVersion: SECTION_SPEC_VERSION,
|
|
163
|
+
adapter: {
|
|
164
|
+
name: "scavold",
|
|
165
|
+
...( adapterVersion && { version: adapterVersion } ),
|
|
166
|
+
},
|
|
167
|
+
sections,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Writes a section-type manifest to `{cwd}/.cratly/sections.json`, creating the
|
|
173
|
+
* folder as needed. Skips the write when the on-disk content is already
|
|
174
|
+
* identical, so emitting during dev does not trigger spurious file-watch churn.
|
|
175
|
+
*
|
|
176
|
+
* @param {string} cwd project root
|
|
177
|
+
* @param {object} manifest manifest object from buildSectionManifest()
|
|
178
|
+
* @returns {Promise<{ path: string, written: boolean }>}
|
|
179
|
+
*/
|
|
180
|
+
export async function writeSectionManifest( cwd, manifest ) {
|
|
181
|
+
const dir = join( cwd, SECTION_MANIFEST_DIR );
|
|
182
|
+
const path = join( dir, SECTION_MANIFEST_FILE );
|
|
183
|
+
const next = JSON.stringify( manifest, null, "\t" ) + "\n";
|
|
184
|
+
|
|
185
|
+
let current = null;
|
|
186
|
+
try {
|
|
187
|
+
current = await readFile( path, "utf-8" );
|
|
188
|
+
} catch {
|
|
189
|
+
// file absent — fall through to write
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if ( current === next ) {
|
|
193
|
+
return { path, written: false };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
await mkdir( dir, { recursive: true } );
|
|
197
|
+
await writeFile( path, next, "utf-8" );
|
|
198
|
+
|
|
199
|
+
return { path, written: true };
|
|
200
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "scavold",
|
|
3
|
+
"version": "0.2.0-rc.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "VitePress theme framework — a scaffold for building custom VitePress themes with Vue at the core",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"vitepress",
|
|
8
|
+
"vue",
|
|
9
|
+
"theme",
|
|
10
|
+
"framework",
|
|
11
|
+
"static-site-generator",
|
|
12
|
+
"i18n",
|
|
13
|
+
"responsive-images",
|
|
14
|
+
"markdown-container",
|
|
15
|
+
"cratly"
|
|
16
|
+
],
|
|
17
|
+
"author": "cepharum GmbH <info@cepharum.de> (https://cepharum.de)",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://gitlab.com/cepharum-foss/cratly/scavold.git"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://scavold.io",
|
|
24
|
+
"bugs": "https://gitlab.com/cepharum-foss/cratly/scavold/-/issues",
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=20"
|
|
27
|
+
},
|
|
28
|
+
"types": "./index.d.ts",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./lib/index.d.ts",
|
|
32
|
+
"default": "./lib/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./config": {
|
|
35
|
+
"types": "./lib/config.d.ts",
|
|
36
|
+
"default": "./lib/config.js"
|
|
37
|
+
},
|
|
38
|
+
"./components/*.vue": "./components/*.vue",
|
|
39
|
+
"./composables/*": "./composables/*",
|
|
40
|
+
"./lib/*": "./lib/*"
|
|
41
|
+
},
|
|
42
|
+
"bin": {
|
|
43
|
+
"scavold-check-csp": "./scripts/check-csp.js"
|
|
44
|
+
},
|
|
45
|
+
"files": [
|
|
46
|
+
"components",
|
|
47
|
+
"composables",
|
|
48
|
+
"l10n",
|
|
49
|
+
"lib",
|
|
50
|
+
"scripts",
|
|
51
|
+
"index.d.ts",
|
|
52
|
+
"COMPONENTS.md",
|
|
53
|
+
"FRONTMATTER.md",
|
|
54
|
+
"README.md",
|
|
55
|
+
"!**/*.test.js"
|
|
56
|
+
],
|
|
57
|
+
"scripts": {
|
|
58
|
+
"check-csp": "node scripts/check-csp.js",
|
|
59
|
+
"typecheck": "tsc --noEmit",
|
|
60
|
+
"docs:dev": "vitepress dev docs",
|
|
61
|
+
"docs:build": "vitepress build docs",
|
|
62
|
+
"docs:preview": "vitepress preview docs",
|
|
63
|
+
"test:unit": "bun test --coverage",
|
|
64
|
+
"test:unit:dev": "bun test --watch",
|
|
65
|
+
"lint": "eslint --fix"
|
|
66
|
+
},
|
|
67
|
+
"peerDependencies": {
|
|
68
|
+
"vitepress": ">=1.0.0",
|
|
69
|
+
"vue": ">=3.0.0"
|
|
70
|
+
},
|
|
71
|
+
"dependencies": {
|
|
72
|
+
"@cepharum/vue3-i18n": "^0.5.4",
|
|
73
|
+
"markdown-it-container": "^4.0.0",
|
|
74
|
+
"sharp": "^0.34.5",
|
|
75
|
+
"yaml": "^2.8.3"
|
|
76
|
+
},
|
|
77
|
+
"devDependencies": {
|
|
78
|
+
"@types/node": "^26.1.2",
|
|
79
|
+
"eslint": "^10.2.1",
|
|
80
|
+
"eslint-config-cepharum": "^2.0.2",
|
|
81
|
+
"eslint-plugin-vue": "^10.9.0",
|
|
82
|
+
"typescript": "^5.9.3",
|
|
83
|
+
"vitepress": "^1.6.4",
|
|
84
|
+
"vue": "^3.5.33"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Extracts SHA-256 hashes of all inline scripts from a built VitePress site and
|
|
3
|
+
// compares them against a committed reference file. Exits 1 and prints the new
|
|
4
|
+
// hashes when they change, so CI can notify the developer to update the
|
|
5
|
+
// Content-Security-Policy header and commit the new reference.
|
|
6
|
+
//
|
|
7
|
+
// Usage: check-csp [dist-dir] [hashes-file]
|
|
8
|
+
// dist-dir Path to the built site directory (default: docs/.vitepress/dist)
|
|
9
|
+
// hashes-file Path to the committed hash store (default: .csp-hashes)
|
|
10
|
+
|
|
11
|
+
import { createHash } from "crypto";
|
|
12
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync } from "fs";
|
|
13
|
+
import { join, extname } from "path";
|
|
14
|
+
|
|
15
|
+
function findHtml( dir ) {
|
|
16
|
+
const out = [];
|
|
17
|
+
for ( const entry of readdirSync( dir, { withFileTypes: true } ) ) {
|
|
18
|
+
const p = join( dir, entry.name );
|
|
19
|
+
if ( entry.isDirectory() ) {
|
|
20
|
+
out.push( ...findHtml( p ) );
|
|
21
|
+
} else if ( extname( entry.name ) === ".html" ) {
|
|
22
|
+
out.push( p );
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function extractHashes( html ) {
|
|
29
|
+
const hashes = new Set();
|
|
30
|
+
for ( const [ , content ] of html.matchAll( /<script(?![^>]*\bsrc\b)[^>]*>([\s\S]*?)<\/script>/gi ) ) {
|
|
31
|
+
if ( content.trim() ) {
|
|
32
|
+
const hash = createHash( "sha256" ).update( content ).digest( "base64" );
|
|
33
|
+
hashes.add( `'sha256-${hash}'` );
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return hashes;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const [ distDir = "docs/.vitepress/dist", hashesFile = ".csp-hashes", caddyFile ] = process.argv.slice( 2 );
|
|
40
|
+
|
|
41
|
+
const hashes = new Set();
|
|
42
|
+
for ( const file of findHtml( distDir ) ) {
|
|
43
|
+
for ( const h of extractHashes( readFileSync( file, "utf8" ) ) ) {
|
|
44
|
+
hashes.add( h );
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const sorted = [ ...hashes ].sort();
|
|
49
|
+
const current = sorted.join( " " );
|
|
50
|
+
|
|
51
|
+
if ( caddyFile ) {
|
|
52
|
+
// caddy-csp format: one bare hash per line, no surrounding single quotes.
|
|
53
|
+
writeFileSync( caddyFile, sorted.map( h => h.replace( /'/g, "" ) ).join( "\n" ) + "\n", "utf8" );
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const stored = existsSync( hashesFile ) ? readFileSync( hashesFile, "utf8" ).trim() : "";
|
|
57
|
+
|
|
58
|
+
if ( current === stored ) {
|
|
59
|
+
console.log( "CSP script hashes unchanged." );
|
|
60
|
+
process.exit( 0 );
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.error( `CSP script hashes have changed.\n` );
|
|
64
|
+
console.error( `Update your Content-Security-Policy script-src to:\n` );
|
|
65
|
+
console.error( ` script-src 'self' ${current}\n` );
|
|
66
|
+
console.error( `Then commit ${hashesFile} with:\n` );
|
|
67
|
+
console.error( ` ${current}` );
|
|
68
|
+
process.exit( 1 );
|