@hyperframes/lint 0.8.66 → 0.8.68
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 +4 -0
- package/dist/browser.js +260 -52
- package/dist/browser.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +266 -54
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
package/dist/browser.d.ts
CHANGED
|
@@ -4,6 +4,9 @@ type HyperframeLintFinding = {
|
|
|
4
4
|
severity: HyperframeLintSeverity;
|
|
5
5
|
message: string;
|
|
6
6
|
file?: string;
|
|
7
|
+
/** One-based coordinates in the original source; absent when no unique location exists. */
|
|
8
|
+
line?: number;
|
|
9
|
+
column?: number;
|
|
7
10
|
selector?: string;
|
|
8
11
|
elementId?: string;
|
|
9
12
|
fixHint?: string;
|
|
@@ -36,6 +39,7 @@ type HyperframeLinterOptions = {
|
|
|
36
39
|
externalStyles?: Array<{
|
|
37
40
|
href: string;
|
|
38
41
|
content: string;
|
|
42
|
+
file?: string;
|
|
39
43
|
}>;
|
|
40
44
|
/**
|
|
41
45
|
* Set to `true` when linting compositions destined for distributed / Lambda
|
package/dist/browser.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
// src/sourceLocations.ts
|
|
2
|
+
import { Parser as Parser2 } from "htmlparser2";
|
|
3
|
+
|
|
1
4
|
// src/utils.ts
|
|
2
5
|
import { Parser } from "htmlparser2";
|
|
6
|
+
import { parse } from "acorn";
|
|
3
7
|
var COMPOSITION_ID_IN_CSS_PATTERN = /\[data-composition-id=["']([^"']+)["']\]/g;
|
|
4
8
|
var TIMELINE_REGISTRY_INIT_PATTERN = /window\.__timelines\s*=\s*window\.__timelines\s*\|\|\s*\{\}|window\.__timelines\s*=\s*\{\}|window\.__timelines\s*\?\?=\s*\{\}/i;
|
|
5
9
|
var TIMELINE_REGISTRY_OBJECT_LITERAL_PATTERN = /window\.__timelines\s*=\s*\{\s*(?:["'][^"']+["']|[A-Za-z_$][\w$]*)\s*:/i;
|
|
@@ -53,6 +57,7 @@ function parseHtmlStructure(source) {
|
|
|
53
57
|
blocks[name].push({
|
|
54
58
|
attrs: block.attrs,
|
|
55
59
|
content: source.slice(block.contentStart, parser.startIndex),
|
|
60
|
+
contentStart: block.contentStart,
|
|
56
61
|
raw: source.slice(block.index, parser.endIndex + 1),
|
|
57
62
|
index: block.index
|
|
58
63
|
});
|
|
@@ -202,11 +207,17 @@ function readTimelineRegistryTopLevelKeys(source) {
|
|
|
202
207
|
function getInlineScriptSyntaxError(source) {
|
|
203
208
|
if (!source.trim()) return null;
|
|
204
209
|
try {
|
|
205
|
-
|
|
210
|
+
parse(source, {
|
|
211
|
+
ecmaVersion: "latest",
|
|
212
|
+
sourceType: "script",
|
|
213
|
+
allowReturnOutsideFunction: true
|
|
214
|
+
});
|
|
206
215
|
return null;
|
|
207
216
|
} catch (error) {
|
|
208
|
-
|
|
209
|
-
|
|
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
|
+
};
|
|
210
221
|
}
|
|
211
222
|
}
|
|
212
223
|
function stripStringLiterals(source) {
|
|
@@ -525,7 +536,7 @@ function stripCssComments(source) {
|
|
|
525
536
|
}
|
|
526
537
|
return out;
|
|
527
538
|
}
|
|
528
|
-
function stripHtmlCommentsOnce(source) {
|
|
539
|
+
function stripHtmlCommentsOnce(source, removed) {
|
|
529
540
|
let out = "";
|
|
530
541
|
let i = 0;
|
|
531
542
|
for (; ; ) {
|
|
@@ -534,14 +545,17 @@ function stripHtmlCommentsOnce(source) {
|
|
|
534
545
|
const end = source.indexOf("-->", start + 4);
|
|
535
546
|
if (end < 0) return out + source.slice(i);
|
|
536
547
|
out += source.slice(i, start);
|
|
548
|
+
removed?.(start, end + 3);
|
|
537
549
|
i = end + 3;
|
|
538
550
|
}
|
|
539
551
|
}
|
|
540
|
-
function stripHtmlComments(source) {
|
|
552
|
+
function stripHtmlComments(source, pass) {
|
|
541
553
|
let out = source;
|
|
542
554
|
for (let prev = ""; prev !== out; ) {
|
|
543
555
|
prev = out;
|
|
544
|
-
|
|
556
|
+
const ranges = [];
|
|
557
|
+
out = stripHtmlCommentsOnce(out, pass ? (start, end) => ranges.push([start, end]) : void 0);
|
|
558
|
+
if (ranges.length) pass?.(ranges);
|
|
545
559
|
}
|
|
546
560
|
return out;
|
|
547
561
|
}
|
|
@@ -557,16 +571,100 @@ function hasCaptionStyles(styles) {
|
|
|
557
571
|
return styles.some((s) => /\.caption[-_]?(?:group|word)/i.test(s.content));
|
|
558
572
|
}
|
|
559
573
|
function truncateSnippet(value, maxLength = 220) {
|
|
560
|
-
const
|
|
561
|
-
if (!
|
|
562
|
-
if (
|
|
563
|
-
return `${
|
|
574
|
+
const normalized2 = value.replace(/\s+/g, " ").trim();
|
|
575
|
+
if (!normalized2) return void 0;
|
|
576
|
+
if (normalized2.length <= maxLength) return normalized2;
|
|
577
|
+
return `${normalized2.slice(0, maxLength - 3)}...`;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// src/sourceLocations.ts
|
|
581
|
+
function normalized(source) {
|
|
582
|
+
let text = "";
|
|
583
|
+
const offsets = [];
|
|
584
|
+
for (let i = 0; i < source.length; i++) {
|
|
585
|
+
const char = source[i];
|
|
586
|
+
if (/\s/.test(char)) {
|
|
587
|
+
if (text.endsWith(" ")) continue;
|
|
588
|
+
text += " ";
|
|
589
|
+
} else text += char;
|
|
590
|
+
offsets.push(i);
|
|
591
|
+
}
|
|
592
|
+
return { text, offsets };
|
|
593
|
+
}
|
|
594
|
+
function createSourceLocator(source) {
|
|
595
|
+
const tags = parseHtmlStructure(source).tags;
|
|
596
|
+
const searchable = source.split("");
|
|
597
|
+
const parser = new Parser2({
|
|
598
|
+
oncomment() {
|
|
599
|
+
searchable.fill("\0", parser.startIndex, parser.endIndex + 1);
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
parser.write(source);
|
|
603
|
+
parser.end();
|
|
604
|
+
const normalizedSource = normalized(searchable.join(""));
|
|
605
|
+
const at = (offset) => {
|
|
606
|
+
const before = source.slice(0, offset);
|
|
607
|
+
return {
|
|
608
|
+
line: before.split(/\r\n|\r|\n/).length,
|
|
609
|
+
column: offset - Math.max(before.lastIndexOf("\n"), before.lastIndexOf("\r"))
|
|
610
|
+
};
|
|
611
|
+
};
|
|
612
|
+
return (finding) => {
|
|
613
|
+
if (finding.line !== void 0) return { line: finding.line, column: finding.column };
|
|
614
|
+
if (finding.snippet) {
|
|
615
|
+
const offset = snippetOffset(normalizedSource, finding.snippet);
|
|
616
|
+
return offset === void 0 ? {} : at(offset);
|
|
617
|
+
}
|
|
618
|
+
const id = finding.elementId ?? /^#([\w-]+)$/.exec(finding.selector ?? "")?.[1];
|
|
619
|
+
if (!id) return {};
|
|
620
|
+
const matches = tags.filter((tag) => readDecodedAttr(tag.raw, "id") === id);
|
|
621
|
+
return matches.length === 1 ? at(matches[0].index) : {};
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
function snippetOffset(source, snippet) {
|
|
625
|
+
const needle = snippet.replace(/\s+/g, " ").trim().replace(/\.\.\.$/, "");
|
|
626
|
+
if (!needle) return void 0;
|
|
627
|
+
const index = source.text.indexOf(needle);
|
|
628
|
+
if (index < 0 || source.text.indexOf(needle, index + 1) >= 0) return void 0;
|
|
629
|
+
return source.offsets[index];
|
|
630
|
+
}
|
|
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
|
+
};
|
|
564
660
|
}
|
|
565
661
|
|
|
566
662
|
// src/context.ts
|
|
567
663
|
function buildLintContext(html, options = {}) {
|
|
568
664
|
const rawSource = html || "";
|
|
569
|
-
|
|
665
|
+
const mapped = mappedHtmlSource(rawSource);
|
|
666
|
+
let source = mapped.source;
|
|
667
|
+
let sourceStart = 0;
|
|
570
668
|
const initialStructure = parseHtmlStructure(source);
|
|
571
669
|
const templateTags = initialStructure.tags.filter(
|
|
572
670
|
(tag) => tag.name === "template" && tag.closeIndex != null
|
|
@@ -579,7 +677,8 @@ function buildLintContext(html, options = {}) {
|
|
|
579
677
|
const template = templateTags[0];
|
|
580
678
|
let structure = initialStructure;
|
|
581
679
|
if (template && !findRootTag(sourceWithoutTemplates)) {
|
|
582
|
-
|
|
680
|
+
sourceStart = template.index + template.raw.length;
|
|
681
|
+
source = source.slice(sourceStart, template.closeIndex);
|
|
583
682
|
structure = parseHtmlStructure(source);
|
|
584
683
|
}
|
|
585
684
|
const tags = structure.tags;
|
|
@@ -589,14 +688,23 @@ function buildLintContext(html, options = {}) {
|
|
|
589
688
|
attrs: `href="${style.href}"`,
|
|
590
689
|
content: style.content,
|
|
591
690
|
raw: style.content,
|
|
592
|
-
index: -1
|
|
691
|
+
index: -1,
|
|
692
|
+
file: style.file ?? style.href
|
|
593
693
|
}))
|
|
594
694
|
];
|
|
595
695
|
const scripts = structure.scripts;
|
|
596
696
|
const compositionIds = collectCompositionIds(tags);
|
|
597
697
|
const rootTag = findRootTag(source, tags);
|
|
598
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
|
+
};
|
|
599
706
|
return {
|
|
707
|
+
locate,
|
|
600
708
|
source,
|
|
601
709
|
rawSource,
|
|
602
710
|
tags,
|
|
@@ -616,7 +724,7 @@ import {
|
|
|
616
724
|
HTML_BODY_CSS_WIDTH_FIRST_RE,
|
|
617
725
|
HTML_BODY_CSS_HEIGHT_FIRST_RE,
|
|
618
726
|
VIEWPORT_META_SIZE_RE
|
|
619
|
-
} from "@hyperframes/parsers";
|
|
727
|
+
} from "@hyperframes/parsers/composition";
|
|
620
728
|
function repeatedDescendantId(selector) {
|
|
621
729
|
let repeated = null;
|
|
622
730
|
const requiredPseudoIds = (pseudo) => {
|
|
@@ -994,7 +1102,7 @@ var coreRules = [
|
|
|
994
1102
|
return findings;
|
|
995
1103
|
},
|
|
996
1104
|
// CSS selector safety
|
|
997
|
-
({ styles }) => {
|
|
1105
|
+
({ styles, locate }) => {
|
|
998
1106
|
const findings = [];
|
|
999
1107
|
const reportedRepeatedIds = /* @__PURE__ */ new Set();
|
|
1000
1108
|
const reportedHiddenStyleSelectors = /* @__PURE__ */ new Set();
|
|
@@ -1004,6 +1112,10 @@ var coreRules = [
|
|
|
1004
1112
|
root = postcss.parse(style.content);
|
|
1005
1113
|
} catch (error) {
|
|
1006
1114
|
findings.push({
|
|
1115
|
+
...locate(
|
|
1116
|
+
style,
|
|
1117
|
+
error instanceof postcss.CssSyntaxError ? cssErrorOffset(style.content, error) : void 0
|
|
1118
|
+
),
|
|
1007
1119
|
code: "css_parse_error",
|
|
1008
1120
|
severity: "error",
|
|
1009
1121
|
message: `CSS parse error: ${error instanceof Error ? error.message : "unknown"}`
|
|
@@ -1069,7 +1181,7 @@ var coreRules = [
|
|
|
1069
1181
|
];
|
|
1070
1182
|
},
|
|
1071
1183
|
// invalid_inline_script_syntax (JS parse error)
|
|
1072
|
-
({ scripts }) => {
|
|
1184
|
+
({ scripts, locate }) => {
|
|
1073
1185
|
const findings = [];
|
|
1074
1186
|
for (const script of scripts) {
|
|
1075
1187
|
const attrs = script.attrs || "";
|
|
@@ -1080,9 +1192,10 @@ var coreRules = [
|
|
|
1080
1192
|
const syntaxError = getInlineScriptSyntaxError(script.content);
|
|
1081
1193
|
if (!syntaxError) continue;
|
|
1082
1194
|
findings.push({
|
|
1195
|
+
...locate(script, syntaxError.offset),
|
|
1083
1196
|
code: "invalid_inline_script_syntax",
|
|
1084
1197
|
severity: "error",
|
|
1085
|
-
message: `Inline script has invalid syntax: ${syntaxError}`,
|
|
1198
|
+
message: `Inline script has invalid syntax: ${syntaxError.message}`,
|
|
1086
1199
|
fixHint: "Fix the inline script syntax before render verification.",
|
|
1087
1200
|
snippet: truncateSnippet(script.content)
|
|
1088
1201
|
});
|
|
@@ -1148,7 +1261,7 @@ var coreRules = [
|
|
|
1148
1261
|
return findings;
|
|
1149
1262
|
},
|
|
1150
1263
|
// non_deterministic_code
|
|
1151
|
-
({ scripts }) => {
|
|
1264
|
+
({ scripts, locate }) => {
|
|
1152
1265
|
const findings = [];
|
|
1153
1266
|
const patterns = [
|
|
1154
1267
|
{
|
|
@@ -1199,8 +1312,10 @@ var coreRules = [
|
|
|
1199
1312
|
const withoutComments = stripJsComments(script.content);
|
|
1200
1313
|
const executable = stripStringLiterals(withoutComments);
|
|
1201
1314
|
for (const { pattern, label, hint, scansStrings } of patterns) {
|
|
1202
|
-
|
|
1315
|
+
const match = pattern.exec(scansStrings ? withoutComments : executable);
|
|
1316
|
+
if (match) {
|
|
1203
1317
|
findings.push({
|
|
1318
|
+
...locate(script, match.index),
|
|
1204
1319
|
code: "non_deterministic_code",
|
|
1205
1320
|
severity: "error",
|
|
1206
1321
|
message: `Script contains \`${label}\` which produces non-deterministic output. Renders may differ between frames or runs.`,
|
|
@@ -1213,10 +1328,24 @@ var coreRules = [
|
|
|
1213
1328
|
return findings;
|
|
1214
1329
|
}
|
|
1215
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
|
+
}
|
|
1216
1345
|
|
|
1217
1346
|
// src/rules/media.ts
|
|
1218
1347
|
import { validateColorGradingContract } from "@hyperframes/parsers/color-grading-contract";
|
|
1219
|
-
import { extractMediaSrcMutations } from "@hyperframes/parsers";
|
|
1348
|
+
import { extractMediaSrcMutations } from "@hyperframes/parsers/composition";
|
|
1220
1349
|
import { parseHTML } from "linkedom";
|
|
1221
1350
|
function tweensVolumeInSameCall(script, id) {
|
|
1222
1351
|
const selector = new RegExp(`#${escapeRegExp(id)}(?![\\w-])`, "g");
|
|
@@ -1363,18 +1492,18 @@ function classNamesFromAttr(classAttr) {
|
|
|
1363
1492
|
return classAttr.split(/\s+/).filter(Boolean);
|
|
1364
1493
|
}
|
|
1365
1494
|
function selectorTargetsManagedMedia(selector, mediaIndex) {
|
|
1366
|
-
const
|
|
1367
|
-
if (!
|
|
1368
|
-
if (mediaIndex.hasVideo && /\bvideo\b/i.test(
|
|
1369
|
-
if (mediaIndex.hasAudio && /\baudio\b/i.test(
|
|
1495
|
+
const normalized2 = selector.trim();
|
|
1496
|
+
if (!normalized2) return false;
|
|
1497
|
+
if (mediaIndex.hasVideo && /\bvideo\b/i.test(normalized2)) return true;
|
|
1498
|
+
if (mediaIndex.hasAudio && /\baudio\b/i.test(normalized2)) return true;
|
|
1370
1499
|
for (const mediaId of mediaIndex.ids) {
|
|
1371
1500
|
const escapedId = escapeRegExp(mediaId);
|
|
1372
|
-
if (new RegExp(`#${escapedId}(?![\\w-])`).test(
|
|
1501
|
+
if (new RegExp(`#${escapedId}(?![\\w-])`).test(normalized2) || normalized2.includes(`[id="${mediaId}"]`) || normalized2.includes(`[id='${mediaId}']`)) {
|
|
1373
1502
|
return true;
|
|
1374
1503
|
}
|
|
1375
1504
|
}
|
|
1376
1505
|
for (const className of mediaIndex.classes) {
|
|
1377
|
-
if (new RegExp(`\\.${escapeRegExp(className)}(?![\\w-])`).test(
|
|
1506
|
+
if (new RegExp(`\\.${escapeRegExp(className)}(?![\\w-])`).test(normalized2)) {
|
|
1378
1507
|
return true;
|
|
1379
1508
|
}
|
|
1380
1509
|
}
|
|
@@ -2075,6 +2204,45 @@ function findAudioGroupCarveAttrFindings(ctx) {
|
|
|
2075
2204
|
return findings;
|
|
2076
2205
|
}
|
|
2077
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
|
+
|
|
2078
2246
|
// src/rules/composition.ts
|
|
2079
2247
|
import { COMPOSITION_VARIABLE_TYPES, isSafeMediaUrl } from "@hyperframes/parsers/composition";
|
|
2080
2248
|
import { COMPOSITION_ATTRIBUTES, readClipTiming } from "@hyperframes/parsers/composition-contract";
|
|
@@ -2113,8 +2281,8 @@ function readTagTiming(rawTag) {
|
|
|
2113
2281
|
}
|
|
2114
2282
|
function countPhysicalLines(source) {
|
|
2115
2283
|
if (source.length === 0) return 0;
|
|
2116
|
-
const
|
|
2117
|
-
const withoutFinalNewline =
|
|
2284
|
+
const normalized2 = source.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
2285
|
+
const withoutFinalNewline = normalized2.endsWith("\n") ? normalized2.slice(0, -1) : normalized2;
|
|
2118
2286
|
return withoutFinalNewline.split("\n").length;
|
|
2119
2287
|
}
|
|
2120
2288
|
function countStructuralLines(source) {
|
|
@@ -2127,8 +2295,8 @@ function isCaptionCue(tag) {
|
|
|
2127
2295
|
}
|
|
2128
2296
|
function isRegistrySourceFile(filePath) {
|
|
2129
2297
|
if (!filePath) return false;
|
|
2130
|
-
const
|
|
2131
|
-
return /(?:^|\/)registry\/blocks\/([^/]+)\/\1\.html$/i.test(
|
|
2298
|
+
const normalized2 = filePath.replace(/\\/g, "/");
|
|
2299
|
+
return /(?:^|\/)registry\/blocks\/([^/]+)\/\1\.html$/i.test(normalized2);
|
|
2132
2300
|
}
|
|
2133
2301
|
function isRegistryInstalledFile(rawSource) {
|
|
2134
2302
|
return /^\s*<!--\s*hyperframes-registry-item:[^>]*-->/i.test(rawSource.slice(0, 512));
|
|
@@ -3078,8 +3246,8 @@ function zeroValue(value) {
|
|
|
3078
3246
|
return Number(value.trim()) === 0;
|
|
3079
3247
|
}
|
|
3080
3248
|
function isGsapColorProperty(property) {
|
|
3081
|
-
const
|
|
3082
|
-
return !
|
|
3249
|
+
const normalized2 = property.toLowerCase();
|
|
3250
|
+
return !normalized2.startsWith("--") && (normalized2.endsWith("color") || normalized2 === "fill" || normalized2 === "stroke");
|
|
3083
3251
|
}
|
|
3084
3252
|
function collectStaticCssVariableDefinitions(tags, styles) {
|
|
3085
3253
|
const definitions = /* @__PURE__ */ new Set();
|
|
@@ -3206,10 +3374,10 @@ function styleHasHiddenInitialState(style) {
|
|
|
3206
3374
|
function styleHasOpaqueBackground(style) {
|
|
3207
3375
|
const background = readStyleProperty(style, "background") || readStyleProperty(style, "background-color");
|
|
3208
3376
|
if (!background) return false;
|
|
3209
|
-
const
|
|
3210
|
-
if (
|
|
3211
|
-
if (/rgba?\([^)]*,0(?:\.0+)?\)$/.test(
|
|
3212
|
-
if (/hsla?\([^)]*,0(?:\.0+)?\)$/.test(
|
|
3377
|
+
const normalized2 = background.toLowerCase().replace(/\s+/g, "");
|
|
3378
|
+
if (normalized2 === "transparent" || normalized2 === "none") return false;
|
|
3379
|
+
if (/rgba?\([^)]*,0(?:\.0+)?\)$/.test(normalized2)) return false;
|
|
3380
|
+
if (/hsla?\([^)]*,0(?:\.0+)?\)$/.test(normalized2)) return false;
|
|
3213
3381
|
return true;
|
|
3214
3382
|
}
|
|
3215
3383
|
function styleLooksFullFrameOverlay(style) {
|
|
@@ -3284,8 +3452,8 @@ function targetedSelectorTokens(selector) {
|
|
|
3284
3452
|
const compounds = group.trim().split(/[\s>+~]+/).filter(Boolean);
|
|
3285
3453
|
const last = compounds[compounds.length - 1];
|
|
3286
3454
|
if (!last) continue;
|
|
3287
|
-
const
|
|
3288
|
-
if (
|
|
3455
|
+
const simple2 = last.match(/[#.][A-Za-z0-9_-]+/g);
|
|
3456
|
+
if (simple2) for (const token of simple2) tokens.add(token);
|
|
3289
3457
|
}
|
|
3290
3458
|
return tokens;
|
|
3291
3459
|
}
|
|
@@ -3324,7 +3492,7 @@ function scanScriptsForRegexMatches(scripts, pattern, options) {
|
|
|
3324
3492
|
content.length,
|
|
3325
3493
|
match.index + match[0].length + options.contextAfter
|
|
3326
3494
|
);
|
|
3327
|
-
hits.push({ match, snippet: content.slice(contextStart, contextEnd) });
|
|
3495
|
+
hits.push({ match, snippet: content.slice(contextStart, contextEnd), script });
|
|
3328
3496
|
}
|
|
3329
3497
|
}
|
|
3330
3498
|
return hits;
|
|
@@ -3591,9 +3759,9 @@ function elementLevelTokens(tokens, tagsByToken) {
|
|
|
3591
3759
|
return expanded;
|
|
3592
3760
|
}
|
|
3593
3761
|
function isMultiComponentDasharray(value) {
|
|
3594
|
-
const
|
|
3595
|
-
if (!
|
|
3596
|
-
return
|
|
3762
|
+
const normalized2 = value.replace(/!important\s*$/i, "").trim();
|
|
3763
|
+
if (!normalized2 || /^none$/i.test(normalized2)) return false;
|
|
3764
|
+
return normalized2.split(/[\s,]+/).filter(Boolean).length >= 2;
|
|
3597
3765
|
}
|
|
3598
3766
|
function gsapDasharrayValueLooksMultiComponent(valueSource) {
|
|
3599
3767
|
const literal = valueSource.trim().match(/^(["'`])([\s\S]*)\1$/)?.[2];
|
|
@@ -4030,19 +4198,20 @@ ${right.raw}`)
|
|
|
4030
4198
|
return findings;
|
|
4031
4199
|
},
|
|
4032
4200
|
// gsap_infinite_repeat
|
|
4033
|
-
({ scripts, rootTag }) => {
|
|
4201
|
+
({ scripts, rootTag, locate }) => {
|
|
4034
4202
|
const findings = [];
|
|
4035
4203
|
const declaredDuration = Number.parseFloat(
|
|
4036
4204
|
rootTag ? readAttr(rootTag.raw, "data-duration") ?? "" : ""
|
|
4037
4205
|
);
|
|
4038
4206
|
const hasFiniteCompositionWindow = Number.isFinite(declaredDuration) && declaredDuration > 0;
|
|
4039
4207
|
const pattern = /repeat\s*:\s*-1(?!\d)/g;
|
|
4040
|
-
for (const { snippet } of scanScriptsForRegexMatches(scripts, pattern, {
|
|
4208
|
+
for (const { snippet, match, script } of scanScriptsForRegexMatches(scripts, pattern, {
|
|
4041
4209
|
stripComments: true,
|
|
4042
4210
|
contextBefore: 60,
|
|
4043
4211
|
contextAfter: 60
|
|
4044
4212
|
})) {
|
|
4045
4213
|
findings.push({
|
|
4214
|
+
...locate(script, containingCallOffset(script, match.index)),
|
|
4046
4215
|
code: "gsap_infinite_repeat",
|
|
4047
4216
|
severity: hasFiniteCompositionWindow ? "warning" : "error",
|
|
4048
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.",
|
|
@@ -4053,15 +4222,16 @@ ${right.raw}`)
|
|
|
4053
4222
|
return findings;
|
|
4054
4223
|
},
|
|
4055
4224
|
// gsap_repeat_ceil_overshoot
|
|
4056
|
-
({ scripts }) => {
|
|
4225
|
+
({ scripts, locate }) => {
|
|
4057
4226
|
const findings = [];
|
|
4058
4227
|
const pattern = /repeat\s*:\s*Math\.ceil\s*\([^)]+\)\s*-\s*1/g;
|
|
4059
|
-
for (const { snippet } of scanScriptsForRegexMatches(scripts, pattern, {
|
|
4228
|
+
for (const { snippet, match, script } of scanScriptsForRegexMatches(scripts, pattern, {
|
|
4060
4229
|
stripComments: false,
|
|
4061
4230
|
contextBefore: 40,
|
|
4062
4231
|
contextAfter: 40
|
|
4063
4232
|
})) {
|
|
4064
4233
|
findings.push({
|
|
4234
|
+
...locate(script, containingCallOffset(script, match.index)),
|
|
4065
4235
|
code: "gsap_repeat_ceil_overshoot",
|
|
4066
4236
|
severity: "warning",
|
|
4067
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.",
|
|
@@ -4072,15 +4242,16 @@ ${right.raw}`)
|
|
|
4072
4242
|
return findings;
|
|
4073
4243
|
},
|
|
4074
4244
|
// gsap_repeat_floor_unclamped
|
|
4075
|
-
({ scripts }) => {
|
|
4245
|
+
({ scripts, locate }) => {
|
|
4076
4246
|
const findings = [];
|
|
4077
4247
|
const pattern = /repeat\s*:\s*Math\.floor\s*\([^)]+\)\s*-\s*1/g;
|
|
4078
|
-
for (const { snippet } of scanScriptsForRegexMatches(scripts, pattern, {
|
|
4248
|
+
for (const { snippet, match, script } of scanScriptsForRegexMatches(scripts, pattern, {
|
|
4079
4249
|
stripComments: false,
|
|
4080
4250
|
contextBefore: 40,
|
|
4081
4251
|
contextAfter: 40
|
|
4082
4252
|
})) {
|
|
4083
4253
|
findings.push({
|
|
4254
|
+
...locate(script, containingCallOffset(script, match.index)),
|
|
4084
4255
|
code: "gsap_repeat_floor_unclamped",
|
|
4085
4256
|
severity: "warning",
|
|
4086
4257
|
message: "GSAP repeat calculation can evaluate to -1 when the composition is shorter than one cycle, which GSAP interprets as an infinite repeat.",
|
|
@@ -4091,7 +4262,7 @@ ${right.raw}`)
|
|
|
4091
4262
|
return findings;
|
|
4092
4263
|
},
|
|
4093
4264
|
// gsap_timeline_not_registered
|
|
4094
|
-
({ scripts, rawSource, options }) => {
|
|
4265
|
+
({ scripts, rawSource, options, locate }) => {
|
|
4095
4266
|
const findings = [];
|
|
4096
4267
|
const canInheritFromHost = options.isSubComposition || rawSource.trimStart().toLowerCase().startsWith("<template");
|
|
4097
4268
|
for (const script of scripts) {
|
|
@@ -4100,6 +4271,7 @@ ${right.raw}`)
|
|
|
4100
4271
|
const hasRegistration = WINDOW_TIMELINE_ASSIGN_PATTERN.test(content) || TIMELINE_REGISTRY_OBJECT_LITERAL_PATTERN.test(content);
|
|
4101
4272
|
if (hasRegistration || canInheritFromHost) continue;
|
|
4102
4273
|
findings.push({
|
|
4274
|
+
...locate(script, gsapCallOffset(script, "timeline")),
|
|
4103
4275
|
code: "gsap_timeline_not_registered",
|
|
4104
4276
|
severity: "error",
|
|
4105
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.",
|
|
@@ -5142,6 +5314,7 @@ var textureRules = [
|
|
|
5142
5314
|
];
|
|
5143
5315
|
|
|
5144
5316
|
// src/rules/fonts.ts
|
|
5317
|
+
import postcss3 from "postcss";
|
|
5145
5318
|
import { FONT_ALIAS_KEYS, resolveAliasDisplayName } from "@hyperframes/parsers/composition";
|
|
5146
5319
|
var GENERIC_FAMILIES = /* @__PURE__ */ new Set([
|
|
5147
5320
|
"serif",
|
|
@@ -5288,7 +5461,7 @@ var fontRules = [
|
|
|
5288
5461
|
];
|
|
5289
5462
|
},
|
|
5290
5463
|
// font_family_without_font_face
|
|
5291
|
-
({ styles, source, rawSource, options }) => {
|
|
5464
|
+
({ styles, source, rawSource, options, locate }) => {
|
|
5292
5465
|
if (isRegistrySourceFile(options.filePath) || isRegistryInstalledFile(rawSource)) return [];
|
|
5293
5466
|
const findings = [];
|
|
5294
5467
|
const declared = extractFontFaceFamilies(styles);
|
|
@@ -5299,6 +5472,7 @@ var fontRules = [
|
|
|
5299
5472
|
);
|
|
5300
5473
|
if (undeclared.length === 0) return findings;
|
|
5301
5474
|
findings.push({
|
|
5475
|
+
...uniqueFontLocation(styles, undeclared, locate),
|
|
5302
5476
|
code: "font_family_without_font_face",
|
|
5303
5477
|
severity: "error",
|
|
5304
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.`,
|
|
@@ -5307,6 +5481,26 @@ var fontRules = [
|
|
|
5307
5481
|
return findings;
|
|
5308
5482
|
}
|
|
5309
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
|
+
}
|
|
5310
5504
|
|
|
5311
5505
|
// src/rules/slideshow.ts
|
|
5312
5506
|
import {
|
|
@@ -5579,13 +5773,27 @@ function collectFindings(produced, seen, filePath, into) {
|
|
|
5579
5773
|
const dedupeKey = dedupeKeyFor(finding);
|
|
5580
5774
|
if (seen.has(dedupeKey)) continue;
|
|
5581
5775
|
seen.add(dedupeKey);
|
|
5582
|
-
into.push(filePath ? { ...finding, file: filePath } : finding);
|
|
5776
|
+
into.push(filePath ? { ...finding, file: finding.file ?? filePath } : finding);
|
|
5583
5777
|
}
|
|
5584
5778
|
}
|
|
5585
5779
|
async function lintHyperframeHtml(html, options = {}) {
|
|
5586
5780
|
const startedAt = performance.now();
|
|
5587
5781
|
const ctx = buildLintContext(html, options);
|
|
5588
|
-
const { findings, timings } = await runRules(ctx, options.filePath);
|
|
5782
|
+
const { findings: rawFindings, timings } = await runRules(ctx, options.filePath);
|
|
5783
|
+
const locate = createSourceLocator(html);
|
|
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;
|
|
5791
|
+
if (finding.snippet && options.externalStyles?.some(
|
|
5792
|
+
(style) => style.content.replace(/\s+/g, " ").includes(finding.snippet.replace(/\.\.\.$/, ""))
|
|
5793
|
+
))
|
|
5794
|
+
return finding;
|
|
5795
|
+
return { ...finding, ...locate(finding) };
|
|
5796
|
+
});
|
|
5589
5797
|
const errorCount = findings.filter((f) => f.severity === "error").length;
|
|
5590
5798
|
const warningCount = findings.filter((f) => f.severity === "warning").length;
|
|
5591
5799
|
const infoCount = findings.filter((f) => f.severity === "info").length;
|