@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/index.d.ts
CHANGED
package/dist/index.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
|
}
|
|
@@ -571,17 +582,17 @@ function mediaSrcTagRe(tagAlternation) {
|
|
|
571
582
|
|
|
572
583
|
// src/sourceLocations.ts
|
|
573
584
|
function normalized(source) {
|
|
574
|
-
|
|
585
|
+
const chars = [];
|
|
575
586
|
const offsets = [];
|
|
587
|
+
let lastWasSpace = false;
|
|
576
588
|
for (let i = 0; i < source.length; i++) {
|
|
577
|
-
const
|
|
578
|
-
if (
|
|
579
|
-
|
|
580
|
-
text += " ";
|
|
581
|
-
} else text += char;
|
|
589
|
+
const isSpace = /\s/.test(source[i]);
|
|
590
|
+
if (isSpace && lastWasSpace) continue;
|
|
591
|
+
chars.push(isSpace ? " " : source[i]);
|
|
582
592
|
offsets.push(i);
|
|
593
|
+
lastWasSpace = isSpace;
|
|
583
594
|
}
|
|
584
|
-
return { text, offsets };
|
|
595
|
+
return { text: chars.join(""), offsets };
|
|
585
596
|
}
|
|
586
597
|
function createSourceLocator(source) {
|
|
587
598
|
const tags = parseHtmlStructure(source).tags;
|
|
@@ -621,10 +632,42 @@ function snippetOffset(source, snippet) {
|
|
|
621
632
|
return source.offsets[index];
|
|
622
633
|
}
|
|
623
634
|
|
|
635
|
+
// src/sourceCoordinates.ts
|
|
636
|
+
function mappedHtmlSource(original) {
|
|
637
|
+
let offsets;
|
|
638
|
+
const source = stripHtmlComments(original, (ranges) => {
|
|
639
|
+
const previous = offsets;
|
|
640
|
+
const length = (previous?.length ?? original.length + 1) - ranges.reduce((n, [a, b]) => n + b - a, 0);
|
|
641
|
+
offsets = new Uint32Array(length);
|
|
642
|
+
let target = 0;
|
|
643
|
+
let start = 0;
|
|
644
|
+
const spans = [
|
|
645
|
+
...ranges,
|
|
646
|
+
[previous ? previous.length : original.length + 1, 0]
|
|
647
|
+
];
|
|
648
|
+
for (const [a, b] of spans) {
|
|
649
|
+
for (let i = start; i < a; i++) offsets[target++] = previous ? previous[i] : i;
|
|
650
|
+
start = b;
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
return { source, originalOffset: (index) => offsets ? offsets[index] : index };
|
|
654
|
+
}
|
|
655
|
+
function sourcePosition(source, offset, file) {
|
|
656
|
+
if (!Number.isInteger(offset) || offset < 0 || offset > source.length) return {};
|
|
657
|
+
const before = source.slice(0, offset);
|
|
658
|
+
return {
|
|
659
|
+
...file ? { file } : {},
|
|
660
|
+
line: before.split(/\r\n|\r|\n/).length,
|
|
661
|
+
column: offset - Math.max(before.lastIndexOf("\n"), before.lastIndexOf("\r"))
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
|
|
624
665
|
// src/context.ts
|
|
625
666
|
function buildLintContext(html, options = {}) {
|
|
626
667
|
const rawSource = html || "";
|
|
627
|
-
|
|
668
|
+
const mapped = mappedHtmlSource(rawSource);
|
|
669
|
+
let source = mapped.source;
|
|
670
|
+
let sourceStart = 0;
|
|
628
671
|
const initialStructure = parseHtmlStructure(source);
|
|
629
672
|
const templateTags = initialStructure.tags.filter(
|
|
630
673
|
(tag) => tag.name === "template" && tag.closeIndex != null
|
|
@@ -637,7 +680,8 @@ function buildLintContext(html, options = {}) {
|
|
|
637
680
|
const template = templateTags[0];
|
|
638
681
|
let structure = initialStructure;
|
|
639
682
|
if (template && !findRootTag(sourceWithoutTemplates)) {
|
|
640
|
-
|
|
683
|
+
sourceStart = template.index + template.raw.length;
|
|
684
|
+
source = source.slice(sourceStart, template.closeIndex);
|
|
641
685
|
structure = parseHtmlStructure(source);
|
|
642
686
|
}
|
|
643
687
|
const tags = structure.tags;
|
|
@@ -647,14 +691,23 @@ function buildLintContext(html, options = {}) {
|
|
|
647
691
|
attrs: `href="${style.href}"`,
|
|
648
692
|
content: style.content,
|
|
649
693
|
raw: style.content,
|
|
650
|
-
index: -1
|
|
694
|
+
index: -1,
|
|
695
|
+
file: style.file ?? style.href
|
|
651
696
|
}))
|
|
652
697
|
];
|
|
653
698
|
const scripts = structure.scripts;
|
|
654
699
|
const compositionIds = collectCompositionIds(tags);
|
|
655
700
|
const rootTag = findRootTag(source, tags);
|
|
656
701
|
const rootCompositionId = readDecodedAttr(rootTag?.raw || "", "data-composition-id");
|
|
702
|
+
const locate = (block, offset) => {
|
|
703
|
+
if (offset === void 0 || offset < 0 || offset > block.content.length) return {};
|
|
704
|
+
if (block.index === -1) return sourcePosition(block.content, offset, block.file);
|
|
705
|
+
if (block.contentStart === void 0) return {};
|
|
706
|
+
const originalOffset = mapped.originalOffset(sourceStart + block.contentStart + offset);
|
|
707
|
+
return originalOffset === void 0 ? {} : sourcePosition(rawSource, originalOffset, options.filePath);
|
|
708
|
+
};
|
|
657
709
|
return {
|
|
710
|
+
locate,
|
|
658
711
|
source,
|
|
659
712
|
rawSource,
|
|
660
713
|
tags,
|
|
@@ -674,7 +727,7 @@ import {
|
|
|
674
727
|
HTML_BODY_CSS_WIDTH_FIRST_RE,
|
|
675
728
|
HTML_BODY_CSS_HEIGHT_FIRST_RE,
|
|
676
729
|
VIEWPORT_META_SIZE_RE
|
|
677
|
-
} from "@hyperframes/parsers";
|
|
730
|
+
} from "@hyperframes/parsers/composition";
|
|
678
731
|
function repeatedDescendantId(selector) {
|
|
679
732
|
let repeated = null;
|
|
680
733
|
const requiredPseudoIds = (pseudo) => {
|
|
@@ -1052,7 +1105,7 @@ var coreRules = [
|
|
|
1052
1105
|
return findings;
|
|
1053
1106
|
},
|
|
1054
1107
|
// CSS selector safety
|
|
1055
|
-
({ styles }) => {
|
|
1108
|
+
({ styles, locate }) => {
|
|
1056
1109
|
const findings = [];
|
|
1057
1110
|
const reportedRepeatedIds = /* @__PURE__ */ new Set();
|
|
1058
1111
|
const reportedHiddenStyleSelectors = /* @__PURE__ */ new Set();
|
|
@@ -1062,6 +1115,10 @@ var coreRules = [
|
|
|
1062
1115
|
root = postcss.parse(style.content);
|
|
1063
1116
|
} catch (error) {
|
|
1064
1117
|
findings.push({
|
|
1118
|
+
...locate(
|
|
1119
|
+
style,
|
|
1120
|
+
error instanceof postcss.CssSyntaxError ? cssErrorOffset(style.content, error) : void 0
|
|
1121
|
+
),
|
|
1065
1122
|
code: "css_parse_error",
|
|
1066
1123
|
severity: "error",
|
|
1067
1124
|
message: `CSS parse error: ${error instanceof Error ? error.message : "unknown"}`
|
|
@@ -1127,7 +1184,7 @@ var coreRules = [
|
|
|
1127
1184
|
];
|
|
1128
1185
|
},
|
|
1129
1186
|
// invalid_inline_script_syntax (JS parse error)
|
|
1130
|
-
({ scripts }) => {
|
|
1187
|
+
({ scripts, locate }) => {
|
|
1131
1188
|
const findings = [];
|
|
1132
1189
|
for (const script of scripts) {
|
|
1133
1190
|
const attrs = script.attrs || "";
|
|
@@ -1138,9 +1195,10 @@ var coreRules = [
|
|
|
1138
1195
|
const syntaxError = getInlineScriptSyntaxError(script.content);
|
|
1139
1196
|
if (!syntaxError) continue;
|
|
1140
1197
|
findings.push({
|
|
1198
|
+
...locate(script, syntaxError.offset),
|
|
1141
1199
|
code: "invalid_inline_script_syntax",
|
|
1142
1200
|
severity: "error",
|
|
1143
|
-
message: `Inline script has invalid syntax: ${syntaxError}`,
|
|
1201
|
+
message: `Inline script has invalid syntax: ${syntaxError.message}`,
|
|
1144
1202
|
fixHint: "Fix the inline script syntax before render verification.",
|
|
1145
1203
|
snippet: truncateSnippet(script.content)
|
|
1146
1204
|
});
|
|
@@ -1206,7 +1264,7 @@ var coreRules = [
|
|
|
1206
1264
|
return findings;
|
|
1207
1265
|
},
|
|
1208
1266
|
// non_deterministic_code
|
|
1209
|
-
({ scripts }) => {
|
|
1267
|
+
({ scripts, locate }) => {
|
|
1210
1268
|
const findings = [];
|
|
1211
1269
|
const patterns = [
|
|
1212
1270
|
{
|
|
@@ -1257,8 +1315,10 @@ var coreRules = [
|
|
|
1257
1315
|
const withoutComments = stripJsComments(script.content);
|
|
1258
1316
|
const executable = stripStringLiterals(withoutComments);
|
|
1259
1317
|
for (const { pattern, label, hint, scansStrings } of patterns) {
|
|
1260
|
-
|
|
1318
|
+
const match = pattern.exec(scansStrings ? withoutComments : executable);
|
|
1319
|
+
if (match) {
|
|
1261
1320
|
findings.push({
|
|
1321
|
+
...locate(script, match.index),
|
|
1262
1322
|
code: "non_deterministic_code",
|
|
1263
1323
|
severity: "error",
|
|
1264
1324
|
message: `Script contains \`${label}\` which produces non-deterministic output. Renders may differ between frames or runs.`,
|
|
@@ -1271,10 +1331,24 @@ var coreRules = [
|
|
|
1271
1331
|
return findings;
|
|
1272
1332
|
}
|
|
1273
1333
|
];
|
|
1334
|
+
function cssErrorOffset(source, error) {
|
|
1335
|
+
if (error.input && "offset" in error.input && typeof error.input.offset === "number")
|
|
1336
|
+
return error.input.offset;
|
|
1337
|
+
const { line, column } = error;
|
|
1338
|
+
if (line === void 0 || column === void 0) return void 0;
|
|
1339
|
+
let offset = 0;
|
|
1340
|
+
let currentLine = 1;
|
|
1341
|
+
for (const match of source.matchAll(/\n/g)) {
|
|
1342
|
+
if (currentLine >= line) break;
|
|
1343
|
+
offset = match.index + 1;
|
|
1344
|
+
currentLine++;
|
|
1345
|
+
}
|
|
1346
|
+
return currentLine === line ? offset + column - 1 : void 0;
|
|
1347
|
+
}
|
|
1274
1348
|
|
|
1275
1349
|
// src/rules/media.ts
|
|
1276
1350
|
import { validateColorGradingContract } from "@hyperframes/parsers/color-grading-contract";
|
|
1277
|
-
import { extractMediaSrcMutations } from "@hyperframes/parsers";
|
|
1351
|
+
import { extractMediaSrcMutations } from "@hyperframes/parsers/composition";
|
|
1278
1352
|
import { parseHTML } from "linkedom";
|
|
1279
1353
|
function tweensVolumeInSameCall(script, id) {
|
|
1280
1354
|
const selector = new RegExp(`#${escapeRegExp(id)}(?![\\w-])`, "g");
|
|
@@ -2133,6 +2207,45 @@ function findAudioGroupCarveAttrFindings(ctx) {
|
|
|
2133
2207
|
return findings;
|
|
2134
2208
|
}
|
|
2135
2209
|
|
|
2210
|
+
// src/scriptPositions.ts
|
|
2211
|
+
import { parse as parse2 } from "acorn";
|
|
2212
|
+
import { simple } from "acorn-walk";
|
|
2213
|
+
var callIndexes = /* @__PURE__ */ new WeakMap();
|
|
2214
|
+
function callsFor(script) {
|
|
2215
|
+
const cached = callIndexes.get(script);
|
|
2216
|
+
if (cached) return cached;
|
|
2217
|
+
const calls = [];
|
|
2218
|
+
try {
|
|
2219
|
+
const ast = parse2(script.content, {
|
|
2220
|
+
ecmaVersion: "latest",
|
|
2221
|
+
sourceType: "script",
|
|
2222
|
+
allowReturnOutsideFunction: true
|
|
2223
|
+
});
|
|
2224
|
+
simple(ast, {
|
|
2225
|
+
CallExpression: (node) => {
|
|
2226
|
+
calls.push(node);
|
|
2227
|
+
}
|
|
2228
|
+
});
|
|
2229
|
+
} catch {
|
|
2230
|
+
}
|
|
2231
|
+
calls.sort((a, b) => a.start - b.start);
|
|
2232
|
+
callIndexes.set(script, calls);
|
|
2233
|
+
return calls;
|
|
2234
|
+
}
|
|
2235
|
+
function gsapCallOffset(script, method) {
|
|
2236
|
+
return callsFor(script).find(
|
|
2237
|
+
({ callee }) => callee.type === "MemberExpression" && !callee.computed && callee.object.type === "Identifier" && callee.object.name === "gsap" && callee.property.type === "Identifier" && callee.property.name === method
|
|
2238
|
+
)?.start;
|
|
2239
|
+
}
|
|
2240
|
+
function containingCallOffset(script, tokenOffset) {
|
|
2241
|
+
let start;
|
|
2242
|
+
for (const call of callsFor(script)) {
|
|
2243
|
+
if (call.start > tokenOffset) break;
|
|
2244
|
+
if (call.end > tokenOffset) start = call.start;
|
|
2245
|
+
}
|
|
2246
|
+
return start;
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2136
2249
|
// src/rules/composition.ts
|
|
2137
2250
|
import { COMPOSITION_VARIABLE_TYPES, isSafeMediaUrl } from "@hyperframes/parsers/composition";
|
|
2138
2251
|
import { COMPOSITION_ATTRIBUTES, readClipTiming } from "@hyperframes/parsers/composition-contract";
|
|
@@ -3342,8 +3455,8 @@ function targetedSelectorTokens(selector) {
|
|
|
3342
3455
|
const compounds = group.trim().split(/[\s>+~]+/).filter(Boolean);
|
|
3343
3456
|
const last = compounds[compounds.length - 1];
|
|
3344
3457
|
if (!last) continue;
|
|
3345
|
-
const
|
|
3346
|
-
if (
|
|
3458
|
+
const simple2 = last.match(/[#.][A-Za-z0-9_-]+/g);
|
|
3459
|
+
if (simple2) for (const token of simple2) tokens.add(token);
|
|
3347
3460
|
}
|
|
3348
3461
|
return tokens;
|
|
3349
3462
|
}
|
|
@@ -3382,7 +3495,7 @@ function scanScriptsForRegexMatches(scripts, pattern, options) {
|
|
|
3382
3495
|
content.length,
|
|
3383
3496
|
match.index + match[0].length + options.contextAfter
|
|
3384
3497
|
);
|
|
3385
|
-
hits.push({ match, snippet: content.slice(contextStart, contextEnd) });
|
|
3498
|
+
hits.push({ match, snippet: content.slice(contextStart, contextEnd), script });
|
|
3386
3499
|
}
|
|
3387
3500
|
}
|
|
3388
3501
|
return hits;
|
|
@@ -4088,19 +4201,20 @@ ${right.raw}`)
|
|
|
4088
4201
|
return findings;
|
|
4089
4202
|
},
|
|
4090
4203
|
// gsap_infinite_repeat
|
|
4091
|
-
({ scripts, rootTag }) => {
|
|
4204
|
+
({ scripts, rootTag, locate }) => {
|
|
4092
4205
|
const findings = [];
|
|
4093
4206
|
const declaredDuration = Number.parseFloat(
|
|
4094
4207
|
rootTag ? readAttr(rootTag.raw, "data-duration") ?? "" : ""
|
|
4095
4208
|
);
|
|
4096
4209
|
const hasFiniteCompositionWindow = Number.isFinite(declaredDuration) && declaredDuration > 0;
|
|
4097
4210
|
const pattern = /repeat\s*:\s*-1(?!\d)/g;
|
|
4098
|
-
for (const { snippet } of scanScriptsForRegexMatches(scripts, pattern, {
|
|
4211
|
+
for (const { snippet, match, script } of scanScriptsForRegexMatches(scripts, pattern, {
|
|
4099
4212
|
stripComments: true,
|
|
4100
4213
|
contextBefore: 60,
|
|
4101
4214
|
contextAfter: 60
|
|
4102
4215
|
})) {
|
|
4103
4216
|
findings.push({
|
|
4217
|
+
...locate(script, containingCallOffset(script, match.index)),
|
|
4104
4218
|
code: "gsap_infinite_repeat",
|
|
4105
4219
|
severity: hasFiniteCompositionWindow ? "warning" : "error",
|
|
4106
4220
|
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.",
|
|
@@ -4111,15 +4225,16 @@ ${right.raw}`)
|
|
|
4111
4225
|
return findings;
|
|
4112
4226
|
},
|
|
4113
4227
|
// gsap_repeat_ceil_overshoot
|
|
4114
|
-
({ scripts }) => {
|
|
4228
|
+
({ scripts, locate }) => {
|
|
4115
4229
|
const findings = [];
|
|
4116
4230
|
const pattern = /repeat\s*:\s*Math\.ceil\s*\([^)]+\)\s*-\s*1/g;
|
|
4117
|
-
for (const { snippet } of scanScriptsForRegexMatches(scripts, pattern, {
|
|
4231
|
+
for (const { snippet, match, script } of scanScriptsForRegexMatches(scripts, pattern, {
|
|
4118
4232
|
stripComments: false,
|
|
4119
4233
|
contextBefore: 40,
|
|
4120
4234
|
contextAfter: 40
|
|
4121
4235
|
})) {
|
|
4122
4236
|
findings.push({
|
|
4237
|
+
...locate(script, containingCallOffset(script, match.index)),
|
|
4123
4238
|
code: "gsap_repeat_ceil_overshoot",
|
|
4124
4239
|
severity: "warning",
|
|
4125
4240
|
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.",
|
|
@@ -4130,15 +4245,16 @@ ${right.raw}`)
|
|
|
4130
4245
|
return findings;
|
|
4131
4246
|
},
|
|
4132
4247
|
// gsap_repeat_floor_unclamped
|
|
4133
|
-
({ scripts }) => {
|
|
4248
|
+
({ scripts, locate }) => {
|
|
4134
4249
|
const findings = [];
|
|
4135
4250
|
const pattern = /repeat\s*:\s*Math\.floor\s*\([^)]+\)\s*-\s*1/g;
|
|
4136
|
-
for (const { snippet } of scanScriptsForRegexMatches(scripts, pattern, {
|
|
4251
|
+
for (const { snippet, match, script } of scanScriptsForRegexMatches(scripts, pattern, {
|
|
4137
4252
|
stripComments: false,
|
|
4138
4253
|
contextBefore: 40,
|
|
4139
4254
|
contextAfter: 40
|
|
4140
4255
|
})) {
|
|
4141
4256
|
findings.push({
|
|
4257
|
+
...locate(script, containingCallOffset(script, match.index)),
|
|
4142
4258
|
code: "gsap_repeat_floor_unclamped",
|
|
4143
4259
|
severity: "warning",
|
|
4144
4260
|
message: "GSAP repeat calculation can evaluate to -1 when the composition is shorter than one cycle, which GSAP interprets as an infinite repeat.",
|
|
@@ -4149,7 +4265,7 @@ ${right.raw}`)
|
|
|
4149
4265
|
return findings;
|
|
4150
4266
|
},
|
|
4151
4267
|
// gsap_timeline_not_registered
|
|
4152
|
-
({ scripts, rawSource, options }) => {
|
|
4268
|
+
({ scripts, rawSource, options, locate }) => {
|
|
4153
4269
|
const findings = [];
|
|
4154
4270
|
const canInheritFromHost = options.isSubComposition || rawSource.trimStart().toLowerCase().startsWith("<template");
|
|
4155
4271
|
for (const script of scripts) {
|
|
@@ -4158,6 +4274,7 @@ ${right.raw}`)
|
|
|
4158
4274
|
const hasRegistration = WINDOW_TIMELINE_ASSIGN_PATTERN.test(content) || TIMELINE_REGISTRY_OBJECT_LITERAL_PATTERN.test(content);
|
|
4159
4275
|
if (hasRegistration || canInheritFromHost) continue;
|
|
4160
4276
|
findings.push({
|
|
4277
|
+
...locate(script, gsapCallOffset(script, "timeline")),
|
|
4161
4278
|
code: "gsap_timeline_not_registered",
|
|
4162
4279
|
severity: "error",
|
|
4163
4280
|
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.",
|
|
@@ -5200,6 +5317,7 @@ var textureRules = [
|
|
|
5200
5317
|
];
|
|
5201
5318
|
|
|
5202
5319
|
// src/rules/fonts.ts
|
|
5320
|
+
import postcss3 from "postcss";
|
|
5203
5321
|
import { FONT_ALIAS_KEYS, resolveAliasDisplayName } from "@hyperframes/parsers/composition";
|
|
5204
5322
|
var GENERIC_FAMILIES = /* @__PURE__ */ new Set([
|
|
5205
5323
|
"serif",
|
|
@@ -5346,7 +5464,7 @@ var fontRules = [
|
|
|
5346
5464
|
];
|
|
5347
5465
|
},
|
|
5348
5466
|
// font_family_without_font_face
|
|
5349
|
-
({ styles, source, rawSource, options }) => {
|
|
5467
|
+
({ styles, source, rawSource, options, locate }) => {
|
|
5350
5468
|
if (isRegistrySourceFile(options.filePath) || isRegistryInstalledFile(rawSource)) return [];
|
|
5351
5469
|
const findings = [];
|
|
5352
5470
|
const declared = extractFontFaceFamilies(styles);
|
|
@@ -5357,6 +5475,7 @@ var fontRules = [
|
|
|
5357
5475
|
);
|
|
5358
5476
|
if (undeclared.length === 0) return findings;
|
|
5359
5477
|
findings.push({
|
|
5478
|
+
...uniqueFontLocation(styles, undeclared, locate),
|
|
5360
5479
|
code: "font_family_without_font_face",
|
|
5361
5480
|
severity: "error",
|
|
5362
5481
|
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.`,
|
|
@@ -5365,6 +5484,26 @@ var fontRules = [
|
|
|
5365
5484
|
return findings;
|
|
5366
5485
|
}
|
|
5367
5486
|
];
|
|
5487
|
+
function uniqueFontLocation(styles, families, locate) {
|
|
5488
|
+
const locations = [];
|
|
5489
|
+
for (const style of styles) {
|
|
5490
|
+
try {
|
|
5491
|
+
postcss3.parse(style.content).walkDecls(/^font-family$/i, (decl) => {
|
|
5492
|
+
let parent = decl.parent;
|
|
5493
|
+
while (parent) {
|
|
5494
|
+
if (parent.type === "atrule" && parent.name.toLowerCase() === "font-face") return;
|
|
5495
|
+
parent = parent.parent;
|
|
5496
|
+
}
|
|
5497
|
+
if (!decl.value.split(",").some((part) => families.includes(normalizeUsedFontName(part) ?? "")))
|
|
5498
|
+
return;
|
|
5499
|
+
locations.push(locate(style, decl.source?.start?.offset));
|
|
5500
|
+
});
|
|
5501
|
+
} catch {
|
|
5502
|
+
return {};
|
|
5503
|
+
}
|
|
5504
|
+
}
|
|
5505
|
+
return locations.length === 1 ? locations[0] : {};
|
|
5506
|
+
}
|
|
5368
5507
|
|
|
5369
5508
|
// src/rules/slideshow.ts
|
|
5370
5509
|
import {
|
|
@@ -5637,7 +5776,7 @@ function collectFindings(produced, seen, filePath, into) {
|
|
|
5637
5776
|
const dedupeKey = dedupeKeyFor(finding);
|
|
5638
5777
|
if (seen.has(dedupeKey)) continue;
|
|
5639
5778
|
seen.add(dedupeKey);
|
|
5640
|
-
into.push(filePath ? { ...finding, file: filePath } : finding);
|
|
5779
|
+
into.push(filePath ? { ...finding, file: finding.file ?? filePath } : finding);
|
|
5641
5780
|
}
|
|
5642
5781
|
}
|
|
5643
5782
|
async function lintHyperframeHtml(html, options = {}) {
|
|
@@ -5646,6 +5785,12 @@ async function lintHyperframeHtml(html, options = {}) {
|
|
|
5646
5785
|
const { findings: rawFindings, timings } = await runRules(ctx, options.filePath);
|
|
5647
5786
|
const locate = createSourceLocator(html);
|
|
5648
5787
|
const findings = rawFindings.map((finding) => {
|
|
5788
|
+
if (finding.line !== void 0 || finding.code === "invalid_inline_script_syntax" || finding.code === "non_deterministic_code" || [
|
|
5789
|
+
"gsap_infinite_repeat",
|
|
5790
|
+
"gsap_repeat_ceil_overshoot",
|
|
5791
|
+
"gsap_repeat_floor_unclamped"
|
|
5792
|
+
].includes(finding.code))
|
|
5793
|
+
return finding;
|
|
5649
5794
|
if (finding.snippet && options.externalStyles?.some(
|
|
5650
5795
|
(style) => style.content.replace(/\s+/g, " ").includes(finding.snippet.replace(/\.\.\.$/, ""))
|
|
5651
5796
|
))
|
|
@@ -6029,8 +6174,12 @@ function collectLocalStylesheets(projectDir, document, compSrcPath) {
|
|
|
6029
6174
|
function collectExternalStyles(projectDir, html, compSrcPath) {
|
|
6030
6175
|
const styles = [];
|
|
6031
6176
|
const { document } = parseHTML3(html);
|
|
6032
|
-
for (const { href, content } of collectLocalStylesheets(
|
|
6033
|
-
|
|
6177
|
+
for (const { href, content, rootRelativePath } of collectLocalStylesheets(
|
|
6178
|
+
projectDir,
|
|
6179
|
+
document,
|
|
6180
|
+
compSrcPath
|
|
6181
|
+
)) {
|
|
6182
|
+
styles.push({ href, content, file: join2(projectDir, rootRelativePath) });
|
|
6034
6183
|
}
|
|
6035
6184
|
return styles;
|
|
6036
6185
|
}
|