frameyard 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +164 -7
- package/GLOSSARY.md +152 -18
- package/README.md +328 -285
- package/dist/assets/integrations.d.ts +8 -4
- package/dist/assets/integrations.d.ts.map +1 -1
- package/dist/assets/integrations.js +8 -4
- package/dist/assets/integrations.js.map +1 -1
- package/dist/assets/provenance.d.ts +2 -2
- package/dist/assets/provenance.d.ts.map +1 -1
- package/dist/assets/provenance.js +5 -3
- package/dist/assets/provenance.js.map +1 -1
- package/dist/assets/tts-supertonic.d.ts +28 -0
- package/dist/assets/tts-supertonic.d.ts.map +1 -0
- package/dist/assets/tts-supertonic.js +156 -0
- package/dist/assets/tts-supertonic.js.map +1 -0
- package/dist/assets/tts.d.ts +18 -6
- package/dist/assets/tts.d.ts.map +1 -1
- package/dist/assets/tts.js +219 -97
- package/dist/assets/tts.js.map +1 -1
- package/dist/cli.js +1705 -106
- package/dist/cli.js.map +1 -1
- package/dist/composition/descriptor.d.ts +17 -3
- package/dist/composition/descriptor.d.ts.map +1 -1
- package/dist/composition/descriptor.js +51 -28
- package/dist/composition/descriptor.js.map +1 -1
- package/dist/composition/format.d.ts +12 -0
- package/dist/composition/format.d.ts.map +1 -0
- package/dist/composition/format.js +28 -0
- package/dist/composition/format.js.map +1 -0
- package/dist/composition/index.d.ts +2 -1
- package/dist/composition/index.d.ts.map +1 -1
- package/dist/composition/index.js +1 -0
- package/dist/composition/index.js.map +1 -1
- package/dist/context.d.ts +82 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +277 -0
- package/dist/context.js.map +1 -0
- package/dist/contracts.d.ts +80 -6
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +3 -0
- package/dist/contracts.js.map +1 -1
- package/dist/doctor.d.ts +14 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +166 -7
- package/dist/doctor.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/fonts/add.d.ts +41 -0
- package/dist/fonts/add.d.ts.map +1 -0
- package/dist/fonts/add.js +259 -0
- package/dist/fonts/add.js.map +1 -0
- package/dist/fonts/binary.d.ts +4 -0
- package/dist/fonts/binary.d.ts.map +1 -0
- package/dist/fonts/binary.js +21 -0
- package/dist/fonts/binary.js.map +1 -0
- package/dist/fonts/css.d.ts +4 -0
- package/dist/fonts/css.d.ts.map +1 -0
- package/dist/fonts/css.js +50 -0
- package/dist/fonts/css.js.map +1 -0
- package/dist/fonts/fontsource.d.ts +107 -0
- package/dist/fonts/fontsource.d.ts.map +1 -0
- package/dist/fonts/fontsource.js +565 -0
- package/dist/fonts/fontsource.js.map +1 -0
- package/dist/fonts/index.d.ts +11 -0
- package/dist/fonts/index.d.ts.map +1 -0
- package/dist/fonts/index.js +11 -0
- package/dist/fonts/index.js.map +1 -0
- package/dist/fonts/install.d.ts +24 -0
- package/dist/fonts/install.d.ts.map +1 -0
- package/dist/fonts/install.js +145 -0
- package/dist/fonts/install.js.map +1 -0
- package/dist/fonts/manifest.d.ts +21 -0
- package/dist/fonts/manifest.d.ts.map +1 -0
- package/dist/fonts/manifest.js +96 -0
- package/dist/fonts/manifest.js.map +1 -0
- package/dist/fonts/options.d.ts +9 -0
- package/dist/fonts/options.d.ts.map +1 -0
- package/dist/fonts/options.js +66 -0
- package/dist/fonts/options.js.map +1 -0
- package/dist/fonts/schema.d.ts +4 -0
- package/dist/fonts/schema.d.ts.map +1 -0
- package/dist/fonts/schema.js +316 -0
- package/dist/fonts/schema.js.map +1 -0
- package/dist/fonts/summary.d.ts +16 -0
- package/dist/fonts/summary.d.ts.map +1 -0
- package/dist/fonts/summary.js +22 -0
- package/dist/fonts/summary.js.map +1 -0
- package/dist/fonts/types.d.ts +56 -0
- package/dist/fonts/types.d.ts.map +1 -0
- package/dist/fonts/types.js +8 -0
- package/dist/fonts/types.js.map +1 -0
- package/dist/fonts/verify.d.ts +33 -0
- package/dist/fonts/verify.d.ts.map +1 -0
- package/dist/fonts/verify.js +200 -0
- package/dist/fonts/verify.js.map +1 -0
- package/dist/html/document-adapter.d.ts +3 -3
- package/dist/html/document-adapter.d.ts.map +1 -1
- package/dist/html/document-adapter.js +1 -1
- package/dist/html/document-adapter.js.map +1 -1
- package/dist/html/index.d.ts +1 -1
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/parse.d.ts +10 -2
- package/dist/html/parse.d.ts.map +1 -1
- package/dist/html/parse.js +14 -5
- package/dist/html/parse.js.map +1 -1
- package/dist/html/validate.d.ts +12 -1
- package/dist/html/validate.d.ts.map +1 -1
- package/dist/html/validate.js +292 -61
- package/dist/html/validate.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts +61 -0
- package/dist/inspect.d.ts.map +1 -0
- package/dist/inspect.js +325 -0
- package/dist/inspect.js.map +1 -0
- package/dist/lint.d.ts +63 -0
- package/dist/lint.d.ts.map +1 -0
- package/dist/lint.js +265 -0
- package/dist/lint.js.map +1 -0
- package/dist/policy.d.ts +69 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +361 -0
- package/dist/policy.js.map +1 -0
- package/dist/preview/main.d.ts +2 -2
- package/dist/preview/main.d.ts.map +1 -1
- package/dist/preview/main.js +35 -14
- package/dist/preview/main.js.map +1 -1
- package/dist/preview/timeline-data.d.ts +17 -65
- package/dist/preview/timeline-data.d.ts.map +1 -1
- package/dist/preview/timeline-host.d.ts.map +1 -1
- package/dist/preview/timeline-host.js +73 -38
- package/dist/preview/timeline-host.js.map +1 -1
- package/dist/preview/timeline-main.d.ts.map +1 -1
- package/dist/preview/timeline-main.js +47 -3
- package/dist/preview/timeline-main.js.map +1 -1
- package/dist/project.d.ts +13 -8
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +20 -17
- package/dist/project.js.map +1 -1
- package/dist/renderer/browser.d.ts +2 -2
- package/dist/renderer/browser.d.ts.map +1 -1
- package/dist/renderer/browser.js +10 -4
- package/dist/renderer/browser.js.map +1 -1
- package/dist/renderer/ffmpeg.d.ts +44 -1
- package/dist/renderer/ffmpeg.d.ts.map +1 -1
- package/dist/renderer/ffmpeg.js +371 -73
- package/dist/renderer/ffmpeg.js.map +1 -1
- package/dist/renderer/index.d.ts +40 -4
- package/dist/renderer/index.d.ts.map +1 -1
- package/dist/renderer/index.js +100 -25
- package/dist/renderer/index.js.map +1 -1
- package/dist/renderer/keyframe-expressions.d.ts +39 -0
- package/dist/renderer/keyframe-expressions.d.ts.map +1 -0
- package/dist/renderer/keyframe-expressions.js +180 -0
- package/dist/renderer/keyframe-expressions.js.map +1 -0
- package/dist/renderer/timeline.d.ts +2 -0
- package/dist/renderer/timeline.d.ts.map +1 -1
- package/dist/renderer/timeline.js +168 -106
- package/dist/renderer/timeline.js.map +1 -1
- package/dist/runtime/host.d.ts.map +1 -1
- package/dist/runtime/host.js +159 -31
- package/dist/runtime/host.js.map +1 -1
- package/dist/scaffold.d.ts +3 -0
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +48 -317
- package/dist/scaffold.js.map +1 -1
- package/dist/server/composition-html.d.ts +15 -7
- package/dist/server/composition-html.d.ts.map +1 -1
- package/dist/server/composition-html.js +72 -16
- package/dist/server/composition-html.js.map +1 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +56 -6
- package/dist/server/server.js.map +1 -1
- package/dist/server/timeline-preview-data.d.ts.map +1 -1
- package/dist/server/timeline-preview-data.js +12 -23
- package/dist/server/timeline-preview-data.js.map +1 -1
- package/dist/server/timeline-preview-html.d.ts.map +1 -1
- package/dist/server/timeline-preview-html.js +20 -3
- package/dist/server/timeline-preview-html.js.map +1 -1
- package/dist/templates.d.ts +16 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/templates.js +186 -0
- package/dist/templates.js.map +1 -0
- package/dist/timeline/easing.d.ts +36 -0
- package/dist/timeline/easing.d.ts.map +1 -0
- package/dist/timeline/easing.js +225 -0
- package/dist/timeline/easing.js.map +1 -0
- package/dist/timeline/keyframes.d.ts +23 -0
- package/dist/timeline/keyframes.d.ts.map +1 -0
- package/dist/timeline/keyframes.js +95 -0
- package/dist/timeline/keyframes.js.map +1 -0
- package/dist/timeline/plan.d.ts +6 -12
- package/dist/timeline/plan.d.ts.map +1 -1
- package/dist/timeline/plan.js +17 -30
- package/dist/timeline/plan.js.map +1 -1
- package/dist/timeline/resolve.d.ts +16 -3
- package/dist/timeline/resolve.d.ts.map +1 -1
- package/dist/timeline/resolve.js +119 -13
- package/dist/timeline/resolve.js.map +1 -1
- package/dist/timeline/scene.d.ts +126 -0
- package/dist/timeline/scene.d.ts.map +1 -0
- package/dist/timeline/scene.js +109 -0
- package/dist/timeline/scene.js.map +1 -0
- package/dist/timeline/schema.d.ts +38 -0
- package/dist/timeline/schema.d.ts.map +1 -1
- package/dist/timeline/schema.js +290 -9
- package/dist/timeline/schema.js.map +1 -1
- package/dist/timeline/transitions.d.ts +21 -0
- package/dist/timeline/transitions.d.ts.map +1 -0
- package/dist/timeline/transitions.js +23 -0
- package/dist/timeline/transitions.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/adr/0007-make-html-documents-the-composition-unit.md +2 -2
- package/docs/adr/0008-share-the-composition-document-grammar.md +1 -1
- package/docs/adr/0010-make-timelines-the-render-unit.md +3 -2
- package/docs/adr/0011-compose-timeline-frames-in-a-live-browser-host.md +1 -1
- package/docs/adr/0012-render-timelines-with-segmented-lossless-clip-intermediates.md +1 -1
- package/docs/adr/0014-compose-timeline-clips-with-css-blend-modes.md +38 -0
- package/docs/adr/0015-inherit-composition-format-from-the-project.md +28 -0
- package/docs/adr/0016-author-compositions-as-html-fragments.md +32 -0
- package/docs/adr/0017-render-timeless-compositions-as-stills.md +99 -0
- package/docs/adr/0018-animate-timeline-clips-with-keyframes.md +35 -0
- package/docs/adr/0019-make-same-track-overlap-the-transition-window.md +36 -0
- package/docs/adr/0020-crossfade-audio-clips-on-audio-tracks.md +27 -0
- package/docs/adr/0021-project-timeline-scenes-once.md +32 -0
- package/docs/adr/0022-gate-heavy-operations-with-a-workspace-resource-policy.md +35 -0
- package/docs/adr/0023-pin-fonts-as-project-assets.md +98 -0
- package/docs/migration-0.4.md +5 -3
- package/docs/migration-0.8.md +163 -0
- package/docs/preview-testing.md +23 -3
- package/docs/{v0.1-design.md → v0.x-design.md} +86 -53
- package/examples/blend/README.md +106 -0
- package/examples/blend/assets/ink.mp4 +0 -0
- package/examples/blend/compositions/aurora.html +55 -0
- package/examples/blend/compositions/backdrop.html +74 -0
- package/examples/blend/compositions/labels.html +149 -0
- package/examples/blend/fy.config.js +11 -0
- package/examples/blend/scripts/make-ink.sh +26 -0
- package/examples/blend/timelines/blend.timeline.json +155 -0
- package/examples/demo/README.md +2 -2
- package/examples/demo/compositions/demo.html +243 -251
- package/examples/intro/README.md +71 -0
- package/examples/intro/assets/fonts/figtree/LICENSE.txt +93 -0
- package/examples/intro/assets/fonts/figtree/figtree-latin-wght-normal.woff2 +0 -0
- package/examples/intro/assets/fonts/instrument-serif/LICENSE.txt +93 -0
- package/examples/intro/assets/fonts/instrument-serif/instrument-serif-latin-400-italic.woff2 +0 -0
- package/examples/intro/assets/fonts/space-mono/LICENSE.txt +93 -0
- package/examples/intro/assets/fonts/space-mono/space-mono-latin-400-normal.woff2 +0 -0
- package/examples/intro/assets/fonts/space-mono/space-mono-latin-700-normal.woff2 +0 -0
- package/examples/intro/assets/grain.png +0 -0
- package/examples/intro/assets/score.ogg +0 -0
- package/examples/intro/compositions/brand.html +412 -0
- package/examples/intro/compositions/chrome.controller.js +63 -0
- package/examples/intro/compositions/chrome.html +256 -0
- package/examples/intro/compositions/lattice.controller.js +148 -0
- package/examples/intro/compositions/lattice.html +308 -0
- package/examples/intro/compositions/opening.html +504 -0
- package/examples/intro/compositions/studio.html +930 -0
- package/examples/intro/compositions/theme.css +218 -0
- package/examples/intro/fy.config.js +13 -0
- package/examples/intro/fy.fonts.json +100 -0
- package/examples/intro/scripts/make-grain.mjs +79 -0
- package/examples/intro/scripts/make-score.mjs +702 -0
- package/examples/intro/timelines/intro.timeline.json +60 -0
- package/examples/keyframes/README.md +76 -0
- package/examples/keyframes/compositions/backdrop.html +60 -0
- package/examples/keyframes/compositions/card.html +99 -0
- package/examples/keyframes/fy.config.js +10 -0
- package/examples/keyframes/timelines/keyframes.timeline.json +60 -0
- package/examples/nested/README.md +112 -0
- package/examples/nested/assets/pulse.wav +0 -0
- package/examples/nested/compositions/backdrop.html +84 -0
- package/examples/nested/compositions/card.html +275 -0
- package/examples/nested/compositions/overlay.html +307 -0
- package/examples/nested/fy.config.js +13 -0
- package/examples/nested/scripts/make-pulse.sh +20 -0
- package/examples/nested/timelines/chapter-a.timeline.json +39 -0
- package/examples/nested/timelines/chapter-b.timeline.json +39 -0
- package/examples/nested/timelines/nested-reel.timeline.json +52 -0
- package/examples/static/README.md +17 -0
- package/examples/static/compositions/still.html +37 -0
- package/examples/static/fy.config.js +9 -0
- package/examples/static/timelines/still.timeline.json +13 -0
- package/examples/transitions/README.md +128 -0
- package/examples/transitions/assets/alpha.wav +0 -0
- package/examples/transitions/assets/beta.wav +0 -0
- package/examples/transitions/assets/gamma.wav +0 -0
- package/examples/transitions/compositions/alpha.html +56 -0
- package/examples/transitions/compositions/beta.html +56 -0
- package/examples/transitions/compositions/gamma.html +56 -0
- package/examples/transitions/fy.config.js +12 -0
- package/examples/transitions/timelines/audio-crossfade.timeline.json +44 -0
- package/examples/transitions/timelines/transitions.timeline.json +31 -0
- package/examples/video/README.md +4 -4
- package/examples/video/compositions/video-mp4.html +349 -357
- package/examples/video/compositions/video-overlay.html +337 -345
- package/examples/video/compositions/video.html +349 -357
- package/examples/webgl/README.md +1 -1
- package/examples/webgl/compositions/webgl.html +205 -213
- package/package.json +13 -5
- package/scaffolds/init/AGENTS.md +131 -0
- package/scaffolds/init/README.md +56 -0
- package/scaffolds/init/compositions/hello.html +79 -0
- package/scaffolds/init/fy.config.js +9 -0
- package/scaffolds/init/gitignore.template +1 -0
- package/scaffolds/init/package.json +11 -0
- package/scaffolds/init/props/hello.json +4 -0
- package/scaffolds/init/props/main.json +6 -0
- package/scaffolds/init/timelines/main.timeline.json +20 -0
- package/scaffolds/local-models/LOCAL-MODELS.md +77 -0
- package/scaffolds/local-models/fy.policy.json +5 -0
- package/scaffolds/local-models/opencode/opencode.jsonc +17 -0
- package/scaffolds/local-models/pi/APPEND_SYSTEM.md +21 -0
- package/scaffolds/local-models/pi/sandbox.json +15 -0
- package/scaffolds/local-models/pi/settings.json +15 -0
- package/skills/frameyard/README.md +1 -0
- package/skills/frameyard/SKILL.md +59 -59
- package/skills/frameyard/references/audio-assets.md +32 -3
- package/skills/frameyard/references/common-mistakes.md +81 -7
- package/skills/frameyard/references/controllers.md +5 -4
- package/skills/frameyard/references/deterministic-authoring.md +49 -37
- package/skills/frameyard/references/diagnostics.md +42 -4
- package/skills/frameyard/references/failure-recovery.md +56 -10
- package/skills/frameyard/references/fonts.md +132 -0
- package/skills/frameyard/references/json-results.md +165 -10
- package/skills/frameyard/references/quick-reference.md +222 -0
- package/skills/frameyard/references/timelines.md +109 -15
- /package/examples/demo/{video.config.js → fy.config.js} +0 -0
- /package/examples/video/{video.config.js → fy.config.js} +0 -0
- /package/examples/webgl/{video.config.js → fy.config.js} +0 -0
package/dist/html/validate.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { readFile, stat } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { KNOWN_DATA_FY_ATTRIBUTES, isAnimationAttribute, isLocalRelative, isMediaAttribute, isRemote, readAnimationDeclarations, readCompositionDescriptor, readMediaDeclarations, readTranscriptDeclarations, } from "../composition/index.js";
|
|
4
|
-
import { mergeProps, } from "../contracts.js";
|
|
3
|
+
import { KNOWN_DATA_FY_ATTRIBUTES, isAnimationAttribute, isLocalRelative, isMediaAttribute, isRemote, readAnimationDeclarations, readCompositionDescriptor, readMediaDeclarations, readTranscriptDeclarations, resolveCompositionMetadata, } from "../composition/index.js";
|
|
4
|
+
import { isPlainObject, mergeProps, } from "../contracts.js";
|
|
5
5
|
import { resolveBindingPath, suggestBindingPath, validateBindingPath } from "../bindings.js";
|
|
6
6
|
import { errorMessage, isFrameyardError } from "../errors.js";
|
|
7
7
|
import { parseTranscript } from "../transcript.js";
|
|
8
8
|
import { createParse5Adapter } from "./document-adapter.js";
|
|
9
|
-
import { findElements, getAttribute, getAttributeLocation, getDocumentDoctype,
|
|
9
|
+
import { findElements, getAttribute, getAttributeLocation, getDocumentDoctype, getElementLocation, getNodeLocation, getTextContent, hasAttribute, isElement, isTemplate, isTextNode, parseHtml, parseHtmlFragment, walk, } from "./parse.js";
|
|
10
10
|
const VOID_ELEMENTS = new Set([
|
|
11
11
|
"area",
|
|
12
12
|
"base",
|
|
@@ -30,11 +30,16 @@ class HtmlCompositionValidator {
|
|
|
30
30
|
projectRoot;
|
|
31
31
|
entry;
|
|
32
32
|
compositionDir;
|
|
33
|
-
|
|
33
|
+
fragment;
|
|
34
|
+
shellPresent;
|
|
35
|
+
shellDiagnostics;
|
|
34
36
|
propsOverrides;
|
|
35
37
|
defaultPropsValue = Object.freeze({});
|
|
36
38
|
props;
|
|
39
|
+
format;
|
|
37
40
|
metadata = null;
|
|
41
|
+
declaredFormat = null;
|
|
42
|
+
formatUnresolved = false;
|
|
38
43
|
controller = null;
|
|
39
44
|
documentValid = true;
|
|
40
45
|
fixedFactsValid = false;
|
|
@@ -44,26 +49,36 @@ class HtmlCompositionValidator {
|
|
|
44
49
|
audioClips = [];
|
|
45
50
|
embeddedAudioClips = [];
|
|
46
51
|
transcriptClips = [];
|
|
47
|
-
constructor(options,
|
|
52
|
+
constructor(options, fragment, shell) {
|
|
48
53
|
this.projectRoot = path.resolve(options.projectRoot);
|
|
49
54
|
this.entry = options.entry.replaceAll("\\", "/");
|
|
50
55
|
this.compositionDir = path.dirname(path.resolve(this.projectRoot, this.entry));
|
|
51
|
-
this.
|
|
56
|
+
this.fragment = fragment;
|
|
57
|
+
this.shellPresent = shell.present;
|
|
58
|
+
this.shellDiagnostics = shell.diagnostics;
|
|
52
59
|
this.propsOverrides = options.props;
|
|
53
60
|
this.props = this.defaultPropsValue;
|
|
61
|
+
this.format = options.format;
|
|
54
62
|
}
|
|
55
63
|
async run() {
|
|
56
|
-
this.
|
|
57
|
-
|
|
64
|
+
if (this.shellDiagnostics.length > 0) {
|
|
65
|
+
this.documentValid = false;
|
|
66
|
+
this.diagnostics.push(...this.shellDiagnostics);
|
|
67
|
+
}
|
|
68
|
+
const adapter = createParse5Adapter(this.fragment);
|
|
58
69
|
const read = readCompositionDescriptor(adapter, {
|
|
59
70
|
path: this.entry,
|
|
60
71
|
resolvePath: (raw) => this.resolveControllerPath(raw),
|
|
72
|
+
rootScope: "fragment",
|
|
61
73
|
});
|
|
62
74
|
this.diagnostics.push(...read.diagnostics);
|
|
63
75
|
this.fixedFactsValid = read.valid;
|
|
64
76
|
const readRoot = read.root;
|
|
65
77
|
this.root = readRoot !== null && isElement(readRoot) ? readRoot : null;
|
|
66
|
-
|
|
78
|
+
const declared = read.descriptor.metadata;
|
|
79
|
+
this.declaredFormat = declared?.format ?? null;
|
|
80
|
+
this.metadata = declared ? resolveCompositionMetadata(declared, this.format) : null;
|
|
81
|
+
this.formatUnresolved = this.fixedFactsValid && declared !== null && this.metadata === null;
|
|
67
82
|
this.defaultPropsValue = read.descriptor.defaultProps;
|
|
68
83
|
this.controller = read.descriptor.controller;
|
|
69
84
|
this.mergeOverrides();
|
|
@@ -81,12 +96,28 @@ class HtmlCompositionValidator {
|
|
|
81
96
|
});
|
|
82
97
|
this.diagnostics.push(...animation.diagnostics);
|
|
83
98
|
this.validateAnimationTargets(animation.entries);
|
|
99
|
+
if (this.metadata?.static === true) {
|
|
100
|
+
for (const entry of animation.entries) {
|
|
101
|
+
if (!isElement(entry.element)) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
this.error("STATIC_ANIMATION_UNSUPPORTED", "A static composition cannot use data-fy-anim-* scheduling. Remove the animation or declare duration-in-frames and fps.", this.attributeDetails(entry.element, entry.attributes[0] ?? "data-fy-anim-start"));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
84
107
|
const media = readMediaDeclarations(adapter, {
|
|
85
108
|
path: this.entry,
|
|
86
109
|
includeTemplateContent: true,
|
|
87
110
|
props: this.props,
|
|
88
111
|
});
|
|
89
112
|
this.diagnostics.push(...media.diagnostics);
|
|
113
|
+
if (this.metadata?.static === true) {
|
|
114
|
+
for (const declaration of [...media.audio, ...media.embedded]) {
|
|
115
|
+
if (!isElement(declaration.element)) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
this.error("STATIC_MEDIA_UNSUPPORTED", "A static composition is visual-only and cannot declare audio or video. Put audio on a timeline track instead.", this.elementDetails(declaration.element));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
90
121
|
await this.validateMediaFiles(media.audio, media.embedded);
|
|
91
122
|
const transcripts = readTranscriptDeclarations(adapter, {
|
|
92
123
|
path: this.entry,
|
|
@@ -94,13 +125,30 @@ class HtmlCompositionValidator {
|
|
|
94
125
|
props: this.props,
|
|
95
126
|
});
|
|
96
127
|
this.diagnostics.push(...transcripts.diagnostics);
|
|
128
|
+
if (this.metadata?.static === true) {
|
|
129
|
+
for (const declaration of transcripts.declarations) {
|
|
130
|
+
if (!isElement(declaration.element)) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
this.error("STATIC_MEDIA_UNSUPPORTED", "A static composition is visual-only and cannot declare transcripts. Put captions on a timeline instead.", this.elementDetails(declaration.element));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
97
136
|
await this.validateTranscriptFiles(transcripts.declarations);
|
|
98
137
|
await this.validateOrdinaryAssets();
|
|
138
|
+
await this.validateRemoteFonts();
|
|
99
139
|
const metadata = this.metadata;
|
|
140
|
+
const declaredFormat = this.declaredFormat;
|
|
100
141
|
const root = this.root;
|
|
101
|
-
const info = metadata &&
|
|
142
|
+
const info = metadata &&
|
|
143
|
+
declaredFormat &&
|
|
144
|
+
root &&
|
|
145
|
+
this.fixedFactsValid &&
|
|
146
|
+
this.overridesValid &&
|
|
147
|
+
this.controllerFileValid &&
|
|
148
|
+
this.documentValid
|
|
102
149
|
? {
|
|
103
150
|
metadata,
|
|
151
|
+
declaredFormat,
|
|
104
152
|
defaultProps: this.defaultPropsValue,
|
|
105
153
|
controller: this.controller,
|
|
106
154
|
audioClips: Object.freeze([...this.audioClips]),
|
|
@@ -108,7 +156,7 @@ class HtmlCompositionValidator {
|
|
|
108
156
|
transcriptClips: Object.freeze([...this.transcriptClips]),
|
|
109
157
|
}
|
|
110
158
|
: null;
|
|
111
|
-
return { info, diagnostics: this.finishDiagnostics() };
|
|
159
|
+
return { info, formatUnresolved: this.formatUnresolved, diagnostics: this.finishDiagnostics() };
|
|
112
160
|
}
|
|
113
161
|
report(severity, code, message, details) {
|
|
114
162
|
const diagnostic = {
|
|
@@ -187,23 +235,6 @@ class HtmlCompositionValidator {
|
|
|
187
235
|
return leftColumn - rightColumn;
|
|
188
236
|
});
|
|
189
237
|
}
|
|
190
|
-
validateDoctype() {
|
|
191
|
-
const doctype = getDocumentDoctype(this.document);
|
|
192
|
-
if (!doctype) {
|
|
193
|
-
this.documentValid = false;
|
|
194
|
-
this.error("COMPOSITION_INVALID", "Composition documents must start with <!doctype html>.", {
|
|
195
|
-
line: 1,
|
|
196
|
-
column: 1,
|
|
197
|
-
});
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
if (doctype.name.toLowerCase() !== "html") {
|
|
201
|
-
this.documentValid = false;
|
|
202
|
-
this.error("COMPOSITION_INVALID", "Composition documents must use the HTML doctype.", {
|
|
203
|
-
...this.locationDetails(doctype.sourceCodeLocation ?? undefined),
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
238
|
mergeOverrides() {
|
|
208
239
|
try {
|
|
209
240
|
this.props = mergeProps(this.defaultPropsValue, this.propsOverrides);
|
|
@@ -238,7 +269,7 @@ class HtmlCompositionValidator {
|
|
|
238
269
|
return path.relative(this.projectRoot, path.resolve(this.compositionDir, raw)).split(path.sep).join("/");
|
|
239
270
|
}
|
|
240
271
|
validateScripts() {
|
|
241
|
-
for (const element of findElements(this.
|
|
272
|
+
for (const element of findElements(this.fragment, (candidate) => candidate.tagName === "script")) {
|
|
242
273
|
if (hasAttribute(element, "data-fy-host")) {
|
|
243
274
|
continue;
|
|
244
275
|
}
|
|
@@ -248,9 +279,91 @@ class HtmlCompositionValidator {
|
|
|
248
279
|
}
|
|
249
280
|
this.error("SCRIPT_NOT_ALLOWED", "Executable <script> elements are not allowed in a composition; move code into the controller.", this.elementDetails(element, { value: getAttribute(element, "src") }));
|
|
250
281
|
}
|
|
282
|
+
this.validateImportMaps();
|
|
283
|
+
}
|
|
284
|
+
validateImportMaps() {
|
|
285
|
+
const forbiddenAttributes = ["src", "async", "nomodule", "defer", "crossorigin", "integrity", "referrerpolicy"];
|
|
286
|
+
for (const element of findElements(this.fragment, (candidate) => candidate.tagName === "script" && getAttribute(candidate, "type")?.toLowerCase() === "importmap")) {
|
|
287
|
+
if (element.parentNode !== this.fragment) {
|
|
288
|
+
this.error("IMPORT_MAP_INVALID", "Import maps must be top-level elements in the composition fragment.", this.elementDetails(element));
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
for (const attribute of forbiddenAttributes) {
|
|
292
|
+
if (hasAttribute(element, attribute)) {
|
|
293
|
+
this.error("IMPORT_MAP_INVALID", `Import maps cannot specify the "${attribute}" attribute.`, this.attributeDetails(element, attribute));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
let parsed;
|
|
297
|
+
try {
|
|
298
|
+
parsed = JSON.parse(getTextContent(element));
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
this.error("IMPORT_MAP_INVALID", `Invalid import map JSON: ${errorMessage(error)}`, this.elementDetails(element));
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (!isPlainObject(parsed)) {
|
|
305
|
+
this.error("IMPORT_MAP_INVALID", "An import map must be a JSON object.", this.elementDetails(element));
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
this.validateImportMapObject(parsed, element);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
validateImportMapObject(value, element) {
|
|
312
|
+
const checkSpecifierMap = (map, label) => {
|
|
313
|
+
if (map === undefined) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (!isPlainObject(map)) {
|
|
317
|
+
this.error("IMPORT_MAP_INVALID", `Import map "${label}" must be a JSON object.`, this.elementDetails(element));
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
for (const [specifier, target] of Object.entries(map)) {
|
|
321
|
+
if (specifier.length === 0) {
|
|
322
|
+
this.error("IMPORT_MAP_INVALID", `Import map "${label}" contains an empty specifier.`, this.elementDetails(element));
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (typeof target !== "string") {
|
|
326
|
+
this.error("IMPORT_MAP_INVALID", `Import map "${label}" target for "${specifier}" must be a string.`, this.elementDetails(element, { value: specifier }));
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (specifier.endsWith("/") && !target.endsWith("/")) {
|
|
330
|
+
this.error("IMPORT_MAP_INVALID", `Import map prefix "${specifier}" must map to a target ending in "/".`, this.elementDetails(element, { value: specifier }));
|
|
331
|
+
}
|
|
332
|
+
if (isReservedImportSpecifier(specifier)) {
|
|
333
|
+
this.error("IMPORT_MAP_INVALID", `Import map specifier "${specifier}" is reserved by Frameyard.`, this.elementDetails(element, { value: specifier, suggestion: "Remove the reserved specifier." }));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
checkSpecifierMap(value.imports, "imports");
|
|
338
|
+
if (value.scopes !== undefined) {
|
|
339
|
+
if (!isPlainObject(value.scopes)) {
|
|
340
|
+
this.error("IMPORT_MAP_INVALID", 'Import map "scopes" must be a JSON object.', this.elementDetails(element));
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
for (const [scope, map] of Object.entries(value.scopes)) {
|
|
344
|
+
if (scope.length === 0) {
|
|
345
|
+
this.error("IMPORT_MAP_INVALID", "Import map scopes contain an empty path.", this.elementDetails(element));
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
checkSpecifierMap(map, `scopes["${scope}"]`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (value.integrity !== undefined) {
|
|
353
|
+
if (!isPlainObject(value.integrity)) {
|
|
354
|
+
this.error("IMPORT_MAP_INVALID", 'Import map "integrity" must be a JSON object.', this.elementDetails(element));
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
for (const [target, integrity] of Object.entries(value.integrity)) {
|
|
358
|
+
if (typeof integrity !== "string") {
|
|
359
|
+
this.error("IMPORT_MAP_INVALID", `Import map integrity for "${target}" must be a string.`, this.elementDetails(element, { value: target }));
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
251
364
|
}
|
|
252
365
|
validateUnsupportedElements() {
|
|
253
|
-
walk(this.
|
|
366
|
+
walk(this.fragment, (node) => {
|
|
254
367
|
if (!isElement(node)) {
|
|
255
368
|
return;
|
|
256
369
|
}
|
|
@@ -272,6 +385,15 @@ class HtmlCompositionValidator {
|
|
|
272
385
|
if (httpEquiv === "refresh" || httpEquiv === "content-security-policy") {
|
|
273
386
|
this.error("ELEMENT_NOT_SUPPORTED", `<meta http-equiv="${httpEquiv}"> is not supported in a composition.`, this.attributeDetails(node, "http-equiv"));
|
|
274
387
|
}
|
|
388
|
+
else if (!this.shellPresent) {
|
|
389
|
+
this.error("ELEMENT_NOT_SUPPORTED", "Document-level <meta> is not supported in a composition fragment; the framework owns the document head.", this.elementDetails(node));
|
|
390
|
+
}
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (tag === "title") {
|
|
394
|
+
if (!this.shellPresent) {
|
|
395
|
+
this.error("ELEMENT_NOT_SUPPORTED", "Document-level <title> is not supported in a composition fragment; the framework owns the document head.", this.elementDetails(node));
|
|
396
|
+
}
|
|
275
397
|
return;
|
|
276
398
|
}
|
|
277
399
|
if (tag === "template" && hasAttribute(node, "shadowrootmode")) {
|
|
@@ -284,7 +406,7 @@ class HtmlCompositionValidator {
|
|
|
284
406
|
});
|
|
285
407
|
}
|
|
286
408
|
validateDocumentAttributes() {
|
|
287
|
-
walk(this.
|
|
409
|
+
walk(this.fragment, (node) => {
|
|
288
410
|
if (!isElement(node)) {
|
|
289
411
|
return;
|
|
290
412
|
}
|
|
@@ -295,37 +417,45 @@ class HtmlCompositionValidator {
|
|
|
295
417
|
if (/^\s*javascript:/i.test(attribute.value)) {
|
|
296
418
|
this.error("URL_NOT_ALLOWED", `"javascript:" URLs are not allowed in a composition.`, this.attributeDetails(node, attribute.name, { value: attribute.value }));
|
|
297
419
|
}
|
|
420
|
+
if (attribute.name === "data-fy-host") {
|
|
421
|
+
this.error("COMPOSITION_INVALID", 'The "data-fy-host" attribute is reserved for framework-injected elements.', this.attributeDetails(node, attribute.name));
|
|
422
|
+
}
|
|
423
|
+
if (attribute.name === "data-fy") {
|
|
424
|
+
if (attribute.value === "format") {
|
|
425
|
+
this.error("COMPOSITION_INVALID", 'data-fy="format" is reserved for framework-injected metadata.', this.attributeDetails(node, attribute.name, { value: attribute.value }));
|
|
426
|
+
}
|
|
427
|
+
else if (attribute.value === "props") {
|
|
428
|
+
if (node.tagName !== "script" || getAttribute(node, "type")?.toLowerCase() !== "application/json") {
|
|
429
|
+
this.error("PROPS_INVALID", 'data-fy="props" must be on a <script type="application/json"> block.', this.attributeDetails(node, attribute.name, { value: attribute.value }));
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
this.error("BINDING_INVALID", "data-fy is only valid on a JSON props block.", this.attributeDetails(node, attribute.name, { value: attribute.value }));
|
|
434
|
+
}
|
|
435
|
+
}
|
|
298
436
|
}
|
|
299
437
|
});
|
|
300
438
|
}
|
|
301
439
|
validateOutsideRoot() {
|
|
302
|
-
const body = getElementByTagName(this.document, "body");
|
|
303
440
|
const root = this.root;
|
|
304
|
-
if (!
|
|
441
|
+
if (!root) {
|
|
305
442
|
return;
|
|
306
443
|
}
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
if (
|
|
313
|
-
if (isDescendantOf(root, child)) {
|
|
314
|
-
inspect(child);
|
|
315
|
-
continue;
|
|
316
|
-
}
|
|
317
|
-
if (NON_VISUAL_ELEMENTS.has(child.tagName)) {
|
|
318
|
-
continue;
|
|
319
|
-
}
|
|
320
|
-
this.warning("CONTENT_OUTSIDE_ROOT", "Visual content outside <fy-composition> is not captured; move it inside the composition root.", this.elementDetails(child));
|
|
444
|
+
for (const child of this.fragment.childNodes) {
|
|
445
|
+
if (child === root) {
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
if (isElement(child)) {
|
|
449
|
+
if (NON_VISUAL_ELEMENTS.has(child.tagName)) {
|
|
321
450
|
continue;
|
|
322
451
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
452
|
+
this.warning("CONTENT_OUTSIDE_ROOT", "Visual content outside <fy-composition> is not captured; move it inside the composition root.", this.elementDetails(child));
|
|
453
|
+
continue;
|
|
326
454
|
}
|
|
327
|
-
|
|
328
|
-
|
|
455
|
+
if (isTextNode(child) && child.value.trim().length > 0) {
|
|
456
|
+
this.warning("CONTENT_OUTSIDE_ROOT", "Text outside <fy-composition> is not captured; move it inside the composition root.", this.nodeDetails(child));
|
|
457
|
+
}
|
|
458
|
+
}
|
|
329
459
|
}
|
|
330
460
|
validateBindings(element, repeatDepth) {
|
|
331
461
|
const repeat = getAttribute(element, "data-fy-repeat");
|
|
@@ -385,12 +515,6 @@ class HtmlCompositionValidator {
|
|
|
385
515
|
if (name === "data-fy-repeat" || name === "data-fy-if" || name === "data-fy-text") {
|
|
386
516
|
continue;
|
|
387
517
|
}
|
|
388
|
-
if (name === "data-fy") {
|
|
389
|
-
if (element.tagName !== "script" || getAttribute(element, "type")?.toLowerCase() !== "application/json") {
|
|
390
|
-
this.error("BINDING_INVALID", "data-fy is only valid on a JSON props block.", this.attributeDetails(element, name));
|
|
391
|
-
}
|
|
392
|
-
continue;
|
|
393
|
-
}
|
|
394
518
|
if (name.startsWith("data-fy-attr-")) {
|
|
395
519
|
this.validateAttributeBinding(element, name, attribute.value, inRepeat);
|
|
396
520
|
continue;
|
|
@@ -526,15 +650,73 @@ class HtmlCompositionValidator {
|
|
|
526
650
|
}
|
|
527
651
|
}
|
|
528
652
|
}
|
|
653
|
+
async validateRemoteFonts() {
|
|
654
|
+
const seen = new Set();
|
|
655
|
+
const report = (url, details) => {
|
|
656
|
+
if (seen.has(url)) {
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
seen.add(url);
|
|
660
|
+
this.warning("FONT_REMOTE", `Remote font or stylesheet URL "${url}" is not deterministic. Vendor the font into the project with "npx frameyard font add --file".`, { ...details, url });
|
|
661
|
+
};
|
|
662
|
+
const scan = (css, details) => {
|
|
663
|
+
const stripped = css.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
664
|
+
for (const match of stripped.matchAll(/@import\s+(?:url\(\s*)?["']?((?:https?:)?\/\/[^"')\s]+)/gi)) {
|
|
665
|
+
const url = match[1];
|
|
666
|
+
if (url !== undefined) {
|
|
667
|
+
report(url, details);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
for (const block of stripped.matchAll(/@font-face\s*\{([^}]*)\}/gi)) {
|
|
671
|
+
const body = block[1] ?? "";
|
|
672
|
+
for (const match of body.matchAll(/url\(\s*["']?((?:https?:)?\/\/[^"')\s]+)/gi)) {
|
|
673
|
+
const url = match[1];
|
|
674
|
+
if (url !== undefined) {
|
|
675
|
+
report(url, details);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
for (const style of findElements(this.fragment, (element) => element.tagName === "style")) {
|
|
681
|
+
scan(getTextContent(style), this.elementDetails(style));
|
|
682
|
+
}
|
|
683
|
+
for (const link of findElements(this.fragment, (element) => element.tagName === "link")) {
|
|
684
|
+
const rel = getAttribute(link, "rel")?.toLowerCase().split(/\s+/) ?? [];
|
|
685
|
+
if (!rel.includes("stylesheet")) {
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
const href = getAttribute(link, "href");
|
|
689
|
+
if (href === undefined) {
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
if (isRemote(href)) {
|
|
693
|
+
report(href, this.attributeDetails(link, "href", { value: href }));
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
if (!isLocalRelative(href)) {
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
const absolute = path.resolve(this.compositionDir, href);
|
|
700
|
+
if (absolute !== this.projectRoot && !absolute.startsWith(this.projectRoot + path.sep)) {
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
try {
|
|
704
|
+
scan(await readFile(absolute, "utf8"), { path: href, element: "link", attribute: "href" });
|
|
705
|
+
}
|
|
706
|
+
catch {
|
|
707
|
+
// Missing local stylesheets are already reported by ordinary asset validation.
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
529
711
|
async validateOrdinaryAssets() {
|
|
530
712
|
const assets = [];
|
|
531
|
-
for (const image of findElements(this.
|
|
713
|
+
for (const image of findElements(this.fragment, (element) => element.tagName === "img")) {
|
|
532
714
|
const src = getAttribute(image, "src");
|
|
533
715
|
if (src !== undefined) {
|
|
534
716
|
assets.push({ element: image, attribute: "src", value: src });
|
|
535
717
|
}
|
|
536
718
|
}
|
|
537
|
-
for (const link of findElements(this.
|
|
719
|
+
for (const link of findElements(this.fragment, (element) => element.tagName === "link")) {
|
|
538
720
|
const rel = getAttribute(link, "rel")?.toLowerCase().split(/\s+/) ?? [];
|
|
539
721
|
if (!rel.includes("stylesheet")) {
|
|
540
722
|
continue;
|
|
@@ -583,6 +765,7 @@ export async function validateHtmlComposition(options) {
|
|
|
583
765
|
if (entryPath !== projectRoot && !entryPath.startsWith(projectRoot + path.sep)) {
|
|
584
766
|
return {
|
|
585
767
|
info: null,
|
|
768
|
+
formatUnresolved: false,
|
|
586
769
|
diagnostics: [
|
|
587
770
|
{
|
|
588
771
|
severity: "error",
|
|
@@ -600,6 +783,7 @@ export async function validateHtmlComposition(options) {
|
|
|
600
783
|
catch {
|
|
601
784
|
return {
|
|
602
785
|
info: null,
|
|
786
|
+
formatUnresolved: false,
|
|
603
787
|
diagnostics: [
|
|
604
788
|
{
|
|
605
789
|
severity: "error",
|
|
@@ -611,12 +795,15 @@ export async function validateHtmlComposition(options) {
|
|
|
611
795
|
};
|
|
612
796
|
}
|
|
613
797
|
let document;
|
|
798
|
+
let fragment;
|
|
614
799
|
try {
|
|
615
800
|
document = parseHtml(html);
|
|
801
|
+
fragment = parseHtmlFragment(html);
|
|
616
802
|
}
|
|
617
803
|
catch (error) {
|
|
618
804
|
return {
|
|
619
805
|
info: null,
|
|
806
|
+
formatUnresolved: false,
|
|
620
807
|
diagnostics: [
|
|
621
808
|
{
|
|
622
809
|
severity: "error",
|
|
@@ -627,9 +814,53 @@ export async function validateHtmlComposition(options) {
|
|
|
627
814
|
],
|
|
628
815
|
};
|
|
629
816
|
}
|
|
630
|
-
const
|
|
817
|
+
const shell = detectDocumentShell(document, options.entry);
|
|
818
|
+
const validator = new HtmlCompositionValidator({ ...options, projectRoot }, fragment, shell);
|
|
631
819
|
return validator.run();
|
|
632
820
|
}
|
|
821
|
+
function detectDocumentShell(document, entry) {
|
|
822
|
+
const diagnostics = [];
|
|
823
|
+
const path = entry.replaceAll("\\", "/");
|
|
824
|
+
const suggestion = "Keep only the fragment body and top-level metadata. See docs/migration-0.8.md.";
|
|
825
|
+
const report = (message, element, location) => {
|
|
826
|
+
diagnostics.push({
|
|
827
|
+
severity: "error",
|
|
828
|
+
code: "COMPOSITION_DOCUMENT_SHELL",
|
|
829
|
+
message,
|
|
830
|
+
details: {
|
|
831
|
+
path,
|
|
832
|
+
element,
|
|
833
|
+
...locationDetails(location),
|
|
834
|
+
suggestion,
|
|
835
|
+
},
|
|
836
|
+
});
|
|
837
|
+
};
|
|
838
|
+
const doctype = getDocumentDoctype(document);
|
|
839
|
+
if (doctype) {
|
|
840
|
+
report("Compositions are HTML fragments in Frameyard 0.8; remove the <!doctype html> declaration.", "!doctype", getNodeLocation(doctype));
|
|
841
|
+
}
|
|
842
|
+
for (const tag of ["html", "head", "body"]) {
|
|
843
|
+
const explicit = findElements(document, (element) => element.tagName === tag && element.sourceCodeLocation?.startTag !== undefined)[0];
|
|
844
|
+
if (explicit) {
|
|
845
|
+
report(`Compositions are HTML fragments in Frameyard 0.8; remove the <${tag}> wrapper.`, tag, getElementLocation(explicit));
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
return { present: diagnostics.length > 0, diagnostics };
|
|
849
|
+
}
|
|
850
|
+
function locationDetails(location) {
|
|
851
|
+
if (!location) {
|
|
852
|
+
return {};
|
|
853
|
+
}
|
|
854
|
+
return {
|
|
855
|
+
line: location.startLine,
|
|
856
|
+
column: location.startCol,
|
|
857
|
+
endLine: location.endLine,
|
|
858
|
+
endColumn: location.endCol,
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
function isReservedImportSpecifier(specifier) {
|
|
862
|
+
return specifier === "frameyard" || specifier.startsWith("frameyard/");
|
|
863
|
+
}
|
|
633
864
|
function describeValue(value) {
|
|
634
865
|
if (value === null) {
|
|
635
866
|
return "null";
|