@hyperframes/lint 0.8.67 → 0.8.69
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.d.ts +1 -0
- package/dist/browser.js +180 -35
- package/dist/browser.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +186 -37
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
package/dist/browser.d.ts
CHANGED
package/dist/browser.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Parser as Parser2 } from "htmlparser2";
|
|
|
3
3
|
|
|
4
4
|
// src/utils.ts
|
|
5
5
|
import { Parser } from "htmlparser2";
|
|
6
|
+
import { parse } from "acorn";
|
|
6
7
|
var COMPOSITION_ID_IN_CSS_PATTERN = /\[data-composition-id=["']([^"']+)["']\]/g;
|
|
7
8
|
var TIMELINE_REGISTRY_INIT_PATTERN = /window\.__timelines\s*=\s*window\.__timelines\s*\|\|\s*\{\}|window\.__timelines\s*=\s*\{\}|window\.__timelines\s*\?\?=\s*\{\}/i;
|
|
8
9
|
var TIMELINE_REGISTRY_OBJECT_LITERAL_PATTERN = /window\.__timelines\s*=\s*\{\s*(?:["'][^"']+["']|[A-Za-z_$][\w$]*)\s*:/i;
|
|
@@ -56,6 +57,7 @@ function parseHtmlStructure(source) {
|
|
|
56
57
|
blocks[name].push({
|
|
57
58
|
attrs: block.attrs,
|
|
58
59
|
content: source.slice(block.contentStart, parser.startIndex),
|
|
60
|
+
contentStart: block.contentStart,
|
|
59
61
|
raw: source.slice(block.index, parser.endIndex + 1),
|
|
60
62
|
index: block.index
|
|
61
63
|
});
|
|
@@ -205,11 +207,17 @@ function readTimelineRegistryTopLevelKeys(source) {
|
|
|
205
207
|
function getInlineScriptSyntaxError(source) {
|
|
206
208
|
if (!source.trim()) return null;
|
|
207
209
|
try {
|
|
208
|
-
|
|
210
|
+
parse(source, {
|
|
211
|
+
ecmaVersion: "latest",
|
|
212
|
+
sourceType: "script",
|
|
213
|
+
allowReturnOutsideFunction: true
|
|
214
|
+
});
|
|
209
215
|
return null;
|
|
210
216
|
} catch (error) {
|
|
211
|
-
|
|
212
|
-
|
|
217
|
+
return {
|
|
218
|
+
message: error instanceof Error ? error.message : String(error),
|
|
219
|
+
offset: error instanceof SyntaxError && "pos" in error && typeof error.pos === "number" ? error.pos : void 0
|
|
220
|
+
};
|
|
213
221
|
}
|
|
214
222
|
}
|
|
215
223
|
function stripStringLiterals(source) {
|
|
@@ -528,7 +536,7 @@ function stripCssComments(source) {
|
|
|
528
536
|
}
|
|
529
537
|
return out;
|
|
530
538
|
}
|
|
531
|
-
function stripHtmlCommentsOnce(source) {
|
|
539
|
+
function stripHtmlCommentsOnce(source, removed) {
|
|
532
540
|
let out = "";
|
|
533
541
|
let i = 0;
|
|
534
542
|
for (; ; ) {
|
|
@@ -537,14 +545,17 @@ function stripHtmlCommentsOnce(source) {
|
|
|
537
545
|
const end = source.indexOf("-->", start + 4);
|
|
538
546
|
if (end < 0) return out + source.slice(i);
|
|
539
547
|
out += source.slice(i, start);
|
|
548
|
+
removed?.(start, end + 3);
|
|
540
549
|
i = end + 3;
|
|
541
550
|
}
|
|
542
551
|
}
|
|
543
|
-
function stripHtmlComments(source) {
|
|
552
|
+
function stripHtmlComments(source, pass) {
|
|
544
553
|
let out = source;
|
|
545
554
|
for (let prev = ""; prev !== out; ) {
|
|
546
555
|
prev = out;
|
|
547
|
-
|
|
556
|
+
const ranges = [];
|
|
557
|
+
out = stripHtmlCommentsOnce(out, pass ? (start, end) => ranges.push([start, end]) : void 0);
|
|
558
|
+
if (ranges.length) pass?.(ranges);
|
|
548
559
|
}
|
|
549
560
|
return out;
|
|
550
561
|
}
|
|
@@ -568,17 +579,17 @@ function truncateSnippet(value, maxLength = 220) {
|
|
|
568
579
|
|
|
569
580
|
// src/sourceLocations.ts
|
|
570
581
|
function normalized(source) {
|
|
571
|
-
|
|
582
|
+
const chars = [];
|
|
572
583
|
const offsets = [];
|
|
584
|
+
let lastWasSpace = false;
|
|
573
585
|
for (let i = 0; i < source.length; i++) {
|
|
574
|
-
const
|
|
575
|
-
if (
|
|
576
|
-
|
|
577
|
-
text += " ";
|
|
578
|
-
} else text += char;
|
|
586
|
+
const isSpace = /\s/.test(source[i]);
|
|
587
|
+
if (isSpace && lastWasSpace) continue;
|
|
588
|
+
chars.push(isSpace ? " " : source[i]);
|
|
579
589
|
offsets.push(i);
|
|
590
|
+
lastWasSpace = isSpace;
|
|
580
591
|
}
|
|
581
|
-
return { text, offsets };
|
|
592
|
+
return { text: chars.join(""), offsets };
|
|
582
593
|
}
|
|
583
594
|
function createSourceLocator(source) {
|
|
584
595
|
const tags = parseHtmlStructure(source).tags;
|
|
@@ -618,10 +629,42 @@ function snippetOffset(source, snippet) {
|
|
|
618
629
|
return source.offsets[index];
|
|
619
630
|
}
|
|
620
631
|
|
|
632
|
+
// src/sourceCoordinates.ts
|
|
633
|
+
function mappedHtmlSource(original) {
|
|
634
|
+
let offsets;
|
|
635
|
+
const source = stripHtmlComments(original, (ranges) => {
|
|
636
|
+
const previous = offsets;
|
|
637
|
+
const length = (previous?.length ?? original.length + 1) - ranges.reduce((n, [a, b]) => n + b - a, 0);
|
|
638
|
+
offsets = new Uint32Array(length);
|
|
639
|
+
let target = 0;
|
|
640
|
+
let start = 0;
|
|
641
|
+
const spans = [
|
|
642
|
+
...ranges,
|
|
643
|
+
[previous ? previous.length : original.length + 1, 0]
|
|
644
|
+
];
|
|
645
|
+
for (const [a, b] of spans) {
|
|
646
|
+
for (let i = start; i < a; i++) offsets[target++] = previous ? previous[i] : i;
|
|
647
|
+
start = b;
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
return { source, originalOffset: (index) => offsets ? offsets[index] : index };
|
|
651
|
+
}
|
|
652
|
+
function sourcePosition(source, offset, file) {
|
|
653
|
+
if (!Number.isInteger(offset) || offset < 0 || offset > source.length) return {};
|
|
654
|
+
const before = source.slice(0, offset);
|
|
655
|
+
return {
|
|
656
|
+
...file ? { file } : {},
|
|
657
|
+
line: before.split(/\r\n|\r|\n/).length,
|
|
658
|
+
column: offset - Math.max(before.lastIndexOf("\n"), before.lastIndexOf("\r"))
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
|
|
621
662
|
// src/context.ts
|
|
622
663
|
function buildLintContext(html, options = {}) {
|
|
623
664
|
const rawSource = html || "";
|
|
624
|
-
|
|
665
|
+
const mapped = mappedHtmlSource(rawSource);
|
|
666
|
+
let source = mapped.source;
|
|
667
|
+
let sourceStart = 0;
|
|
625
668
|
const initialStructure = parseHtmlStructure(source);
|
|
626
669
|
const templateTags = initialStructure.tags.filter(
|
|
627
670
|
(tag) => tag.name === "template" && tag.closeIndex != null
|
|
@@ -634,7 +677,8 @@ function buildLintContext(html, options = {}) {
|
|
|
634
677
|
const template = templateTags[0];
|
|
635
678
|
let structure = initialStructure;
|
|
636
679
|
if (template && !findRootTag(sourceWithoutTemplates)) {
|
|
637
|
-
|
|
680
|
+
sourceStart = template.index + template.raw.length;
|
|
681
|
+
source = source.slice(sourceStart, template.closeIndex);
|
|
638
682
|
structure = parseHtmlStructure(source);
|
|
639
683
|
}
|
|
640
684
|
const tags = structure.tags;
|
|
@@ -644,14 +688,23 @@ function buildLintContext(html, options = {}) {
|
|
|
644
688
|
attrs: `href="${style.href}"`,
|
|
645
689
|
content: style.content,
|
|
646
690
|
raw: style.content,
|
|
647
|
-
index: -1
|
|
691
|
+
index: -1,
|
|
692
|
+
file: style.file ?? style.href
|
|
648
693
|
}))
|
|
649
694
|
];
|
|
650
695
|
const scripts = structure.scripts;
|
|
651
696
|
const compositionIds = collectCompositionIds(tags);
|
|
652
697
|
const rootTag = findRootTag(source, tags);
|
|
653
698
|
const rootCompositionId = readDecodedAttr(rootTag?.raw || "", "data-composition-id");
|
|
699
|
+
const locate = (block, offset) => {
|
|
700
|
+
if (offset === void 0 || offset < 0 || offset > block.content.length) return {};
|
|
701
|
+
if (block.index === -1) return sourcePosition(block.content, offset, block.file);
|
|
702
|
+
if (block.contentStart === void 0) return {};
|
|
703
|
+
const originalOffset = mapped.originalOffset(sourceStart + block.contentStart + offset);
|
|
704
|
+
return originalOffset === void 0 ? {} : sourcePosition(rawSource, originalOffset, options.filePath);
|
|
705
|
+
};
|
|
654
706
|
return {
|
|
707
|
+
locate,
|
|
655
708
|
source,
|
|
656
709
|
rawSource,
|
|
657
710
|
tags,
|
|
@@ -671,7 +724,7 @@ import {
|
|
|
671
724
|
HTML_BODY_CSS_WIDTH_FIRST_RE,
|
|
672
725
|
HTML_BODY_CSS_HEIGHT_FIRST_RE,
|
|
673
726
|
VIEWPORT_META_SIZE_RE
|
|
674
|
-
} from "@hyperframes/parsers";
|
|
727
|
+
} from "@hyperframes/parsers/composition";
|
|
675
728
|
function repeatedDescendantId(selector) {
|
|
676
729
|
let repeated = null;
|
|
677
730
|
const requiredPseudoIds = (pseudo) => {
|
|
@@ -1049,7 +1102,7 @@ var coreRules = [
|
|
|
1049
1102
|
return findings;
|
|
1050
1103
|
},
|
|
1051
1104
|
// CSS selector safety
|
|
1052
|
-
({ styles }) => {
|
|
1105
|
+
({ styles, locate }) => {
|
|
1053
1106
|
const findings = [];
|
|
1054
1107
|
const reportedRepeatedIds = /* @__PURE__ */ new Set();
|
|
1055
1108
|
const reportedHiddenStyleSelectors = /* @__PURE__ */ new Set();
|
|
@@ -1059,6 +1112,10 @@ var coreRules = [
|
|
|
1059
1112
|
root = postcss.parse(style.content);
|
|
1060
1113
|
} catch (error) {
|
|
1061
1114
|
findings.push({
|
|
1115
|
+
...locate(
|
|
1116
|
+
style,
|
|
1117
|
+
error instanceof postcss.CssSyntaxError ? cssErrorOffset(style.content, error) : void 0
|
|
1118
|
+
),
|
|
1062
1119
|
code: "css_parse_error",
|
|
1063
1120
|
severity: "error",
|
|
1064
1121
|
message: `CSS parse error: ${error instanceof Error ? error.message : "unknown"}`
|
|
@@ -1124,7 +1181,7 @@ var coreRules = [
|
|
|
1124
1181
|
];
|
|
1125
1182
|
},
|
|
1126
1183
|
// invalid_inline_script_syntax (JS parse error)
|
|
1127
|
-
({ scripts }) => {
|
|
1184
|
+
({ scripts, locate }) => {
|
|
1128
1185
|
const findings = [];
|
|
1129
1186
|
for (const script of scripts) {
|
|
1130
1187
|
const attrs = script.attrs || "";
|
|
@@ -1135,9 +1192,10 @@ var coreRules = [
|
|
|
1135
1192
|
const syntaxError = getInlineScriptSyntaxError(script.content);
|
|
1136
1193
|
if (!syntaxError) continue;
|
|
1137
1194
|
findings.push({
|
|
1195
|
+
...locate(script, syntaxError.offset),
|
|
1138
1196
|
code: "invalid_inline_script_syntax",
|
|
1139
1197
|
severity: "error",
|
|
1140
|
-
message: `Inline script has invalid syntax: ${syntaxError}`,
|
|
1198
|
+
message: `Inline script has invalid syntax: ${syntaxError.message}`,
|
|
1141
1199
|
fixHint: "Fix the inline script syntax before render verification.",
|
|
1142
1200
|
snippet: truncateSnippet(script.content)
|
|
1143
1201
|
});
|
|
@@ -1203,7 +1261,7 @@ var coreRules = [
|
|
|
1203
1261
|
return findings;
|
|
1204
1262
|
},
|
|
1205
1263
|
// non_deterministic_code
|
|
1206
|
-
({ scripts }) => {
|
|
1264
|
+
({ scripts, locate }) => {
|
|
1207
1265
|
const findings = [];
|
|
1208
1266
|
const patterns = [
|
|
1209
1267
|
{
|
|
@@ -1254,8 +1312,10 @@ var coreRules = [
|
|
|
1254
1312
|
const withoutComments = stripJsComments(script.content);
|
|
1255
1313
|
const executable = stripStringLiterals(withoutComments);
|
|
1256
1314
|
for (const { pattern, label, hint, scansStrings } of patterns) {
|
|
1257
|
-
|
|
1315
|
+
const match = pattern.exec(scansStrings ? withoutComments : executable);
|
|
1316
|
+
if (match) {
|
|
1258
1317
|
findings.push({
|
|
1318
|
+
...locate(script, match.index),
|
|
1259
1319
|
code: "non_deterministic_code",
|
|
1260
1320
|
severity: "error",
|
|
1261
1321
|
message: `Script contains \`${label}\` which produces non-deterministic output. Renders may differ between frames or runs.`,
|
|
@@ -1268,10 +1328,24 @@ var coreRules = [
|
|
|
1268
1328
|
return findings;
|
|
1269
1329
|
}
|
|
1270
1330
|
];
|
|
1331
|
+
function cssErrorOffset(source, error) {
|
|
1332
|
+
if (error.input && "offset" in error.input && typeof error.input.offset === "number")
|
|
1333
|
+
return error.input.offset;
|
|
1334
|
+
const { line, column } = error;
|
|
1335
|
+
if (line === void 0 || column === void 0) return void 0;
|
|
1336
|
+
let offset = 0;
|
|
1337
|
+
let currentLine = 1;
|
|
1338
|
+
for (const match of source.matchAll(/\n/g)) {
|
|
1339
|
+
if (currentLine >= line) break;
|
|
1340
|
+
offset = match.index + 1;
|
|
1341
|
+
currentLine++;
|
|
1342
|
+
}
|
|
1343
|
+
return currentLine === line ? offset + column - 1 : void 0;
|
|
1344
|
+
}
|
|
1271
1345
|
|
|
1272
1346
|
// src/rules/media.ts
|
|
1273
1347
|
import { validateColorGradingContract } from "@hyperframes/parsers/color-grading-contract";
|
|
1274
|
-
import { extractMediaSrcMutations } from "@hyperframes/parsers";
|
|
1348
|
+
import { extractMediaSrcMutations } from "@hyperframes/parsers/composition";
|
|
1275
1349
|
import { parseHTML } from "linkedom";
|
|
1276
1350
|
function tweensVolumeInSameCall(script, id) {
|
|
1277
1351
|
const selector = new RegExp(`#${escapeRegExp(id)}(?![\\w-])`, "g");
|
|
@@ -2130,6 +2204,45 @@ function findAudioGroupCarveAttrFindings(ctx) {
|
|
|
2130
2204
|
return findings;
|
|
2131
2205
|
}
|
|
2132
2206
|
|
|
2207
|
+
// src/scriptPositions.ts
|
|
2208
|
+
import { parse as parse2 } from "acorn";
|
|
2209
|
+
import { simple } from "acorn-walk";
|
|
2210
|
+
var callIndexes = /* @__PURE__ */ new WeakMap();
|
|
2211
|
+
function callsFor(script) {
|
|
2212
|
+
const cached = callIndexes.get(script);
|
|
2213
|
+
if (cached) return cached;
|
|
2214
|
+
const calls = [];
|
|
2215
|
+
try {
|
|
2216
|
+
const ast = parse2(script.content, {
|
|
2217
|
+
ecmaVersion: "latest",
|
|
2218
|
+
sourceType: "script",
|
|
2219
|
+
allowReturnOutsideFunction: true
|
|
2220
|
+
});
|
|
2221
|
+
simple(ast, {
|
|
2222
|
+
CallExpression: (node) => {
|
|
2223
|
+
calls.push(node);
|
|
2224
|
+
}
|
|
2225
|
+
});
|
|
2226
|
+
} catch {
|
|
2227
|
+
}
|
|
2228
|
+
calls.sort((a, b) => a.start - b.start);
|
|
2229
|
+
callIndexes.set(script, calls);
|
|
2230
|
+
return calls;
|
|
2231
|
+
}
|
|
2232
|
+
function gsapCallOffset(script, method) {
|
|
2233
|
+
return callsFor(script).find(
|
|
2234
|
+
({ callee }) => callee.type === "MemberExpression" && !callee.computed && callee.object.type === "Identifier" && callee.object.name === "gsap" && callee.property.type === "Identifier" && callee.property.name === method
|
|
2235
|
+
)?.start;
|
|
2236
|
+
}
|
|
2237
|
+
function containingCallOffset(script, tokenOffset) {
|
|
2238
|
+
let start;
|
|
2239
|
+
for (const call of callsFor(script)) {
|
|
2240
|
+
if (call.start > tokenOffset) break;
|
|
2241
|
+
if (call.end > tokenOffset) start = call.start;
|
|
2242
|
+
}
|
|
2243
|
+
return start;
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2133
2246
|
// src/rules/composition.ts
|
|
2134
2247
|
import { COMPOSITION_VARIABLE_TYPES, isSafeMediaUrl } from "@hyperframes/parsers/composition";
|
|
2135
2248
|
import { COMPOSITION_ATTRIBUTES, readClipTiming } from "@hyperframes/parsers/composition-contract";
|
|
@@ -3339,8 +3452,8 @@ function targetedSelectorTokens(selector) {
|
|
|
3339
3452
|
const compounds = group.trim().split(/[\s>+~]+/).filter(Boolean);
|
|
3340
3453
|
const last = compounds[compounds.length - 1];
|
|
3341
3454
|
if (!last) continue;
|
|
3342
|
-
const
|
|
3343
|
-
if (
|
|
3455
|
+
const simple2 = last.match(/[#.][A-Za-z0-9_-]+/g);
|
|
3456
|
+
if (simple2) for (const token of simple2) tokens.add(token);
|
|
3344
3457
|
}
|
|
3345
3458
|
return tokens;
|
|
3346
3459
|
}
|
|
@@ -3379,7 +3492,7 @@ function scanScriptsForRegexMatches(scripts, pattern, options) {
|
|
|
3379
3492
|
content.length,
|
|
3380
3493
|
match.index + match[0].length + options.contextAfter
|
|
3381
3494
|
);
|
|
3382
|
-
hits.push({ match, snippet: content.slice(contextStart, contextEnd) });
|
|
3495
|
+
hits.push({ match, snippet: content.slice(contextStart, contextEnd), script });
|
|
3383
3496
|
}
|
|
3384
3497
|
}
|
|
3385
3498
|
return hits;
|
|
@@ -4085,19 +4198,20 @@ ${right.raw}`)
|
|
|
4085
4198
|
return findings;
|
|
4086
4199
|
},
|
|
4087
4200
|
// gsap_infinite_repeat
|
|
4088
|
-
({ scripts, rootTag }) => {
|
|
4201
|
+
({ scripts, rootTag, locate }) => {
|
|
4089
4202
|
const findings = [];
|
|
4090
4203
|
const declaredDuration = Number.parseFloat(
|
|
4091
4204
|
rootTag ? readAttr(rootTag.raw, "data-duration") ?? "" : ""
|
|
4092
4205
|
);
|
|
4093
4206
|
const hasFiniteCompositionWindow = Number.isFinite(declaredDuration) && declaredDuration > 0;
|
|
4094
4207
|
const pattern = /repeat\s*:\s*-1(?!\d)/g;
|
|
4095
|
-
for (const { snippet } of scanScriptsForRegexMatches(scripts, pattern, {
|
|
4208
|
+
for (const { snippet, match, script } of scanScriptsForRegexMatches(scripts, pattern, {
|
|
4096
4209
|
stripComments: true,
|
|
4097
4210
|
contextBefore: 60,
|
|
4098
4211
|
contextAfter: 60
|
|
4099
4212
|
})) {
|
|
4100
4213
|
findings.push({
|
|
4214
|
+
...locate(script, containingCallOffset(script, match.index)),
|
|
4101
4215
|
code: "gsap_infinite_repeat",
|
|
4102
4216
|
severity: hasFiniteCompositionWindow ? "warning" : "error",
|
|
4103
4217
|
message: hasFiniteCompositionWindow ? `GSAP tween uses \`repeat: -1\` (infinite), but the composition declares a finite ${declaredDuration}s window. HyperFrames clips deterministic seeking and export to that explicit duration.` : "GSAP tween uses `repeat: -1` (infinite) without a finite composition `data-duration`. The timeline can report an unbounded duration and make render planning fail.",
|
|
@@ -4108,15 +4222,16 @@ ${right.raw}`)
|
|
|
4108
4222
|
return findings;
|
|
4109
4223
|
},
|
|
4110
4224
|
// gsap_repeat_ceil_overshoot
|
|
4111
|
-
({ scripts }) => {
|
|
4225
|
+
({ scripts, locate }) => {
|
|
4112
4226
|
const findings = [];
|
|
4113
4227
|
const pattern = /repeat\s*:\s*Math\.ceil\s*\([^)]+\)\s*-\s*1/g;
|
|
4114
|
-
for (const { snippet } of scanScriptsForRegexMatches(scripts, pattern, {
|
|
4228
|
+
for (const { snippet, match, script } of scanScriptsForRegexMatches(scripts, pattern, {
|
|
4115
4229
|
stripComments: false,
|
|
4116
4230
|
contextBefore: 40,
|
|
4117
4231
|
contextAfter: 40
|
|
4118
4232
|
})) {
|
|
4119
4233
|
findings.push({
|
|
4234
|
+
...locate(script, containingCallOffset(script, match.index)),
|
|
4120
4235
|
code: "gsap_repeat_ceil_overshoot",
|
|
4121
4236
|
severity: "warning",
|
|
4122
4237
|
message: "GSAP repeat calculation uses `Math.ceil` which can overshoot the composition duration. For example, Math.ceil(10.5 / 2) - 1 = 5 repeats \u2192 6 cycles \xD7 2s = 12s, exceeding 10.5s.",
|
|
@@ -4127,15 +4242,16 @@ ${right.raw}`)
|
|
|
4127
4242
|
return findings;
|
|
4128
4243
|
},
|
|
4129
4244
|
// gsap_repeat_floor_unclamped
|
|
4130
|
-
({ scripts }) => {
|
|
4245
|
+
({ scripts, locate }) => {
|
|
4131
4246
|
const findings = [];
|
|
4132
4247
|
const pattern = /repeat\s*:\s*Math\.floor\s*\([^)]+\)\s*-\s*1/g;
|
|
4133
|
-
for (const { snippet } of scanScriptsForRegexMatches(scripts, pattern, {
|
|
4248
|
+
for (const { snippet, match, script } of scanScriptsForRegexMatches(scripts, pattern, {
|
|
4134
4249
|
stripComments: false,
|
|
4135
4250
|
contextBefore: 40,
|
|
4136
4251
|
contextAfter: 40
|
|
4137
4252
|
})) {
|
|
4138
4253
|
findings.push({
|
|
4254
|
+
...locate(script, containingCallOffset(script, match.index)),
|
|
4139
4255
|
code: "gsap_repeat_floor_unclamped",
|
|
4140
4256
|
severity: "warning",
|
|
4141
4257
|
message: "GSAP repeat calculation can evaluate to -1 when the composition is shorter than one cycle, which GSAP interprets as an infinite repeat.",
|
|
@@ -4146,7 +4262,7 @@ ${right.raw}`)
|
|
|
4146
4262
|
return findings;
|
|
4147
4263
|
},
|
|
4148
4264
|
// gsap_timeline_not_registered
|
|
4149
|
-
({ scripts, rawSource, options }) => {
|
|
4265
|
+
({ scripts, rawSource, options, locate }) => {
|
|
4150
4266
|
const findings = [];
|
|
4151
4267
|
const canInheritFromHost = options.isSubComposition || rawSource.trimStart().toLowerCase().startsWith("<template");
|
|
4152
4268
|
for (const script of scripts) {
|
|
@@ -4155,6 +4271,7 @@ ${right.raw}`)
|
|
|
4155
4271
|
const hasRegistration = WINDOW_TIMELINE_ASSIGN_PATTERN.test(content) || TIMELINE_REGISTRY_OBJECT_LITERAL_PATTERN.test(content);
|
|
4156
4272
|
if (hasRegistration || canInheritFromHost) continue;
|
|
4157
4273
|
findings.push({
|
|
4274
|
+
...locate(script, gsapCallOffset(script, "timeline")),
|
|
4158
4275
|
code: "gsap_timeline_not_registered",
|
|
4159
4276
|
severity: "error",
|
|
4160
4277
|
message: "GSAP timeline is created but never registered in window.__timelines. The runtime discovers timelines from this registry \u2014 without registration, animations will not play during preview or render.",
|
|
@@ -5197,6 +5314,7 @@ var textureRules = [
|
|
|
5197
5314
|
];
|
|
5198
5315
|
|
|
5199
5316
|
// src/rules/fonts.ts
|
|
5317
|
+
import postcss3 from "postcss";
|
|
5200
5318
|
import { FONT_ALIAS_KEYS, resolveAliasDisplayName } from "@hyperframes/parsers/composition";
|
|
5201
5319
|
var GENERIC_FAMILIES = /* @__PURE__ */ new Set([
|
|
5202
5320
|
"serif",
|
|
@@ -5343,7 +5461,7 @@ var fontRules = [
|
|
|
5343
5461
|
];
|
|
5344
5462
|
},
|
|
5345
5463
|
// font_family_without_font_face
|
|
5346
|
-
({ styles, source, rawSource, options }) => {
|
|
5464
|
+
({ styles, source, rawSource, options, locate }) => {
|
|
5347
5465
|
if (isRegistrySourceFile(options.filePath) || isRegistryInstalledFile(rawSource)) return [];
|
|
5348
5466
|
const findings = [];
|
|
5349
5467
|
const declared = extractFontFaceFamilies(styles);
|
|
@@ -5354,6 +5472,7 @@ var fontRules = [
|
|
|
5354
5472
|
);
|
|
5355
5473
|
if (undeclared.length === 0) return findings;
|
|
5356
5474
|
findings.push({
|
|
5475
|
+
...uniqueFontLocation(styles, undeclared, locate),
|
|
5357
5476
|
code: "font_family_without_font_face",
|
|
5358
5477
|
severity: "error",
|
|
5359
5478
|
message: `Font ${undeclared.length === 1 ? "family" : "families"} used without @font-face declaration: ${undeclared.join(", ")}. These are not in the auto-resolved font list, so the renderer cannot supply them automatically. Text will fall back to a generic font, producing incorrect typography in the video.`,
|
|
@@ -5362,6 +5481,26 @@ var fontRules = [
|
|
|
5362
5481
|
return findings;
|
|
5363
5482
|
}
|
|
5364
5483
|
];
|
|
5484
|
+
function uniqueFontLocation(styles, families, locate) {
|
|
5485
|
+
const locations = [];
|
|
5486
|
+
for (const style of styles) {
|
|
5487
|
+
try {
|
|
5488
|
+
postcss3.parse(style.content).walkDecls(/^font-family$/i, (decl) => {
|
|
5489
|
+
let parent = decl.parent;
|
|
5490
|
+
while (parent) {
|
|
5491
|
+
if (parent.type === "atrule" && parent.name.toLowerCase() === "font-face") return;
|
|
5492
|
+
parent = parent.parent;
|
|
5493
|
+
}
|
|
5494
|
+
if (!decl.value.split(",").some((part) => families.includes(normalizeUsedFontName(part) ?? "")))
|
|
5495
|
+
return;
|
|
5496
|
+
locations.push(locate(style, decl.source?.start?.offset));
|
|
5497
|
+
});
|
|
5498
|
+
} catch {
|
|
5499
|
+
return {};
|
|
5500
|
+
}
|
|
5501
|
+
}
|
|
5502
|
+
return locations.length === 1 ? locations[0] : {};
|
|
5503
|
+
}
|
|
5365
5504
|
|
|
5366
5505
|
// src/rules/slideshow.ts
|
|
5367
5506
|
import {
|
|
@@ -5634,7 +5773,7 @@ function collectFindings(produced, seen, filePath, into) {
|
|
|
5634
5773
|
const dedupeKey = dedupeKeyFor(finding);
|
|
5635
5774
|
if (seen.has(dedupeKey)) continue;
|
|
5636
5775
|
seen.add(dedupeKey);
|
|
5637
|
-
into.push(filePath ? { ...finding, file: filePath } : finding);
|
|
5776
|
+
into.push(filePath ? { ...finding, file: finding.file ?? filePath } : finding);
|
|
5638
5777
|
}
|
|
5639
5778
|
}
|
|
5640
5779
|
async function lintHyperframeHtml(html, options = {}) {
|
|
@@ -5643,6 +5782,12 @@ async function lintHyperframeHtml(html, options = {}) {
|
|
|
5643
5782
|
const { findings: rawFindings, timings } = await runRules(ctx, options.filePath);
|
|
5644
5783
|
const locate = createSourceLocator(html);
|
|
5645
5784
|
const findings = rawFindings.map((finding) => {
|
|
5785
|
+
if (finding.line !== void 0 || finding.code === "invalid_inline_script_syntax" || finding.code === "non_deterministic_code" || [
|
|
5786
|
+
"gsap_infinite_repeat",
|
|
5787
|
+
"gsap_repeat_ceil_overshoot",
|
|
5788
|
+
"gsap_repeat_floor_unclamped"
|
|
5789
|
+
].includes(finding.code))
|
|
5790
|
+
return finding;
|
|
5646
5791
|
if (finding.snippet && options.externalStyles?.some(
|
|
5647
5792
|
(style) => style.content.replace(/\s+/g, " ").includes(finding.snippet.replace(/\.\.\.$/, ""))
|
|
5648
5793
|
))
|