@agent-native/core-corpus 0.217.0 → 0.219.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 (81) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/hubspot/SKILL.md +5 -3
  3. package/corpus/templates/analytics/CHANGELOG.md +2 -0
  4. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +38 -167
  5. package/corpus/templates/analytics/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
  6. package/corpus/templates/assets/CHANGELOG.md +6 -0
  7. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +51 -117
  8. package/corpus/templates/assets/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
  9. package/corpus/templates/brain/app/components/layout/Sidebar.tsx +34 -101
  10. package/corpus/templates/brain/app/routes/sources.tsx +1 -1
  11. package/corpus/templates/brain/vitest.config.ts +7 -0
  12. package/corpus/templates/calendar/CHANGELOG.md +6 -0
  13. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +1 -1
  14. package/corpus/templates/calendar/changelog/2026-10-16-booking-links-show-localized-visibility-badges.md +6 -0
  15. package/corpus/templates/chat/app/components/layout/Sidebar.tsx +62 -101
  16. package/corpus/templates/clips/CHANGELOG.md +7 -20
  17. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +20 -77
  18. package/corpus/templates/clips/app/components/player/share-dialog.tsx +63 -90
  19. package/corpus/templates/clips/app/components/sharing/share-test-support.tsx +180 -0
  20. package/corpus/templates/clips/app/components/sharing/share-ui.tsx +5 -584
  21. package/corpus/templates/clips/changelog/2026-10-16-share-dialogs-make-it-easier-to-see-who-has-access-and-manag.md +6 -0
  22. package/corpus/templates/clips/changelog/2026-10-21-share-panels-describe-general-access-in-a-sentence.md +6 -0
  23. package/corpus/templates/community/qr/package.json +1 -1
  24. package/corpus/templates/content/CHANGELOG.md +7 -0
  25. package/corpus/templates/content/app/components/editor/CommentStack.tsx +301 -0
  26. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +770 -179
  27. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +75 -27
  28. package/corpus/templates/content/app/components/editor/SuggestionText.tsx +18 -3
  29. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +89 -22
  30. package/corpus/templates/content/app/components/editor/block-field-comment-threads.ts +15 -0
  31. package/corpus/templates/content/app/components/editor/block-field-comments.tsx +5 -9
  32. package/corpus/templates/content/app/components/editor/comment-stacks.ts +143 -0
  33. package/corpus/templates/content/app/components/editor/extensions/SuggestionHighlight.ts +383 -34
  34. package/corpus/templates/content/app/components/editor/suggestions/draft-session.ts +31 -0
  35. package/corpus/templates/content/app/components/editor/suggestions/presentation-rebase.ts +82 -4
  36. package/corpus/templates/content/app/global.css +31 -0
  37. package/corpus/templates/content/app/i18n-data.ts +8 -0
  38. package/corpus/templates/content/changelog/2026-10-16-accepting-a-suggestion-on-a-page-that-changed-since-it-was-s.md +6 -0
  39. package/corpus/templates/content/changelog/2026-10-20-suggested-edits-that-add-remove-split-or-merge-paragraphs-li.md +6 -0
  40. package/corpus/templates/content/changelog/2026-10-22-blocks-with-several-comment-threads-fold-into-one-stack-in-t.md +6 -0
  41. package/corpus/templates/content/changelog/2026-10-22-opening-a-page-no-longer-saves-an-edit-again-after-another-o.md +6 -0
  42. package/corpus/templates/content/docs/product/capabilities/content.comment.page-owned.md +8 -1
  43. package/corpus/templates/content/shared/suggestion-diff.ts +23 -6
  44. package/corpus/templates/content/shared/suggestion-formatting.ts +44 -14
  45. package/corpus/templates/content/shared/suggestion-text.ts +20 -5
  46. package/corpus/templates/design/.generated/bridge/interact-links.generated.ts +8 -8
  47. package/corpus/templates/design/CHANGELOG.md +8 -0
  48. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +18 -1
  49. package/corpus/templates/design/app/components/design/bridge/interact-links.bridge.ts +8 -8
  50. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +15 -13
  51. package/corpus/templates/design/app/i18n/en-US.ts +1 -1
  52. package/corpus/templates/design/app/pages/DesignSystems.tsx +1 -1
  53. package/corpus/templates/design/app/pages/Index.tsx +18 -64
  54. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-modes.ts +15 -6
  55. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-screen-rendering.tsx +29 -5
  56. package/corpus/templates/design/app/pages/design-editor/prototype-navigation-transition.ts +423 -0
  57. package/corpus/templates/design/app/pages/design-editor/view/single-screen-canvas.tsx +35 -9
  58. package/corpus/templates/design/changelog/2026-10-16-design-pages-use-localized-visibility-badges-and-a-shared-ow.md +6 -0
  59. package/corpus/templates/design/changelog/2026-10-20-the-design-full-app-toggle-on-the-home-page-no-longer-hides-.md +6 -0
  60. package/corpus/templates/design/changelog/2026-10-22-prototype-links-can-play-code-authored-screen-transitions.md +6 -0
  61. package/corpus/templates/design/shared/motion-authored-css-import.ts +938 -7
  62. package/corpus/templates/design/shared/motion-html-source.ts +1689 -135
  63. package/corpus/templates/design/shared/motion-prototype-transition.ts +329 -0
  64. package/corpus/templates/design/shared/motion-react-source.ts +2154 -126
  65. package/corpus/templates/design/shared/motion-text-runtime-v1.fixture.txt +221 -0
  66. package/corpus/templates/dispatch/CHANGELOG.md +4 -0
  67. package/corpus/templates/dispatch/app/i18n-data.ts +75 -0
  68. package/corpus/templates/dispatch/changelog/2026-10-21-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
  69. package/corpus/templates/factory/app/components/layout/Sidebar.tsx +34 -100
  70. package/corpus/templates/forms/app/pages/FormsListPage.tsx +1 -1
  71. package/corpus/templates/plan/CHANGELOG.md +6 -0
  72. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +34 -109
  73. package/corpus/templates/plan/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
  74. package/corpus/templates/slides/CHANGELOG.md +1 -0
  75. package/corpus/templates/slides/app/components/deck/DeckCard.tsx +1 -1
  76. package/corpus/templates/slides/app/components/design-system/DesignSystemCard.tsx +1 -1
  77. package/corpus/templates/slides/app/pages/Index.tsx +7 -2
  78. package/corpus/templates/slides/changelog/2026-10-16-deck-lists-use-the-shared-ownership-filter-and-visibility-ba.md +6 -0
  79. package/package.json +1 -1
  80. package/corpus/templates/chat/app/lib/sidebar-thread-state.ts +0 -29
  81. package/corpus/templates/slides/app/components/deck/DeckFilterMenu.tsx +0 -83
@@ -33,15 +33,18 @@ import {
33
33
  import {
34
34
  decodeMotionTextDocument,
35
35
  injectMotionTextRuntime,
36
+ MOTION_TEXT_RUNTIME_SOURCE,
36
37
  MOTION_TEXT_RUNTIME_ATTRIBUTE,
37
38
  MOTION_TEXT_SOURCE_ATTRIBUTE,
38
39
  MOTION_TEXT_SOURCE_VERSION,
40
+ MOTION_TEXT_SOURCE_VERSION_PROVENANCE,
39
41
  readMotionTextDocument,
40
42
  removeMotionTextRuntime,
41
43
  type MotionTextDocument,
42
44
  } from "./motion-text-code";
43
45
  import {
44
46
  MOTION_DEFAULT_PLAYBACK_MODE,
47
+ parseMotionPlaybackMode,
45
48
  readTimelinePlaybackMode,
46
49
  type MotionPlaybackMode,
47
50
  type MotionTimeline,
@@ -50,6 +53,8 @@ import {
50
53
  const NODE_ID_ATTRIBUTE = "data-agent-native-node-id";
51
54
  const MANAGED_STYLE_ATTRIBUTE = "data-agent-native-motion";
52
55
  const SAFE_NODE_ID = /^[A-Za-z0-9_-]{1,512}$/;
56
+ const MOTION_TEXT_V1_RUNTIME_SHA256 =
57
+ "ba4c65a0893f8c551897d6ede05f9a2167d70832a7c8050b152859fb1828addb";
53
58
 
54
59
  const FATAL_HTML_PARSE_ERRORS = new Set([
55
60
  "control-character-in-input-stream",
@@ -67,6 +72,7 @@ type HtmlNode = {
67
72
  tagName?: string;
68
73
  value?: string;
69
74
  attrs?: Array<{ name: string; value: string }>;
75
+ parentNode?: HtmlNode;
70
76
  childNodes?: HtmlNode[];
71
77
  content?: HtmlNode;
72
78
  insideTemplate?: boolean;
@@ -188,20 +194,23 @@ function parseHtml(source: string): ParsedHtml {
188
194
  }
189
195
 
190
196
  const nodes: HtmlNode[] = [];
191
- const pending: Array<{ node: HtmlNode; insideTemplate: boolean }> = [
192
- { node: document, insideTemplate: false },
193
- ];
197
+ const pending: Array<{
198
+ node: HtmlNode;
199
+ insideTemplate: boolean;
200
+ parent?: HtmlNode;
201
+ }> = [{ node: document, insideTemplate: false }];
194
202
  while (pending.length > 0) {
195
- const { node, insideTemplate } = pending.pop()!;
203
+ const { node, insideTemplate, parent } = pending.pop()!;
204
+ if (parent) node.parentNode = parent;
196
205
  node.insideTemplate = insideTemplate;
197
206
  nodes.push(node);
198
207
  pending.push(
199
208
  ...(node.childNodes ?? [])
200
209
  .reverse()
201
- .map((child) => ({ node: child, insideTemplate })),
210
+ .map((child) => ({ node: child, insideTemplate, parent: node })),
202
211
  );
203
212
  if (node.tagName === "template" && node.content) {
204
- pending.push({ node: node.content, insideTemplate: true });
213
+ pending.push({ node: node.content, insideTemplate: true, parent: node });
205
214
  }
206
215
  }
207
216
  return { nodes };
@@ -275,117 +284,1439 @@ function findManagedStyle(
275
284
  "Managed Motion style elements cannot live inside an HTML template.",
276
285
  );
277
286
  }
278
- if (marked.length > 1) {
279
- sourceError(
280
- "ambiguous-managed-style",
281
- "HTML contains more than one managed Motion style element.",
282
- );
287
+ if (marked.length > 1) {
288
+ sourceError(
289
+ "ambiguous-managed-style",
290
+ "HTML contains more than one managed Motion style element.",
291
+ );
292
+ }
293
+ const node = marked[0];
294
+ if (!node) return null;
295
+ const range = assertSourceRanges(source, node);
296
+ return { node, css: textContent(node), range };
297
+ }
298
+
299
+ function motionTextError(error: Error): MotionHtmlSourceError {
300
+ return new MotionHtmlSourceError("invalid-motion-text", error.message);
301
+ }
302
+
303
+ function readMotionText(source: string): MotionTextDocument | null {
304
+ const read = readMotionTextDocument(source);
305
+ if (read.status === "invalid") throw motionTextError(read.error);
306
+ return read.status === "valid" ? read.document : null;
307
+ }
308
+
309
+ function assertMotionTextOutsideTemplates(nodes: HtmlNode[]): void {
310
+ if (
311
+ nodes.some(
312
+ (node) =>
313
+ node.insideTemplate &&
314
+ node.tagName === "script" &&
315
+ (attributes(node, MOTION_TEXT_SOURCE_ATTRIBUTE).length > 0 ||
316
+ attributes(node, MOTION_TEXT_RUNTIME_ATTRIBUTE).length > 0),
317
+ )
318
+ ) {
319
+ sourceError(
320
+ "invalid-motion-text",
321
+ "Managed Motion Text scripts cannot live inside an HTML template.",
322
+ );
323
+ }
324
+ }
325
+
326
+ function targetsFromDocuments(
327
+ document: MotionCodeDocument | null,
328
+ textDocument: MotionTextDocument | null,
329
+ ): string[] {
330
+ return [
331
+ ...new Set([
332
+ ...(document?.tracks.map((track) => track.targetNodeId) ?? []),
333
+ ...(document?.nativeStyles?.map((style) => style.targetNodeId) ?? []),
334
+ ...(textDocument?.animations.map((animation) => animation.targetNodeId) ??
335
+ []),
336
+ ]),
337
+ ];
338
+ }
339
+
340
+ function pathTrimTargetsFromDocument(
341
+ document: MotionCodeDocument | null,
342
+ ): string[] {
343
+ if (!document) return [];
344
+ return [
345
+ ...new Set([
346
+ ...document.tracks
347
+ .filter((track) => track.property === "path-trim")
348
+ .map((track) => track.targetNodeId),
349
+ ...(document.nativeStyles ?? [])
350
+ .filter((style) => style.enabled !== false && style.styleKey === "Path")
351
+ .map((style) => style.targetNodeId),
352
+ ]),
353
+ ];
354
+ }
355
+
356
+ function pathTrimTargetsFromTimeline(timeline: MotionTimeline): string[] {
357
+ return [
358
+ ...new Set([
359
+ ...timeline.tracks
360
+ .filter((track) => track.property === "path-trim")
361
+ .map((track) => track.targetNodeId),
362
+ ...(timeline.nativeStyles ?? [])
363
+ .filter((style) => style.enabled !== false && style.styleKey === "Path")
364
+ .map((style) => style.targetNodeId),
365
+ ]),
366
+ ];
367
+ }
368
+
369
+ function applyPathTrimTargets(source: string, targetNodeIds: string[]): string {
370
+ try {
371
+ return applyMotionPathTrimSource(source, targetNodeIds, "html");
372
+ } catch (error) {
373
+ if (error instanceof MotionPathTrimSourceError) {
374
+ sourceError("invalid-path-trim", error.message);
375
+ }
376
+ throw error;
377
+ }
378
+ }
379
+
380
+ function restorePathTrimTargets(source: string): string {
381
+ try {
382
+ return restoreMotionPathTrimSource(source, "html");
383
+ } catch (error) {
384
+ if (error instanceof MotionPathTrimSourceError) {
385
+ sourceError("invalid-path-trim", error.message);
386
+ }
387
+ throw error;
388
+ }
389
+ }
390
+
391
+ function assertSafeTargetId(targetNodeId: string): void {
392
+ if (!SAFE_NODE_ID.test(targetNodeId)) {
393
+ sourceError(
394
+ "unsupported-target",
395
+ `Motion target ID "${targetNodeId}" is not a safe static HTML node ID.`,
396
+ );
397
+ }
398
+ }
399
+
400
+ function isRuntimeStyleMutationHandler(name: string, value = ""): boolean {
401
+ const normalized = name.toLowerCase();
402
+ return (
403
+ ((normalized === "x-data" || normalized === "v-scope") &&
404
+ !isStaticRuntimeScopeData(value)) ||
405
+ isRuntimeLifecycleMutationAttribute(name) ||
406
+ normalized === "x-show" ||
407
+ normalized.startsWith("x-show.") ||
408
+ normalized === "v-show" ||
409
+ normalized.startsWith("v-show.") ||
410
+ normalized === ":class" ||
411
+ normalized.startsWith(":class.") ||
412
+ normalized === "x-bind:class" ||
413
+ normalized.startsWith("x-bind:class.") ||
414
+ normalized === "v-bind:class" ||
415
+ normalized.startsWith("v-bind:class.") ||
416
+ normalized === ":style" ||
417
+ normalized.startsWith(":style.") ||
418
+ normalized === "x-bind:style" ||
419
+ normalized.startsWith("x-bind:style.") ||
420
+ normalized === "v-bind:style" ||
421
+ normalized.startsWith("v-bind:style.") ||
422
+ normalized === "x-on" ||
423
+ normalized.startsWith("x-on:") ||
424
+ normalized === "v-on" ||
425
+ normalized.startsWith("v-on:") ||
426
+ normalized.startsWith("@")
427
+ );
428
+ }
429
+
430
+ function isRuntimeLifecycleMutationAttribute(name: string): boolean {
431
+ const normalized = name.toLowerCase();
432
+ return (
433
+ normalized === "x-init" ||
434
+ normalized.startsWith("x-init.") ||
435
+ normalized === "x-effect" ||
436
+ normalized.startsWith("x-effect.")
437
+ );
438
+ }
439
+
440
+ function hasPotentialRuntimeMutationHandler(
441
+ attrs: readonly { name: string; value: string }[] | undefined,
442
+ ): boolean {
443
+ return (attrs ?? []).some(
444
+ ({ name, value }) =>
445
+ isRuntimeStyleMutationHandler(name, value) ||
446
+ isNativeInlineEventHandler(name),
447
+ );
448
+ }
449
+
450
+ function isRuntimeStyleMutationScope(name: string): boolean {
451
+ const normalized = name.toLowerCase();
452
+ return normalized === "x-data" || normalized === "v-scope";
453
+ }
454
+
455
+ function isStaticRuntimeScopeData(value: string): boolean {
456
+ const normalized = value.trim();
457
+ if (!normalized || normalized === "{}") return true;
458
+ if (!normalized.startsWith("{") || !normalized.endsWith("}")) return false;
459
+
460
+ const entries = splitStaticObjectProperties(normalized.slice(1, -1).trim());
461
+ return (
462
+ entries !== null &&
463
+ entries.every((entry) => {
464
+ const match =
465
+ /^(?:[A-Za-z_$][\w$]*|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')\s*:\s*(.+)$/s.exec(
466
+ entry,
467
+ );
468
+ return match !== null && isStaticRuntimeScopeValue(match[1]!.trim());
469
+ })
470
+ );
471
+ }
472
+
473
+ function splitStaticObjectProperties(value: string): string[] | null {
474
+ if (!value) return [];
475
+ const properties: string[] = [];
476
+ let quote: "'" | '"' | null = null;
477
+ let escaped = false;
478
+ let start = 0;
479
+
480
+ for (let index = 0; index < value.length; index++) {
481
+ const character = value[index]!;
482
+ if (quote) {
483
+ if (escaped) {
484
+ escaped = false;
485
+ } else if (character === "\\") {
486
+ escaped = true;
487
+ } else if (character === quote) {
488
+ quote = null;
489
+ }
490
+ continue;
491
+ }
492
+ if (character === "'" || character === '"') {
493
+ quote = character;
494
+ } else if (character === ",") {
495
+ properties.push(value.slice(start, index).trim());
496
+ start = index + 1;
497
+ } else if ("{}[]()".includes(character)) {
498
+ return null;
499
+ }
500
+ }
501
+
502
+ if (quote || escaped) return null;
503
+ properties.push(value.slice(start).trim());
504
+ return properties.every(Boolean) ? properties : null;
505
+ }
506
+
507
+ function isStaticRuntimeScopeValue(value: string): boolean {
508
+ return /^(?:true|false|null|undefined|-?(?:\d+(?:\.\d*)?|\.\d+)|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')$/.test(
509
+ value,
510
+ );
511
+ }
512
+
513
+ function findRuntimeStyleMutationAttribute(target: HtmlNode):
514
+ | {
515
+ attribute: { name: string; value: string };
516
+ location: "target" | "ancestor" | "runtime-scope";
517
+ }
518
+ | undefined {
519
+ const ancestors = [target];
520
+ let current = target.parentNode;
521
+ let outermostScopeIndex = -1;
522
+ while (current) {
523
+ ancestors.push(current);
524
+ if (current.attrs?.some(({ name }) => isRuntimeStyleMutationScope(name))) {
525
+ outermostScopeIndex = ancestors.length - 1;
526
+ }
527
+ current = current.parentNode;
528
+ }
529
+
530
+ for (let index = 0; index < ancestors.length; index++) {
531
+ const node = ancestors[index]!;
532
+ const attribute = node.attrs?.find(({ name, value }) =>
533
+ isRuntimeStyleMutationHandler(name, value),
534
+ );
535
+ if (attribute) {
536
+ return {
537
+ attribute,
538
+ location: index === 0 ? "target" : "ancestor",
539
+ };
540
+ }
541
+ }
542
+
543
+ if (outermostScopeIndex >= 0) {
544
+ const scope = ancestors[outermostScopeIndex]!;
545
+ const attribute = findRuntimeStyleMutationInScope(scope);
546
+ if (attribute) return { attribute, location: "runtime-scope" };
547
+ }
548
+
549
+ return undefined;
550
+ }
551
+
552
+ const EXECUTABLE_SCRIPT_TYPES = new Set([
553
+ "application/ecmascript",
554
+ "application/javascript",
555
+ "application/x-ecmascript",
556
+ "application/x-javascript",
557
+ "text/ecmascript",
558
+ "text/javascript",
559
+ "text/javascript1.0",
560
+ "text/javascript1.1",
561
+ "text/javascript1.2",
562
+ "text/javascript1.3",
563
+ "text/javascript1.4",
564
+ "text/javascript1.5",
565
+ "text/jscript",
566
+ "text/livescript",
567
+ "text/x-ecmascript",
568
+ "text/x-javascript",
569
+ ]);
570
+
571
+ function isExecutableScript(node: HtmlNode): boolean {
572
+ const type = attributes(node, "type")[0]?.value.trim().toLowerCase();
573
+ if (!type || type === "module") return true;
574
+ return EXECUTABLE_SCRIPT_TYPES.has(type.split(";", 1)[0]!.trim());
575
+ }
576
+
577
+ const JAVASCRIPT_URL_ATTRIBUTES = new Set([
578
+ "href",
579
+ "src",
580
+ "xlink:href",
581
+ "action",
582
+ "formaction",
583
+ "data",
584
+ ]);
585
+
586
+ function isJavascriptUrl(value: string): boolean {
587
+ return value
588
+ .replace(/[\u0000-\u0020\u007f]/g, "")
589
+ .toLowerCase()
590
+ .startsWith("javascript:");
591
+ }
592
+
593
+ function isJavascriptUrlAttribute(name: string, value: string): boolean {
594
+ return (
595
+ JAVASCRIPT_URL_ATTRIBUTES.has(name.toLowerCase()) && isJavascriptUrl(value)
596
+ );
597
+ }
598
+
599
+ function srcdocContainsExecutableContentOrNestedFrame(value: string): boolean {
600
+ return parseHtml(value).nodes.some((node) => {
601
+ if (node.insideTemplate) return false;
602
+ if (
603
+ (node.tagName === "script" && isExecutableScript(node)) ||
604
+ node.tagName === "iframe" ||
605
+ (node.attrs ?? []).some(
606
+ ({ name, value: attributeValue }) =>
607
+ isEventHandlerAttribute(name) ||
608
+ isRuntimeLifecycleMutationAttribute(name) ||
609
+ isJavascriptUrlAttribute(name, attributeValue),
610
+ )
611
+ ) {
612
+ return true;
613
+ }
614
+ return false;
615
+ });
616
+ }
617
+
618
+ function isManagedMotionTextRuntime(source: string, node: HtmlNode): boolean {
619
+ const marker = attributes(node, MOTION_TEXT_RUNTIME_ATTRIBUTE);
620
+ if (
621
+ marker.length !== 1 ||
622
+ node.attrs?.length !== 1 ||
623
+ !["v1", `v${MOTION_TEXT_SOURCE_VERSION}`].includes(marker[0]?.value ?? "")
624
+ ) {
625
+ return false;
626
+ }
627
+
628
+ const textRead = readMotionTextDocument(source);
629
+ if (textRead.status !== "valid") return false;
630
+ const sourceMarker = parseHtml(source)
631
+ .nodes.filter((candidate) => candidate.tagName === "script")
632
+ .flatMap((candidate) =>
633
+ attributes(candidate, MOTION_TEXT_SOURCE_ATTRIBUTE),
634
+ );
635
+ if (sourceMarker.length !== 1) return false;
636
+ const sourceVersion =
637
+ textRead.document[MOTION_TEXT_SOURCE_VERSION_PROVENANCE] ??
638
+ MOTION_TEXT_SOURCE_VERSION;
639
+ const expectedMarker = `v${sourceVersion}`;
640
+ if (
641
+ marker[0]?.value !== expectedMarker ||
642
+ sourceMarker[0]?.value !== expectedMarker
643
+ ) {
644
+ return false;
645
+ }
646
+
647
+ const runtime = extractManagedMotionTextRuntime(textContent(node).trim());
648
+ if (
649
+ !runtime ||
650
+ !runtimeAnimationsMatch(runtime.payload, textRead.document, sourceVersion)
651
+ ) {
652
+ return false;
653
+ }
654
+
655
+ if (sourceVersion === 1) {
656
+ return (
657
+ bytesToHex(sha256(new TextEncoder().encode(runtime.template))) ===
658
+ MOTION_TEXT_V1_RUNTIME_SHA256
659
+ );
660
+ }
661
+ return runtime.template === MOTION_TEXT_RUNTIME_SOURCE;
662
+ }
663
+
664
+ interface ManagedMotionTextRuntimePayload {
665
+ animations: unknown[];
666
+ playbackMode: MotionPlaybackMode;
667
+ }
668
+
669
+ function extractManagedMotionTextRuntime(
670
+ source: string,
671
+ ): { template: string; payload: ManagedMotionTextRuntimePayload } | null {
672
+ if (!source.endsWith(");")) return null;
673
+
674
+ let markerIndex = source.lastIndexOf("})(", source.length - 3);
675
+ while (markerIndex >= 0) {
676
+ const payloadSource = source.slice(markerIndex + 3, -2);
677
+ let payload: unknown;
678
+ try {
679
+ payload = JSON.parse(payloadSource);
680
+ } catch {
681
+ markerIndex = source.lastIndexOf("})(", markerIndex - 1);
682
+ continue;
683
+ }
684
+ if (isManagedMotionTextRuntimePayload(payload)) {
685
+ return {
686
+ template: `${source.slice(0, markerIndex + 3)}__MOTION_TEXT_SOURCE__);`,
687
+ payload: {
688
+ animations: payload.animations,
689
+ playbackMode: payload.playbackMode,
690
+ },
691
+ };
692
+ }
693
+ markerIndex = source.lastIndexOf("})(", markerIndex - 1);
694
+ }
695
+ return null;
696
+ }
697
+
698
+ function isManagedMotionTextRuntimePayload(
699
+ value: unknown,
700
+ ): value is ManagedMotionTextRuntimePayload {
701
+ return (
702
+ value !== null &&
703
+ typeof value === "object" &&
704
+ Array.isArray((value as { animations?: unknown }).animations) &&
705
+ parseMotionPlaybackMode(
706
+ (value as { playbackMode?: unknown }).playbackMode,
707
+ ) !== null
708
+ );
709
+ }
710
+
711
+ function runtimeAnimationsMatch(
712
+ payload: ManagedMotionTextRuntimePayload,
713
+ document: MotionTextDocument,
714
+ sourceVersion: 1 | typeof MOTION_TEXT_SOURCE_VERSION,
715
+ ): boolean {
716
+ if (sourceVersion === MOTION_TEXT_SOURCE_VERSION) {
717
+ return (
718
+ JSON.stringify(payload.animations) === JSON.stringify(document.animations)
719
+ );
720
+ }
721
+
722
+ const legacyAnimations = document.animations.map((animation) => ({
723
+ targetNodeId: animation.targetNodeId,
724
+ unit: animation.unit,
725
+ direction: animation.direction,
726
+ staggerMs: animation.staggerMs,
727
+ delayMs: animation.delayMs,
728
+ property: animation.property,
729
+ durationMs: animation.durationMs,
730
+ keyframes: animation.keyframes,
731
+ }));
732
+ return (
733
+ JSON.stringify(payload.animations) === JSON.stringify(legacyAnimations)
734
+ );
735
+ }
736
+
737
+ function assertExecutableScriptsSafe(
738
+ source: string,
739
+ nodes: readonly HtmlNode[],
740
+ targetNodeIds: readonly string[],
741
+ generatedIds: Map<number | undefined, string>,
742
+ ): void {
743
+ for (const node of nodes) {
744
+ if (node.insideTemplate) continue;
745
+
746
+ const targetMutatingHandler = node.attrs?.find(
747
+ ({ name, value }) =>
748
+ (isEventHandlerAttribute(name) ||
749
+ isRuntimeLifecycleMutationAttribute(name)) &&
750
+ handlerMayMutateMotionTarget(
751
+ value,
752
+ node,
753
+ nodes,
754
+ targetNodeIds,
755
+ generatedIds,
756
+ ),
757
+ );
758
+ if (targetMutatingHandler) {
759
+ sourceError(
760
+ "unsupported-target",
761
+ `HTML Motion targets cannot be synchronized safely while an inline ${targetMutatingHandler.name} handler may mutate a Motion target.`,
762
+ );
763
+ }
764
+
765
+ const javascriptUrl = node.attrs?.find(({ name, value }) =>
766
+ isJavascriptUrlAttribute(name, value),
767
+ );
768
+ if (javascriptUrl) {
769
+ sourceError(
770
+ "unsupported-target",
771
+ `HTML Motion targets cannot be synchronized safely while ${javascriptUrl.name} contains a javascript URL.`,
772
+ );
773
+ }
774
+
775
+ const srcdoc = attributes(node, "srcdoc")[0]?.value;
776
+ if (
777
+ node.tagName === "iframe" &&
778
+ srcdoc &&
779
+ srcdocContainsExecutableContentOrNestedFrame(srcdoc)
780
+ ) {
781
+ sourceError(
782
+ "unsupported-target",
783
+ "HTML Motion targets cannot be synchronized safely while an iframe srcdoc contains an executable script, event handler, javascript URL, or nested frame.",
784
+ );
785
+ }
786
+
787
+ if (node.tagName !== "script") continue;
788
+ if (
789
+ !isExecutableScript(node) &&
790
+ !hasPotentialRuntimeMutationHandler(node.attrs)
791
+ ) {
792
+ continue;
793
+ }
794
+ if (
795
+ !hasPotentialRuntimeMutationHandler(node.attrs) &&
796
+ isManagedMotionTextRuntime(source, node)
797
+ ) {
798
+ continue;
799
+ }
800
+ sourceError(
801
+ "unsupported-target",
802
+ "HTML Motion targets cannot be synchronized safely while an executable script may change their class or style.",
803
+ );
804
+ }
805
+ }
806
+
807
+ function isRuntimeEventHandler(name: string): boolean {
808
+ const normalized = name.toLowerCase();
809
+ return (
810
+ normalized === "x-on" ||
811
+ normalized.startsWith("x-on:") ||
812
+ normalized === "v-on" ||
813
+ normalized.startsWith("v-on:") ||
814
+ normalized.startsWith("@")
815
+ );
816
+ }
817
+
818
+ function isEventHandlerAttribute(name: string): boolean {
819
+ return isRuntimeEventHandler(name) || isNativeInlineEventHandler(name);
820
+ }
821
+
822
+ function isNativeInlineEventHandler(name: string): boolean {
823
+ return /^on[a-z]/i.test(name);
824
+ }
825
+
826
+ function handlerMayMutateMotionTarget(
827
+ value: string,
828
+ handlerNode: HtmlNode,
829
+ nodes: readonly HtmlNode[],
830
+ targetNodeIds: readonly string[],
831
+ generatedIds: Map<number | undefined, string>,
832
+ ): boolean {
833
+ if (targetNodeIds.length > 0 && mayInjectRuntimeStylesheet(value)) {
834
+ return true;
835
+ }
836
+ if (mayMutateThroughDomTraversal(value)) {
837
+ return true;
838
+ }
839
+
840
+ const targetIds = new Set(targetNodeIds);
841
+ if (mayMutateThroughAlternateDomRoot(value, nodes, targetIds, generatedIds)) {
842
+ return true;
843
+ }
844
+ const directTargetMutation =
845
+ DIRECT_ELEMENT_MUTATION.test(value) || handlerTargetAliasMayMutate(value);
846
+ if (
847
+ directTargetMutation &&
848
+ isMotionTargetNode(handlerNode, targetIds, generatedIds)
849
+ ) {
850
+ return true;
851
+ }
852
+ if (
853
+ (DIRECT_EVENT_TARGET_MUTATION.test(value) ||
854
+ eventTargetAliasMayMutate(value)) &&
855
+ containsMotionTarget(handlerNode, nodes, targetIds, generatedIds)
856
+ ) {
857
+ return true;
858
+ }
859
+ if (
860
+ handlerElementSelectorMayMutateMotionTarget(
861
+ value,
862
+ handlerNode,
863
+ nodes,
864
+ targetIds,
865
+ generatedIds,
866
+ )
867
+ ) {
868
+ return true;
869
+ }
870
+ return handlerSelectsAndMutatesMotionTarget(
871
+ value,
872
+ nodes,
873
+ targetIds,
874
+ generatedIds,
875
+ );
876
+ }
877
+
878
+ const MUTATION_MEMBER_ACCESS = String.raw`(?:\?\.|\.)\s*(?:style\b|classList\b|className\b|setAttribute(?:NS)?\s*\(|removeAttribute\s*\(|toggleAttribute\s*\(|animate\s*\()|\[\s*["'](?:style|classList|className|setAttribute(?:NS)?|removeAttribute|toggleAttribute|animate)["']\s*\]`;
879
+ const MUTATION_MEMBER_ACCESS_PATTERN = new RegExp(MUTATION_MEMBER_ACCESS, "i");
880
+
881
+ function handlerElementSelectorMayMutateMotionTarget(
882
+ value: string,
883
+ handlerNode: HtmlNode,
884
+ nodes: readonly HtmlNode[],
885
+ targetIds: ReadonlySet<string>,
886
+ generatedIds: Map<number | undefined, string>,
887
+ ): boolean {
888
+ const selectorNames = String.raw`querySelector(?:All)?|getElementById|getElementsBy(?:ClassName|Name|TagName)`;
889
+ const scope = String.raw`(?:this|\$el|(?:event|e|\$event)\s*(?:\.\s*(?:currentTarget|target)|\[\s*["'](?:currentTarget|target)["']\s*\]))`;
890
+ const selectorCall = new RegExp(
891
+ String.raw`(?:^|[^\w$])${scope}\s*(?:\.\s*(${selectorNames})|\[\s*["'](${selectorNames})["']\s*\])\s*\(`,
892
+ "gi",
893
+ );
894
+ const hasMutation = MUTATION_MEMBER_ACCESS_PATTERN.test(value);
895
+ if (!hasMutation) return false;
896
+
897
+ const aliases = assignedAliases(
898
+ value,
899
+ /^(?:this|\$el|(?:event|e|\$event)\s*(?:\.\s*(?:currentTarget|target)|\[\s*["'](?:currentTarget|target)["']\s*\]))/i,
900
+ );
901
+ const escapedAliases = [...aliases]
902
+ .map((alias) => alias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
903
+ .join("|");
904
+ if (escapedAliases) {
905
+ const aliasSelectorCall = new RegExp(
906
+ String.raw`\b(?:${escapedAliases})\s*(?:\.\s*(${selectorNames})|\[\s*["'](${selectorNames})["']\s*\])\s*\(`,
907
+ "gi",
908
+ );
909
+ if (aliasSelectorCall.test(value)) {
910
+ return containsMotionTarget(handlerNode, nodes, targetIds, generatedIds);
911
+ }
912
+ }
913
+
914
+ if (!selectorCall.test(value)) return false;
915
+ return containsMotionTarget(handlerNode, nodes, targetIds, generatedIds);
916
+ }
917
+
918
+ const DIRECT_ELEMENT_MUTATION = new RegExp(
919
+ String.raw`(?:\bthis\b|\b(?:event|e)\s*(?:\.\s*(?:currentTarget|target)|\[\s*["'](?:currentTarget|target)["']\s*\])|\$el|\$event\s*(?:\.\s*(?:currentTarget|target)|\[\s*["'](?:currentTarget|target)["']\s*\]))\s*(?:${MUTATION_MEMBER_ACCESS})`,
920
+ "i",
921
+ );
922
+
923
+ const DIRECT_EVENT_TARGET_MUTATION = new RegExp(
924
+ String.raw`(?:\b(?:event|e|\$event)\s*(?:\.\s*target|\[\s*["']target["']\s*\]))\s*(?:${MUTATION_MEMBER_ACCESS})`,
925
+ "i",
926
+ );
927
+
928
+ function mayInjectRuntimeStylesheet(value: string): boolean {
929
+ if (
930
+ /\b(?:adoptedStyleSheets|CSSStyleSheet|styleSheets|cssRules|insertRule|addRule|replaceSync)\b/i.test(
931
+ value,
932
+ )
933
+ ) {
934
+ return true;
935
+ }
936
+
937
+ const createElementCalls = /\bcreateElement(?:NS)?\s*\(([^)]*)\)/gi;
938
+ let match: RegExpExecArray | null;
939
+ while ((match = createElementCalls.exec(value)) !== null) {
940
+ const argument = match[1]?.trim() ?? "";
941
+ const tag = /^(['"])([^'"]+)\1$/.exec(argument)?.[2]?.toLowerCase();
942
+ if (!tag || tag === "style" || tag === "link") return true;
943
+ }
944
+
945
+ return /(?:innerHTML|insertAdjacentHTML)\s*(?:\+=|=|\()\s*[^;]*(?:<\s*style\b|<\s*link\b)/i.test(
946
+ value,
947
+ );
948
+ }
949
+
950
+ function mayMutateThroughDomTraversal(value: string): boolean {
951
+ const traversesDom =
952
+ /(?:\.\s*(?:parentElement|parentNode|children|childNodes|firstElementChild|lastElementChild|firstChild|lastChild|nextElementSibling|previousElementSibling|nextSibling|previousSibling|closest|matches)\b|\[\s*["'](?:parentElement|parentNode|children|childNodes|firstElementChild|lastElementChild|firstChild|lastChild|nextElementSibling|previousElementSibling|nextSibling|previousSibling|closest|matches)["']\s*\])/i.test(
953
+ value,
954
+ );
955
+ return traversesDom && MUTATION_MEMBER_ACCESS_PATTERN.test(value);
956
+ }
957
+
958
+ function mayMutateThroughAlternateDomRoot(
959
+ value: string,
960
+ nodes: readonly HtmlNode[],
961
+ targetIds: ReadonlySet<string>,
962
+ generatedIds: Map<number | undefined, string>,
963
+ ): boolean {
964
+ const root = String.raw`(?:this|\$el|(?:event|e|\$event)\s*(?:\.\s*(?:currentTarget|target)|\[\s*["'](?:currentTarget|target)["']\s*\]))`;
965
+ const selectorNames = String.raw`querySelector(?:All)?|getElementById|getElementsBy(?:ClassName|Name|TagName)`;
966
+ const selectorCalls = new RegExp(
967
+ String.raw`(?<![\w$])${root}\s*(?:\.\s*(?:ownerDocument)|\.\s*getRootNode\s*\(\s*\)|\[\s*["'](?:ownerDocument|getRootNode)["']\s*\](?:\s*\(\s*\))?)\s*(?:\.\s*(${selectorNames})|\[\s*["'](${selectorNames})["']\s*\])\s*\(`,
968
+ "gi",
969
+ );
970
+ let match: RegExpExecArray | null;
971
+
972
+ while ((match = selectorCalls.exec(value)) !== null) {
973
+ const callStartIndex = match.index;
974
+ const method = match[1] ?? match[2]!;
975
+ const openParenIndex = selectorCalls.lastIndex - 1;
976
+ const callEndIndex = findSelectorCallEnd(value, openParenIndex);
977
+ if (callEndIndex === null) return true;
978
+ if (
979
+ !alternateRootSelectorCallFeedsMutationSink(
980
+ value,
981
+ callStartIndex,
982
+ callEndIndex,
983
+ method,
984
+ )
985
+ ) {
986
+ selectorCalls.lastIndex = callEndIndex;
987
+ continue;
988
+ }
989
+
990
+ const selectorArgument = readStaticSelectorArgument(
991
+ value.slice(openParenIndex + 1, callEndIndex - 1),
992
+ );
993
+ if (!selectorArgument) return true;
994
+
995
+ const selector = selectorForDocumentCall(method, selectorArgument);
996
+ if (!selector) return true;
997
+ if (selectorMayTargetMotionNode(selector, nodes, targetIds, generatedIds)) {
998
+ return true;
999
+ }
1000
+ }
1001
+
1002
+ return false;
1003
+ }
1004
+
1005
+ function alternateRootSelectorCallFeedsMutationSink(
1006
+ source: string,
1007
+ callStartIndex: number,
1008
+ callEndIndex: number,
1009
+ method: string,
1010
+ ): boolean {
1011
+ const afterCall = source.slice(callEndIndex);
1012
+ if (selectedElementHasActualMutationSink(afterCall)) return true;
1013
+
1014
+ const assignment =
1015
+ /(?:\b(?:const|let|var)\s+)?([A-Za-z_$][\w$]*)\s*=\s*$/.exec(
1016
+ source.slice(0, callStartIndex),
1017
+ );
1018
+ if (
1019
+ assignment &&
1020
+ identifierHasActualMutationSink(afterCall, assignment[1]!)
1021
+ ) {
1022
+ return true;
1023
+ }
1024
+
1025
+ if (method.toLowerCase() === "queryselectorall") {
1026
+ const callbackStatus = querySelectorAllCallbackMutationStatus(afterCall);
1027
+ const forOfStatus = querySelectorAllForOfMutationStatus(
1028
+ source,
1029
+ callStartIndex,
1030
+ callEndIndex,
1031
+ );
1032
+ if (callbackStatus === true || forOfStatus === true) return true;
1033
+ if (callbackStatus === false || forOfStatus === false) return false;
1034
+ }
1035
+
1036
+ return destructuredSelectorCallHasActualMutationSink(
1037
+ source.slice(0, callStartIndex),
1038
+ afterCall,
1039
+ );
1040
+ }
1041
+
1042
+ const ACTUAL_MUTATION_MEMBER_ACCESS = String.raw`(?:\?\.|\.)\s*(?:style(?:\s*(?:\.\s*[A-Za-z_$][\w$]*|\[[^\]]+\]))*\s*(?:=(?!=)|\+=|-=|\+\+|--)|style\s*(?:\?\.|\.)\s*(?:setProperty|removeProperty)\s*\(|classList\s*(?:\?\.|\.)\s*(?:add|remove|toggle|replace)\s*\(|className\s*(?:=(?!=)|\+=)|(?:setAttribute(?:NS)?|removeAttribute|toggleAttribute|animate)\s*\()|\[\s*["'](?:setAttribute(?:NS)?|removeAttribute|toggleAttribute|animate)["']\s*\]\s*\(`;
1043
+
1044
+ function selectedElementHasActualMutationSink(suffix: string): boolean {
1045
+ return new RegExp(
1046
+ String.raw`^\s*(?:\[\s*\d+\s*\]\s*)?${ACTUAL_MUTATION_MEMBER_ACCESS}`,
1047
+ "i",
1048
+ ).test(suffix);
1049
+ }
1050
+
1051
+ function identifierHasActualMutationSink(
1052
+ source: string,
1053
+ identifier: string,
1054
+ ): boolean {
1055
+ const escaped = identifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1056
+ return (
1057
+ new RegExp(
1058
+ `\\b${escaped}\\s*(?:${ACTUAL_MUTATION_MEMBER_ACCESS})`,
1059
+ "i",
1060
+ ).test(source) ||
1061
+ new RegExp(
1062
+ `\\bObject\\s*\\.\\s*assign\\s*\\(\\s*${escaped}\\s*(?:\\?\\.|\\.)\\s*style\\b`,
1063
+ "i",
1064
+ ).test(source)
1065
+ );
1066
+ }
1067
+
1068
+ function identifierAliasHasActualMutationSink(
1069
+ source: string,
1070
+ identifier: string,
1071
+ ): boolean {
1072
+ return identifierAliases(source, identifier).some((alias) =>
1073
+ identifierHasActualMutationSink(source, alias),
1074
+ );
1075
+ }
1076
+
1077
+ function styleAliasHasActualMutationSink(
1078
+ source: string,
1079
+ identifier: string,
1080
+ ): boolean {
1081
+ return identifierAliases(source, identifier).some((alias) => {
1082
+ const escaped = alias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1083
+ return (
1084
+ new RegExp(
1085
+ `\\b${escaped}\\s*(?:(?:\\.\\s*[A-Za-z_$][\\w$]*|\\[\\s*[^\\]]+\\s*\\])\\s*(?:=(?!=)|\\+=|-=|\\+\\+|--)|(?:\\?\\.|\\.)\\s*(?:setProperty|removeProperty)\\s*\\()`,
1086
+ "i",
1087
+ ).test(source) ||
1088
+ new RegExp(
1089
+ `\\bObject\\s*\\.\\s*assign\\s*\\(\\s*${escaped}(?:\\s*[,\\)])`,
1090
+ "i",
1091
+ ).test(source)
1092
+ );
1093
+ });
1094
+ }
1095
+
1096
+ function destructuredBindingsMayMutate(
1097
+ bindings: string,
1098
+ source: string,
1099
+ ): boolean {
1100
+ return bindings.split(",").some((binding) => {
1101
+ const [property, alias] = binding.split(/\s*:\s*/);
1102
+ const name = alias?.trim() || property?.trim();
1103
+ if (!name) return false;
1104
+ if (property?.trim() === "classList") {
1105
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1106
+ return new RegExp(
1107
+ `\\b${escaped}\\s*(?:\\?\\.|\\.)\\s*(?:add|remove|toggle|replace)\\s*\\(`,
1108
+ "i",
1109
+ ).test(source);
1110
+ }
1111
+ return (
1112
+ property?.trim() === "style" &&
1113
+ styleAliasHasActualMutationSink(source, name)
1114
+ );
1115
+ });
1116
+ }
1117
+
1118
+ function querySelectorAllCallbackMutationStatus(
1119
+ source: string,
1120
+ ): boolean | null {
1121
+ const methodCall =
1122
+ /^\s*[\])]*\s*\.\s*(?:forEach|map|filter|some|every)\s*\(/i.exec(source);
1123
+ const callback =
1124
+ /^\s*[\])]*\s*\.\s*(?:forEach|map|filter|some|every)\s*\(\s*(?:async\s+)?(?:\(\s*([\w$]+)\s*\)|([\w$]+)|\(\s*\{\s*([^}]+)\s*\}\s*\))\s*=>/i.exec(
1125
+ source,
1126
+ );
1127
+ if (!methodCall) return null;
1128
+ if (!callback) return true;
1129
+
1130
+ const methodOpenIndex = methodCall[0].lastIndexOf("(");
1131
+ const callEnd = findSelectorCallEnd(
1132
+ source,
1133
+ methodCall.index + methodOpenIndex,
1134
+ );
1135
+ if (callEnd === null) return true;
1136
+
1137
+ const callbackBody = source.slice(callback[0].length, callEnd - 1);
1138
+ const callbackParameter = callback[1] ?? callback[2];
1139
+ if (callbackParameter) {
1140
+ return identifierAliasHasActualMutationSink(
1141
+ callbackBody,
1142
+ callbackParameter,
1143
+ );
1144
+ }
1145
+ return (
1146
+ callback[3] !== undefined &&
1147
+ destructuredBindingsMayMutate(callback[3], callbackBody)
1148
+ );
1149
+ }
1150
+
1151
+ function destructuredSelectorCallHasActualMutationSink(
1152
+ prefix: string,
1153
+ suffix: string,
1154
+ ): boolean {
1155
+ const assignment = /\b(?:const|let|var)\s*\{\s*([^}]+)\s*\}\s*=\s*$/.exec(
1156
+ prefix,
1157
+ );
1158
+ if (!assignment) return false;
1159
+
1160
+ return destructuredBindingsMayMutate(assignment[1]!, suffix);
1161
+ }
1162
+
1163
+ function assignedAliases(
1164
+ source: string,
1165
+ initialExpression: RegExp,
1166
+ ): Set<string> {
1167
+ const aliases = new Set<string>();
1168
+ const assignments =
1169
+ /(?:\b(?:const|let|var)\s+)?([A-Za-z_$][\w$]*)\s*=\s*([^;,\n]+)/g;
1170
+ let match: RegExpExecArray | null;
1171
+ let changed = true;
1172
+ while (changed) {
1173
+ changed = false;
1174
+ assignments.lastIndex = 0;
1175
+ while ((match = assignments.exec(source)) !== null) {
1176
+ const name = match[1]!;
1177
+ const expression = match[2]!.trim().replace(/^\(|\)$/g, "");
1178
+ const sourceAlias = /^([A-Za-z_$][\w$]*)\b/.exec(expression)?.[1];
1179
+ if (
1180
+ !aliases.has(name) &&
1181
+ (initialExpression.test(expression) ||
1182
+ (sourceAlias !== undefined && aliases.has(sourceAlias)))
1183
+ ) {
1184
+ aliases.add(name);
1185
+ changed = true;
1186
+ }
1187
+ }
1188
+ }
1189
+ return aliases;
1190
+ }
1191
+
1192
+ function identifierMutationSink(identifier: string): RegExp {
1193
+ const escaped = identifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1194
+ return new RegExp(`\\b${escaped}\\s*(?:${MUTATION_MEMBER_ACCESS})`, "i");
1195
+ }
1196
+
1197
+ function identifierMayFeedMutationSink(
1198
+ source: string,
1199
+ identifier: string,
1200
+ ): boolean {
1201
+ const aliases = identifierAliases(source, identifier);
1202
+ return aliases.some((alias) => {
1203
+ if (elementMutationSinkForIdentifier(alias).test(source)) return true;
1204
+ const escaped = alias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1205
+ return new RegExp(
1206
+ `\\b${escaped}(?:\\s*(?:\\?\\.|\\.)\\s*[A-Za-z_$][\\w$]*|\\s*\\[\\s*["'][^"']+["']\\s*\\]){1,8}\\s*(?:${MUTATION_MEMBER_ACCESS})`,
1207
+ "i",
1208
+ ).test(source);
1209
+ });
1210
+ }
1211
+
1212
+ function handlerTargetAliasMayMutate(value: string): boolean {
1213
+ const targetExpression =
1214
+ /^(?:this|\$el|(?:event|e|\$event)\s*(?:\.\s*(?:currentTarget|target)|\[\s*["'](?:currentTarget|target)["']\s*\]))/i;
1215
+ const aliases = assignedAliases(value, targetExpression);
1216
+ return [...aliases].some((alias) =>
1217
+ identifierMutationSink(alias).test(value),
1218
+ );
1219
+ }
1220
+
1221
+ function eventTargetAliasMayMutate(value: string): boolean {
1222
+ const targetExpression =
1223
+ /^(?:event|e|\$event)\s*(?:\.\s*target|\[\s*["']target["']\s*\])/i;
1224
+ const aliases = assignedAliases(value, targetExpression);
1225
+ return [...aliases].some((alias) =>
1226
+ identifierMutationSink(alias).test(value),
1227
+ );
1228
+ }
1229
+
1230
+ function containsMotionTarget(
1231
+ node: HtmlNode,
1232
+ nodes: readonly HtmlNode[],
1233
+ targetNodeIds: ReadonlySet<string>,
1234
+ generatedIds: Map<number | undefined, string>,
1235
+ ): boolean {
1236
+ return nodes.some((candidate) => {
1237
+ if (!isMotionTargetNode(candidate, targetNodeIds, generatedIds))
1238
+ return false;
1239
+ let current = candidate.parentNode;
1240
+ while (current) {
1241
+ if (current === node) return true;
1242
+ current = current.parentNode;
1243
+ }
1244
+ return false;
1245
+ });
1246
+ }
1247
+
1248
+ function isMotionTargetNode(
1249
+ node: HtmlNode,
1250
+ targetNodeIds: ReadonlySet<string>,
1251
+ generatedIds: Map<number | undefined, string>,
1252
+ ): boolean {
1253
+ const targetAttrs = attributes(node, NODE_ID_ATTRIBUTE);
1254
+ if (targetAttrs.some(({ value }) => targetNodeIds.has(value))) return true;
1255
+ const generatedId = generatedIds.get(
1256
+ node.sourceCodeLocation?.startTag?.startOffset,
1257
+ );
1258
+ return generatedId !== undefined && targetNodeIds.has(generatedId);
1259
+ }
1260
+
1261
+ function handlerSelectsAndMutatesMotionTarget(
1262
+ value: string,
1263
+ nodes: readonly HtmlNode[],
1264
+ targetIds: ReadonlySet<string>,
1265
+ generatedIds: Map<number | undefined, string>,
1266
+ ): boolean {
1267
+ const usesDocumentSelector =
1268
+ /\b(?:document|window|globalThis|self)\b/i.test(value) &&
1269
+ /\b(?:querySelector(?:All)?|getElementById|getElementsBy(?:ClassName|Name|TagName))\b/i.test(
1270
+ value,
1271
+ );
1272
+ const destructuredMutationCandidate =
1273
+ /\b(?:const|let|var)\s*\{[^}]*\b(?:style|classList)\b[^}]*\}\s*=|\(\s*\{[^}]*\b(?:style|classList)\b[^}]*\}\s*\)\s*=>/i.test(
1274
+ value,
1275
+ );
1276
+ if (
1277
+ !usesDocumentSelector ||
1278
+ (!MUTATION_MEMBER_ACCESS_PATTERN.test(value) &&
1279
+ !destructuredMutationCandidate)
1280
+ )
1281
+ return false;
1282
+
1283
+ const selectorCalls =
1284
+ /\b(?:document|window|globalThis|self)\s*(?:\.\s*document|\[\s*["']document["']\s*\])?\s*(?:\.\s*(querySelector(?:All)?|getElementById|getElementsBy(?:ClassName|Name|TagName))|\[\s*["'](querySelector(?:All)?|getElementById|getElementsBy(?:ClassName|Name|TagName))["']\s*\])\s*\(/gi;
1285
+ let foundSelectorCall = false;
1286
+ let match: RegExpExecArray | null;
1287
+
1288
+ while ((match = selectorCalls.exec(value)) !== null) {
1289
+ foundSelectorCall = true;
1290
+ const openParenIndex = selectorCalls.lastIndex - 1;
1291
+ const method = match[1] ?? match[2]!;
1292
+ const callEndIndex = findSelectorCallEnd(value, openParenIndex);
1293
+ if (callEndIndex === null) return true;
1294
+ if (
1295
+ !selectorCallFeedsMutationSink(value, match.index, callEndIndex, method)
1296
+ ) {
1297
+ selectorCalls.lastIndex = callEndIndex;
1298
+ continue;
1299
+ }
1300
+
1301
+ const selectorArgument = readStaticSelectorArgument(
1302
+ value.slice(openParenIndex + 1, callEndIndex - 1),
1303
+ );
1304
+ if (!selectorArgument) return true;
1305
+
1306
+ const selector = selectorForDocumentCall(method, selectorArgument);
1307
+ if (!selector) return true;
1308
+
1309
+ const traversal = selectorCallTraversal(
1310
+ value,
1311
+ match.index,
1312
+ callEndIndex,
1313
+ method,
1314
+ );
1315
+ if (
1316
+ selectorMayTargetMotionNode(
1317
+ selector,
1318
+ nodes,
1319
+ targetIds,
1320
+ generatedIds,
1321
+ traversal,
1322
+ )
1323
+ ) {
1324
+ return true;
1325
+ }
1326
+ }
1327
+
1328
+ return !foundSelectorCall;
1329
+ }
1330
+
1331
+ function findSelectorCallEnd(
1332
+ source: string,
1333
+ openParenIndex: number,
1334
+ ): number | null {
1335
+ let depth = 0;
1336
+ let quote: "'" | '"' | "`" | null = null;
1337
+ let escaped = false;
1338
+
1339
+ for (let index = openParenIndex; index < source.length; index++) {
1340
+ const character = source[index]!;
1341
+ if (quote) {
1342
+ if (escaped) escaped = false;
1343
+ else if (character === "\\") escaped = true;
1344
+ else if (character === quote) quote = null;
1345
+ continue;
1346
+ }
1347
+ if (character === "'" || character === '"' || character === "`") {
1348
+ quote = character;
1349
+ } else if (character === "(") {
1350
+ depth++;
1351
+ } else if (character === ")" && --depth === 0) {
1352
+ return index + 1;
1353
+ }
1354
+ }
1355
+
1356
+ return null;
1357
+ }
1358
+
1359
+ function readStaticSelectorArgument(source: string): string | null {
1360
+ const argument = source.trim();
1361
+ const quote = argument[0];
1362
+ if (
1363
+ (quote !== "'" && quote !== '"') ||
1364
+ argument[argument.length - 1] !== quote
1365
+ ) {
1366
+ return null;
1367
+ }
1368
+ const value = argument.slice(1, -1);
1369
+ return value.includes("\\") || value.includes(quote) ? null : value;
1370
+ }
1371
+
1372
+ function selectorCallFeedsMutationSink(
1373
+ source: string,
1374
+ callStartIndex: number,
1375
+ callEndIndex: number,
1376
+ method: string,
1377
+ ): boolean {
1378
+ const afterCall = source.slice(callEndIndex);
1379
+ if (SELECTED_ELEMENT_MUTATION_SINK.test(afterCall)) return true;
1380
+ if (
1381
+ destructuredSelectorCallHasActualMutationSink(
1382
+ source.slice(0, callStartIndex),
1383
+ afterCall,
1384
+ )
1385
+ ) {
1386
+ return true;
1387
+ }
1388
+
1389
+ if (method.toLowerCase() === "queryselectorall") {
1390
+ const callbackStatus = querySelectorAllCallbackMutationStatus(afterCall);
1391
+ if (callbackStatus !== null) return callbackStatus;
1392
+ if (
1393
+ arrayFromSelectorCallbackMayMutate(source, callStartIndex, callEndIndex)
1394
+ ) {
1395
+ return true;
1396
+ }
1397
+ const forOfStatus = querySelectorAllForOfMutationStatus(
1398
+ source,
1399
+ callStartIndex,
1400
+ callEndIndex,
1401
+ );
1402
+ if (forOfStatus !== null) return forOfStatus;
1403
+ }
1404
+
1405
+ const assignment =
1406
+ /(?:\b(?:const|let|var)\s+)?([A-Za-z_$][\w$]*)\s*=\s*$/.exec(
1407
+ source.slice(0, callStartIndex),
1408
+ );
1409
+ if (assignment) {
1410
+ return identifierMayFeedMutationSink(afterCall, assignment[1]!);
1411
+ }
1412
+
1413
+ const remainingExpression = afterCall.trimStart();
1414
+ if (!remainingExpression || /^[;),&|?]/.test(remainingExpression))
1415
+ return false;
1416
+ return true;
1417
+ }
1418
+
1419
+ function arrayFromSelectorCallbackMayMutate(
1420
+ source: string,
1421
+ callStartIndex: number,
1422
+ callEndIndex: number,
1423
+ ): boolean {
1424
+ if (!/\bArray\s*\.\s*from\s*\(\s*$/.test(source.slice(0, callStartIndex)))
1425
+ return false;
1426
+
1427
+ const callback =
1428
+ /^\s*,\s*(?:async\s+)?(?:\(\s*([A-Za-z_$][\w$]*)(?:\s*,[^)]*)?\)|([A-Za-z_$][\w$]*))\s*=>/i.exec(
1429
+ source.slice(callEndIndex),
1430
+ );
1431
+ const callbackParameter = callback?.[1] ?? callback?.[2];
1432
+ return (
1433
+ callbackParameter !== undefined &&
1434
+ identifierAliasHasActualMutationSink(
1435
+ source.slice(callEndIndex),
1436
+ callbackParameter,
1437
+ )
1438
+ );
1439
+ }
1440
+
1441
+ function querySelectorAllForOfMutationStatus(
1442
+ source: string,
1443
+ callStartIndex: number,
1444
+ callEndIndex: number,
1445
+ ): boolean | null {
1446
+ const prefix = source.slice(0, callStartIndex);
1447
+ const forOfHeader = /\bfor\s*\(([^)]*)\bof\s*(?:\[\s*\.\.\.\s*)?$/i.exec(
1448
+ prefix,
1449
+ );
1450
+ if (!forOfHeader) return null;
1451
+
1452
+ const binding = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*$/.exec(
1453
+ forOfHeader[1]!,
1454
+ );
1455
+ if (!binding) return true;
1456
+
1457
+ const afterCall = source.slice(callEndIndex);
1458
+ const loopHeaderEnd = /^\s*\]?\s*\)\s*/.exec(afterCall);
1459
+ if (!loopHeaderEnd) return true;
1460
+
1461
+ const loopBody = afterCall.slice(loopHeaderEnd[0].length);
1462
+ if (loopBody.startsWith("{")) {
1463
+ const bodyEnd = findMatchingBlockEnd(loopBody, 0);
1464
+ if (bodyEnd === null) return true;
1465
+ return identifierAliasHasActualMutationSink(
1466
+ loopBody.slice(1, bodyEnd),
1467
+ binding[1]!,
1468
+ );
1469
+ }
1470
+
1471
+ const statementEnd = loopBody.indexOf(";");
1472
+ const statement =
1473
+ statementEnd < 0 ? loopBody : loopBody.slice(0, statementEnd);
1474
+ return identifierAliasHasActualMutationSink(statement, binding[1]!);
1475
+ }
1476
+
1477
+ function findMatchingBlockEnd(
1478
+ source: string,
1479
+ openBraceIndex: number,
1480
+ ): number | null {
1481
+ let depth = 0;
1482
+ let quote: "'" | '"' | "`" | null = null;
1483
+ let escaped = false;
1484
+
1485
+ for (let index = openBraceIndex; index < source.length; index++) {
1486
+ const character = source[index]!;
1487
+ if (quote) {
1488
+ if (escaped) escaped = false;
1489
+ else if (character === "\\") escaped = true;
1490
+ else if (character === quote) quote = null;
1491
+ continue;
1492
+ }
1493
+ if (character === "'" || character === '"' || character === "`") {
1494
+ quote = character;
1495
+ } else if (character === "{") {
1496
+ depth++;
1497
+ } else if (character === "}" && --depth === 0) {
1498
+ return index;
1499
+ }
283
1500
  }
284
- const node = marked[0];
285
- if (!node) return null;
286
- const range = assertSourceRanges(source, node);
287
- return { node, css: textContent(node), range };
1501
+
1502
+ return null;
288
1503
  }
289
1504
 
290
- function motionTextError(error: Error): MotionHtmlSourceError {
291
- return new MotionHtmlSourceError("invalid-motion-text", error.message);
1505
+ const SELECTED_ELEMENT_MUTATION_SINK = new RegExp(
1506
+ `^\\s*(?:\\[\\s*\\d+\\s*\\]\\s*)?(?:${MUTATION_MEMBER_ACCESS})`,
1507
+ "i",
1508
+ );
1509
+
1510
+ function elementMutationSinkForIdentifier(identifier: string): RegExp {
1511
+ const escapedIdentifier = identifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1512
+ return new RegExp(
1513
+ `\\b${escapedIdentifier}\\s*(?:${MUTATION_MEMBER_ACCESS})`,
1514
+ "i",
1515
+ );
292
1516
  }
293
1517
 
294
- function readMotionText(source: string): MotionTextDocument | null {
295
- const read = readMotionTextDocument(source);
296
- if (read.status === "invalid") throw motionTextError(read.error);
297
- return read.status === "valid" ? read.document : null;
1518
+ function identifierAliases(source: string, initial: string): string[] {
1519
+ const aliases = new Set([initial]);
1520
+ const assignments =
1521
+ /(?:\b(?:const|let|var)\s+)?([A-Za-z_$][\w$]*)\s*=\s*([A-Za-z_$][\w$]*)\b/g;
1522
+ let changed = true;
1523
+ while (changed) {
1524
+ changed = false;
1525
+ assignments.lastIndex = 0;
1526
+ let match: RegExpExecArray | null;
1527
+ while ((match = assignments.exec(source)) !== null) {
1528
+ if (!aliases.has(match[2]!) || aliases.has(match[1]!)) continue;
1529
+ aliases.add(match[1]!);
1530
+ changed = true;
1531
+ }
1532
+ }
1533
+ return [...aliases];
298
1534
  }
299
1535
 
300
- function assertMotionTextOutsideTemplates(nodes: HtmlNode[]): void {
1536
+ function selectorCallTraversal(
1537
+ source: string,
1538
+ callStartIndex: number,
1539
+ callEndIndex: number,
1540
+ method: string,
1541
+ ): { steps: string[]; unsupported: boolean } | null {
1542
+ if (method.toLowerCase() === "queryselectorall") return null;
1543
+
1544
+ const afterCall = source.slice(callEndIndex);
301
1545
  if (
302
- nodes.some(
303
- (node) =>
304
- node.insideTemplate &&
305
- node.tagName === "script" &&
306
- (attributes(node, MOTION_TEXT_SOURCE_ATTRIBUTE).length > 0 ||
307
- attributes(node, MOTION_TEXT_RUNTIME_ATTRIBUTE).length > 0),
1546
+ /(?:\.\s*(?:parentElement|parentNode|children|childNodes|firstElementChild|lastElementChild|firstChild|lastChild|closest|matches|querySelector(?:All)?)\b|\[\s*["'](?:parentElement|parentNode|children|childNodes|firstElementChild|lastElementChild|firstChild|lastChild|closest|matches|querySelector(?:All)?)["']\s*\])/i.test(
1547
+ afterCall,
308
1548
  )
309
1549
  ) {
310
- sourceError(
311
- "invalid-motion-text",
312
- "Managed Motion Text scripts cannot live inside an HTML template.",
1550
+ return { steps: [], unsupported: true };
1551
+ }
1552
+ const parseSteps = (suffix: string) => {
1553
+ const access =
1554
+ /\s*(?:\?\.|\.)\s*(nextElementSibling|previousElementSibling)\b|\s*\[\s*["'](nextElementSibling|previousElementSibling)["']\s*\]/iy;
1555
+ const steps: string[] = [];
1556
+ let offset = 0;
1557
+ while (true) {
1558
+ access.lastIndex = offset;
1559
+ const match = access.exec(suffix);
1560
+ if (!match) break;
1561
+ steps.push((match[1] ?? match[2]!).toLowerCase());
1562
+ offset = access.lastIndex;
1563
+ }
1564
+ if (steps.length > 0) {
1565
+ return SELECTED_ELEMENT_MUTATION_SINK.test(suffix.slice(offset))
1566
+ ? { steps, unsupported: false }
1567
+ : null;
1568
+ }
1569
+ return null;
1570
+ };
1571
+
1572
+ const direct = parseSteps(afterCall);
1573
+ if (direct) return direct;
1574
+
1575
+ const assignment =
1576
+ /(?:\b(?:const|let|var)\s+)?([A-Za-z_$][\w$]*)\s*=\s*$/.exec(
1577
+ source.slice(0, callStartIndex),
313
1578
  );
1579
+ const receiver = assignment?.[1];
1580
+ if (!receiver) {
1581
+ return /^\s*(?:\?\.|\.)[A-Za-z_$]/.test(afterCall) &&
1582
+ !SELECTED_ELEMENT_MUTATION_SINK.test(afterCall)
1583
+ ? { steps: [], unsupported: true }
1584
+ : null;
1585
+ }
1586
+ const escaped = receiver.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1587
+ const traversal = new RegExp(
1588
+ `\\b${escaped}((?:(?:\\?\\.|\\.)\\s*(?:nextElementSibling|previousElementSibling)\\b|\\[\\s*["'](?:nextElementSibling|previousElementSibling)["']\\s*\\]))+`,
1589
+ "i",
1590
+ );
1591
+ const match = traversal.exec(afterCall);
1592
+ if (!match) {
1593
+ const intermediate = new RegExp(
1594
+ `\\b${escaped}(?:\\s*(?:\\?\\.|\\.)\\s*|\\s*\\[\\s*["'][^"']+["']\\s*\\])([A-Za-z_$][\\w$]*)[^;]*(?:${MUTATION_MEMBER_ACCESS})`,
1595
+ "i",
1596
+ ).exec(afterCall);
1597
+ return intermediate &&
1598
+ !/^(?:style|classList|className|setAttribute|removeAttribute|animate)$/i.test(
1599
+ intermediate[1]!,
1600
+ )
1601
+ ? { steps: [], unsupported: true }
1602
+ : null;
314
1603
  }
315
- }
316
1604
 
317
- function targetsFromDocuments(
318
- document: MotionCodeDocument | null,
319
- textDocument: MotionTextDocument | null,
320
- ): string[] {
321
- return [
322
- ...new Set([
323
- ...(document?.tracks.map((track) => track.targetNodeId) ?? []),
324
- ...(document?.nativeStyles?.map((style) => style.targetNodeId) ?? []),
325
- ...(textDocument?.animations.map((animation) => animation.targetNodeId) ??
326
- []),
327
- ]),
328
- ];
1605
+ const steps: string[] = [];
1606
+ const access =
1607
+ /(?:\?\.|\.)\s*(nextElementSibling|previousElementSibling)\b|\[\s*["'](nextElementSibling|previousElementSibling)["']\s*\]/gi;
1608
+ let accessMatch: RegExpExecArray | null;
1609
+ while ((accessMatch = access.exec(match[1]!)) !== null) {
1610
+ steps.push((accessMatch[1] ?? accessMatch[2]!).toLowerCase());
1611
+ }
1612
+ const suffix = afterCall.slice(match.index + match[0].length);
1613
+ if (SELECTED_ELEMENT_MUTATION_SINK.test(suffix)) {
1614
+ return { steps, unsupported: false };
1615
+ }
1616
+ const assignedTraversal =
1617
+ /^\s*;?\s*(?:\b(?:const|let|var)\s+)?([A-Za-z_$][\w$]*)\s*=/.exec(suffix);
1618
+ if (
1619
+ assignedTraversal &&
1620
+ identifierMayFeedMutationSink(suffix, assignedTraversal[1]!)
1621
+ ) {
1622
+ return { steps, unsupported: false };
1623
+ }
1624
+ return { steps, unsupported: true };
329
1625
  }
330
1626
 
331
- function pathTrimTargetsFromDocument(
332
- document: MotionCodeDocument | null,
333
- ): string[] {
334
- if (!document) return [];
335
- return [
336
- ...new Set([
337
- ...document.tracks
338
- .filter((track) => track.property === "path-trim")
339
- .map((track) => track.targetNodeId),
340
- ...(document.nativeStyles ?? [])
341
- .filter((style) => style.enabled !== false && style.styleKey === "Path")
342
- .map((style) => style.targetNodeId),
343
- ]),
344
- ];
1627
+ function selectorForDocumentCall(
1628
+ method: string,
1629
+ argument: string,
1630
+ ): string | null {
1631
+ switch (method.toLowerCase()) {
1632
+ case "queryselector":
1633
+ case "queryselectorall":
1634
+ return argument;
1635
+ case "getelementbyid":
1636
+ return `#${argument}`;
1637
+ case "getelementsbyclassname":
1638
+ return `.${argument}`;
1639
+ case "getelementsbyname":
1640
+ return `[name="${argument}"]`;
1641
+ case "getelementsbytagname":
1642
+ return argument;
1643
+ default:
1644
+ return null;
1645
+ }
345
1646
  }
346
1647
 
347
- function pathTrimTargetsFromTimeline(timeline: MotionTimeline): string[] {
348
- return [
349
- ...new Set([
350
- ...timeline.tracks
351
- .filter((track) => track.property === "path-trim")
352
- .map((track) => track.targetNodeId),
353
- ...(timeline.nativeStyles ?? [])
354
- .filter((style) => style.enabled !== false && style.styleKey === "Path")
355
- .map((style) => style.targetNodeId),
356
- ]),
357
- ];
358
- }
1648
+ function selectorMayTargetMotionNode(
1649
+ selector: string,
1650
+ nodes: readonly HtmlNode[],
1651
+ targetNodeIds: ReadonlySet<string>,
1652
+ generatedIds: Map<number | undefined, string>,
1653
+ traversal: { steps: string[]; unsupported: boolean } | null = null,
1654
+ ): boolean {
1655
+ const parsed = parseMotionAuthoredCssSimpleSelector(selector);
1656
+ if (!parsed) return true;
1657
+ const selectorAttribute =
1658
+ parsed.kind === "id"
1659
+ ? "id"
1660
+ : parsed.kind === "class"
1661
+ ? "class"
1662
+ : parsed.kind === "attribute"
1663
+ ? parsed.name
1664
+ : null;
359
1665
 
360
- function applyPathTrimTargets(source: string, targetNodeIds: string[]): string {
361
- try {
362
- return applyMotionPathTrimSource(source, targetNodeIds, "html");
363
- } catch (error) {
364
- if (error instanceof MotionPathTrimSourceError) {
365
- sourceError("invalid-path-trim", error.message);
366
- }
367
- throw error;
1666
+ const motionTargets = nodes.filter((node) =>
1667
+ isMotionTargetNode(node, targetNodeIds, generatedIds),
1668
+ );
1669
+ if (traversal?.unsupported) return motionTargets.length > 0;
1670
+
1671
+ if (
1672
+ motionTargets.some((node) =>
1673
+ (node.attrs ?? []).some(({ name }) =>
1674
+ isDynamicHtmlAttribute(name, selectorAttribute),
1675
+ ),
1676
+ )
1677
+ ) {
1678
+ return true;
368
1679
  }
369
- }
370
1680
 
371
- function restorePathTrimTargets(source: string): string {
372
- try {
373
- return restoreMotionPathTrimSource(source, "html");
374
- } catch (error) {
375
- if (error instanceof MotionPathTrimSourceError) {
376
- sourceError("invalid-path-trim", error.message);
377
- }
378
- throw error;
1681
+ let candidates = nodes.filter(
1682
+ (node) => node.tagName && matchesHtmlSelector(node, parsed),
1683
+ );
1684
+ for (const step of traversal?.steps ?? []) {
1685
+ candidates = candidates.flatMap((node) => {
1686
+ const parent = node.parentNode;
1687
+ const siblings = (parent?.childNodes ?? []).filter(
1688
+ (sibling) => sibling.tagName !== undefined,
1689
+ );
1690
+ const index = siblings.indexOf(node);
1691
+ const nextIndex = index + (step === "nextelementsibling" ? 1 : -1);
1692
+ const sibling = siblings[nextIndex];
1693
+ return sibling ? [sibling] : [];
1694
+ });
379
1695
  }
1696
+
1697
+ return motionTargets.some((target) => candidates.includes(target));
380
1698
  }
381
1699
 
382
- function assertSafeTargetId(targetNodeId: string): void {
383
- if (!SAFE_NODE_ID.test(targetNodeId)) {
384
- sourceError(
385
- "unsupported-target",
386
- `Motion target ID "${targetNodeId}" is not a safe static HTML node ID.`,
1700
+ function findRuntimeStyleMutationInScope(
1701
+ scope: HtmlNode,
1702
+ ): { name: string; value: string } | undefined {
1703
+ const pending = [...(scope.childNodes ?? [])];
1704
+ if (scope.content) pending.push(scope.content);
1705
+
1706
+ while (pending.length > 0) {
1707
+ const node = pending.pop()!;
1708
+ if (node.insideTemplate || node.tagName === "template") continue;
1709
+
1710
+ const attribute = node.attrs?.find(({ name, value }) =>
1711
+ isRuntimeStyleMutationHandler(name, value),
387
1712
  );
1713
+ if (attribute) return attribute;
1714
+
1715
+ pending.push(...(node.childNodes ?? []));
1716
+ if (node.content) pending.push(node.content);
388
1717
  }
1718
+
1719
+ return undefined;
389
1720
  }
390
1721
 
391
1722
  function assertSafeSourceFilePath(sourceFilePath: string): string {
@@ -538,9 +1869,23 @@ function assertTargetsExist(
538
1869
  source: string,
539
1870
  targetNodeIds: readonly string[],
540
1871
  sourceFilePath?: string,
1872
+ checkExecutableScripts = true,
1873
+ checkEditSafety = true,
541
1874
  ): void {
542
1875
  if (targetNodeIds.length === 0) return;
543
1876
  const { nodes } = parseHtml(source);
1877
+ const generatedTargets = sourceFilePath
1878
+ ? generatedHtmlTargets(source, sourceFilePath)
1879
+ : [];
1880
+ const generatedIds = new Map(
1881
+ generatedTargets.map((target) => [
1882
+ target.node.sourceCodeLocation?.startTag?.startOffset,
1883
+ target.nodeId,
1884
+ ]),
1885
+ );
1886
+ if (checkExecutableScripts) {
1887
+ assertExecutableScriptsSafe(source, nodes, targetNodeIds, generatedIds);
1888
+ }
544
1889
  const targets = new Map<string, HtmlNode[]>();
545
1890
  for (const node of nodes) {
546
1891
  if (!node.tagName) continue;
@@ -558,12 +1903,7 @@ function assertTargetsExist(
558
1903
  targets.set(nodeId, matches);
559
1904
  }
560
1905
  const generatedById = new Map(
561
- sourceFilePath
562
- ? generatedHtmlTargets(source, sourceFilePath).map((target) => [
563
- target.nodeId,
564
- target.node,
565
- ])
566
- : [],
1906
+ generatedTargets.map((target) => [target.nodeId, target.node]),
567
1907
  );
568
1908
 
569
1909
  for (const nodeId of targetNodeIds) {
@@ -606,23 +1946,39 @@ function assertTargetsExist(
606
1946
  `Motion target "${nodeId}" is on a non-rendered ${tagName} element.`,
607
1947
  );
608
1948
  }
609
- const conflictingMotionBinding = target.attrs?.find(
610
- ({ name }) =>
611
- name.startsWith("x-transition") ||
612
- [":style", "x-bind", "x-bind:style", "v-bind:style"].includes(name),
613
- );
614
- if (conflictingMotionBinding) {
615
- sourceError(
616
- "unsupported-target",
617
- `Motion target "${nodeId}" uses ${conflictingMotionBinding.name}, which cannot be synchronized with CSS Motion yet.`,
618
- );
619
- }
620
- const inlineStyle = target.attrs?.find(({ name }) => name === "style");
621
- if (inlineStyle && hasInlineMotionDeclarations(inlineStyle.value)) {
622
- sourceError(
623
- "unsupported-target",
624
- `Motion target "${nodeId}" uses style, which cannot be synchronized with CSS Motion yet.`,
1949
+ if (checkEditSafety) {
1950
+ const conflictingMotionBinding = target.attrs?.find(
1951
+ ({ name }) =>
1952
+ name.startsWith("x-transition") ||
1953
+ [":style", "x-bind", "x-bind:style", "v-bind:style"].includes(name),
625
1954
  );
1955
+ if (conflictingMotionBinding) {
1956
+ sourceError(
1957
+ "unsupported-target",
1958
+ `Motion target "${nodeId}" uses ${conflictingMotionBinding.name}, which cannot be synchronized with CSS Motion yet.`,
1959
+ );
1960
+ }
1961
+ const runtimeStyleMutation = findRuntimeStyleMutationAttribute(target);
1962
+ if (runtimeStyleMutation) {
1963
+ const { attribute, location } = runtimeStyleMutation;
1964
+ const runtimeScope =
1965
+ location === "target"
1966
+ ? `uses ${attribute.name}`
1967
+ : location === "ancestor"
1968
+ ? `has an ancestor using ${attribute.name}`
1969
+ : `is in a runtime scope using ${attribute.name}`;
1970
+ sourceError(
1971
+ "unsupported-target",
1972
+ `Motion target "${nodeId}" ${runtimeScope}, whose runtime expression cannot be proven free of style mutations.`,
1973
+ );
1974
+ }
1975
+ const inlineStyle = target.attrs?.find(({ name }) => name === "style");
1976
+ if (inlineStyle && hasInlineMotionDeclarations(inlineStyle.value)) {
1977
+ sourceError(
1978
+ "unsupported-target",
1979
+ `Motion target "${nodeId}" uses style, which cannot be synchronized with CSS Motion yet.`,
1980
+ );
1981
+ }
626
1982
  }
627
1983
  }
628
1984
  }
@@ -685,10 +2041,57 @@ interface AuthoredHtmlMotion {
685
2041
  rangesToRemove: MotionAuthoredCssSourceRange[];
686
2042
  }
687
2043
 
2044
+ function htmlStyleTextMatchesSource(
2045
+ parsedText: string,
2046
+ sourceText: string,
2047
+ ): boolean {
2048
+ const normalizeLineEndings = (text: string) => text.replace(/\r\n?/g, "\n");
2049
+ return normalizeLineEndings(parsedText) === normalizeLineEndings(sourceText);
2050
+ }
2051
+
2052
+ function staticAuthoredHtmlStyleContents(
2053
+ source: string,
2054
+ nodes: readonly HtmlNode[],
2055
+ ): string[] {
2056
+ return nodes.flatMap((node) => {
2057
+ if (
2058
+ node.tagName !== "style" ||
2059
+ node.insideTemplate ||
2060
+ !isPotentialCssStyle(node) ||
2061
+ attributes(node, MANAGED_STYLE_ATTRIBUTE).length > 0
2062
+ ) {
2063
+ return [];
2064
+ }
2065
+
2066
+ const location = node.sourceCodeLocation;
2067
+ const contentStart = location?.startTag?.endOffset;
2068
+ const contentEnd = location?.endTag?.startOffset;
2069
+ if (
2070
+ contentStart === undefined ||
2071
+ contentEnd === undefined ||
2072
+ contentStart < 0 ||
2073
+ contentEnd < contentStart ||
2074
+ contentEnd > source.length
2075
+ ) {
2076
+ return [];
2077
+ }
2078
+
2079
+ const css = source.slice(contentStart, contentEnd);
2080
+ if (
2081
+ !htmlStyleTextMatchesSource(textContent(node), css) ||
2082
+ hasCssTemplateInterpolation(css)
2083
+ ) {
2084
+ return [];
2085
+ }
2086
+ return [css];
2087
+ });
2088
+ }
2089
+
688
2090
  function readAuthoredHtmlMotion(
689
2091
  source: string,
690
2092
  nodes: HtmlNode[],
691
2093
  sourceFilePath?: string,
2094
+ skipUnresolvedStylesheetCheck = false,
692
2095
  ): AuthoredHtmlMotion {
693
2096
  const generatedIds = sourceFilePath
694
2097
  ? new Map(
@@ -703,6 +2106,7 @@ function readAuthoredHtmlMotion(
703
2106
  const importedTargetNodeIds = new Set<string>();
704
2107
  const usedKeyframeNames = new Set<string>();
705
2108
  const keyframeNameCounts = new Map<string, number>();
2109
+ const transitionCssBlocks = staticAuthoredHtmlStyleContents(source, nodes);
706
2110
  let durationMs = 1;
707
2111
 
708
2112
  for (const node of nodes) {
@@ -727,7 +2131,7 @@ function readAuthoredHtmlMotion(
727
2131
  continue;
728
2132
  }
729
2133
  const css = source.slice(contentStart, contentEnd);
730
- if (textContent(node) !== css) {
2134
+ if (!htmlStyleTextMatchesSource(textContent(node), css)) {
731
2135
  if (/(?:^|[;{\s])(?:-?[a-z]+-)?animation(?:-[a-z-]+)?\s*:/i.test(css)) {
732
2136
  sourceError(
733
2137
  "unsupported-authored-motion",
@@ -736,8 +2140,11 @@ function readAuthoredHtmlMotion(
736
2140
  }
737
2141
  continue;
738
2142
  }
739
- const result = importAuthoredCssAnimations(css, (selector) =>
740
- resolveAuthoredHtmlTarget(source, nodes, selector, generatedIds),
2143
+ const result = importAuthoredCssAnimations(
2144
+ css,
2145
+ (selector) =>
2146
+ resolveAuthoredHtmlTarget(source, nodes, selector, generatedIds),
2147
+ { transitionCssBlocks },
741
2148
  );
742
2149
  if (result.status === "absent") {
743
2150
  for (const name of result.definedKeyframeNames) {
@@ -748,10 +2155,10 @@ function readAuthoredHtmlMotion(
748
2155
  if (result.status === "unsupported") {
749
2156
  sourceError("unsupported-authored-motion", result.reason);
750
2157
  }
751
- if (hasUnresolvedStylesheet(nodes)) {
2158
+ if (!skipUnresolvedStylesheetCheck && hasUnresolvedStylesheet(nodes)) {
752
2159
  sourceError(
753
2160
  "unresolved-stylesheet",
754
- "Resolve external stylesheets before editing or adding Motion.",
2161
+ "Resolve external or runtime-generated styles before editing or adding Motion.",
755
2162
  );
756
2163
  }
757
2164
  for (const name of result.definedKeyframeNames) {
@@ -817,7 +2224,11 @@ function readAuthoredHtmlMotion(
817
2224
  }
818
2225
 
819
2226
  function hasUnresolvedStylesheet(nodes: readonly HtmlNode[]): boolean {
820
- return hasLinkedStylesheet(nodes) || hasImportedStylesheet(nodes);
2227
+ return (
2228
+ hasLinkedStylesheet(nodes) ||
2229
+ hasImportedStylesheet(nodes) ||
2230
+ hasDynamicStyleContent(nodes)
2231
+ );
821
2232
  }
822
2233
 
823
2234
  function dynamicBoundAttributeName(attributeName: string): string | null {
@@ -834,6 +2245,9 @@ function hasLinkedStylesheet(nodes: readonly HtmlNode[]): boolean {
834
2245
  if (node.tagName !== "link" || node.insideTemplate) return false;
835
2246
  const relAttributes = attributes(node, "rel");
836
2247
  const hrefAttributes = attributes(node, "href");
2248
+ const relTokens = relAttributes.flatMap((attribute) =>
2249
+ attribute.value.split(/[\t\n\f\r ]+/).map((value) => value.toLowerCase()),
2250
+ );
837
2251
  const objectBinding = (node.attrs ?? []).some((attribute) =>
838
2252
  ["x-bind", "v-bind"].includes(attribute.name.toLowerCase()),
839
2253
  );
@@ -849,17 +2263,93 @@ function hasLinkedStylesheet(nodes: readonly HtmlNode[]): boolean {
849
2263
  hrefAttributes.some((attribute) => attribute.value.trim()) ||
850
2264
  hasBoundAttribute("href") ||
851
2265
  objectBinding;
852
- const stylesheetRel = relAttributes.some((attribute) =>
853
- attribute.value
854
- .split(/[\t\n\f\r ]+/)
855
- .some((value) => value.toLowerCase() === "stylesheet"),
856
- );
2266
+ const stylesheetRel = relTokens.includes("stylesheet");
2267
+ const stylePreload =
2268
+ relTokens.includes("preload") &&
2269
+ (attributes(node, "as").some(
2270
+ (attribute) => attribute.value.toLowerCase() === "style",
2271
+ ) ||
2272
+ hasBoundAttribute("as") ||
2273
+ objectBinding) &&
2274
+ (attributes(node, "onload").length > 0 ||
2275
+ hasBoundAttribute("onload") ||
2276
+ objectBinding);
857
2277
  return (
858
- hasHref && (stylesheetRel || hasBoundAttribute("rel") || objectBinding)
2278
+ hasHref &&
2279
+ (stylesheetRel ||
2280
+ hasBoundAttribute("rel") ||
2281
+ stylePreload ||
2282
+ objectBinding)
859
2283
  );
860
2284
  });
861
2285
  }
862
2286
 
2287
+ function hasDynamicStyleContent(nodes: readonly HtmlNode[]): boolean {
2288
+ return nodes.some((node) => {
2289
+ if (node.tagName !== "style" || node.insideTemplate) return false;
2290
+ if (
2291
+ hasPotentialRuntimeMutationHandler(node.attrs) ||
2292
+ (node.attrs ?? []).some(({ name }) => {
2293
+ const normalized = name.toLowerCase();
2294
+ return (
2295
+ (normalized.startsWith("x-") && normalized !== "x-cloak") ||
2296
+ (normalized.startsWith("v-") && normalized !== "v-cloak") ||
2297
+ (normalized.startsWith("ng-") && normalized !== "ng-cloak") ||
2298
+ normalized.startsWith(":")
2299
+ );
2300
+ })
2301
+ ) {
2302
+ return true;
2303
+ }
2304
+ if (!isPotentialCssStyle(node)) return false;
2305
+ if (hasCssTemplateInterpolation(textContent(node))) {
2306
+ return true;
2307
+ }
2308
+ return (node.attrs ?? []).some((attribute) => {
2309
+ const name = attribute.name.toLowerCase();
2310
+ return (
2311
+ [
2312
+ "x-text",
2313
+ "x-html",
2314
+ "v-text",
2315
+ "v-html",
2316
+ "ng-bind",
2317
+ "ng-bind-html",
2318
+ ].includes(name) ||
2319
+ ["innerhtml", "textcontent"].includes(
2320
+ dynamicBoundAttributeName(name) ?? "",
2321
+ )
2322
+ );
2323
+ });
2324
+ });
2325
+ }
2326
+
2327
+ function hasCssTemplateInterpolation(css: string): boolean {
2328
+ let quote: "'" | '"' | null = null;
2329
+ let escaped = false;
2330
+
2331
+ for (let index = 0; index < css.length; index++) {
2332
+ const character = css[index]!;
2333
+ if (quote) {
2334
+ if (escaped) escaped = false;
2335
+ else if (character === "\\") escaped = true;
2336
+ else if (character === quote) quote = null;
2337
+ continue;
2338
+ }
2339
+ if (character === "'" || character === '"') {
2340
+ quote = character;
2341
+ } else if (
2342
+ css.startsWith("{{", index) ||
2343
+ css.startsWith("${", index) ||
2344
+ css.startsWith("<%", index)
2345
+ ) {
2346
+ return true;
2347
+ }
2348
+ }
2349
+
2350
+ return false;
2351
+ }
2352
+
863
2353
  function hasImportedStylesheet(nodes: readonly HtmlNode[]): boolean {
864
2354
  for (const node of nodes) {
865
2355
  if (
@@ -907,6 +2397,21 @@ function isPotentialCssStyle(node: HtmlNode): boolean {
907
2397
  function isSupportedAuthoredHtmlStyle(node: HtmlNode): boolean {
908
2398
  for (const attribute of node.attrs ?? []) {
909
2399
  const name = attribute.name.toLowerCase();
2400
+ if (
2401
+ [
2402
+ "x-text",
2403
+ "x-html",
2404
+ "v-text",
2405
+ "v-html",
2406
+ "ng-bind",
2407
+ "ng-bind-html",
2408
+ ].includes(name) ||
2409
+ ["innerhtml", "textcontent"].includes(
2410
+ dynamicBoundAttributeName(name) ?? "",
2411
+ )
2412
+ ) {
2413
+ return false;
2414
+ }
910
2415
  if (
911
2416
  /^(?::|x-bind:|v-bind:)(?:media|disabled|title|scoped|type)$/.test(name)
912
2417
  ) {
@@ -982,6 +2487,7 @@ function isDynamicHtmlAttribute(
982
2487
  const normalized = name.toLowerCase();
983
2488
  return (
984
2489
  normalized === "x-bind" ||
2490
+ normalized === "v-bind" ||
985
2491
  [
986
2492
  `:${selectorAttribute}`,
987
2493
  `x-bind:${selectorAttribute}`,
@@ -1018,9 +2524,15 @@ function matchesHtmlSelector(
1018
2524
  function removeAuthoredHtmlMotion(
1019
2525
  source: string,
1020
2526
  sourceFilePath?: string,
2527
+ cleanupOnly = false,
1021
2528
  ): string {
1022
2529
  const { nodes } = parseHtml(source);
1023
- const authored = readAuthoredHtmlMotion(source, nodes, sourceFilePath);
2530
+ const authored = readAuthoredHtmlMotion(
2531
+ source,
2532
+ nodes,
2533
+ sourceFilePath,
2534
+ cleanupOnly,
2535
+ );
1024
2536
  return authored.rangesToRemove.length > 0
1025
2537
  ? removeAuthoredCssSourceRanges(source, authored.rangesToRemove)
1026
2538
  : source;
@@ -1029,6 +2541,10 @@ function removeAuthoredHtmlMotion(
1029
2541
  function readSource(
1030
2542
  source: string,
1031
2543
  sourceFilePath?: string,
2544
+ options: {
2545
+ allowUnmanagedExecutableScripts?: boolean;
2546
+ cleanupOnly?: boolean;
2547
+ } = {},
1032
2548
  ): Exclude<MotionHtmlSourceReadResult, { status: "invalid" }> {
1033
2549
  const { nodes } = parseHtml(source);
1034
2550
  assertMotionTextOutsideTemplates(nodes);
@@ -1062,7 +2578,12 @@ function readSource(
1062
2578
  );
1063
2579
  }
1064
2580
  }
1065
- const authoredMotion = readAuthoredHtmlMotion(source, nodes, sourceFilePath);
2581
+ const authoredMotion = readAuthoredHtmlMotion(
2582
+ source,
2583
+ nodes,
2584
+ sourceFilePath,
2585
+ options.cleanupOnly,
2586
+ );
1066
2587
  if (authoredMotion.document) {
1067
2588
  try {
1068
2589
  document = mergeAuthoredCssMotionDocument(
@@ -1079,14 +2600,24 @@ function readSource(
1079
2600
  }
1080
2601
  }
1081
2602
  const textDocument = readMotionText(source);
1082
- if ((document || textDocument) && hasUnresolvedStylesheet(nodes)) {
2603
+ if (
2604
+ !options.cleanupOnly &&
2605
+ (document || textDocument) &&
2606
+ hasUnresolvedStylesheet(nodes)
2607
+ ) {
1083
2608
  sourceError(
1084
2609
  "unresolved-stylesheet",
1085
- "Resolve external stylesheets before editing or adding Motion.",
2610
+ "Resolve external or runtime-generated styles before editing or adding Motion.",
1086
2611
  );
1087
2612
  }
1088
2613
  const targetNodeIds = targetsFromDocuments(document, textDocument);
1089
- assertTargetsExist(source, targetNodeIds, sourceFilePath);
2614
+ assertTargetsExist(
2615
+ source,
2616
+ targetNodeIds,
2617
+ sourceFilePath,
2618
+ !options.allowUnmanagedExecutableScripts,
2619
+ !options.cleanupOnly,
2620
+ );
1090
2621
  const pathTrimmedSource = applyPathTrimTargets(
1091
2622
  source,
1092
2623
  pathTrimTargetsFromDocument(document),
@@ -1098,10 +2629,10 @@ function readSource(
1098
2629
  );
1099
2630
  }
1100
2631
  if (!document && !textDocument) {
1101
- if (hasUnresolvedStylesheet(nodes)) {
2632
+ if (!options.cleanupOnly && hasUnresolvedStylesheet(nodes)) {
1102
2633
  sourceError(
1103
2634
  "unresolved-stylesheet",
1104
- "Resolve external stylesheets before editing or adding Motion.",
2635
+ "Resolve external or runtime-generated styles before editing or adding Motion.",
1105
2636
  );
1106
2637
  }
1107
2638
  return { status: "absent" };
@@ -1382,10 +2913,33 @@ export function removeMotionHtmlSource(
1382
2913
  source: string,
1383
2914
  sourceFilePath?: string,
1384
2915
  ): MotionHtmlSourceRemoval {
1385
- const read = readSource(source, sourceFilePath);
2916
+ const read = readSource(source, sourceFilePath, {
2917
+ allowUnmanagedExecutableScripts: true,
2918
+ cleanupOnly: true,
2919
+ });
1386
2920
  if (read.status === "absent") return { source, changed: false };
1387
2921
 
1388
- const withoutAuthored = removeAuthoredHtmlMotion(source, sourceFilePath);
2922
+ const managedRuntimeScripts = parseHtml(source).nodes.filter(
2923
+ (node) =>
2924
+ node.tagName === "script" &&
2925
+ attributes(node, MOTION_TEXT_RUNTIME_ATTRIBUTE).length > 0,
2926
+ );
2927
+ if (
2928
+ managedRuntimeScripts.some(
2929
+ (node) => !isManagedMotionTextRuntime(source, node),
2930
+ )
2931
+ ) {
2932
+ sourceError(
2933
+ "invalid-motion-text",
2934
+ "The managed Motion Text runtime does not match a supported generated version.",
2935
+ );
2936
+ }
2937
+
2938
+ const withoutAuthored = removeAuthoredHtmlMotion(
2939
+ source,
2940
+ sourceFilePath,
2941
+ true,
2942
+ );
1389
2943
  const { nodes } = parseHtml(withoutAuthored);
1390
2944
  const managedStyle = findManagedStyle(withoutAuthored, nodes);
1391
2945
  let withoutStyle = withoutAuthored;