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.
Files changed (334) hide show
  1. package/CHANGELOG.md +164 -7
  2. package/GLOSSARY.md +152 -18
  3. package/README.md +328 -285
  4. package/dist/assets/integrations.d.ts +8 -4
  5. package/dist/assets/integrations.d.ts.map +1 -1
  6. package/dist/assets/integrations.js +8 -4
  7. package/dist/assets/integrations.js.map +1 -1
  8. package/dist/assets/provenance.d.ts +2 -2
  9. package/dist/assets/provenance.d.ts.map +1 -1
  10. package/dist/assets/provenance.js +5 -3
  11. package/dist/assets/provenance.js.map +1 -1
  12. package/dist/assets/tts-supertonic.d.ts +28 -0
  13. package/dist/assets/tts-supertonic.d.ts.map +1 -0
  14. package/dist/assets/tts-supertonic.js +156 -0
  15. package/dist/assets/tts-supertonic.js.map +1 -0
  16. package/dist/assets/tts.d.ts +18 -6
  17. package/dist/assets/tts.d.ts.map +1 -1
  18. package/dist/assets/tts.js +219 -97
  19. package/dist/assets/tts.js.map +1 -1
  20. package/dist/cli.js +1705 -106
  21. package/dist/cli.js.map +1 -1
  22. package/dist/composition/descriptor.d.ts +17 -3
  23. package/dist/composition/descriptor.d.ts.map +1 -1
  24. package/dist/composition/descriptor.js +51 -28
  25. package/dist/composition/descriptor.js.map +1 -1
  26. package/dist/composition/format.d.ts +12 -0
  27. package/dist/composition/format.d.ts.map +1 -0
  28. package/dist/composition/format.js +28 -0
  29. package/dist/composition/format.js.map +1 -0
  30. package/dist/composition/index.d.ts +2 -1
  31. package/dist/composition/index.d.ts.map +1 -1
  32. package/dist/composition/index.js +1 -0
  33. package/dist/composition/index.js.map +1 -1
  34. package/dist/context.d.ts +82 -0
  35. package/dist/context.d.ts.map +1 -0
  36. package/dist/context.js +277 -0
  37. package/dist/context.js.map +1 -0
  38. package/dist/contracts.d.ts +80 -6
  39. package/dist/contracts.d.ts.map +1 -1
  40. package/dist/contracts.js +3 -0
  41. package/dist/contracts.js.map +1 -1
  42. package/dist/doctor.d.ts +14 -1
  43. package/dist/doctor.d.ts.map +1 -1
  44. package/dist/doctor.js +166 -7
  45. package/dist/doctor.js.map +1 -1
  46. package/dist/errors.d.ts +1 -1
  47. package/dist/errors.d.ts.map +1 -1
  48. package/dist/errors.js.map +1 -1
  49. package/dist/fonts/add.d.ts +41 -0
  50. package/dist/fonts/add.d.ts.map +1 -0
  51. package/dist/fonts/add.js +259 -0
  52. package/dist/fonts/add.js.map +1 -0
  53. package/dist/fonts/binary.d.ts +4 -0
  54. package/dist/fonts/binary.d.ts.map +1 -0
  55. package/dist/fonts/binary.js +21 -0
  56. package/dist/fonts/binary.js.map +1 -0
  57. package/dist/fonts/css.d.ts +4 -0
  58. package/dist/fonts/css.d.ts.map +1 -0
  59. package/dist/fonts/css.js +50 -0
  60. package/dist/fonts/css.js.map +1 -0
  61. package/dist/fonts/fontsource.d.ts +107 -0
  62. package/dist/fonts/fontsource.d.ts.map +1 -0
  63. package/dist/fonts/fontsource.js +565 -0
  64. package/dist/fonts/fontsource.js.map +1 -0
  65. package/dist/fonts/index.d.ts +11 -0
  66. package/dist/fonts/index.d.ts.map +1 -0
  67. package/dist/fonts/index.js +11 -0
  68. package/dist/fonts/index.js.map +1 -0
  69. package/dist/fonts/install.d.ts +24 -0
  70. package/dist/fonts/install.d.ts.map +1 -0
  71. package/dist/fonts/install.js +145 -0
  72. package/dist/fonts/install.js.map +1 -0
  73. package/dist/fonts/manifest.d.ts +21 -0
  74. package/dist/fonts/manifest.d.ts.map +1 -0
  75. package/dist/fonts/manifest.js +96 -0
  76. package/dist/fonts/manifest.js.map +1 -0
  77. package/dist/fonts/options.d.ts +9 -0
  78. package/dist/fonts/options.d.ts.map +1 -0
  79. package/dist/fonts/options.js +66 -0
  80. package/dist/fonts/options.js.map +1 -0
  81. package/dist/fonts/schema.d.ts +4 -0
  82. package/dist/fonts/schema.d.ts.map +1 -0
  83. package/dist/fonts/schema.js +316 -0
  84. package/dist/fonts/schema.js.map +1 -0
  85. package/dist/fonts/summary.d.ts +16 -0
  86. package/dist/fonts/summary.d.ts.map +1 -0
  87. package/dist/fonts/summary.js +22 -0
  88. package/dist/fonts/summary.js.map +1 -0
  89. package/dist/fonts/types.d.ts +56 -0
  90. package/dist/fonts/types.d.ts.map +1 -0
  91. package/dist/fonts/types.js +8 -0
  92. package/dist/fonts/types.js.map +1 -0
  93. package/dist/fonts/verify.d.ts +33 -0
  94. package/dist/fonts/verify.d.ts.map +1 -0
  95. package/dist/fonts/verify.js +200 -0
  96. package/dist/fonts/verify.js.map +1 -0
  97. package/dist/html/document-adapter.d.ts +3 -3
  98. package/dist/html/document-adapter.d.ts.map +1 -1
  99. package/dist/html/document-adapter.js +1 -1
  100. package/dist/html/document-adapter.js.map +1 -1
  101. package/dist/html/index.d.ts +1 -1
  102. package/dist/html/index.d.ts.map +1 -1
  103. package/dist/html/parse.d.ts +10 -2
  104. package/dist/html/parse.d.ts.map +1 -1
  105. package/dist/html/parse.js +14 -5
  106. package/dist/html/parse.js.map +1 -1
  107. package/dist/html/validate.d.ts +12 -1
  108. package/dist/html/validate.d.ts.map +1 -1
  109. package/dist/html/validate.js +292 -61
  110. package/dist/html/validate.js.map +1 -1
  111. package/dist/index.d.ts +4 -4
  112. package/dist/index.d.ts.map +1 -1
  113. package/dist/index.js +2 -2
  114. package/dist/index.js.map +1 -1
  115. package/dist/inspect.d.ts +61 -0
  116. package/dist/inspect.d.ts.map +1 -0
  117. package/dist/inspect.js +325 -0
  118. package/dist/inspect.js.map +1 -0
  119. package/dist/lint.d.ts +63 -0
  120. package/dist/lint.d.ts.map +1 -0
  121. package/dist/lint.js +265 -0
  122. package/dist/lint.js.map +1 -0
  123. package/dist/policy.d.ts +69 -0
  124. package/dist/policy.d.ts.map +1 -0
  125. package/dist/policy.js +361 -0
  126. package/dist/policy.js.map +1 -0
  127. package/dist/preview/main.d.ts +2 -2
  128. package/dist/preview/main.d.ts.map +1 -1
  129. package/dist/preview/main.js +35 -14
  130. package/dist/preview/main.js.map +1 -1
  131. package/dist/preview/timeline-data.d.ts +17 -65
  132. package/dist/preview/timeline-data.d.ts.map +1 -1
  133. package/dist/preview/timeline-host.d.ts.map +1 -1
  134. package/dist/preview/timeline-host.js +73 -38
  135. package/dist/preview/timeline-host.js.map +1 -1
  136. package/dist/preview/timeline-main.d.ts.map +1 -1
  137. package/dist/preview/timeline-main.js +47 -3
  138. package/dist/preview/timeline-main.js.map +1 -1
  139. package/dist/project.d.ts +13 -8
  140. package/dist/project.d.ts.map +1 -1
  141. package/dist/project.js +20 -17
  142. package/dist/project.js.map +1 -1
  143. package/dist/renderer/browser.d.ts +2 -2
  144. package/dist/renderer/browser.d.ts.map +1 -1
  145. package/dist/renderer/browser.js +10 -4
  146. package/dist/renderer/browser.js.map +1 -1
  147. package/dist/renderer/ffmpeg.d.ts +44 -1
  148. package/dist/renderer/ffmpeg.d.ts.map +1 -1
  149. package/dist/renderer/ffmpeg.js +371 -73
  150. package/dist/renderer/ffmpeg.js.map +1 -1
  151. package/dist/renderer/index.d.ts +40 -4
  152. package/dist/renderer/index.d.ts.map +1 -1
  153. package/dist/renderer/index.js +100 -25
  154. package/dist/renderer/index.js.map +1 -1
  155. package/dist/renderer/keyframe-expressions.d.ts +39 -0
  156. package/dist/renderer/keyframe-expressions.d.ts.map +1 -0
  157. package/dist/renderer/keyframe-expressions.js +180 -0
  158. package/dist/renderer/keyframe-expressions.js.map +1 -0
  159. package/dist/renderer/timeline.d.ts +2 -0
  160. package/dist/renderer/timeline.d.ts.map +1 -1
  161. package/dist/renderer/timeline.js +168 -106
  162. package/dist/renderer/timeline.js.map +1 -1
  163. package/dist/runtime/host.d.ts.map +1 -1
  164. package/dist/runtime/host.js +159 -31
  165. package/dist/runtime/host.js.map +1 -1
  166. package/dist/scaffold.d.ts +3 -0
  167. package/dist/scaffold.d.ts.map +1 -1
  168. package/dist/scaffold.js +48 -317
  169. package/dist/scaffold.js.map +1 -1
  170. package/dist/server/composition-html.d.ts +15 -7
  171. package/dist/server/composition-html.d.ts.map +1 -1
  172. package/dist/server/composition-html.js +72 -16
  173. package/dist/server/composition-html.js.map +1 -1
  174. package/dist/server/server.d.ts.map +1 -1
  175. package/dist/server/server.js +56 -6
  176. package/dist/server/server.js.map +1 -1
  177. package/dist/server/timeline-preview-data.d.ts.map +1 -1
  178. package/dist/server/timeline-preview-data.js +12 -23
  179. package/dist/server/timeline-preview-data.js.map +1 -1
  180. package/dist/server/timeline-preview-html.d.ts.map +1 -1
  181. package/dist/server/timeline-preview-html.js +20 -3
  182. package/dist/server/timeline-preview-html.js.map +1 -1
  183. package/dist/templates.d.ts +16 -0
  184. package/dist/templates.d.ts.map +1 -0
  185. package/dist/templates.js +186 -0
  186. package/dist/templates.js.map +1 -0
  187. package/dist/timeline/easing.d.ts +36 -0
  188. package/dist/timeline/easing.d.ts.map +1 -0
  189. package/dist/timeline/easing.js +225 -0
  190. package/dist/timeline/easing.js.map +1 -0
  191. package/dist/timeline/keyframes.d.ts +23 -0
  192. package/dist/timeline/keyframes.d.ts.map +1 -0
  193. package/dist/timeline/keyframes.js +95 -0
  194. package/dist/timeline/keyframes.js.map +1 -0
  195. package/dist/timeline/plan.d.ts +6 -12
  196. package/dist/timeline/plan.d.ts.map +1 -1
  197. package/dist/timeline/plan.js +17 -30
  198. package/dist/timeline/plan.js.map +1 -1
  199. package/dist/timeline/resolve.d.ts +16 -3
  200. package/dist/timeline/resolve.d.ts.map +1 -1
  201. package/dist/timeline/resolve.js +119 -13
  202. package/dist/timeline/resolve.js.map +1 -1
  203. package/dist/timeline/scene.d.ts +126 -0
  204. package/dist/timeline/scene.d.ts.map +1 -0
  205. package/dist/timeline/scene.js +109 -0
  206. package/dist/timeline/scene.js.map +1 -0
  207. package/dist/timeline/schema.d.ts +38 -0
  208. package/dist/timeline/schema.d.ts.map +1 -1
  209. package/dist/timeline/schema.js +290 -9
  210. package/dist/timeline/schema.js.map +1 -1
  211. package/dist/timeline/transitions.d.ts +21 -0
  212. package/dist/timeline/transitions.d.ts.map +1 -0
  213. package/dist/timeline/transitions.js +23 -0
  214. package/dist/timeline/transitions.js.map +1 -0
  215. package/dist/version.d.ts +1 -1
  216. package/dist/version.js +1 -1
  217. package/docs/adr/0007-make-html-documents-the-composition-unit.md +2 -2
  218. package/docs/adr/0008-share-the-composition-document-grammar.md +1 -1
  219. package/docs/adr/0010-make-timelines-the-render-unit.md +3 -2
  220. package/docs/adr/0011-compose-timeline-frames-in-a-live-browser-host.md +1 -1
  221. package/docs/adr/0012-render-timelines-with-segmented-lossless-clip-intermediates.md +1 -1
  222. package/docs/adr/0014-compose-timeline-clips-with-css-blend-modes.md +38 -0
  223. package/docs/adr/0015-inherit-composition-format-from-the-project.md +28 -0
  224. package/docs/adr/0016-author-compositions-as-html-fragments.md +32 -0
  225. package/docs/adr/0017-render-timeless-compositions-as-stills.md +99 -0
  226. package/docs/adr/0018-animate-timeline-clips-with-keyframes.md +35 -0
  227. package/docs/adr/0019-make-same-track-overlap-the-transition-window.md +36 -0
  228. package/docs/adr/0020-crossfade-audio-clips-on-audio-tracks.md +27 -0
  229. package/docs/adr/0021-project-timeline-scenes-once.md +32 -0
  230. package/docs/adr/0022-gate-heavy-operations-with-a-workspace-resource-policy.md +35 -0
  231. package/docs/adr/0023-pin-fonts-as-project-assets.md +98 -0
  232. package/docs/migration-0.4.md +5 -3
  233. package/docs/migration-0.8.md +163 -0
  234. package/docs/preview-testing.md +23 -3
  235. package/docs/{v0.1-design.md → v0.x-design.md} +86 -53
  236. package/examples/blend/README.md +106 -0
  237. package/examples/blend/assets/ink.mp4 +0 -0
  238. package/examples/blend/compositions/aurora.html +55 -0
  239. package/examples/blend/compositions/backdrop.html +74 -0
  240. package/examples/blend/compositions/labels.html +149 -0
  241. package/examples/blend/fy.config.js +11 -0
  242. package/examples/blend/scripts/make-ink.sh +26 -0
  243. package/examples/blend/timelines/blend.timeline.json +155 -0
  244. package/examples/demo/README.md +2 -2
  245. package/examples/demo/compositions/demo.html +243 -251
  246. package/examples/intro/README.md +71 -0
  247. package/examples/intro/assets/fonts/figtree/LICENSE.txt +93 -0
  248. package/examples/intro/assets/fonts/figtree/figtree-latin-wght-normal.woff2 +0 -0
  249. package/examples/intro/assets/fonts/instrument-serif/LICENSE.txt +93 -0
  250. package/examples/intro/assets/fonts/instrument-serif/instrument-serif-latin-400-italic.woff2 +0 -0
  251. package/examples/intro/assets/fonts/space-mono/LICENSE.txt +93 -0
  252. package/examples/intro/assets/fonts/space-mono/space-mono-latin-400-normal.woff2 +0 -0
  253. package/examples/intro/assets/fonts/space-mono/space-mono-latin-700-normal.woff2 +0 -0
  254. package/examples/intro/assets/grain.png +0 -0
  255. package/examples/intro/assets/score.ogg +0 -0
  256. package/examples/intro/compositions/brand.html +412 -0
  257. package/examples/intro/compositions/chrome.controller.js +63 -0
  258. package/examples/intro/compositions/chrome.html +256 -0
  259. package/examples/intro/compositions/lattice.controller.js +148 -0
  260. package/examples/intro/compositions/lattice.html +308 -0
  261. package/examples/intro/compositions/opening.html +504 -0
  262. package/examples/intro/compositions/studio.html +930 -0
  263. package/examples/intro/compositions/theme.css +218 -0
  264. package/examples/intro/fy.config.js +13 -0
  265. package/examples/intro/fy.fonts.json +100 -0
  266. package/examples/intro/scripts/make-grain.mjs +79 -0
  267. package/examples/intro/scripts/make-score.mjs +702 -0
  268. package/examples/intro/timelines/intro.timeline.json +60 -0
  269. package/examples/keyframes/README.md +76 -0
  270. package/examples/keyframes/compositions/backdrop.html +60 -0
  271. package/examples/keyframes/compositions/card.html +99 -0
  272. package/examples/keyframes/fy.config.js +10 -0
  273. package/examples/keyframes/timelines/keyframes.timeline.json +60 -0
  274. package/examples/nested/README.md +112 -0
  275. package/examples/nested/assets/pulse.wav +0 -0
  276. package/examples/nested/compositions/backdrop.html +84 -0
  277. package/examples/nested/compositions/card.html +275 -0
  278. package/examples/nested/compositions/overlay.html +307 -0
  279. package/examples/nested/fy.config.js +13 -0
  280. package/examples/nested/scripts/make-pulse.sh +20 -0
  281. package/examples/nested/timelines/chapter-a.timeline.json +39 -0
  282. package/examples/nested/timelines/chapter-b.timeline.json +39 -0
  283. package/examples/nested/timelines/nested-reel.timeline.json +52 -0
  284. package/examples/static/README.md +17 -0
  285. package/examples/static/compositions/still.html +37 -0
  286. package/examples/static/fy.config.js +9 -0
  287. package/examples/static/timelines/still.timeline.json +13 -0
  288. package/examples/transitions/README.md +128 -0
  289. package/examples/transitions/assets/alpha.wav +0 -0
  290. package/examples/transitions/assets/beta.wav +0 -0
  291. package/examples/transitions/assets/gamma.wav +0 -0
  292. package/examples/transitions/compositions/alpha.html +56 -0
  293. package/examples/transitions/compositions/beta.html +56 -0
  294. package/examples/transitions/compositions/gamma.html +56 -0
  295. package/examples/transitions/fy.config.js +12 -0
  296. package/examples/transitions/timelines/audio-crossfade.timeline.json +44 -0
  297. package/examples/transitions/timelines/transitions.timeline.json +31 -0
  298. package/examples/video/README.md +4 -4
  299. package/examples/video/compositions/video-mp4.html +349 -357
  300. package/examples/video/compositions/video-overlay.html +337 -345
  301. package/examples/video/compositions/video.html +349 -357
  302. package/examples/webgl/README.md +1 -1
  303. package/examples/webgl/compositions/webgl.html +205 -213
  304. package/package.json +13 -5
  305. package/scaffolds/init/AGENTS.md +131 -0
  306. package/scaffolds/init/README.md +56 -0
  307. package/scaffolds/init/compositions/hello.html +79 -0
  308. package/scaffolds/init/fy.config.js +9 -0
  309. package/scaffolds/init/gitignore.template +1 -0
  310. package/scaffolds/init/package.json +11 -0
  311. package/scaffolds/init/props/hello.json +4 -0
  312. package/scaffolds/init/props/main.json +6 -0
  313. package/scaffolds/init/timelines/main.timeline.json +20 -0
  314. package/scaffolds/local-models/LOCAL-MODELS.md +77 -0
  315. package/scaffolds/local-models/fy.policy.json +5 -0
  316. package/scaffolds/local-models/opencode/opencode.jsonc +17 -0
  317. package/scaffolds/local-models/pi/APPEND_SYSTEM.md +21 -0
  318. package/scaffolds/local-models/pi/sandbox.json +15 -0
  319. package/scaffolds/local-models/pi/settings.json +15 -0
  320. package/skills/frameyard/README.md +1 -0
  321. package/skills/frameyard/SKILL.md +59 -59
  322. package/skills/frameyard/references/audio-assets.md +32 -3
  323. package/skills/frameyard/references/common-mistakes.md +81 -7
  324. package/skills/frameyard/references/controllers.md +5 -4
  325. package/skills/frameyard/references/deterministic-authoring.md +49 -37
  326. package/skills/frameyard/references/diagnostics.md +42 -4
  327. package/skills/frameyard/references/failure-recovery.md +56 -10
  328. package/skills/frameyard/references/fonts.md +132 -0
  329. package/skills/frameyard/references/json-results.md +165 -10
  330. package/skills/frameyard/references/quick-reference.md +222 -0
  331. package/skills/frameyard/references/timelines.md +109 -15
  332. /package/examples/demo/{video.config.js → fy.config.js} +0 -0
  333. /package/examples/video/{video.config.js → fy.config.js} +0 -0
  334. /package/examples/webgl/{video.config.js → fy.config.js} +0 -0
@@ -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, getElementByTagName, getElementLocation, getNodeLocation, hasAttribute, isDescendantOf, isElement, isTemplate, isTextNode, parseHtml, walk, } from "./parse.js";
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
- document;
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, document) {
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.document = document;
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.validateDoctype();
57
- const adapter = createParse5Adapter(this.document);
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
- this.metadata = read.descriptor.metadata;
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 && root && this.fixedFactsValid && this.overridesValid && this.controllerFileValid && this.documentValid
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.document, (candidate) => candidate.tagName === "script")) {
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.document, (node) => {
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.document, (node) => {
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 (!body || !root) {
441
+ if (!root) {
305
442
  return;
306
443
  }
307
- const inspect = (parent) => {
308
- for (const child of parent.childNodes) {
309
- if (child === root) {
310
- continue;
311
- }
312
- if (isElement(child)) {
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
- if (isTextNode(child) && child.value.trim().length > 0) {
324
- this.warning("CONTENT_OUTSIDE_ROOT", "Text outside <fy-composition> is not captured; move it inside the composition root.", this.nodeDetails(child));
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
- inspect(body);
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.document, (element) => element.tagName === "img")) {
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.document, (element) => element.tagName === "link")) {
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 validator = new HtmlCompositionValidator({ ...options, projectRoot }, document);
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";