@anvpx/motion-build 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +67 -2
- package/dist/.out/demo/boot.js +90 -0
- package/dist/.out/demo/runtime/easing.js +45 -0
- package/dist/.out/demo/runtime/mount.js +153 -0
- package/dist/.out/demo/runtime/timeline.js +199 -0
- package/dist/.out/demo/scenes/count/scene.js +6 -0
- package/dist/.out/demo/scenes/intro/scene.js +6 -0
- package/dist/.out/demo/scenes/outro/scene.js +6 -0
- package/dist/.out/demo/scenes/path/scene.js +6 -0
- package/dist/.out/demo/scenes/signals/scene.js +6 -0
- package/dist/.out/demo/shared/format.js +59 -0
- package/dist/.out/demo/shared/motion.js +117 -0
- package/dist/.out/demo-overlays/boot.js +95 -0
- package/dist/.out/demo-overlays/runtime/easing.js +45 -0
- package/dist/.out/demo-overlays/runtime/mount.js +153 -0
- package/dist/.out/demo-overlays/runtime/timeline.js +199 -0
- package/dist/.out/demo-overlays/scenes/chapter-signals/scene.js +6 -0
- package/dist/.out/demo-overlays/scenes/count/scene.js +6 -0
- package/dist/.out/demo-overlays/scenes/flow-progress/scene.js +17 -0
- package/dist/.out/demo-overlays/scenes/intro/scene.js +6 -0
- package/dist/.out/demo-overlays/scenes/mark/scene.js +6 -0
- package/dist/.out/demo-overlays/scenes/outro/scene.js +6 -0
- package/dist/.out/demo-overlays/scenes/path/scene.js +6 -0
- package/dist/.out/demo-overlays/scenes/presenter/scene.js +6 -0
- package/dist/.out/demo-overlays/scenes/signals/scene.js +6 -0
- package/dist/.out/demo-overlays/scenes/tick/scene.js +21 -0
- package/dist/.out/demo-overlays/shared/format.js +59 -0
- package/dist/.out/demo-overlays/shared/motion.js +117 -0
- package/dist/assets.d.ts +36 -0
- package/dist/assets.js +95 -0
- package/dist/audio/approval.d.ts +45 -0
- package/dist/audio/approval.js +82 -0
- package/dist/audio/mix.d.ts +238 -0
- package/dist/audio/mix.js +497 -0
- package/dist/audio/music.d.ts +82 -0
- package/dist/audio/music.js +134 -0
- package/dist/audio/preview.d.ts +31 -0
- package/dist/audio/preview.js +31 -0
- package/dist/build.d.ts +65 -0
- package/dist/build.js +260 -0
- package/dist/cache.d.ts +59 -0
- package/dist/cache.js +249 -0
- package/dist/emit/boot.d.ts +16 -0
- package/dist/emit/boot.js +32 -0
- package/dist/emit/boot.template.js +85 -0
- package/dist/emit/fragment-guard.d.ts +5 -0
- package/dist/emit/fragment-guard.js +15 -0
- package/dist/emit/index-html.d.ts +26 -0
- package/dist/emit/index-html.js +63 -0
- package/dist/emit/safe-json.d.ts +2 -0
- package/dist/emit/safe-json.js +7 -0
- package/dist/emit/vendor.d.ts +7 -0
- package/dist/emit/vendor.js +57 -0
- package/dist/fonts.d.ts +39 -0
- package/dist/fonts.js +112 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +19 -0
- package/dist/integration/render.integration.d.ts +1 -0
- package/dist/integration/render.integration.js +162 -0
- package/dist/lint/custom-source.d.ts +12 -0
- package/dist/lint/custom-source.js +130 -0
- package/dist/lint/fixtures.d.ts +24 -0
- package/dist/lint/fixtures.js +42 -0
- package/dist/lint/flow.d.ts +12 -0
- package/dist/lint/flow.js +38 -0
- package/dist/lint/frame.d.ts +3 -0
- package/dist/lint/frame.js +2 -0
- package/dist/lint/hook-first.d.ts +6 -0
- package/dist/lint/hook-first.js +27 -0
- package/dist/lint/index.d.ts +25 -0
- package/dist/lint/index.js +55 -0
- package/dist/lint/layout-estimate.d.ts +18 -0
- package/dist/lint/layout-estimate.js +165 -0
- package/dist/lint/linear-ease.d.ts +2 -0
- package/dist/lint/linear-ease.js +14 -0
- package/dist/lint/long-hold.d.ts +9 -0
- package/dist/lint/long-hold.js +52 -0
- package/dist/lint/min-hold.d.ts +4 -0
- package/dist/lint/min-hold.js +10 -0
- package/dist/lint/motion-craft.d.ts +18 -0
- package/dist/lint/motion-craft.js +112 -0
- package/dist/lint/narration-pace.d.ts +6 -0
- package/dist/lint/narration-pace.js +35 -0
- package/dist/lint/overlays.d.ts +9 -0
- package/dist/lint/overlays.js +64 -0
- package/dist/lint/reading-time.d.ts +5 -0
- package/dist/lint/reading-time.js +17 -0
- package/dist/lint/safe-zone-9x16.d.ts +11 -0
- package/dist/lint/safe-zone-9x16.js +101 -0
- package/dist/lint/text-floor.d.ts +8 -0
- package/dist/lint/text-floor.js +34 -0
- package/dist/lint/transition-implemented.d.ts +3 -0
- package/dist/lint/transition-implemented.js +15 -0
- package/dist/lint/transition-length.d.ts +4 -0
- package/dist/lint/transition-length.js +10 -0
- package/dist/lint/type-grid.d.ts +30 -0
- package/dist/lint/type-grid.js +147 -0
- package/dist/lint/types.d.ts +41 -0
- package/dist/lint/types.js +1 -0
- package/dist/lint/words-on-screen.d.ts +5 -0
- package/dist/lint/words-on-screen.js +24 -0
- package/dist/manifest.d.ts +53 -0
- package/dist/manifest.js +55 -0
- package/dist/render-manifest.d.ts +21 -0
- package/dist/render-manifest.js +7 -0
- package/dist/review/review.d.ts +15 -0
- package/dist/review/review.js +56 -0
- package/dist/templates/discover-check.d.ts +11 -0
- package/dist/templates/discover-check.js +36 -0
- package/dist/templates/discover.d.ts +37 -0
- package/dist/templates/discover.js +63 -0
- package/dist/templates/expand.d.ts +17 -0
- package/dist/templates/expand.js +184 -0
- package/dist/templates/fixtures.d.ts +4 -0
- package/dist/templates/fixtures.js +63 -0
- package/dist/templates/index.d.ts +8 -0
- package/dist/templates/index.js +6 -0
- package/dist/templates/instantiate.d.ts +43 -0
- package/dist/templates/instantiate.js +160 -0
- package/dist/templates/load.d.ts +16 -0
- package/dist/templates/load.js +122 -0
- package/dist/templates/pacing.d.ts +21 -0
- package/dist/templates/pacing.js +61 -0
- package/dist/templates/search.d.ts +64 -0
- package/dist/templates/search.js +129 -0
- package/dist/templates/semver.d.ts +2 -0
- package/dist/templates/semver.js +12 -0
- package/dist/templates/slots.d.ts +8 -0
- package/dist/templates/slots.js +116 -0
- package/dist/templates/style.d.ts +25 -0
- package/dist/templates/style.js +56 -0
- package/dist/templates/types.d.ts +29 -0
- package/dist/templates/types.js +1 -0
- package/dist/templates/validate.d.ts +17 -0
- package/dist/templates/validate.js +203 -0
- package/dist/timing.d.ts +45 -0
- package/dist/timing.js +98 -0
- package/dist/validate-full.d.ts +3 -0
- package/dist/validate-full.js +132 -0
- package/dist/verify.d.ts +32 -0
- package/dist/verify.js +185 -0
- package/dist/versions.d.ts +26 -0
- package/dist/versions.js +74 -0
- package/package.json +40 -3
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ValidationError } from "@anvpx/motion-schema";
|
|
2
|
+
import type { LintFinding } from "../lint/index.ts";
|
|
3
|
+
import type { LoadedTemplate } from "./types.ts";
|
|
4
|
+
/**
|
|
5
|
+
* `discover` on the manifest and every blueprint: an error in built-in templates (fully populated),
|
|
6
|
+
* a warning elsewhere (optional for user and project templates).
|
|
7
|
+
*/
|
|
8
|
+
export declare const discoverFindings: (template: LoadedTemplate, builtIn: boolean) => {
|
|
9
|
+
errors: ValidationError[];
|
|
10
|
+
warnings: LintFinding[];
|
|
11
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Fields a built-in `discover` must carry beyond the schema minimum. */
|
|
2
|
+
const MANIFEST_REQUIRED = ["audience", "notFor", "tone", "aspects"];
|
|
3
|
+
const BLUEPRINT_REQUIRED = [...MANIFEST_REQUIRED, "lengths"];
|
|
4
|
+
const missingFields = (discover, required) => required.filter((field) => discover[field] === undefined);
|
|
5
|
+
const targetsOf = (template) => [
|
|
6
|
+
{
|
|
7
|
+
path: "template.json#/discover",
|
|
8
|
+
discover: template.manifest.discover,
|
|
9
|
+
required: MANIFEST_REQUIRED,
|
|
10
|
+
},
|
|
11
|
+
...[...template.blueprints.values()].map((blueprint) => ({
|
|
12
|
+
path: `blueprints/${blueprint.id}.json#/discover`,
|
|
13
|
+
discover: blueprint.discover,
|
|
14
|
+
required: BLUEPRINT_REQUIRED,
|
|
15
|
+
})),
|
|
16
|
+
];
|
|
17
|
+
const problemOf = ({ discover, required }) => {
|
|
18
|
+
if (discover === undefined)
|
|
19
|
+
return "missing discover";
|
|
20
|
+
const missing = missingFields(discover, required);
|
|
21
|
+
return missing.length === 0 ? undefined : `discover missing: ${missing.join(", ")}`;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* `discover` on the manifest and every blueprint: an error in built-in templates (fully populated),
|
|
25
|
+
* a warning elsewhere (optional for user and project templates).
|
|
26
|
+
*/
|
|
27
|
+
export const discoverFindings = (template, builtIn) => {
|
|
28
|
+
const found = targetsOf(template).flatMap((target) => {
|
|
29
|
+
const message = problemOf(target);
|
|
30
|
+
return message === undefined ? [] : [{ path: target.path, message }];
|
|
31
|
+
});
|
|
32
|
+
const suggestion = "add the discover fields; mk template list --for reads them";
|
|
33
|
+
return builtIn
|
|
34
|
+
? { errors: found.map((f) => ({ ...f, suggestion })), warnings: [] }
|
|
35
|
+
: { errors: [], warnings: found.map((f) => ({ severity: "warning", ...f, suggestion })) };
|
|
36
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type ValidationError } from "@anvpx/motion-schema";
|
|
2
|
+
/** A directory that may hold templates; earlier roots win. */
|
|
3
|
+
export interface TemplateRoot {
|
|
4
|
+
readonly path: string;
|
|
5
|
+
/** Label shown in listings, e.g. `project`, `user`, `built-in`. */
|
|
6
|
+
readonly source: string;
|
|
7
|
+
}
|
|
8
|
+
export interface TemplateCandidate {
|
|
9
|
+
readonly id: string;
|
|
10
|
+
readonly version: string;
|
|
11
|
+
readonly dir: string;
|
|
12
|
+
readonly source: string;
|
|
13
|
+
readonly rootIndex: number;
|
|
14
|
+
}
|
|
15
|
+
export interface ShadowedTemplate extends TemplateCandidate {
|
|
16
|
+
readonly reason: "root" | "version";
|
|
17
|
+
/** `dir` of the candidate that won. */
|
|
18
|
+
readonly by: string;
|
|
19
|
+
}
|
|
20
|
+
export interface Discovery {
|
|
21
|
+
/** Winners, one per id, sorted by id. */
|
|
22
|
+
readonly templates: readonly TemplateCandidate[];
|
|
23
|
+
readonly shadowed: readonly ShadowedTemplate[];
|
|
24
|
+
/** Manifests that exist but do not parse; never silent. */
|
|
25
|
+
readonly problems: readonly ValidationError[];
|
|
26
|
+
}
|
|
27
|
+
export interface RootsInput {
|
|
28
|
+
readonly projectDir: string;
|
|
29
|
+
/** Value of `MK_TEMPLATES_PATH` (list separated by the platform path delimiter). */
|
|
30
|
+
readonly envPath?: string | undefined;
|
|
31
|
+
readonly homeDir: string;
|
|
32
|
+
readonly builtInDir: string;
|
|
33
|
+
}
|
|
34
|
+
/** Search roots, highest precedence first: project, env path, user, built-in. */
|
|
35
|
+
export declare const templateRoots: (input: RootsInput) => TemplateRoot[];
|
|
36
|
+
/** Finds templates under the roots; the highest-precedence root wins, then the highest semver. */
|
|
37
|
+
export declare const discoverTemplates: (roots: readonly TemplateRoot[]) => Discovery;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { delimiter, join } from "node:path";
|
|
3
|
+
import { parseTemplateManifest } from "@anvpx/motion-schema";
|
|
4
|
+
import { MANIFEST_FILE } from "./load.js";
|
|
5
|
+
import { compareSemver } from "./semver.js";
|
|
6
|
+
/** Search roots, highest precedence first: project, env path, user, built-in. */
|
|
7
|
+
export const templateRoots = (input) => [
|
|
8
|
+
{ path: join(input.projectDir, "templates"), source: "project" },
|
|
9
|
+
...(input.envPath ?? "")
|
|
10
|
+
.split(delimiter)
|
|
11
|
+
.filter((path) => path !== "")
|
|
12
|
+
.map((path) => ({ path, source: "env" })),
|
|
13
|
+
{ path: join(input.homeDir, ".motion-kit", "templates"), source: "user" },
|
|
14
|
+
{ path: input.builtInDir, source: "built-in" },
|
|
15
|
+
];
|
|
16
|
+
const subdirs = (dir) => {
|
|
17
|
+
try {
|
|
18
|
+
return readdirSync(dir)
|
|
19
|
+
.sort()
|
|
20
|
+
.filter((name) => statSync(join(dir, name)).isDirectory())
|
|
21
|
+
.map((name) => join(dir, name));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
/** `<root>/<id>/template.json` or the versioned layout `<root>/<id>/<version>/template.json`. */
|
|
28
|
+
const manifestDirs = (root) => subdirs(root).flatMap((dir) => existsSync(join(dir, MANIFEST_FILE))
|
|
29
|
+
? [dir]
|
|
30
|
+
: subdirs(dir).filter((child) => existsSync(join(child, MANIFEST_FILE))));
|
|
31
|
+
const readCandidate = (dir, root, rootIndex) => {
|
|
32
|
+
const file = join(dir, MANIFEST_FILE);
|
|
33
|
+
try {
|
|
34
|
+
const parsed = parseTemplateManifest(JSON.parse(readFileSync(file, "utf8")));
|
|
35
|
+
if (!parsed.ok) {
|
|
36
|
+
const [first] = parsed.errors;
|
|
37
|
+
return { path: file, message: `${first?.path ?? ""} ${first?.message ?? "invalid"}`.trim() };
|
|
38
|
+
}
|
|
39
|
+
const { id, version } = parsed.value;
|
|
40
|
+
return { id, version, dir, source: root.source, rootIndex };
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
return { path: file, message: `unreadable: ${error.message}` };
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const isCandidate = (item) => "id" in item;
|
|
47
|
+
const precedence = (a, b) => a.rootIndex - b.rootIndex || compareSemver(b.version, a.version) || a.dir.localeCompare(b.dir);
|
|
48
|
+
/** Finds templates under the roots; the highest-precedence root wins, then the highest semver. */
|
|
49
|
+
export const discoverTemplates = (roots) => {
|
|
50
|
+
const found = roots.flatMap((root, index) => manifestDirs(root.path).map((dir) => readCandidate(dir, root, index)));
|
|
51
|
+
const candidates = found.filter(isCandidate);
|
|
52
|
+
const problems = found.filter((item) => !isCandidate(item));
|
|
53
|
+
const ordered = [...candidates].sort((a, b) => a.id.localeCompare(b.id) || precedence(a, b));
|
|
54
|
+
const winners = ordered.filter((candidate, index) => candidate.id !== ordered[index - 1]?.id);
|
|
55
|
+
const shadowed = ordered.flatMap((candidate) => {
|
|
56
|
+
const winner = winners.find((item) => item.id === candidate.id);
|
|
57
|
+
if (winner === undefined || winner === candidate)
|
|
58
|
+
return [];
|
|
59
|
+
const reason = candidate.rootIndex === winner.rootIndex ? "version" : "root";
|
|
60
|
+
return [{ ...candidate, reason, by: winner.dir }];
|
|
61
|
+
});
|
|
62
|
+
return { templates: winners, shadowed, problems };
|
|
63
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type BlueprintDoc, type BlueprintScene, type Result } from "@anvpx/motion-schema";
|
|
2
|
+
import { type SlotValues } from "./slots.ts";
|
|
3
|
+
type Scene = Omit<BlueprintScene, "when">;
|
|
4
|
+
export interface Expanded {
|
|
5
|
+
readonly meta: Record<string, unknown>;
|
|
6
|
+
readonly scenes: readonly Scene[];
|
|
7
|
+
}
|
|
8
|
+
/** Expands `$slot`/`$spread`/`$item`/`when`/`$repeat` into plain storyboard scenes and meta. */
|
|
9
|
+
export declare const expandBlueprint: (blueprint: BlueprintDoc, slots: SlotValues) => Result<Expanded>;
|
|
10
|
+
/** Every slot name a blueprint refers to, with where; used by template validation. */
|
|
11
|
+
export interface SlotReference {
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly path: string;
|
|
14
|
+
readonly list: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare const slotReferences: (blueprint: BlueprintDoc) => SlotReference[];
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { SLOT_TRANSFORMS, toJsonPointer, } from "@anvpx/motion-schema";
|
|
2
|
+
import { isSet } from "./slots.js";
|
|
3
|
+
const ABSENT = Symbol("absent");
|
|
4
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
|
+
const fail = (scope, path, message, suggestion) => {
|
|
6
|
+
const base = { path: toJsonPointer(path), message };
|
|
7
|
+
scope.errors.push(suggestion === undefined ? base : { ...base, suggestion });
|
|
8
|
+
return ABSENT;
|
|
9
|
+
};
|
|
10
|
+
const slugify = (label) => label
|
|
11
|
+
.toLowerCase()
|
|
12
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
13
|
+
.replace(/^-+|-+$/g, "") || "node";
|
|
14
|
+
const labelsOf = (value) => Array.isArray(value) ? value.map((item) => String(item)) : [];
|
|
15
|
+
const flowNodes = (value, scope, path) => {
|
|
16
|
+
const labels = labelsOf(value);
|
|
17
|
+
const ids = labels.map(slugify);
|
|
18
|
+
const dup = ids.find((id, index) => ids.indexOf(id) !== index);
|
|
19
|
+
if (dup !== undefined) {
|
|
20
|
+
return fail(scope, path, `two labels both become node id "${dup}"`, "make the labels distinct");
|
|
21
|
+
}
|
|
22
|
+
return labels.map((label, index) => ({ id: ids[index], label }));
|
|
23
|
+
};
|
|
24
|
+
const EDGE = /^([^>]+)>([^|]+)(?:\|(.+))?$/;
|
|
25
|
+
const flowEdges = (value, scope, path) => labelsOf(value).flatMap((text, index) => {
|
|
26
|
+
const match = EDGE.exec(text);
|
|
27
|
+
if (match === null) {
|
|
28
|
+
fail(scope, [...path, index], `"${text}" is not an edge`, 'write "From>To" or "From>To|label"');
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
const [, from = "", to = "", label] = match;
|
|
32
|
+
const edge = { from: slugify(from.trim()), to: slugify(to.trim()) };
|
|
33
|
+
return [label === undefined ? edge : { ...edge, label: label.trim() }];
|
|
34
|
+
});
|
|
35
|
+
const flowChain = (value) => {
|
|
36
|
+
const ids = labelsOf(value).map(slugify);
|
|
37
|
+
return ids.slice(1).map((to, index) => ({ from: ids[index], to }));
|
|
38
|
+
};
|
|
39
|
+
const transform = (name, value, scope, path) => {
|
|
40
|
+
if (name === "flow-nodes")
|
|
41
|
+
return flowNodes(value, scope, path);
|
|
42
|
+
if (name === "flow-edges")
|
|
43
|
+
return flowEdges(value, scope, path);
|
|
44
|
+
if (name === "flow-chain")
|
|
45
|
+
return flowChain(value);
|
|
46
|
+
return fail(scope, path, `unknown transform "${name}"`, `use one of: ${SLOT_TRANSFORMS.join("|")}`);
|
|
47
|
+
};
|
|
48
|
+
const slotRef = (node, scope, path) => {
|
|
49
|
+
const extra = Object.keys(node).filter((key) => !["$slot", "as", "orElse"].includes(key));
|
|
50
|
+
if (extra.length > 0)
|
|
51
|
+
return fail(scope, path, `$slot takes only as/orElse, got ${extra.join(", ")}`);
|
|
52
|
+
const name = String(node.$slot);
|
|
53
|
+
if (!scope.known.has(name)) {
|
|
54
|
+
return fail(scope, path, `unknown slot "${name}"`, `use one of: ${[...scope.known].join("|")}`);
|
|
55
|
+
}
|
|
56
|
+
const value = scope.slots[name];
|
|
57
|
+
if (!isSet(value)) {
|
|
58
|
+
return node.orElse === undefined ? ABSENT : walk(node.orElse, scope, [...path, "orElse"]);
|
|
59
|
+
}
|
|
60
|
+
return typeof node.as === "string" ? transform(node.as, value, scope, path) : value;
|
|
61
|
+
};
|
|
62
|
+
const itemRef = (node, scope, path) => {
|
|
63
|
+
if (scope.item === undefined)
|
|
64
|
+
return fail(scope, path, "$item is only valid inside $repeat");
|
|
65
|
+
const field = node.$item;
|
|
66
|
+
if (field === ".")
|
|
67
|
+
return scope.item.value;
|
|
68
|
+
if (field === "#")
|
|
69
|
+
return scope.item.index + 1;
|
|
70
|
+
if (typeof field === "string" && isRecord(scope.item.value) && field in scope.item.value) {
|
|
71
|
+
return scope.item.value[field];
|
|
72
|
+
}
|
|
73
|
+
return fail(scope, path, `item has no field "${String(field)}"`, 'use "." for the whole item, "#" for its 1-based position');
|
|
74
|
+
};
|
|
75
|
+
const objectOf = (node, scope, path) => {
|
|
76
|
+
const spread = node.$spread === undefined ? ABSENT : spreadOf(String(node.$spread), scope, path);
|
|
77
|
+
const own = Object.entries(node)
|
|
78
|
+
.filter(([key]) => key !== "$spread")
|
|
79
|
+
.map(([key, child]) => [key, walk(child, scope, [...path, key])])
|
|
80
|
+
.filter(([, value]) => value !== ABSENT);
|
|
81
|
+
return { ...(spread === ABSENT ? {} : spread), ...Object.fromEntries(own) };
|
|
82
|
+
};
|
|
83
|
+
const spreadOf = (name, scope, path) => {
|
|
84
|
+
if (!scope.known.has(name))
|
|
85
|
+
return fail(scope, path, `unknown slot "${name}"`);
|
|
86
|
+
const value = scope.slots[name];
|
|
87
|
+
if (value === undefined)
|
|
88
|
+
return ABSENT;
|
|
89
|
+
return isRecord(value) ? value : fail(scope, path, `$spread slot "${name}" is not an object`);
|
|
90
|
+
};
|
|
91
|
+
const walk = (node, scope, path) => {
|
|
92
|
+
if (Array.isArray(node)) {
|
|
93
|
+
return node
|
|
94
|
+
.map((child, index) => walk(child, scope, [...path, index]))
|
|
95
|
+
.filter((child) => child !== ABSENT);
|
|
96
|
+
}
|
|
97
|
+
if (!isRecord(node))
|
|
98
|
+
return node;
|
|
99
|
+
if ("$slot" in node)
|
|
100
|
+
return slotRef(node, scope, path);
|
|
101
|
+
if ("$item" in node)
|
|
102
|
+
return itemRef(node, scope, path);
|
|
103
|
+
return objectOf(node, scope, path);
|
|
104
|
+
};
|
|
105
|
+
const isRepeat = (entry) => "$repeat" in entry;
|
|
106
|
+
const sceneOf = (scene, scope, path, suffix) => {
|
|
107
|
+
const { when: _when, props, id, ...rest } = scene;
|
|
108
|
+
const expanded = walk(props, scope, [...path, "props"]);
|
|
109
|
+
return { ...rest, id: `${id}${suffix}`, props: isRecord(expanded) ? expanded : {} };
|
|
110
|
+
};
|
|
111
|
+
const passes = (when, scope, path) => {
|
|
112
|
+
if (when === undefined)
|
|
113
|
+
return true;
|
|
114
|
+
if (!scope.known.has(when)) {
|
|
115
|
+
fail(scope, [...path, "when"], `unknown slot "${when}"`);
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
return isSet(scope.slots[when]);
|
|
119
|
+
};
|
|
120
|
+
const repeated = (entry, scope, path) => {
|
|
121
|
+
if (!passes(entry.when, scope, path))
|
|
122
|
+
return [];
|
|
123
|
+
if (!scope.known.has(entry.$repeat)) {
|
|
124
|
+
fail(scope, [...path, "$repeat"], `unknown slot "${entry.$repeat}"`);
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
const items = scope.slots[entry.$repeat];
|
|
128
|
+
if (!Array.isArray(items))
|
|
129
|
+
return [];
|
|
130
|
+
return items.map((value, index) => sceneOf(entry.scene, { ...scope, item: { value, index } }, [...path, "scene"], `-${index + 1}`));
|
|
131
|
+
};
|
|
132
|
+
/** Expands `$slot`/`$spread`/`$item`/`when`/`$repeat` into plain storyboard scenes and meta. */
|
|
133
|
+
export const expandBlueprint = (blueprint, slots) => {
|
|
134
|
+
const errors = [];
|
|
135
|
+
const scope = { slots, known: new Set(Object.keys(blueprint.slots)), errors };
|
|
136
|
+
const scenes = blueprint.scenes.flatMap((entry, index) => {
|
|
137
|
+
const path = ["scenes", index];
|
|
138
|
+
if (isRepeat(entry))
|
|
139
|
+
return repeated(entry, scope, path);
|
|
140
|
+
return passes(entry.when, scope, path) ? [sceneOf(entry, scope, path, "")] : [];
|
|
141
|
+
});
|
|
142
|
+
const rawMeta = walk(blueprint.meta ?? {}, scope, ["meta"]);
|
|
143
|
+
const meta = isRecord(rawMeta) ? rawMeta : {};
|
|
144
|
+
return errors.length > 0 ? { ok: false, errors } : { ok: true, value: { meta, scenes } };
|
|
145
|
+
};
|
|
146
|
+
const refsIn = (node, path, out) => {
|
|
147
|
+
if (Array.isArray(node)) {
|
|
148
|
+
for (const [index, child] of node.entries())
|
|
149
|
+
refsIn(child, [...path, index], out);
|
|
150
|
+
}
|
|
151
|
+
if (!isRecord(node))
|
|
152
|
+
return;
|
|
153
|
+
for (const key of ["$slot", "$spread"]) {
|
|
154
|
+
if (typeof node[key] === "string") {
|
|
155
|
+
out.push({ name: node[key], path: toJsonPointer([...path, key]), list: false });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
for (const [key, child] of Object.entries(node))
|
|
159
|
+
refsIn(child, [...path, key], out);
|
|
160
|
+
};
|
|
161
|
+
export const slotReferences = (blueprint) => {
|
|
162
|
+
const out = [];
|
|
163
|
+
refsIn(blueprint.meta, ["meta"], out);
|
|
164
|
+
blueprint.scenes.forEach((entry, index) => {
|
|
165
|
+
const path = ["scenes", index];
|
|
166
|
+
if (entry.when !== undefined) {
|
|
167
|
+
out.push({ name: entry.when, path: toJsonPointer([...path, "when"]), list: false });
|
|
168
|
+
}
|
|
169
|
+
if (isRepeat(entry)) {
|
|
170
|
+
out.push({ name: entry.$repeat, path: toJsonPointer([...path, "$repeat"]), list: true });
|
|
171
|
+
refsIn(entry.scene.props, [...path, "scene", "props"], out);
|
|
172
|
+
if (entry.scene.when !== undefined) {
|
|
173
|
+
out.push({
|
|
174
|
+
name: entry.scene.when,
|
|
175
|
+
path: toJsonPointer([...path, "scene", "when"]),
|
|
176
|
+
list: false,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
refsIn(entry.props, [...path, "props"], out);
|
|
182
|
+
});
|
|
183
|
+
return out;
|
|
184
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Test helpers: write small valid template folders. */
|
|
2
|
+
export declare const put: (dir: string, file: string, content: unknown) => void;
|
|
3
|
+
export declare const manifestOf: (id: string, version: string) => Record<string, unknown>;
|
|
4
|
+
export declare const writeTemplate: (root: string, id: string, version: string, folder?: string) => string;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { copyFileSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
/** Test helpers: write small valid template folders. */
|
|
5
|
+
export const put = (dir, file, content) => {
|
|
6
|
+
const path = join(dir, file);
|
|
7
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
8
|
+
writeFileSync(path, typeof content === "string" ? content : JSON.stringify(content, null, 2));
|
|
9
|
+
};
|
|
10
|
+
const TOKENS = fileURLToPath(new URL("../../../themes/src/tokens/", import.meta.url));
|
|
11
|
+
const copyTheme = (dir, file, token) => {
|
|
12
|
+
mkdirSync(join(dir, "themes"), { recursive: true });
|
|
13
|
+
copyFileSync(join(TOKENS, `${token}.json`), join(dir, file));
|
|
14
|
+
};
|
|
15
|
+
export const manifestOf = (id, version) => ({
|
|
16
|
+
templateVersion: 1,
|
|
17
|
+
id,
|
|
18
|
+
version,
|
|
19
|
+
title: `Template ${id}`,
|
|
20
|
+
summary: "A test template.",
|
|
21
|
+
domains: ["test"],
|
|
22
|
+
requires: { storyboard: 1, scenes: ["title", "bullets"] },
|
|
23
|
+
themes: [
|
|
24
|
+
{ id: `${id}-light`, file: "themes/light.json", mode: "light", pair: `${id}-dark` },
|
|
25
|
+
{ id: `${id}-dark`, file: "themes/dark.json", mode: "dark", pair: `${id}-light` },
|
|
26
|
+
],
|
|
27
|
+
styles: [{ id: "calm", file: "styles/calm.json" }],
|
|
28
|
+
blueprints: [{ id: "basic", file: "blueprints/basic.json" }],
|
|
29
|
+
pacing: "pacing.json",
|
|
30
|
+
copy: "copy.md",
|
|
31
|
+
license: "Apache-2.0",
|
|
32
|
+
});
|
|
33
|
+
const range = { scenes: [1, 6], totalSec: [4, 60] };
|
|
34
|
+
export const writeTemplate = (root, id, version, folder = id) => {
|
|
35
|
+
const dir = join(root, folder);
|
|
36
|
+
put(dir, "template.json", manifestOf(id, version));
|
|
37
|
+
copyTheme(dir, "themes/light.json", "default");
|
|
38
|
+
copyTheme(dir, "themes/dark.json", "dark");
|
|
39
|
+
put(dir, "styles/calm.json", { id: "calm", theme: `${id}-light` });
|
|
40
|
+
put(dir, "blueprints/basic.json", {
|
|
41
|
+
id: "basic",
|
|
42
|
+
purpose: "Title plus bullets.",
|
|
43
|
+
slots: {
|
|
44
|
+
title: { type: "text", max: 40, required: true },
|
|
45
|
+
points: { type: "text[]", min: 2, max: 4, itemMax: 40 },
|
|
46
|
+
},
|
|
47
|
+
example: { title: "Hello", points: ["One", "Two"] },
|
|
48
|
+
scenes: [
|
|
49
|
+
{ id: "intro", type: "title", props: { title: { $slot: "title" } } },
|
|
50
|
+
{ when: "points", id: "list", type: "bullets", props: { items: { $slot: "points" } } },
|
|
51
|
+
],
|
|
52
|
+
});
|
|
53
|
+
put(dir, "pacing.json", {
|
|
54
|
+
wordsPerSecond: 2.5,
|
|
55
|
+
sceneSeconds: {
|
|
56
|
+
title: { short: 3, standard: 4, long: 5 },
|
|
57
|
+
bullets: { short: 4, standard: 6, long: 8 },
|
|
58
|
+
},
|
|
59
|
+
lengths: { short: range, standard: range, long: range },
|
|
60
|
+
});
|
|
61
|
+
put(dir, "copy.md", "Be brief.\n");
|
|
62
|
+
return dir;
|
|
63
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { type Discovery, discoverTemplates, type RootsInput, type ShadowedTemplate, type TemplateCandidate, type TemplateRoot, templateRoots, } from "./discover.ts";
|
|
2
|
+
export { type Instantiated, type InstantiateOptions, instantiate, type VendoredTheme, } from "./instantiate.ts";
|
|
3
|
+
export { type LoadOptions, loadTemplate, resolveThemeFile, SHARED_PREFIX } from "./load.ts";
|
|
4
|
+
export type { LengthClass } from "./pacing.ts";
|
|
5
|
+
export { type BlueprintEntry, DEFAULT_STOPWORDS, entriesOf, filterEntries, type Hit, rankEntries, type SearchFilters, templateLengths, tokenize, WEIGHTS, } from "./search.ts";
|
|
6
|
+
export { compareSemver } from "./semver.ts";
|
|
7
|
+
export type { LoadedTemplate, PlatformFrame, PlatformLookup, ThemeFile } from "./types.ts";
|
|
8
|
+
export { type TemplateReport, type ValidateOptions, validateTemplate } from "./validate.ts";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { discoverTemplates, templateRoots, } from "./discover.js";
|
|
2
|
+
export { instantiate, } from "./instantiate.js";
|
|
3
|
+
export { loadTemplate, resolveThemeFile, SHARED_PREFIX } from "./load.js";
|
|
4
|
+
export { DEFAULT_STOPWORDS, entriesOf, filterEntries, rankEntries, templateLengths, tokenize, WEIGHTS, } from "./search.js";
|
|
5
|
+
export { compareSemver } from "./semver.js";
|
|
6
|
+
export { validateTemplate } from "./validate.js";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type Result, type Storyboard } from "@anvpx/motion-schema";
|
|
2
|
+
import { type LintFinding } from "../lint/index.ts";
|
|
3
|
+
import { type LengthClass } from "./pacing.ts";
|
|
4
|
+
import type { LoadedTemplate, PlatformLookup } from "./types.ts";
|
|
5
|
+
export interface InstantiateOptions {
|
|
6
|
+
/** Style id; overrides the `style` slot. Default: slot default, else the first style. */
|
|
7
|
+
readonly style?: string;
|
|
8
|
+
/** Length class; overrides the `length` slot. Default: slot default, else `standard`. */
|
|
9
|
+
readonly length?: LengthClass;
|
|
10
|
+
/** Theme id from the template; overrides the style's theme (e.g. its dark pair). */
|
|
11
|
+
readonly theme?: string;
|
|
12
|
+
/** Platform id; overrides the `platform` slot (only for blueprints that declare it). */
|
|
13
|
+
readonly platform?: string;
|
|
14
|
+
readonly platforms?: PlatformLookup;
|
|
15
|
+
}
|
|
16
|
+
/** A theme file the project must carry to build without the template installed. */
|
|
17
|
+
export interface VendoredTheme {
|
|
18
|
+
readonly id: string;
|
|
19
|
+
/** Path relative to the project, `themes/<id>.json`. */
|
|
20
|
+
readonly file: string;
|
|
21
|
+
readonly text: string;
|
|
22
|
+
}
|
|
23
|
+
export interface Instantiated {
|
|
24
|
+
readonly storyboard: Storyboard;
|
|
25
|
+
/** Canonical JSON of the storyboard: same inputs give the same bytes. */
|
|
26
|
+
readonly text: string;
|
|
27
|
+
readonly sceneCount: number;
|
|
28
|
+
readonly totalSec: number;
|
|
29
|
+
readonly warnings: readonly LintFinding[];
|
|
30
|
+
readonly themes: readonly VendoredTheme[];
|
|
31
|
+
readonly provenance: {
|
|
32
|
+
readonly template: string;
|
|
33
|
+
readonly blueprint: string;
|
|
34
|
+
readonly style: string;
|
|
35
|
+
readonly length: LengthClass;
|
|
36
|
+
readonly slotsSha256: string;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* blueprint + slots + style + length -> a plain, fully validated storyboard.
|
|
41
|
+
* Pipeline: slots -> expand -> style -> pacing -> storyboard validation -> lint.
|
|
42
|
+
*/
|
|
43
|
+
export declare const instantiate: (template: LoadedTemplate, blueprintId: string, slots: unknown, options?: InstantiateOptions) => Result<Instantiated>;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { sceneWords } from "@anvpx/motion-scenes";
|
|
2
|
+
import { canonicalJson, LENGTH_CLASSES, } from "@anvpx/motion-schema";
|
|
3
|
+
import { resolveTheme } from "@anvpx/motion-themes";
|
|
4
|
+
import { sha256Of } from "../assets.js";
|
|
5
|
+
import { lintStoryboard } from "../lint/index.js";
|
|
6
|
+
import { resolveTimeline } from "../timing.js";
|
|
7
|
+
import { validateStoryboardFull } from "../validate-full.js";
|
|
8
|
+
import { expandBlueprint } from "./expand.js";
|
|
9
|
+
import { applyPacing, pacingWarnings } from "./pacing.js";
|
|
10
|
+
import { resolveSlots } from "./slots.js";
|
|
11
|
+
import { applyStyle } from "./style.js";
|
|
12
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13
|
+
const bad = (path, message, suggestion) => ({
|
|
14
|
+
ok: false,
|
|
15
|
+
errors: [suggestion === undefined ? { path, message } : { path, message, suggestion }],
|
|
16
|
+
});
|
|
17
|
+
const isLength = (value) => LENGTH_CLASSES.some((length) => length === value);
|
|
18
|
+
const enumDefault = (blueprint, name) => {
|
|
19
|
+
const def = blueprint.slots[name];
|
|
20
|
+
return def?.type === "enum" ? def.default : undefined;
|
|
21
|
+
};
|
|
22
|
+
const asText = (value) => typeof value === "string" ? value : undefined;
|
|
23
|
+
/** `style` and `length` steer the engine; they are slots only when the blueprint declares them. */
|
|
24
|
+
const controls = (template, blueprint, input, options) => ({
|
|
25
|
+
style: options.style ??
|
|
26
|
+
asText(input.style) ??
|
|
27
|
+
enumDefault(blueprint, "style") ??
|
|
28
|
+
template.manifest.styles[0]?.id ??
|
|
29
|
+
"",
|
|
30
|
+
length: options.length ?? input.length ?? enumDefault(blueprint, "length") ?? "standard",
|
|
31
|
+
...(Object.hasOwn(blueprint.slots, "platform")
|
|
32
|
+
? {
|
|
33
|
+
platform: options.platform ?? asText(input.platform) ?? enumDefault(blueprint, "platform") ?? "",
|
|
34
|
+
}
|
|
35
|
+
: {}),
|
|
36
|
+
});
|
|
37
|
+
const slotInput = (blueprint, input, chosen) => {
|
|
38
|
+
const { style: _style, length: _length, platform: _platform, ...rest } = input;
|
|
39
|
+
return {
|
|
40
|
+
...rest,
|
|
41
|
+
...(chosen.platform === undefined ? {} : { platform: chosen.platform }),
|
|
42
|
+
...(Object.hasOwn(blueprint.slots, "style") ? { style: chosen.style } : {}),
|
|
43
|
+
...(Object.hasOwn(blueprint.slots, "length") ? { length: chosen.length } : {}),
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
const toErrors = (findings) => findings.map(({ path, message, suggestion }) => suggestion === undefined ? { path, message } : { path, message, suggestion });
|
|
47
|
+
/** The template theme resolved for lint; an unresolvable one is reported by template validation. */
|
|
48
|
+
const themeInput = (theme) => {
|
|
49
|
+
try {
|
|
50
|
+
return { theme: resolveTheme(theme.id, { [theme.id]: theme.doc }) };
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const checkBoard = (board, theme, platforms) => {
|
|
57
|
+
const timeline = resolveTimeline(board);
|
|
58
|
+
if (!timeline.ok)
|
|
59
|
+
return timeline;
|
|
60
|
+
const words = board.scenes.map((scene) => sceneWords(scene.props, scene.type));
|
|
61
|
+
const lint = lintStoryboard({
|
|
62
|
+
storyboard: board,
|
|
63
|
+
timeline: timeline.value,
|
|
64
|
+
words,
|
|
65
|
+
...themeInput(theme),
|
|
66
|
+
...(platforms === undefined ? {} : { platforms }),
|
|
67
|
+
});
|
|
68
|
+
if (lint.errors.length > 0)
|
|
69
|
+
return { ok: false, errors: toErrors(lint.errors) };
|
|
70
|
+
return { ok: true, value: { timeline: timeline.value, warnings: [...lint.warnings] } };
|
|
71
|
+
};
|
|
72
|
+
const metaOf = (expanded, slots, theme, frame, platform) => ({
|
|
73
|
+
title: expanded.title ?? slots.title,
|
|
74
|
+
...frame,
|
|
75
|
+
theme,
|
|
76
|
+
...(platform === undefined ? {} : { platform }),
|
|
77
|
+
...(expanded.lang === undefined ? {} : { lang: expanded.lang }),
|
|
78
|
+
});
|
|
79
|
+
const listOf = (keys) => `use one of: ${[...keys].join("|")}`;
|
|
80
|
+
/** Picks blueprint, style, length and theme; every miss names the known ids. */
|
|
81
|
+
const choose = (template, blueprintId, slots, options) => {
|
|
82
|
+
const blueprint = template.blueprints.get(blueprintId);
|
|
83
|
+
if (blueprint === undefined) {
|
|
84
|
+
return bad("/blueprint", `unknown blueprint "${blueprintId}"`, listOf(template.blueprints.keys()));
|
|
85
|
+
}
|
|
86
|
+
if (slots !== undefined && !isRecord(slots))
|
|
87
|
+
return bad("/slots", "slots must be an object");
|
|
88
|
+
const input = slots ?? {};
|
|
89
|
+
const chosen = controls(template, blueprint, input, options);
|
|
90
|
+
const style = template.styles.get(chosen.style);
|
|
91
|
+
if (style === undefined) {
|
|
92
|
+
return bad("/slots/style", `unknown style "${chosen.style}"`, listOf(template.styles.keys()));
|
|
93
|
+
}
|
|
94
|
+
if (!isLength(chosen.length)) {
|
|
95
|
+
return bad("/slots/length", `unknown length "${String(chosen.length)}"`, listOf(LENGTH_CLASSES));
|
|
96
|
+
}
|
|
97
|
+
const themeId = options.theme ?? style.theme;
|
|
98
|
+
const theme = template.themes.get(themeId);
|
|
99
|
+
if (theme === undefined) {
|
|
100
|
+
const path = options.theme === undefined ? `styles/${style.id}.json#/theme` : "/theme";
|
|
101
|
+
return bad(path, `theme "${themeId}" is not in the template`, listOf(template.themes.keys()));
|
|
102
|
+
}
|
|
103
|
+
return { ok: true, value: { blueprint, input, chosen, style, length: chosen.length, theme } };
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* blueprint + slots + style + length -> a plain, fully validated storyboard.
|
|
107
|
+
* Pipeline: slots -> expand -> style -> pacing -> storyboard validation -> lint.
|
|
108
|
+
*/
|
|
109
|
+
export const instantiate = (template, blueprintId, slots, options = {}) => {
|
|
110
|
+
const choice = choose(template, blueprintId, slots, options);
|
|
111
|
+
if (!choice.ok)
|
|
112
|
+
return choice;
|
|
113
|
+
const { blueprint, input, chosen, style, length, theme } = choice.value;
|
|
114
|
+
const resolved = resolveSlots(blueprint.slots, slotInput(blueprint, input, chosen));
|
|
115
|
+
if (!resolved.ok)
|
|
116
|
+
return resolved;
|
|
117
|
+
const expanded = expandBlueprint(blueprint, resolved.value);
|
|
118
|
+
if (!expanded.ok)
|
|
119
|
+
return expanded;
|
|
120
|
+
const styled = applyStyle(expanded.value.scenes, style, options.platforms, chosen.platform);
|
|
121
|
+
if (!styled.ok)
|
|
122
|
+
return styled;
|
|
123
|
+
const paced = applyPacing(styled.value.scenes, template.pacing, length);
|
|
124
|
+
if (!paced.ok)
|
|
125
|
+
return paced;
|
|
126
|
+
const draft = {
|
|
127
|
+
version: 1,
|
|
128
|
+
meta: metaOf(expanded.value.meta, resolved.value, theme.id, styled.value.frame, chosen.platform),
|
|
129
|
+
scenes: paced.value,
|
|
130
|
+
};
|
|
131
|
+
const valid = validateStoryboardFull(draft);
|
|
132
|
+
if (!valid.ok)
|
|
133
|
+
return valid;
|
|
134
|
+
const checked = checkBoard(valid.value, theme, options.platforms);
|
|
135
|
+
if (!checked.ok)
|
|
136
|
+
return checked;
|
|
137
|
+
const { timeline, warnings } = checked.value;
|
|
138
|
+
const sceneCount = valid.value.scenes.length;
|
|
139
|
+
return {
|
|
140
|
+
ok: true,
|
|
141
|
+
value: {
|
|
142
|
+
storyboard: valid.value,
|
|
143
|
+
text: canonicalJson(valid.value),
|
|
144
|
+
sceneCount,
|
|
145
|
+
totalSec: timeline.total,
|
|
146
|
+
warnings: [
|
|
147
|
+
...warnings,
|
|
148
|
+
...pacingWarnings(template.pacing, length, sceneCount, timeline.total),
|
|
149
|
+
],
|
|
150
|
+
themes: [{ id: theme.id, file: `themes/${theme.id}.json`, text: theme.text }],
|
|
151
|
+
provenance: {
|
|
152
|
+
template: `${template.manifest.id}@${template.manifest.version}`,
|
|
153
|
+
blueprint: blueprint.id,
|
|
154
|
+
style: style.id,
|
|
155
|
+
length,
|
|
156
|
+
slotsSha256: sha256Of(canonicalJson(resolved.value)),
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Result } from "@anvpx/motion-schema";
|
|
2
|
+
import type { LoadedTemplate } from "./types.ts";
|
|
3
|
+
export declare const MANIFEST_FILE = "template.json";
|
|
4
|
+
/** A theme `file` starting with this prefix is read from the shared themes folder. */
|
|
5
|
+
export declare const SHARED_PREFIX = "@shared/";
|
|
6
|
+
export interface LoadOptions {
|
|
7
|
+
/** Folder holding `@shared/<name>` theme files (supplied by the caller; `build` never imports templates). */
|
|
8
|
+
readonly sharedThemesDir?: string | undefined;
|
|
9
|
+
}
|
|
10
|
+
/** Where a theme ref is read from: the template folder, or (for `@shared/<name>`) the shared folder. */
|
|
11
|
+
export declare const resolveThemeFile: (file: string, sharedThemesDir: string | undefined) => Result<{
|
|
12
|
+
shared: boolean;
|
|
13
|
+
file: string;
|
|
14
|
+
}>;
|
|
15
|
+
/** Reads a template folder: manifest, themes, styles, blueprints, pacing and copy guide. */
|
|
16
|
+
export declare const loadTemplate: (dir: string, options?: LoadOptions) => Result<LoadedTemplate>;
|