@hyperframes/lint 0.7.58 → 0.7.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser.js +77 -25
- package/dist/browser.js.map +1 -1
- package/dist/index.js +81 -27
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/browser.js
CHANGED
|
@@ -57,7 +57,7 @@ function findHtmlTag(tags) {
|
|
|
57
57
|
function findRootTag(source, parsedTags) {
|
|
58
58
|
const tags = parsedTags ?? parseHtmlStructure(source).tags;
|
|
59
59
|
const bodyTag = tags.find((tag) => tag.name === "body");
|
|
60
|
-
if (bodyTag && (
|
|
60
|
+
if (bodyTag && (readDecodedAttr(bodyTag.raw, "data-composition-id") || readAttr(bodyTag.raw, "data-width") || readAttr(bodyTag.raw, "data-height"))) {
|
|
61
61
|
return bodyTag;
|
|
62
62
|
}
|
|
63
63
|
const bodyStart = bodyTag ? bodyTag.index + bodyTag.raw.length : 0;
|
|
@@ -67,7 +67,7 @@ function findRootTag(source, parsedTags) {
|
|
|
67
67
|
for (const tag of bodyTags) {
|
|
68
68
|
if (tag.index < skipBefore) continue;
|
|
69
69
|
if (["script", "style", "meta", "link", "title"].includes(tag.name)) continue;
|
|
70
|
-
if (tag.name === "svg" && !
|
|
70
|
+
if (tag.name === "svg" && !readDecodedAttr(tag.raw, "data-composition-id") && !readAttr(tag.raw, "data-width") && !readAttr(tag.raw, "data-height")) {
|
|
71
71
|
skipBefore = tag.endIndex ?? Infinity;
|
|
72
72
|
continue;
|
|
73
73
|
}
|
|
@@ -81,6 +81,20 @@ function readAttr(tagSource, attr) {
|
|
|
81
81
|
const match = tagSource.match(new RegExp(`(?<![\\w-])${escaped}\\s*=\\s*["']([^"']+)["']`, "i"));
|
|
82
82
|
return match?.[1] || null;
|
|
83
83
|
}
|
|
84
|
+
function readDecodedAttr(tagSource, attr) {
|
|
85
|
+
if (!tagSource) return null;
|
|
86
|
+
let value = null;
|
|
87
|
+
const parser = new Parser(
|
|
88
|
+
{
|
|
89
|
+
onattribute(name, decodedValue) {
|
|
90
|
+
if (value === null && name.toLowerCase() === attr.toLowerCase()) value = decodedValue;
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
{ decodeEntities: true, lowerCaseAttributeNames: false, lowerCaseTags: true }
|
|
94
|
+
);
|
|
95
|
+
parser.end(tagSource);
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
84
98
|
function readJsonAttr(tagSource, attr) {
|
|
85
99
|
if (!tagSource) return null;
|
|
86
100
|
const escaped = attr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -93,7 +107,7 @@ function readJsonAttr(tagSource, attr) {
|
|
|
93
107
|
function collectCompositionIds(tags) {
|
|
94
108
|
const ids = /* @__PURE__ */ new Set();
|
|
95
109
|
for (const tag of tags) {
|
|
96
|
-
const compId =
|
|
110
|
+
const compId = readDecodedAttr(tag.raw, "data-composition-id");
|
|
97
111
|
if (compId) ids.add(compId);
|
|
98
112
|
}
|
|
99
113
|
return ids;
|
|
@@ -270,7 +284,7 @@ function buildLintContext(html, options = {}) {
|
|
|
270
284
|
const scripts = structure.scripts;
|
|
271
285
|
const compositionIds = collectCompositionIds(tags);
|
|
272
286
|
const rootTag = findRootTag(source, tags);
|
|
273
|
-
const rootCompositionId =
|
|
287
|
+
const rootCompositionId = readDecodedAttr(rootTag?.raw || "", "data-composition-id");
|
|
274
288
|
return {
|
|
275
289
|
source,
|
|
276
290
|
rawSource,
|
|
@@ -306,7 +320,7 @@ function isStudioTimelineElement(tag) {
|
|
|
306
320
|
function describeStudioElement(tag) {
|
|
307
321
|
const parts = [`<${tag.name}`];
|
|
308
322
|
const className = readAttr(tag.raw, "class");
|
|
309
|
-
const compositionId =
|
|
323
|
+
const compositionId = readDecodedAttr(tag.raw, "data-composition-id");
|
|
310
324
|
const dataStart = readAttr(tag.raw, "data-start");
|
|
311
325
|
const dataTrack = readAttr(tag.raw, "data-track-index") ?? readAttr(tag.raw, "data-track");
|
|
312
326
|
if (className) {
|
|
@@ -428,7 +442,7 @@ var coreRules = [
|
|
|
428
442
|
// root_missing_composition_id + root_missing_dimensions
|
|
429
443
|
({ rootTag }) => {
|
|
430
444
|
const findings = [];
|
|
431
|
-
if (!rootTag || !
|
|
445
|
+
if (!rootTag || !readDecodedAttr(rootTag.raw, "data-composition-id")) {
|
|
432
446
|
findings.push({
|
|
433
447
|
code: "root_missing_composition_id",
|
|
434
448
|
severity: "error",
|
|
@@ -504,15 +518,10 @@ var coreRules = [
|
|
|
504
518
|
return findings;
|
|
505
519
|
},
|
|
506
520
|
// timeline_id_mismatch
|
|
507
|
-
({ source }) => {
|
|
521
|
+
({ source, compositionIds }) => {
|
|
508
522
|
const findings = [];
|
|
509
|
-
const htmlCompIds =
|
|
523
|
+
const htmlCompIds = new Set(compositionIds);
|
|
510
524
|
const timelineRegKeys = /* @__PURE__ */ new Set();
|
|
511
|
-
const compIdRe = /data-composition-id\s*=\s*["']([^"']+)["']/gi;
|
|
512
|
-
let m;
|
|
513
|
-
while ((m = compIdRe.exec(source)) !== null) {
|
|
514
|
-
if (m[1]) htmlCompIds.add(m[1]);
|
|
515
|
-
}
|
|
516
525
|
for (const key of extractTimelineRegistryKeys(source)) {
|
|
517
526
|
timelineRegKeys.add(key);
|
|
518
527
|
}
|
|
@@ -567,7 +576,7 @@ var coreRules = [
|
|
|
567
576
|
for (const tag of tags) {
|
|
568
577
|
const src = readAttr(tag.raw, "data-composition-src");
|
|
569
578
|
if (!src) continue;
|
|
570
|
-
if (
|
|
579
|
+
if (readDecodedAttr(tag.raw, "data-composition-id")) continue;
|
|
571
580
|
findings.push({
|
|
572
581
|
code: "host_missing_composition_id",
|
|
573
582
|
severity: "error",
|
|
@@ -642,7 +651,7 @@ var coreRules = [
|
|
|
642
651
|
code: "studio_missing_editable_id",
|
|
643
652
|
severity: "warning",
|
|
644
653
|
message: `${descriptor} has no id, so Studio cannot use a stable edit target for its timeline and canvas controls.`,
|
|
645
|
-
selector:
|
|
654
|
+
selector: readDecodedAttr(tag.raw, "data-composition-id") ? `[data-composition-id="${readDecodedAttr(tag.raw, "data-composition-id")}"]` : void 0,
|
|
646
655
|
fixHint: 'Add a stable, human-readable id such as id="hero-title" or id="scene-1-card" to every timeline-visible element you want agents or Studio to edit.',
|
|
647
656
|
snippet: truncateSnippet(tag.raw)
|
|
648
657
|
});
|
|
@@ -1001,7 +1010,7 @@ var mediaRules = [
|
|
|
1001
1010
|
for (const tag of tags) {
|
|
1002
1011
|
if (tag.name === "video" || tag.name === "audio") continue;
|
|
1003
1012
|
if (voidElements.has(tag.name)) continue;
|
|
1004
|
-
if (
|
|
1013
|
+
if (readDecodedAttr(tag.raw, "data-composition-id")) continue;
|
|
1005
1014
|
if (readAttr(tag.raw, "data-start")) {
|
|
1006
1015
|
timedTagPositions.push({
|
|
1007
1016
|
name: tag.name,
|
|
@@ -1247,6 +1256,10 @@ async function loadParseGsapScript() {
|
|
|
1247
1256
|
}
|
|
1248
1257
|
var SCENE_BOUNDARY_EPSILON_SECONDS = 0.05;
|
|
1249
1258
|
var UNRESOLVED_TARGET = "__unresolved__";
|
|
1259
|
+
function targetHasNoStableIdentity(selector, identity) {
|
|
1260
|
+
if (identity) return false;
|
|
1261
|
+
return selector === UNRESOLVED_TARGET || selector === "dwell/hold" || selector.startsWith("proxy \u2192 ");
|
|
1262
|
+
}
|
|
1250
1263
|
function countClassUsage(tags) {
|
|
1251
1264
|
const counts = /* @__PURE__ */ new Map();
|
|
1252
1265
|
for (const tag of tags) {
|
|
@@ -1304,6 +1317,7 @@ async function extractGsapWindows(script) {
|
|
|
1304
1317
|
const effectiveDuration = animation.method === "set" ? 0 : (animation.duration ?? 0) * cycleCount;
|
|
1305
1318
|
windows.push({
|
|
1306
1319
|
targetSelector: animation.targetSelector,
|
|
1320
|
+
targetIdentity: animation.targetIdentity,
|
|
1307
1321
|
position: animation.position,
|
|
1308
1322
|
end: animation.position + effectiveDuration,
|
|
1309
1323
|
properties: Object.keys(animation.properties),
|
|
@@ -1395,7 +1409,7 @@ function findTagEnd(source, tag) {
|
|
|
1395
1409
|
}
|
|
1396
1410
|
function collectCompositionRanges(source, tags) {
|
|
1397
1411
|
return tags.map((tag) => {
|
|
1398
|
-
const id =
|
|
1412
|
+
const id = readDecodedAttr(tag.raw, "data-composition-id");
|
|
1399
1413
|
if (!id) return null;
|
|
1400
1414
|
return {
|
|
1401
1415
|
id,
|
|
@@ -1628,12 +1642,14 @@ var gsapRules = [
|
|
|
1628
1642
|
const left = gsapWindows[i];
|
|
1629
1643
|
if (!left) continue;
|
|
1630
1644
|
if (left.end <= left.position) continue;
|
|
1631
|
-
if (left.targetSelector
|
|
1645
|
+
if (targetHasNoStableIdentity(left.targetSelector, left.targetIdentity)) continue;
|
|
1632
1646
|
for (let j = i + 1; j < gsapWindows.length; j++) {
|
|
1633
1647
|
const right = gsapWindows[j];
|
|
1634
1648
|
if (!right) continue;
|
|
1635
1649
|
if (right.end <= right.position) continue;
|
|
1636
|
-
|
|
1650
|
+
const leftIdentity = left.targetIdentity ?? left.targetSelector;
|
|
1651
|
+
const rightIdentity = right.targetIdentity ?? right.targetSelector;
|
|
1652
|
+
if (leftIdentity !== rightIdentity) continue;
|
|
1637
1653
|
const overlapStart = Math.max(left.position, right.position);
|
|
1638
1654
|
const overlapEnd = Math.min(left.end, right.end);
|
|
1639
1655
|
if (overlapEnd <= overlapStart) continue;
|
|
@@ -2404,6 +2420,7 @@ import { COMPOSITION_VARIABLE_TYPES } from "@hyperframes/parsers/composition";
|
|
|
2404
2420
|
var MAX_COMPOSITION_LINES = 300;
|
|
2405
2421
|
var MAX_TIMED_ELEMENTS_PER_TRACK = 3;
|
|
2406
2422
|
var TRACK_DENSITY_EXEMPT_TAGS = /* @__PURE__ */ new Set(["audio", "script", "style", "video"]);
|
|
2423
|
+
var CAPTION_CUE_TOKEN = /^(?:caption(?:[-_](?:group|word|line|block|cue|text))?|subtitle(?:[-_](?:group|line|cue|text))?|cg-.+)$/i;
|
|
2407
2424
|
var OVERLAP_EPSILON_SECONDS = 1e-6;
|
|
2408
2425
|
function countPhysicalLines(source) {
|
|
2409
2426
|
if (source.length === 0) return 0;
|
|
@@ -2414,6 +2431,11 @@ function countPhysicalLines(source) {
|
|
|
2414
2431
|
function countStructuralLines(source) {
|
|
2415
2432
|
return countPhysicalLines(source.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, "<style></style>"));
|
|
2416
2433
|
}
|
|
2434
|
+
function isCaptionCue(tag) {
|
|
2435
|
+
const classTokens = (readAttr(tag.raw, "class") || "").split(/\s+/).filter(Boolean);
|
|
2436
|
+
const id = readAttr(tag.raw, "id");
|
|
2437
|
+
return classTokens.some((token) => CAPTION_CUE_TOKEN.test(token)) || Boolean(id && CAPTION_CUE_TOKEN.test(id));
|
|
2438
|
+
}
|
|
2417
2439
|
function isRegistrySourceFile(filePath) {
|
|
2418
2440
|
if (!filePath) return false;
|
|
2419
2441
|
const normalized = filePath.replace(/\\/g, "/");
|
|
@@ -2424,7 +2446,7 @@ function isRegistryInstalledFile(rawSource) {
|
|
|
2424
2446
|
}
|
|
2425
2447
|
function isCompositionRootOrMount(rawTag) {
|
|
2426
2448
|
return Boolean(
|
|
2427
|
-
|
|
2449
|
+
readDecodedAttr(rawTag, "data-composition-id") || readAttr(rawTag, "data-composition-src")
|
|
2428
2450
|
);
|
|
2429
2451
|
}
|
|
2430
2452
|
function extractCssUrlReferences(css) {
|
|
@@ -2500,7 +2522,36 @@ function declaredIdsForBindingCheck(tags) {
|
|
|
2500
2522
|
if (declared.size === 0 && !findHtmlTag(tags)) return null;
|
|
2501
2523
|
return declared;
|
|
2502
2524
|
}
|
|
2525
|
+
function isInsideInertTemplate(tag, tags) {
|
|
2526
|
+
return tags.some(
|
|
2527
|
+
(candidate) => candidate.name === "template" && candidate.closeIndex != null && tag.index > candidate.index && tag.index < candidate.closeIndex
|
|
2528
|
+
);
|
|
2529
|
+
}
|
|
2503
2530
|
var compositionRules = [
|
|
2531
|
+
// duplicate_composition_id catches meta-tag/root collisions that create duplicate composition entries.
|
|
2532
|
+
({ tags }) => {
|
|
2533
|
+
const tagsByCompositionId = /* @__PURE__ */ new Map();
|
|
2534
|
+
for (const tag of tags) {
|
|
2535
|
+
if (isInsideInertTemplate(tag, tags)) continue;
|
|
2536
|
+
const compositionId = readDecodedAttr(tag.raw, "data-composition-id");
|
|
2537
|
+
if (!compositionId || compositionId.trim().length === 0) continue;
|
|
2538
|
+
const matchingTags = tagsByCompositionId.get(compositionId) ?? [];
|
|
2539
|
+
matchingTags.push(tag.raw);
|
|
2540
|
+
tagsByCompositionId.set(compositionId, matchingTags);
|
|
2541
|
+
}
|
|
2542
|
+
const findings = [];
|
|
2543
|
+
for (const [compositionId, matchingTags] of tagsByCompositionId) {
|
|
2544
|
+
if (matchingTags.length < 2) continue;
|
|
2545
|
+
findings.push({
|
|
2546
|
+
code: "duplicate_composition_id",
|
|
2547
|
+
severity: "error",
|
|
2548
|
+
message: `Composition id "${compositionId}" is used by ${matchingTags.length} elements. Each data-composition-id value must be unique within a composition file.`,
|
|
2549
|
+
fixHint: "Keep data-composition-id on exactly one element, the composition root. Remove it from metadata or duplicate hosts, especially a <meta> tag carrying the same data-composition-id as the root <div>, which causes a silent duplicate-id collision.",
|
|
2550
|
+
snippet: truncateSnippet(matchingTags[0] ?? "")
|
|
2551
|
+
});
|
|
2552
|
+
}
|
|
2553
|
+
return findings;
|
|
2554
|
+
},
|
|
2504
2555
|
// invalid_parent_traversal_in_asset_path — catches `../` traversal in src,
|
|
2505
2556
|
// href, inline-style url(), and <style> url() asset references on
|
|
2506
2557
|
// compositions. Sub-compositions live under compositions/ but are served
|
|
@@ -2575,6 +2626,7 @@ var compositionRules = [
|
|
|
2575
2626
|
const trackCounts = /* @__PURE__ */ new Map();
|
|
2576
2627
|
for (const tag of tags) {
|
|
2577
2628
|
if (TRACK_DENSITY_EXEMPT_TAGS.has(tag.name)) continue;
|
|
2629
|
+
if (isCaptionCue(tag)) continue;
|
|
2578
2630
|
if (isCompositionRootOrMount(tag.raw)) continue;
|
|
2579
2631
|
if (!readAttr(tag.raw, "data-start")) continue;
|
|
2580
2632
|
const track = readAttr(tag.raw, "data-track-index");
|
|
@@ -2601,7 +2653,7 @@ var compositionRules = [
|
|
|
2601
2653
|
for (const tag of tags) {
|
|
2602
2654
|
if (tag.name === "audio" || tag.name === "script" || tag.name === "style") continue;
|
|
2603
2655
|
if (!readAttr(tag.raw, "data-start")) continue;
|
|
2604
|
-
if (
|
|
2656
|
+
if (readDecodedAttr(tag.raw, "data-composition-id")) continue;
|
|
2605
2657
|
if (readAttr(tag.raw, "data-composition-src")) continue;
|
|
2606
2658
|
const classAttr = readAttr(tag.raw, "class") || "";
|
|
2607
2659
|
const styleAttr = readAttr(tag.raw, "style") || "";
|
|
@@ -2701,7 +2753,7 @@ var compositionRules = [
|
|
|
2701
2753
|
const skipTags = /* @__PURE__ */ new Set(["audio", "video", "script", "style", "template"]);
|
|
2702
2754
|
for (const tag of tags) {
|
|
2703
2755
|
if (skipTags.has(tag.name)) continue;
|
|
2704
|
-
if (
|
|
2756
|
+
if (readDecodedAttr(tag.raw, "data-composition-id")) continue;
|
|
2705
2757
|
if (readAttr(tag.raw, "data-composition-src")) continue;
|
|
2706
2758
|
const hasStart = readAttr(tag.raw, "data-start") !== null;
|
|
2707
2759
|
const hasDuration = readAttr(tag.raw, "data-duration") !== null;
|
|
@@ -2767,7 +2819,7 @@ var compositionRules = [
|
|
|
2767
2819
|
const findings = [];
|
|
2768
2820
|
if (options.isSubComposition) return findings;
|
|
2769
2821
|
if (!rootTag) return findings;
|
|
2770
|
-
const compId =
|
|
2822
|
+
const compId = readDecodedAttr(rootTag.raw, "data-composition-id");
|
|
2771
2823
|
if (!compId) return findings;
|
|
2772
2824
|
const hasStart = readAttr(rootTag.raw, "data-start") !== null;
|
|
2773
2825
|
if (!hasStart) {
|
|
@@ -3125,7 +3177,7 @@ var compositionRules = [
|
|
|
3125
3177
|
({ rootTag, scripts, styles, tags, options }) => {
|
|
3126
3178
|
if (options.isSubComposition) return [];
|
|
3127
3179
|
if (!rootTag) return [];
|
|
3128
|
-
if (
|
|
3180
|
+
if (readDecodedAttr(rootTag.raw, "data-composition-id") === null) return [];
|
|
3129
3181
|
if (readAttr(rootTag.raw, "data-duration") !== null) return [];
|
|
3130
3182
|
const allScriptTexts = scripts.map((s) => stripJsComments(s.content));
|
|
3131
3183
|
const hasGsapTimeline = allScriptTexts.some((t) => /gsap\.timeline\s*\(/.test(t));
|
|
@@ -3606,7 +3658,7 @@ function parseTiming(raw) {
|
|
|
3606
3658
|
}
|
|
3607
3659
|
function collectCompositionIdScenes(ctx, seen, out) {
|
|
3608
3660
|
for (const tag of ctx.tags) {
|
|
3609
|
-
const compositionId =
|
|
3661
|
+
const compositionId = readDecodedAttr(tag.raw, "data-composition-id");
|
|
3610
3662
|
if (!compositionId || !isSceneLikeCompositionId(compositionId) || seen.has(compositionId))
|
|
3611
3663
|
continue;
|
|
3612
3664
|
const timing = parseTiming(tag.raw);
|