@hyperframes/core 0.7.14 → 0.7.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/compiler/staticGuard.js +1 -1
- package/dist/compiler/staticGuard.js.map +1 -1
- package/dist/core.types.d.ts +2 -369
- package/dist/core.types.d.ts.map +1 -1
- package/dist/core.types.js +1 -84
- package/dist/core.types.js.map +1 -1
- package/dist/generated/runtime-inline.js.map +1 -1
- package/dist/generators/hyperframes.d.ts +1 -1
- package/dist/generators/hyperframes.d.ts.map +1 -1
- package/dist/generators/hyperframes.js +1 -1
- package/dist/generators/hyperframes.js.map +1 -1
- package/dist/hyperframe.runtime.iife.js +20 -20
- package/dist/index.d.ts +5 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -4
- package/dist/index.js.map +1 -1
- package/dist/lint/index.d.ts +2 -2
- package/dist/lint/index.d.ts.map +1 -1
- package/dist/lint/index.js +2 -1
- package/dist/lint/index.js.map +1 -1
- package/dist/parsers/gsapConstants.d.ts +2 -12
- package/dist/parsers/gsapConstants.d.ts.map +1 -1
- package/dist/parsers/gsapConstants.js +2 -110
- package/dist/parsers/gsapConstants.js.map +1 -1
- package/dist/parsers/gsapParserAcorn.d.ts +2 -48
- package/dist/parsers/gsapParserAcorn.d.ts.map +1 -1
- package/dist/parsers/gsapParserAcorn.js +2 -1057
- package/dist/parsers/gsapParserAcorn.js.map +1 -1
- package/dist/parsers/gsapParserExports.d.ts +2 -20
- package/dist/parsers/gsapParserExports.d.ts.map +1 -1
- package/dist/parsers/gsapParserExports.js +2 -8
- package/dist/parsers/gsapParserExports.js.map +1 -1
- package/dist/parsers/gsapWriterAcorn.d.ts +2 -90
- package/dist/parsers/gsapWriterAcorn.d.ts.map +1 -1
- package/dist/parsers/gsapWriterAcorn.js +2 -2016
- package/dist/parsers/gsapWriterAcorn.js.map +1 -1
- package/dist/parsers/hfIds.d.ts +2 -16
- package/dist/parsers/hfIds.d.ts.map +1 -1
- package/dist/parsers/hfIds.js +2 -127
- package/dist/parsers/hfIds.js.map +1 -1
- package/dist/parsers/springEase.d.ts +2 -28
- package/dist/parsers/springEase.d.ts.map +1 -1
- package/dist/parsers/springEase.js +2 -69
- package/dist/parsers/springEase.js.map +1 -1
- package/dist/slideshow/index.d.ts +1 -0
- package/dist/slideshow/index.d.ts.map +1 -1
- package/dist/slideshow/index.js +1 -0
- package/dist/slideshow/index.js.map +1 -1
- package/dist/studio-api/helpers/draftMarkers.d.ts +2 -10
- package/dist/studio-api/helpers/draftMarkers.d.ts.map +1 -1
- package/dist/studio-api/helpers/draftMarkers.js +2 -10
- package/dist/studio-api/helpers/draftMarkers.js.map +1 -1
- package/dist/studio-api/helpers/finiteMutation.d.ts +2 -10
- package/dist/studio-api/helpers/finiteMutation.d.ts.map +1 -1
- package/dist/studio-api/helpers/finiteMutation.js +2 -20
- package/dist/studio-api/helpers/finiteMutation.js.map +1 -1
- package/dist/studio-api/helpers/manualEditsRenderScript.d.ts +2 -12
- package/dist/studio-api/helpers/manualEditsRenderScript.d.ts.map +1 -1
- package/dist/studio-api/helpers/manualEditsRenderScript.js +2 -632
- package/dist/studio-api/helpers/manualEditsRenderScript.js.map +1 -1
- package/dist/studio-api/helpers/screenshotClip.d.ts +2 -7
- package/dist/studio-api/helpers/screenshotClip.d.ts.map +1 -1
- package/dist/studio-api/helpers/screenshotClip.js +2 -21
- package/dist/studio-api/helpers/screenshotClip.js.map +1 -1
- package/dist/studio-api/helpers/studioMotionRenderScript.d.ts +2 -8
- package/dist/studio-api/helpers/studioMotionRenderScript.d.ts.map +1 -1
- package/dist/studio-api/helpers/studioMotionRenderScript.js +2 -207
- package/dist/studio-api/helpers/studioMotionRenderScript.js.map +1 -1
- package/dist/studio-api/index.d.ts +2 -9
- package/dist/studio-api/index.d.ts.map +1 -1
- package/dist/studio-api/index.js +2 -8
- package/dist/studio-api/index.js.map +1 -1
- package/package.json +16 -18
- package/dist/generated/runtime-inline.js +0 -11
- package/dist/hyperframe.manifest.json +0 -22
- package/dist/hyperframe.runtime.mjs +0 -325
- package/dist/lint/context.d.ts +0 -17
- package/dist/lint/context.d.ts.map +0 -1
- package/dist/lint/context.js +0 -37
- package/dist/lint/context.js.map +0 -1
- package/dist/lint/hyperframeLinter.d.ts +0 -14
- package/dist/lint/hyperframeLinter.d.ts.map +0 -1
- package/dist/lint/hyperframeLinter.js +0 -132
- package/dist/lint/hyperframeLinter.js.map +0 -1
- package/dist/lint/rules/adapters.d.ts +0 -3
- package/dist/lint/rules/adapters.d.ts.map +0 -1
- package/dist/lint/rules/adapters.js +0 -40
- package/dist/lint/rules/adapters.js.map +0 -1
- package/dist/lint/rules/captions.d.ts +0 -3
- package/dist/lint/rules/captions.d.ts.map +0 -1
- package/dist/lint/rules/captions.js +0 -242
- package/dist/lint/rules/captions.js.map +0 -1
- package/dist/lint/rules/composition.d.ts +0 -5
- package/dist/lint/rules/composition.d.ts.map +0 -1
- package/dist/lint/rules/composition.js +0 -694
- package/dist/lint/rules/composition.js.map +0 -1
- package/dist/lint/rules/core.d.ts +0 -3
- package/dist/lint/rules/core.d.ts.map +0 -1
- package/dist/lint/rules/core.js +0 -432
- package/dist/lint/rules/core.js.map +0 -1
- package/dist/lint/rules/fonts.d.ts +0 -3
- package/dist/lint/rules/fonts.d.ts.map +0 -1
- package/dist/lint/rules/fonts.js +0 -205
- package/dist/lint/rules/fonts.js.map +0 -1
- package/dist/lint/rules/gsap.d.ts +0 -4
- package/dist/lint/rules/gsap.d.ts.map +0 -1
- package/dist/lint/rules/gsap.js +0 -1039
- package/dist/lint/rules/gsap.js.map +0 -1
- package/dist/lint/rules/media.d.ts +0 -3
- package/dist/lint/rules/media.d.ts.map +0 -1
- package/dist/lint/rules/media.js +0 -491
- package/dist/lint/rules/media.js.map +0 -1
- package/dist/lint/rules/slideshow.d.ts +0 -4
- package/dist/lint/rules/slideshow.d.ts.map +0 -1
- package/dist/lint/rules/slideshow.js +0 -74
- package/dist/lint/rules/slideshow.js.map +0 -1
- package/dist/lint/rules/textures.d.ts +0 -3
- package/dist/lint/rules/textures.d.ts.map +0 -1
- package/dist/lint/rules/textures.js +0 -192
- package/dist/lint/rules/textures.js.map +0 -1
- package/dist/lint/types.d.ts +0 -36
- package/dist/lint/types.d.ts.map +0 -1
- package/dist/lint/types.js +0 -2
- package/dist/lint/types.js.map +0 -1
- package/dist/lint/utils.d.ts +0 -55
- package/dist/lint/utils.d.ts.map +0 -1
- package/dist/lint/utils.js +0 -264
- package/dist/lint/utils.js.map +0 -1
- package/dist/parsers/gsapInline.d.ts +0 -47
- package/dist/parsers/gsapInline.d.ts.map +0 -1
- package/dist/parsers/gsapInline.js +0 -513
- package/dist/parsers/gsapInline.js.map +0 -1
- package/dist/parsers/gsapParser.d.ts +0 -158
- package/dist/parsers/gsapParser.d.ts.map +0 -1
- package/dist/parsers/gsapParser.js +0 -2634
- package/dist/parsers/gsapParser.js.map +0 -1
- package/dist/parsers/gsapParser.test-helpers.d.ts +0 -49
- package/dist/parsers/gsapParser.test-helpers.d.ts.map +0 -1
- package/dist/parsers/gsapParser.test-helpers.js +0 -97
- package/dist/parsers/gsapParser.test-helpers.js.map +0 -1
- package/dist/parsers/gsapSerialize.d.ts +0 -191
- package/dist/parsers/gsapSerialize.d.ts.map +0 -1
- package/dist/parsers/gsapSerialize.js +0 -410
- package/dist/parsers/gsapSerialize.js.map +0 -1
- package/dist/parsers/gsapUnroll.d.ts +0 -7
- package/dist/parsers/gsapUnroll.d.ts.map +0 -1
- package/dist/parsers/gsapUnroll.js +0 -137
- package/dist/parsers/gsapUnroll.js.map +0 -1
- package/dist/parsers/htmlParser.d.ts +0 -27
- package/dist/parsers/htmlParser.d.ts.map +0 -1
- package/dist/parsers/htmlParser.js +0 -731
- package/dist/parsers/htmlParser.js.map +0 -1
- package/dist/studio-api/createStudioApi.d.ts +0 -10
- package/dist/studio-api/createStudioApi.d.ts.map +0 -1
- package/dist/studio-api/createStudioApi.js +0 -32
- package/dist/studio-api/createStudioApi.js.map +0 -1
- package/dist/studio-api/helpers/backupJournal.d.ts +0 -9
- package/dist/studio-api/helpers/backupJournal.d.ts.map +0 -1
- package/dist/studio-api/helpers/backupJournal.js +0 -81
- package/dist/studio-api/helpers/backupJournal.js.map +0 -1
- package/dist/studio-api/helpers/hfIdPersist.d.ts +0 -11
- package/dist/studio-api/helpers/hfIdPersist.d.ts.map +0 -1
- package/dist/studio-api/helpers/hfIdPersist.js +0 -39
- package/dist/studio-api/helpers/hfIdPersist.js.map +0 -1
- package/dist/studio-api/helpers/mediaValidation.d.ts +0 -20
- package/dist/studio-api/helpers/mediaValidation.d.ts.map +0 -1
- package/dist/studio-api/helpers/mediaValidation.js +0 -56
- package/dist/studio-api/helpers/mediaValidation.js.map +0 -1
- package/dist/studio-api/helpers/mime.d.ts +0 -4
- package/dist/studio-api/helpers/mime.d.ts.map +0 -1
- package/dist/studio-api/helpers/mime.js +0 -39
- package/dist/studio-api/helpers/mime.js.map +0 -1
- package/dist/studio-api/helpers/previewAdapter.d.ts +0 -43
- package/dist/studio-api/helpers/previewAdapter.d.ts.map +0 -1
- package/dist/studio-api/helpers/previewAdapter.js +0 -118
- package/dist/studio-api/helpers/previewAdapter.js.map +0 -1
- package/dist/studio-api/helpers/projectSignature.d.ts +0 -5
- package/dist/studio-api/helpers/projectSignature.d.ts.map +0 -1
- package/dist/studio-api/helpers/projectSignature.js +0 -154
- package/dist/studio-api/helpers/projectSignature.js.map +0 -1
- package/dist/studio-api/helpers/safePath.d.ts +0 -12
- package/dist/studio-api/helpers/safePath.d.ts.map +0 -1
- package/dist/studio-api/helpers/safePath.js +0 -38
- package/dist/studio-api/helpers/safePath.js.map +0 -1
- package/dist/studio-api/helpers/sourceMutation.d.ts +0 -28
- package/dist/studio-api/helpers/sourceMutation.d.ts.map +0 -1
- package/dist/studio-api/helpers/sourceMutation.js +0 -339
- package/dist/studio-api/helpers/sourceMutation.js.map +0 -1
- package/dist/studio-api/helpers/subComposition.d.ts +0 -20
- package/dist/studio-api/helpers/subComposition.d.ts.map +0 -1
- package/dist/studio-api/helpers/subComposition.js +0 -289
- package/dist/studio-api/helpers/subComposition.js.map +0 -1
- package/dist/studio-api/helpers/waveform.d.ts +0 -4
- package/dist/studio-api/helpers/waveform.d.ts.map +0 -1
- package/dist/studio-api/helpers/waveform.js +0 -80
- package/dist/studio-api/helpers/waveform.js.map +0 -1
- package/dist/studio-api/routes/files.d.ts +0 -4
- package/dist/studio-api/routes/files.d.ts.map +0 -1
- package/dist/studio-api/routes/files.js +0 -1200
- package/dist/studio-api/routes/files.js.map +0 -1
- package/dist/studio-api/routes/fonts.d.ts +0 -3
- package/dist/studio-api/routes/fonts.d.ts.map +0 -1
- package/dist/studio-api/routes/fonts.js +0 -157
- package/dist/studio-api/routes/fonts.js.map +0 -1
- package/dist/studio-api/routes/lint.d.ts +0 -4
- package/dist/studio-api/routes/lint.d.ts.map +0 -1
- package/dist/studio-api/routes/lint.js +0 -29
- package/dist/studio-api/routes/lint.js.map +0 -1
- package/dist/studio-api/routes/preview.d.ts +0 -4
- package/dist/studio-api/routes/preview.d.ts.map +0 -1
- package/dist/studio-api/routes/preview.js +0 -331
- package/dist/studio-api/routes/preview.js.map +0 -1
- package/dist/studio-api/routes/projects.d.ts +0 -4
- package/dist/studio-api/routes/projects.d.ts.map +0 -1
- package/dist/studio-api/routes/projects.js +0 -45
- package/dist/studio-api/routes/projects.js.map +0 -1
- package/dist/studio-api/routes/registry.d.ts +0 -4
- package/dist/studio-api/routes/registry.d.ts.map +0 -1
- package/dist/studio-api/routes/registry.js +0 -31
- package/dist/studio-api/routes/registry.js.map +0 -1
- package/dist/studio-api/routes/render.d.ts +0 -4
- package/dist/studio-api/routes/render.d.ts.map +0 -1
- package/dist/studio-api/routes/render.js +0 -269
- package/dist/studio-api/routes/render.js.map +0 -1
- package/dist/studio-api/routes/storyboard.d.ts +0 -4
- package/dist/studio-api/routes/storyboard.d.ts.map +0 -1
- package/dist/studio-api/routes/storyboard.js +0 -65
- package/dist/studio-api/routes/storyboard.js.map +0 -1
- package/dist/studio-api/routes/thumbnail.d.ts +0 -4
- package/dist/studio-api/routes/thumbnail.d.ts.map +0 -1
- package/dist/studio-api/routes/thumbnail.js +0 -107
- package/dist/studio-api/routes/thumbnail.js.map +0 -1
- package/dist/studio-api/routes/waveform.d.ts +0 -4
- package/dist/studio-api/routes/waveform.d.ts.map +0 -1
- package/dist/studio-api/routes/waveform.js +0 -41
- package/dist/studio-api/routes/waveform.js.map +0 -1
- package/dist/studio-api/types.d.ts +0 -120
- package/dist/studio-api/types.d.ts.map +0 -1
- package/dist/studio-api/types.js +0 -2
- package/dist/studio-api/types.js.map +0 -1
|
@@ -1,694 +0,0 @@
|
|
|
1
|
-
import { findHtmlTag, readAttr, readJsonAttr, stripJsComments, truncateSnippet } from "../utils.js";
|
|
2
|
-
import { COMPOSITION_VARIABLE_TYPES } from "../../core.types.js";
|
|
3
|
-
// Agent guidance thresholds: warning-only nudges for files/tracks that become hard
|
|
4
|
-
// to inspect and revise reliably in a single composition.
|
|
5
|
-
const MAX_COMPOSITION_LINES = 300;
|
|
6
|
-
const MAX_TIMED_ELEMENTS_PER_TRACK = 3;
|
|
7
|
-
const TRACK_DENSITY_EXEMPT_TAGS = new Set(["audio", "script", "style", "video"]);
|
|
8
|
-
function countPhysicalLines(source) {
|
|
9
|
-
if (source.length === 0)
|
|
10
|
-
return 0;
|
|
11
|
-
const normalized = source.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
12
|
-
const withoutFinalNewline = normalized.endsWith("\n") ? normalized.slice(0, -1) : normalized;
|
|
13
|
-
return withoutFinalNewline.split("\n").length;
|
|
14
|
-
}
|
|
15
|
-
function countStructuralLines(source) {
|
|
16
|
-
return countPhysicalLines(source.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, "<style></style>"));
|
|
17
|
-
}
|
|
18
|
-
export function isRegistrySourceFile(filePath) {
|
|
19
|
-
if (!filePath)
|
|
20
|
-
return false;
|
|
21
|
-
const normalized = filePath.replace(/\\/g, "/");
|
|
22
|
-
return /(?:^|\/)registry\/blocks\/([^/]+)\/\1\.html$/i.test(normalized);
|
|
23
|
-
}
|
|
24
|
-
export function isRegistryInstalledFile(rawSource) {
|
|
25
|
-
return /^\s*<!--\s*hyperframes-registry-item:[^>]*-->/i.test(rawSource.slice(0, 512));
|
|
26
|
-
}
|
|
27
|
-
function isCompositionRootOrMount(rawTag) {
|
|
28
|
-
return Boolean(readAttr(rawTag, "data-composition-id") || readAttr(rawTag, "data-composition-src"));
|
|
29
|
-
}
|
|
30
|
-
// Asset references inside CSS `url(...)`/`url("...")`/`url('...')` functions.
|
|
31
|
-
// Returns the inner path without quotes; comments are stripped first so
|
|
32
|
-
// `/* url(foo) */` is ignored. Bare `url()` and `data:` are excluded by the
|
|
33
|
-
// rules that consume this — the helper just yields raw URL values.
|
|
34
|
-
function extractCssUrlReferences(css) {
|
|
35
|
-
const out = [];
|
|
36
|
-
const noComments = css.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
37
|
-
const urlPattern = /\burl\(\s*(["']?)([^)"']+)\1\s*\)/g;
|
|
38
|
-
let m;
|
|
39
|
-
while ((m = urlPattern.exec(noComments)) !== null) {
|
|
40
|
-
const raw = (m[2] ?? "").trim();
|
|
41
|
-
if (raw)
|
|
42
|
-
out.push(raw);
|
|
43
|
-
}
|
|
44
|
-
return out;
|
|
45
|
-
}
|
|
46
|
-
// Top-level CSS selectors (comma-split) in a stylesheet, skipping at-rule headers
|
|
47
|
-
// (@media/@keyframes/...) and keyframe stops. Heuristic — the lint layer has no
|
|
48
|
-
// full CSS parser, and rules elsewhere in this file scan CSS the same way.
|
|
49
|
-
function extractCssSelectors(css) {
|
|
50
|
-
const out = [];
|
|
51
|
-
const noComments = css.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
52
|
-
const ruleHeader = /([^{}]+)\{/g;
|
|
53
|
-
let m;
|
|
54
|
-
while ((m = ruleHeader.exec(noComments)) !== null) {
|
|
55
|
-
const header = (m[1] ?? "").trim();
|
|
56
|
-
if (!header || header.startsWith("@"))
|
|
57
|
-
continue;
|
|
58
|
-
for (const sel of header.split(",")) {
|
|
59
|
-
const s = sel.trim();
|
|
60
|
-
if (s)
|
|
61
|
-
out.push(s);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return out;
|
|
65
|
-
}
|
|
66
|
-
// Class tokens in a selector's leftmost compound (before the first descendant /
|
|
67
|
-
// child / sibling combinator). `.frame .title` → ["frame"]; `.a.b > .c` → ["a","b"].
|
|
68
|
-
function leftmostCompoundClasses(selector) {
|
|
69
|
-
const leftmost = selector.trim().split(/[\s>+~]+/)[0] ?? "";
|
|
70
|
-
return (leftmost.match(/\.([\w-]+)/g) ?? []).map((c) => c.slice(1));
|
|
71
|
-
}
|
|
72
|
-
// Distinct selectors across all <style> blocks whose leftmost compound keys off one
|
|
73
|
-
// of the root element's own classes — the ones that break under id-scoping.
|
|
74
|
-
function rootClassStyledSelectors(styles, rootClasses) {
|
|
75
|
-
const offenders = [];
|
|
76
|
-
for (const style of styles) {
|
|
77
|
-
for (const selector of extractCssSelectors(style.content)) {
|
|
78
|
-
const hitsRoot = leftmostCompoundClasses(selector).some((c) => rootClasses.includes(c));
|
|
79
|
-
if (hitsRoot && !offenders.includes(selector))
|
|
80
|
-
offenders.push(selector);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return offenders;
|
|
84
|
-
}
|
|
85
|
-
export const compositionRules = [
|
|
86
|
-
// invalid_parent_traversal_in_asset_path — catches `../` traversal in src,
|
|
87
|
-
// href, inline-style url(), and <style> url() asset references on
|
|
88
|
-
// compositions. Sub-compositions live under compositions/ but are served
|
|
89
|
-
// with the project root as their base URL, so any `../`-traversing path
|
|
90
|
-
// climbs above the project root and 404s in Studio preview. Renders
|
|
91
|
-
// tolerate it because the server-side bundler rewrites `../foo` against
|
|
92
|
-
// each sub-composition's source path; the runtime now mirrors that fallback
|
|
93
|
-
// (see rewriteSubCompositionAssetPaths in runtime/compositionLoader.ts), but
|
|
94
|
-
// the authoring-time signal is still wrong — flag it at lint time so the
|
|
95
|
-
// baked path is plain root-relative and matches what the bundler emits.
|
|
96
|
-
//
|
|
97
|
-
// Mirrors the runtime fallback's surface: `[src]` / `[href]` attribute
|
|
98
|
-
// values, `[style]` inline url(), and `<style>` block url() references.
|
|
99
|
-
// Skips absolute URLs (http(s)://, //, data:, /-prefixed root-relative),
|
|
100
|
-
// hash anchors, and plain relative paths (`assets/x.mp4`) — only `../`
|
|
101
|
-
// traversal is flagged. Subsumes the older `../capture/`-specific rule.
|
|
102
|
-
// fallow-ignore-next-line complexity
|
|
103
|
-
({ tags, styles, rawSource, options }) => {
|
|
104
|
-
if (isRegistrySourceFile(options.filePath) || isRegistryInstalledFile(rawSource))
|
|
105
|
-
return [];
|
|
106
|
-
const offenders = [];
|
|
107
|
-
const collect = (value) => {
|
|
108
|
-
if (!value)
|
|
109
|
-
return;
|
|
110
|
-
const trimmed = value.trim();
|
|
111
|
-
if (!trimmed.startsWith("../") && trimmed !== "..")
|
|
112
|
-
return;
|
|
113
|
-
offenders.push(trimmed);
|
|
114
|
-
};
|
|
115
|
-
for (const tag of tags) {
|
|
116
|
-
collect(readAttr(tag.raw, "src"));
|
|
117
|
-
collect(readAttr(tag.raw, "href"));
|
|
118
|
-
// Use readJsonAttr for `style` — inline url('...') values contain the
|
|
119
|
-
// opposite quote, which readAttr's [^"']+ class would truncate.
|
|
120
|
-
const styleAttr = readJsonAttr(tag.raw, "style");
|
|
121
|
-
if (styleAttr) {
|
|
122
|
-
for (const url of extractCssUrlReferences(styleAttr))
|
|
123
|
-
collect(url);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
for (const style of styles) {
|
|
127
|
-
for (const url of extractCssUrlReferences(style.content))
|
|
128
|
-
collect(url);
|
|
129
|
-
}
|
|
130
|
-
if (offenders.length === 0)
|
|
131
|
-
return [];
|
|
132
|
-
// Group counts by leading path token (e.g. ../capture/, ../assets/, ../../assets/)
|
|
133
|
-
// so the message names the offending prefixes instead of a bare count.
|
|
134
|
-
const prefixCounts = new Map();
|
|
135
|
-
for (const path of offenders) {
|
|
136
|
-
const prefix = path.match(/^(?:\.\.\/)+[^/]+\//)?.[0] ?? path;
|
|
137
|
-
prefixCounts.set(prefix, (prefixCounts.get(prefix) ?? 0) + 1);
|
|
138
|
-
}
|
|
139
|
-
const prefixSummary = Array.from(prefixCounts.entries())
|
|
140
|
-
.sort(([, a], [, b]) => b - a)
|
|
141
|
-
.map(([prefix, count]) => (count > 1 ? `${prefix} (${count})` : prefix))
|
|
142
|
-
.join(", ");
|
|
143
|
-
return [
|
|
144
|
-
{
|
|
145
|
-
code: "invalid_parent_traversal_in_asset_path",
|
|
146
|
-
severity: "error",
|
|
147
|
-
message: `Found ${offenders.length} asset path(s) traversing above the project root with "../" ` +
|
|
148
|
-
`(${prefixSummary}). Renders rewrite this against each sub-composition's source path, but Studio preview and other live consumers resolve against the project root and 404.`,
|
|
149
|
-
fixHint: 'Use plain root-relative paths (e.g. "assets/...", "capture/...", "fonts/...") — compositions are served with the project root as their base URL, so paths must be root-relative, not relative to the compositions/ directory.',
|
|
150
|
-
},
|
|
151
|
-
];
|
|
152
|
-
},
|
|
153
|
-
// composition_file_too_large
|
|
154
|
-
({ rawSource, options }) => {
|
|
155
|
-
if (isRegistrySourceFile(options.filePath) || isRegistryInstalledFile(rawSource))
|
|
156
|
-
return [];
|
|
157
|
-
const lineCount = countStructuralLines(rawSource);
|
|
158
|
-
if (lineCount <= MAX_COMPOSITION_LINES)
|
|
159
|
-
return [];
|
|
160
|
-
const splitTarget = options.isSubComposition
|
|
161
|
-
? "Split this sub-composition further into smaller .html files"
|
|
162
|
-
: "Split coherent scenes or layers into separate .html files under compositions/";
|
|
163
|
-
return [
|
|
164
|
-
{
|
|
165
|
-
code: "composition_file_too_large",
|
|
166
|
-
severity: "warning",
|
|
167
|
-
message: `This HTML composition file has ${lineCount} lines. Smaller sub-compositions are easier to read, iterate on, and diff.`,
|
|
168
|
-
fixHint: `${splitTarget}, then mount them from the parent with data-composition-src so each file stays small enough to inspect, revise, and validate independently.`,
|
|
169
|
-
},
|
|
170
|
-
];
|
|
171
|
-
},
|
|
172
|
-
// timeline_track_too_dense
|
|
173
|
-
// fallow-ignore-next-line complexity
|
|
174
|
-
({ tags, options }) => {
|
|
175
|
-
const trackCounts = new Map();
|
|
176
|
-
for (const tag of tags) {
|
|
177
|
-
if (TRACK_DENSITY_EXEMPT_TAGS.has(tag.name))
|
|
178
|
-
continue;
|
|
179
|
-
if (isCompositionRootOrMount(tag.raw))
|
|
180
|
-
continue;
|
|
181
|
-
if (!readAttr(tag.raw, "data-start"))
|
|
182
|
-
continue;
|
|
183
|
-
const track = readAttr(tag.raw, "data-track-index");
|
|
184
|
-
if (!track)
|
|
185
|
-
continue;
|
|
186
|
-
trackCounts.set(track, (trackCounts.get(track) ?? 0) + 1);
|
|
187
|
-
}
|
|
188
|
-
const findings = [];
|
|
189
|
-
for (const [track, count] of trackCounts) {
|
|
190
|
-
if (count <= MAX_TIMED_ELEMENTS_PER_TRACK)
|
|
191
|
-
continue;
|
|
192
|
-
const splitTarget = options.isSubComposition
|
|
193
|
-
? "Move coherent scene groups into smaller .html files"
|
|
194
|
-
: "Move coherent scene groups into separate .html files under compositions/";
|
|
195
|
-
findings.push({
|
|
196
|
-
code: "timeline_track_too_dense",
|
|
197
|
-
severity: "warning",
|
|
198
|
-
message: `Track ${track} has ${count} timed elements in this HTML file. Smaller sub-compositions keep timelines easier to read, iterate on, and diff.`,
|
|
199
|
-
fixHint: `${splitTarget} and mount them from the parent with data-composition-src so the timeline stays easier to inspect, revise, and validate.`,
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
return findings;
|
|
203
|
-
},
|
|
204
|
-
// timed_element_missing_visibility_hidden
|
|
205
|
-
// fallow-ignore-next-line complexity
|
|
206
|
-
({ tags }) => {
|
|
207
|
-
const findings = [];
|
|
208
|
-
for (const tag of tags) {
|
|
209
|
-
if (tag.name === "audio" || tag.name === "script" || tag.name === "style")
|
|
210
|
-
continue;
|
|
211
|
-
if (!readAttr(tag.raw, "data-start"))
|
|
212
|
-
continue;
|
|
213
|
-
if (readAttr(tag.raw, "data-composition-id"))
|
|
214
|
-
continue;
|
|
215
|
-
if (readAttr(tag.raw, "data-composition-src"))
|
|
216
|
-
continue;
|
|
217
|
-
const classAttr = readAttr(tag.raw, "class") || "";
|
|
218
|
-
const styleAttr = readAttr(tag.raw, "style") || "";
|
|
219
|
-
const hasClip = classAttr.split(/\s+/).includes("clip");
|
|
220
|
-
const hasHiddenStyle = /visibility\s*:\s*hidden/i.test(styleAttr) || /opacity\s*:\s*0/i.test(styleAttr);
|
|
221
|
-
if (!hasClip && !hasHiddenStyle) {
|
|
222
|
-
const elementId = readAttr(tag.raw, "id") || undefined;
|
|
223
|
-
findings.push({
|
|
224
|
-
code: "timed_element_missing_visibility_hidden",
|
|
225
|
-
severity: "info",
|
|
226
|
-
message: `<${tag.name}${elementId ? ` id="${elementId}"` : ""}> has data-start but no class="clip", visibility:hidden, or opacity:0. Consider adding initial hidden state if the element should not be visible before its start time.`,
|
|
227
|
-
elementId,
|
|
228
|
-
fixHint: 'Add class="clip" (with CSS: .clip { visibility: hidden; }) or style="opacity:0" if the element should start hidden.',
|
|
229
|
-
snippet: truncateSnippet(tag.raw),
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
return findings;
|
|
234
|
-
},
|
|
235
|
-
// deprecated_data_layer + deprecated_data_end
|
|
236
|
-
// fallow-ignore-next-line complexity
|
|
237
|
-
({ tags }) => {
|
|
238
|
-
const findings = [];
|
|
239
|
-
for (const tag of tags) {
|
|
240
|
-
if (readAttr(tag.raw, "data-layer") && !readAttr(tag.raw, "data-track-index")) {
|
|
241
|
-
const elementId = readAttr(tag.raw, "id") || undefined;
|
|
242
|
-
findings.push({
|
|
243
|
-
code: "deprecated_data_layer",
|
|
244
|
-
severity: "error",
|
|
245
|
-
message: `<${tag.name}${elementId ? ` id="${elementId}"` : ""}> uses data-layer instead of data-track-index.`,
|
|
246
|
-
elementId,
|
|
247
|
-
fixHint: "Replace data-layer with data-track-index. The runtime reads data-track-index.",
|
|
248
|
-
snippet: truncateSnippet(tag.raw),
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
if (readAttr(tag.raw, "data-end") && !readAttr(tag.raw, "data-duration")) {
|
|
252
|
-
const elementId = readAttr(tag.raw, "id") || undefined;
|
|
253
|
-
findings.push({
|
|
254
|
-
code: "deprecated_data_end",
|
|
255
|
-
severity: "error",
|
|
256
|
-
message: `<${tag.name}${elementId ? ` id="${elementId}"` : ""}> uses data-end without data-duration. Use data-duration in source HTML.`,
|
|
257
|
-
elementId,
|
|
258
|
-
fixHint: "Replace data-end with data-duration. The compiler generates data-end from data-duration automatically.",
|
|
259
|
-
snippet: truncateSnippet(tag.raw),
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
return findings;
|
|
264
|
-
},
|
|
265
|
-
// split_data_attribute_selector
|
|
266
|
-
({ scripts, styles }) => {
|
|
267
|
-
const findings = [];
|
|
268
|
-
const splitDataAttrSelectorPattern = /\[data-composition-id=(["'])([^"'\]]+)\1\s+(data-[\w:-]+)=(["'])([^"'\]]*)\4\]/g;
|
|
269
|
-
const scan = (content) => {
|
|
270
|
-
splitDataAttrSelectorPattern.lastIndex = 0;
|
|
271
|
-
let match;
|
|
272
|
-
while ((match = splitDataAttrSelectorPattern.exec(content)) !== null) {
|
|
273
|
-
const compId = match[2] ?? "";
|
|
274
|
-
const attrName = match[3] ?? "";
|
|
275
|
-
const attrValue = match[5] ?? "";
|
|
276
|
-
findings.push({
|
|
277
|
-
code: "split_data_attribute_selector",
|
|
278
|
-
severity: "error",
|
|
279
|
-
message: `Selector "${match[0]}" combines two attributes inside one CSS attribute selector. ` +
|
|
280
|
-
"Browsers reject it, so GSAP timelines or querySelector calls will fail before registering.",
|
|
281
|
-
selector: match[0],
|
|
282
|
-
fixHint: `Use separate attribute selectors: [data-composition-id="${compId}"][${attrName}="${attrValue}"].`,
|
|
283
|
-
snippet: truncateSnippet(match[0]),
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
};
|
|
287
|
-
for (const style of styles)
|
|
288
|
-
scan(style.content);
|
|
289
|
-
for (const script of scripts)
|
|
290
|
-
scan(script.content);
|
|
291
|
-
return findings;
|
|
292
|
-
},
|
|
293
|
-
// template_literal_selector
|
|
294
|
-
({ scripts }) => {
|
|
295
|
-
const findings = [];
|
|
296
|
-
for (const script of scripts) {
|
|
297
|
-
const templateLiteralSelectorPattern = /(?:querySelector|querySelectorAll)\s*\(\s*`[^`]*\$\{[^}]+\}[^`]*`\s*\)/g;
|
|
298
|
-
let tlMatch;
|
|
299
|
-
while ((tlMatch = templateLiteralSelectorPattern.exec(script.content)) !== null) {
|
|
300
|
-
findings.push({
|
|
301
|
-
code: "template_literal_selector",
|
|
302
|
-
severity: "error",
|
|
303
|
-
message: "querySelector uses a template literal variable (e.g. `${compId}`). " +
|
|
304
|
-
"The HTML bundler's CSS parser crashes on these. Use a hardcoded string instead.",
|
|
305
|
-
fixHint: "Replace the template literal variable with a hardcoded string. The bundler's CSS parser cannot handle interpolated variables in script content.",
|
|
306
|
-
snippet: truncateSnippet(tlMatch[0]),
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
return findings;
|
|
311
|
-
},
|
|
312
|
-
// timed_element_missing_clip_class
|
|
313
|
-
// fallow-ignore-next-line complexity
|
|
314
|
-
({ tags }) => {
|
|
315
|
-
const findings = [];
|
|
316
|
-
const skipTags = new Set(["audio", "video", "script", "style", "template"]);
|
|
317
|
-
for (const tag of tags) {
|
|
318
|
-
if (skipTags.has(tag.name))
|
|
319
|
-
continue;
|
|
320
|
-
// Skip composition hosts
|
|
321
|
-
if (readAttr(tag.raw, "data-composition-id"))
|
|
322
|
-
continue;
|
|
323
|
-
if (readAttr(tag.raw, "data-composition-src"))
|
|
324
|
-
continue;
|
|
325
|
-
const hasStart = readAttr(tag.raw, "data-start") !== null;
|
|
326
|
-
const hasDuration = readAttr(tag.raw, "data-duration") !== null;
|
|
327
|
-
// data-track-index alone marks a layer container, not a time-bounded clip
|
|
328
|
-
if (!hasStart && !hasDuration)
|
|
329
|
-
continue;
|
|
330
|
-
const classAttr = readAttr(tag.raw, "class") || "";
|
|
331
|
-
const hasClip = classAttr.split(/\s+/).includes("clip");
|
|
332
|
-
if (hasClip)
|
|
333
|
-
continue;
|
|
334
|
-
const elementId = readAttr(tag.raw, "id") || undefined;
|
|
335
|
-
findings.push({
|
|
336
|
-
code: "timed_element_missing_clip_class",
|
|
337
|
-
severity: "error",
|
|
338
|
-
message: `<${tag.name}${elementId ? ` id="${elementId}"` : ""}> has timing attributes but no class="clip". The element will be visible for the entire composition instead of only during its scheduled time range.`,
|
|
339
|
-
elementId,
|
|
340
|
-
fixHint: 'Add class="clip" to the element. The HyperFrames runtime uses .clip to control visibility based on data-start/data-duration.',
|
|
341
|
-
snippet: truncateSnippet(tag.raw),
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
|
-
return findings;
|
|
345
|
-
},
|
|
346
|
-
// overlapping_clips_same_track
|
|
347
|
-
// fallow-ignore-next-line complexity
|
|
348
|
-
({ tags }) => {
|
|
349
|
-
const findings = [];
|
|
350
|
-
const trackMap = new Map();
|
|
351
|
-
for (const tag of tags) {
|
|
352
|
-
const startStr = readAttr(tag.raw, "data-start");
|
|
353
|
-
const durationStr = readAttr(tag.raw, "data-duration");
|
|
354
|
-
const trackStr = readAttr(tag.raw, "data-track-index");
|
|
355
|
-
if (!startStr || !durationStr || !trackStr)
|
|
356
|
-
continue;
|
|
357
|
-
const start = Number(startStr);
|
|
358
|
-
const duration = Number(durationStr);
|
|
359
|
-
const track = trackStr;
|
|
360
|
-
// Skip non-numeric (relative timing references like "intro-comp")
|
|
361
|
-
if (Number.isNaN(start) || Number.isNaN(duration))
|
|
362
|
-
continue;
|
|
363
|
-
const clips = trackMap.get(track) || [];
|
|
364
|
-
clips.push({
|
|
365
|
-
start,
|
|
366
|
-
end: start + duration,
|
|
367
|
-
elementId: readAttr(tag.raw, "id") || undefined,
|
|
368
|
-
snippet: truncateSnippet(tag.raw) || "",
|
|
369
|
-
});
|
|
370
|
-
trackMap.set(track, clips);
|
|
371
|
-
}
|
|
372
|
-
for (const [track, clips] of trackMap) {
|
|
373
|
-
clips.sort((a, b) => a.start - b.start);
|
|
374
|
-
for (let i = 0; i < clips.length - 1; i++) {
|
|
375
|
-
const current = clips[i];
|
|
376
|
-
const next = clips[i + 1];
|
|
377
|
-
if (!current || !next)
|
|
378
|
-
continue;
|
|
379
|
-
if (current.end > next.start) {
|
|
380
|
-
findings.push({
|
|
381
|
-
code: "overlapping_clips_same_track",
|
|
382
|
-
severity: "error",
|
|
383
|
-
message: `Track ${track}: clip ending at ${current.end}s overlaps with clip starting at ${next.start}s. Overlapping clips on the same track cause rendering conflicts.`,
|
|
384
|
-
fixHint: "Adjust data-start or data-duration so clips on the same track do not overlap, or move one clip to a different data-track-index.",
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
return findings;
|
|
390
|
-
},
|
|
391
|
-
// root_composition_missing_data_start
|
|
392
|
-
({ rootTag, options }) => {
|
|
393
|
-
const findings = [];
|
|
394
|
-
if (options.isSubComposition)
|
|
395
|
-
return findings;
|
|
396
|
-
if (!rootTag)
|
|
397
|
-
return findings;
|
|
398
|
-
const compId = readAttr(rootTag.raw, "data-composition-id");
|
|
399
|
-
if (!compId)
|
|
400
|
-
return findings;
|
|
401
|
-
const hasStart = readAttr(rootTag.raw, "data-start") !== null;
|
|
402
|
-
if (!hasStart) {
|
|
403
|
-
findings.push({
|
|
404
|
-
code: "root_composition_missing_data_start",
|
|
405
|
-
severity: "error",
|
|
406
|
-
message: `Root composition "${compId}" is missing data-start. The runtime needs data-start="0" on the root element to begin playback.`,
|
|
407
|
-
fixHint: 'Add data-start="0" to the root composition element.',
|
|
408
|
-
snippet: truncateSnippet(rootTag.raw),
|
|
409
|
-
});
|
|
410
|
-
}
|
|
411
|
-
return findings;
|
|
412
|
-
},
|
|
413
|
-
// standalone_composition_wrapped_in_template
|
|
414
|
-
({ rawSource, options }) => {
|
|
415
|
-
const findings = [];
|
|
416
|
-
if (options.isSubComposition)
|
|
417
|
-
return findings;
|
|
418
|
-
const trimmed = rawSource.trimStart().toLowerCase();
|
|
419
|
-
if (trimmed.startsWith("<template")) {
|
|
420
|
-
findings.push({
|
|
421
|
-
code: "standalone_composition_wrapped_in_template",
|
|
422
|
-
severity: "error",
|
|
423
|
-
message: "Root index.html is wrapped in a <template> tag. " +
|
|
424
|
-
"Only sub-compositions loaded via data-composition-src should use <template> wrappers. " +
|
|
425
|
-
"The runtime cannot play a standalone composition inside a template.",
|
|
426
|
-
fixHint: "Remove the <template> wrapper. Use <!DOCTYPE html><html>...<div data-composition-id>...</div>...</html> instead.",
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
|
-
return findings;
|
|
430
|
-
},
|
|
431
|
-
// root_composition_missing_html_wrapper
|
|
432
|
-
({ rawSource, rootTag, options }) => {
|
|
433
|
-
const findings = [];
|
|
434
|
-
if (options.isSubComposition)
|
|
435
|
-
return findings;
|
|
436
|
-
const trimmed = rawSource.trimStart().toLowerCase();
|
|
437
|
-
// Compositions inside <template> are caught by standalone_composition_wrapped_in_template
|
|
438
|
-
if (trimmed.startsWith("<template"))
|
|
439
|
-
return findings;
|
|
440
|
-
const hasDoctype = trimmed.startsWith("<!doctype") || trimmed.startsWith("<html");
|
|
441
|
-
const hasComposition = rawSource.includes("data-composition-id");
|
|
442
|
-
if (hasComposition && !hasDoctype) {
|
|
443
|
-
findings.push({
|
|
444
|
-
code: "root_composition_missing_html_wrapper",
|
|
445
|
-
severity: "error",
|
|
446
|
-
message: "Composition starts with a bare element instead of a proper HTML document. " +
|
|
447
|
-
"An index.html that contains data-composition-id but no <!DOCTYPE html>, <html>, or <body> " +
|
|
448
|
-
"is a fragment — browsers quirks-mode it, the preview server cannot load it, and " +
|
|
449
|
-
"the bundler will fail to inject runtime scripts.",
|
|
450
|
-
fixHint: 'Wrap the composition in <!DOCTYPE html><html><head><meta charset="UTF-8"></head><body>...</body></html>.',
|
|
451
|
-
snippet: rootTag ? truncateSnippet(rootTag.raw) : undefined,
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
return findings;
|
|
455
|
-
},
|
|
456
|
-
// requestanimationframe_in_composition
|
|
457
|
-
({ scripts, rawSource, options }) => {
|
|
458
|
-
if (isRegistrySourceFile(options.filePath) || isRegistryInstalledFile(rawSource))
|
|
459
|
-
return [];
|
|
460
|
-
const findings = [];
|
|
461
|
-
for (const script of scripts) {
|
|
462
|
-
const stripped = stripJsComments(script.content);
|
|
463
|
-
if (/requestAnimationFrame\s*\(/.test(stripped)) {
|
|
464
|
-
findings.push({
|
|
465
|
-
code: "requestanimationframe_in_composition",
|
|
466
|
-
severity: "error",
|
|
467
|
-
message: "`requestAnimationFrame` runs on wall-clock time, not the GSAP timeline. It will not sync with frame capture and may cause flickering or missed frames during rendering.",
|
|
468
|
-
fixHint: "Use GSAP tweens or onUpdate callbacks instead of requestAnimationFrame for animation logic.",
|
|
469
|
-
snippet: truncateSnippet(script.content),
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
return findings;
|
|
474
|
-
},
|
|
475
|
-
// invalid_variable_values_json
|
|
476
|
-
// Host elements (`[data-composition-src]`) carry per-instance values via
|
|
477
|
-
// `data-variable-values`. The runtime swallows JSON errors silently and
|
|
478
|
-
// falls back to declared defaults, which masks typos. This rule surfaces
|
|
479
|
-
// the parse failure so authors notice before render time.
|
|
480
|
-
// fallow-ignore-next-line complexity
|
|
481
|
-
({ tags }) => {
|
|
482
|
-
const findings = [];
|
|
483
|
-
for (const tag of tags) {
|
|
484
|
-
const raw = readJsonAttr(tag.raw, "data-variable-values");
|
|
485
|
-
if (!raw)
|
|
486
|
-
continue;
|
|
487
|
-
let parsed;
|
|
488
|
-
try {
|
|
489
|
-
parsed = JSON.parse(raw);
|
|
490
|
-
}
|
|
491
|
-
catch (err) {
|
|
492
|
-
const reason = err instanceof Error ? err.message : "unknown";
|
|
493
|
-
findings.push({
|
|
494
|
-
code: "invalid_variable_values_json",
|
|
495
|
-
severity: "error",
|
|
496
|
-
message: `data-variable-values is not valid JSON (${reason}).`,
|
|
497
|
-
fixHint: 'Wrap the attribute value in single quotes and the JSON keys/values in double quotes, e.g. data-variable-values=\'{"title":"Hello"}\'.',
|
|
498
|
-
elementId: readAttr(tag.raw, "id") || undefined,
|
|
499
|
-
snippet: truncateSnippet(tag.raw),
|
|
500
|
-
});
|
|
501
|
-
continue;
|
|
502
|
-
}
|
|
503
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
504
|
-
findings.push({
|
|
505
|
-
code: "invalid_variable_values_json",
|
|
506
|
-
severity: "error",
|
|
507
|
-
message: 'data-variable-values must be a JSON object keyed by variable id (e.g. {"title":"Hello"}).',
|
|
508
|
-
fixHint: "Replace the value with a JSON object whose keys are variable ids declared in the sub-composition's data-composition-variables.",
|
|
509
|
-
elementId: readAttr(tag.raw, "id") || undefined,
|
|
510
|
-
snippet: truncateSnippet(tag.raw),
|
|
511
|
-
});
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
return findings;
|
|
515
|
-
},
|
|
516
|
-
// invalid_composition_variables_declaration
|
|
517
|
-
// The runtime parses `data-composition-variables` and silently returns []
|
|
518
|
-
// on any structural problem. Surface JSON / shape failures so authors
|
|
519
|
-
// catch them at lint time rather than wondering why their `getVariables()`
|
|
520
|
-
// defaults aren't applied.
|
|
521
|
-
// fallow-ignore-next-line complexity
|
|
522
|
-
({ source }) => {
|
|
523
|
-
const htmlTag = findHtmlTag(source);
|
|
524
|
-
if (!htmlTag)
|
|
525
|
-
return [];
|
|
526
|
-
const raw = readJsonAttr(htmlTag.raw, "data-composition-variables");
|
|
527
|
-
if (!raw)
|
|
528
|
-
return [];
|
|
529
|
-
let parsed;
|
|
530
|
-
try {
|
|
531
|
-
parsed = JSON.parse(raw);
|
|
532
|
-
}
|
|
533
|
-
catch (err) {
|
|
534
|
-
const reason = err instanceof Error ? err.message : "unknown";
|
|
535
|
-
return [
|
|
536
|
-
{
|
|
537
|
-
code: "invalid_composition_variables_declaration",
|
|
538
|
-
severity: "error",
|
|
539
|
-
message: `data-composition-variables is not valid JSON (${reason}).`,
|
|
540
|
-
fixHint: 'Provide a JSON array of variable declarations: data-composition-variables=\'[{"id":"title","type":"string","label":"Title","default":"Hello"}]\'.',
|
|
541
|
-
snippet: truncateSnippet(htmlTag.raw),
|
|
542
|
-
},
|
|
543
|
-
];
|
|
544
|
-
}
|
|
545
|
-
if (!Array.isArray(parsed)) {
|
|
546
|
-
return [
|
|
547
|
-
{
|
|
548
|
-
code: "invalid_composition_variables_declaration",
|
|
549
|
-
severity: "error",
|
|
550
|
-
message: "data-composition-variables must be a JSON array of variable declarations.",
|
|
551
|
-
fixHint: 'Wrap declarations in [] and give each an id, type, label, and default: \'[{"id":"title","type":"string","label":"Title","default":"Hello"}]\'.',
|
|
552
|
-
snippet: truncateSnippet(htmlTag.raw),
|
|
553
|
-
},
|
|
554
|
-
];
|
|
555
|
-
}
|
|
556
|
-
const findings = [];
|
|
557
|
-
const knownTypes = new Set(COMPOSITION_VARIABLE_TYPES);
|
|
558
|
-
for (let i = 0; i < parsed.length; i += 1) {
|
|
559
|
-
const entry = parsed[i];
|
|
560
|
-
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
561
|
-
findings.push({
|
|
562
|
-
code: "invalid_composition_variables_declaration",
|
|
563
|
-
severity: "error",
|
|
564
|
-
message: `data-composition-variables entry [${i}] must be an object with id, type, label, and default.`,
|
|
565
|
-
snippet: truncateSnippet(htmlTag.raw),
|
|
566
|
-
});
|
|
567
|
-
continue;
|
|
568
|
-
}
|
|
569
|
-
const e = entry;
|
|
570
|
-
const missing = [];
|
|
571
|
-
if (typeof e.id !== "string")
|
|
572
|
-
missing.push("id");
|
|
573
|
-
if (typeof e.type !== "string" || !knownTypes.has(e.type))
|
|
574
|
-
missing.push("type");
|
|
575
|
-
if (typeof e.label !== "string")
|
|
576
|
-
missing.push("label");
|
|
577
|
-
if (!("default" in e))
|
|
578
|
-
missing.push("default");
|
|
579
|
-
if (missing.length > 0) {
|
|
580
|
-
findings.push({
|
|
581
|
-
code: "invalid_composition_variables_declaration",
|
|
582
|
-
severity: "error",
|
|
583
|
-
message: `data-composition-variables entry [${i}] is missing or has invalid: ${missing.join(", ")}. Type must be one of string, number, color, boolean, enum, font, image.`,
|
|
584
|
-
snippet: truncateSnippet(htmlTag.raw),
|
|
585
|
-
});
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
return findings;
|
|
589
|
-
},
|
|
590
|
-
// subcomposition_blanks_before_host
|
|
591
|
-
// Warns when a full-bleed sub-composition slot ends before the host composition
|
|
592
|
-
// does, leaving the slot blank for the remainder (issue #1540). Scoped narrowly to
|
|
593
|
-
// the high-signal shape — a sole/dominant external mount starting at ~0 — so it
|
|
594
|
-
// stays silent on intentional short clips (an intro followed by other clips that
|
|
595
|
-
// carry the timeline forward).
|
|
596
|
-
// fallow-ignore-next-line complexity
|
|
597
|
-
({ tags, rootTag }) => {
|
|
598
|
-
if (!rootTag)
|
|
599
|
-
return [];
|
|
600
|
-
const rootDuration = Number(readAttr(rootTag.raw, "data-duration"));
|
|
601
|
-
if (!Number.isFinite(rootDuration) || rootDuration <= 0)
|
|
602
|
-
return [];
|
|
603
|
-
// Two independent knobs that happen to share a 0.5s magnitude. Tuned for
|
|
604
|
-
// real hosts (tens to hundreds of seconds); on a very short host (~6s) the
|
|
605
|
-
// EPSILON slack would let a ~10% blank tail pass unflagged — acceptable
|
|
606
|
-
// because the silent-blank trap this rule targets only matters at scale.
|
|
607
|
-
const EPSILON = 0.5; // seconds; tolerance for "ends/covers near the host end"
|
|
608
|
-
const START_TOLERANCE = 0.5; // seconds; "starts at the composition start"
|
|
609
|
-
const round3 = (n) => Math.round(n * 1000) / 1000;
|
|
610
|
-
// Timed children of the root. An element with data-start but no usable
|
|
611
|
-
// data-duration is treated as covering the tail (end = Infinity), so an
|
|
612
|
-
// unknown-length sibling suppresses the warning rather than triggering it.
|
|
613
|
-
const timed = tags
|
|
614
|
-
.filter((tag) => tag.index !== rootTag.index && readAttr(tag.raw, "data-start") !== null)
|
|
615
|
-
.map((tag) => {
|
|
616
|
-
const start = Number(readAttr(tag.raw, "data-start")) || 0;
|
|
617
|
-
const dur = Number(readAttr(tag.raw, "data-duration"));
|
|
618
|
-
const end = Number.isFinite(dur) && dur > 0 ? start + dur : Infinity;
|
|
619
|
-
return { tag, start, end };
|
|
620
|
-
});
|
|
621
|
-
// `tags` is a flat list (no nesting depth), so a timed element nested
|
|
622
|
-
// *inside* a candidate slot is treated as a tail-covering sibling rather
|
|
623
|
-
// than a descendant. Acceptable: external src mounts are empty by
|
|
624
|
-
// convention (content is loaded from the linked file), so the only
|
|
625
|
-
// false-negative path is rare and matches the flat-tag scope of the
|
|
626
|
-
// sibling rules in this file.
|
|
627
|
-
const tailCovered = (exceptIndex) => timed.some((t) => t.tag.index !== exceptIndex && t.end >= rootDuration - EPSILON);
|
|
628
|
-
const findings = [];
|
|
629
|
-
for (const t of timed) {
|
|
630
|
-
if (readAttr(t.tag.raw, "data-composition-src") === null)
|
|
631
|
-
continue; // external slot only
|
|
632
|
-
if (t.start > START_TOLERANCE)
|
|
633
|
-
continue; // must start at the composition start
|
|
634
|
-
if (!Number.isFinite(t.end))
|
|
635
|
-
continue; // known, finite slot length
|
|
636
|
-
if (t.end >= rootDuration - EPSILON)
|
|
637
|
-
continue; // already fills the host window
|
|
638
|
-
if (tailCovered(t.tag.index))
|
|
639
|
-
continue; // another clip covers the tail — not full-bleed
|
|
640
|
-
const elementId = readAttr(t.tag.raw, "id") || undefined;
|
|
641
|
-
const gap = round3(rootDuration - t.end);
|
|
642
|
-
findings.push({
|
|
643
|
-
code: "subcomposition_blanks_before_host",
|
|
644
|
-
severity: "warning",
|
|
645
|
-
message: `<${t.tag.name}${elementId ? ` id="${elementId}"` : ""}> sub-composition ends at ${round3(t.end)}s but the composition runs to ${round3(rootDuration)}s — its slot will be blank for ~${gap}s.`,
|
|
646
|
-
elementId,
|
|
647
|
-
fixHint: `data-duration is the slot's visible window. Set this sub-composition's data-duration to ${round3(rootDuration - t.start)} to fill the host window, or add another clip to cover the remaining ~${gap}s.`,
|
|
648
|
-
snippet: truncateSnippet(t.tag.raw),
|
|
649
|
-
});
|
|
650
|
-
}
|
|
651
|
-
return findings;
|
|
652
|
-
},
|
|
653
|
-
// subcomposition_root_styled_by_class
|
|
654
|
-
// A sub-composition's <style> is scoped at render time to
|
|
655
|
-
// `[data-composition-id="<id>"] <selector>` so scenes inlined into one document
|
|
656
|
-
// can't leak styles into each other. A rule whose LEFTMOST selector is the ROOT
|
|
657
|
-
// element's own class (e.g. `.frame { ... }` on the same element that carries
|
|
658
|
-
// data-composition-id) therefore becomes a DESCENDANT selector that can never
|
|
659
|
-
// match the root — the whole scene renders unstyled (tiny text top-left, images
|
|
660
|
-
// at natural size). lint/validate/inspect evaluate the file in isolation (no
|
|
661
|
-
// scoping) and Studio previews each scene in its own iframe (no scoping), so the
|
|
662
|
-
// break is invisible until the composited MP4 render. Style the root via `#root`
|
|
663
|
-
// (the scoper special-cases the root id) and descendants via plain selectors,
|
|
664
|
-
// like the registry blocks — the runtime already scopes each scene by id, so a
|
|
665
|
-
// class namespace on the root is redundant.
|
|
666
|
-
({ rootTag, rootCompositionId, styles, options }) => {
|
|
667
|
-
if (!options.isSubComposition)
|
|
668
|
-
return [];
|
|
669
|
-
if (isRegistrySourceFile(options.filePath))
|
|
670
|
-
return [];
|
|
671
|
-
if (!rootTag || !rootCompositionId)
|
|
672
|
-
return [];
|
|
673
|
-
const rootClasses = (readAttr(rootTag.raw, "class") || "").split(/\s+/).filter(Boolean);
|
|
674
|
-
if (rootClasses.length === 0)
|
|
675
|
-
return [];
|
|
676
|
-
const offenders = rootClassStyledSelectors(styles, rootClasses);
|
|
677
|
-
if (offenders.length === 0)
|
|
678
|
-
return [];
|
|
679
|
-
const example = offenders.slice(0, 3).join(", ");
|
|
680
|
-
return [
|
|
681
|
-
{
|
|
682
|
-
code: "subcomposition_root_styled_by_class",
|
|
683
|
-
severity: "error",
|
|
684
|
-
message: `Root element has class="${rootClasses.join(" ")}" and is styled by ${offenders.length} rule(s) keyed off that class (e.g. ${example}). ` +
|
|
685
|
-
`At render, every sub-composition rule is scoped to [data-composition-id="${rootCompositionId}"] <selector>, so a selector whose leftmost part is the ROOT's own class becomes a descendant selector that cannot match the root — the scene renders unstyled (tiny text top-left, full-size images). ` +
|
|
686
|
-
`lint/validate/inspect and Studio's per-frame iframe preview do not scope, so this passes every static check and looks correct in preview.`,
|
|
687
|
-
selector: example,
|
|
688
|
-
fixHint: `Give the root id="root" and style it with \`#root { ... }\` plus plain descendant selectors (\`.kicker\`, \`#hero\`) — the runtime already scopes each sub-composition by data-composition-id, so a class namespace on the root is redundant and breaks under scoping.`,
|
|
689
|
-
snippet: truncateSnippet(rootTag.raw),
|
|
690
|
-
},
|
|
691
|
-
];
|
|
692
|
-
},
|
|
693
|
-
];
|
|
694
|
-
//# sourceMappingURL=composition.js.map
|