@hyperframes/lint 0.7.52 → 0.7.54
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 +108 -88
- package/dist/browser.js.map +1 -1
- package/dist/index.js +144 -127
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/browser.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
// src/utils.ts
|
|
2
|
-
|
|
3
|
-
var STYLE_BLOCK_PATTERN = /<style\b([^>]*)>([\s\S]*?)<\/style>/gi;
|
|
4
|
-
var SCRIPT_BLOCK_PATTERN = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
|
|
2
|
+
import { Parser } from "htmlparser2";
|
|
5
3
|
var COMPOSITION_ID_IN_CSS_PATTERN = /\[data-composition-id=["']([^"']+)["']\]/g;
|
|
6
4
|
var TIMELINE_REGISTRY_INIT_PATTERN = /window\.__timelines\s*=\s*window\.__timelines\s*\|\|\s*\{\}|window\.__timelines\s*=\s*\{\}|window\.__timelines\s*\?\?=\s*\{\}/i;
|
|
7
5
|
var TIMELINE_REGISTRY_OBJECT_LITERAL_PATTERN = /window\.__timelines\s*=\s*\{\s*(?:["'][^"']+["']|[A-Za-z_$][\w$]*)\s*:/i;
|
|
@@ -11,71 +9,69 @@ var INVALID_SCRIPT_CLOSE_PATTERN = /<script[^>]*>[\s\S]*?<\s*\/\s*script(?!>)/i;
|
|
|
11
9
|
var TIMELINE_REGISTRY_KEY_PATTERN = /window\.__timelines(?:\[\s*["']([^"']+)["']\s*\]|\.\s*([A-Za-z_$][\w$]*))\s*=/g;
|
|
12
10
|
var TIMELINE_REGISTRY_OBJECT_BODY_PATTERN = /window\.__timelines\s*=\s*\{([\s\S]*?)\}/i;
|
|
13
11
|
var TIMELINE_REGISTRY_OBJECT_ENTRY_PATTERN = /(?:["']([^"']+)["']|([A-Za-z_$][\w$]*))\s*:\s*[A-Za-z_$][\w$]*/g;
|
|
14
|
-
function
|
|
12
|
+
function parseHtmlStructure(source) {
|
|
15
13
|
const tags = [];
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
14
|
+
const blocks = { script: [], style: [] };
|
|
15
|
+
const openTagsByName = /* @__PURE__ */ new Map();
|
|
16
|
+
const openBlocks = [];
|
|
17
|
+
const parser = new Parser(
|
|
18
|
+
{
|
|
19
|
+
onopentag(name) {
|
|
20
|
+
const index = parser.startIndex;
|
|
21
|
+
const raw = source.slice(index, parser.endIndex + 1);
|
|
22
|
+
const attrs = raw.slice(name.length + 1, -1).replace(/\s*\/$/, "");
|
|
23
|
+
const tag = { raw, name, attrs, index };
|
|
24
|
+
tags.push(tag);
|
|
25
|
+
const sameNameStack = openTagsByName.get(name) ?? [];
|
|
26
|
+
sameNameStack.push(tag);
|
|
27
|
+
openTagsByName.set(name, sameNameStack);
|
|
28
|
+
if (name === "script" || name === "style") {
|
|
29
|
+
openBlocks.push({ name, attrs, contentStart: parser.endIndex + 1, index });
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
onclosetag(name) {
|
|
33
|
+
const tag = openTagsByName.get(name)?.pop();
|
|
34
|
+
if (tag) {
|
|
35
|
+
tag.closeIndex = parser.startIndex;
|
|
36
|
+
tag.endIndex = parser.endIndex + 1;
|
|
37
|
+
}
|
|
38
|
+
if (name !== "script" && name !== "style") return;
|
|
39
|
+
const block = openBlocks.pop();
|
|
40
|
+
if (!block || block.name !== name) return;
|
|
41
|
+
blocks[name].push({
|
|
42
|
+
attrs: block.attrs,
|
|
43
|
+
content: source.slice(block.contentStart, parser.startIndex),
|
|
44
|
+
raw: source.slice(block.index, parser.endIndex + 1),
|
|
45
|
+
index: block.index
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
{ decodeEntities: false, lowerCaseAttributeNames: false, lowerCaseTags: true }
|
|
50
|
+
);
|
|
51
|
+
parser.end(source);
|
|
52
|
+
return { tags, scripts: blocks.script, styles: blocks.style };
|
|
43
53
|
}
|
|
44
|
-
function findHtmlTag(
|
|
45
|
-
|
|
46
|
-
if (!match) return null;
|
|
47
|
-
return {
|
|
48
|
-
raw: match[0],
|
|
49
|
-
name: "html",
|
|
50
|
-
attrs: match[1] ?? "",
|
|
51
|
-
index: match.index
|
|
52
|
-
};
|
|
54
|
+
function findHtmlTag(tags) {
|
|
55
|
+
return tags.find((tag) => tag.name === "html") ?? null;
|
|
53
56
|
}
|
|
54
|
-
function findRootTag(source) {
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
if (
|
|
58
|
-
return
|
|
59
|
-
raw: bodyOpenMatch[0],
|
|
60
|
-
name: "body",
|
|
61
|
-
attrs: bodyOpenMatch[1] ?? "",
|
|
62
|
-
index: bodyOpenMatch.index
|
|
63
|
-
};
|
|
57
|
+
function findRootTag(source, parsedTags) {
|
|
58
|
+
const tags = parsedTags ?? parseHtmlStructure(source).tags;
|
|
59
|
+
const bodyTag = tags.find((tag) => tag.name === "body");
|
|
60
|
+
if (bodyTag && (readAttr(bodyTag.raw, "data-composition-id") || readAttr(bodyTag.raw, "data-width") || readAttr(bodyTag.raw, "data-height"))) {
|
|
61
|
+
return bodyTag;
|
|
64
62
|
}
|
|
65
|
-
const bodyStart =
|
|
66
|
-
const bodyEnd =
|
|
67
|
-
const
|
|
68
|
-
const bodyTags = extractOpenTags(bodyContent);
|
|
63
|
+
const bodyStart = bodyTag ? bodyTag.index + bodyTag.raw.length : 0;
|
|
64
|
+
const bodyEnd = bodyTag?.closeIndex ?? source.length;
|
|
65
|
+
const bodyTags = tags.filter((tag) => tag.index >= bodyStart && tag.index < bodyEnd);
|
|
69
66
|
let skipBefore = -1;
|
|
70
67
|
for (const tag of bodyTags) {
|
|
71
68
|
if (tag.index < skipBefore) continue;
|
|
72
69
|
if (["script", "style", "meta", "link", "title"].includes(tag.name)) continue;
|
|
73
70
|
if (tag.name === "svg" && !readAttr(tag.raw, "data-composition-id") && !readAttr(tag.raw, "data-width") && !readAttr(tag.raw, "data-height")) {
|
|
74
|
-
|
|
75
|
-
skipBefore = closeMatch ? tag.index + closeMatch.index + closeMatch[0].length : Infinity;
|
|
71
|
+
skipBefore = tag.endIndex ?? Infinity;
|
|
76
72
|
continue;
|
|
77
73
|
}
|
|
78
|
-
return
|
|
74
|
+
return tag;
|
|
79
75
|
}
|
|
80
76
|
return null;
|
|
81
77
|
}
|
|
@@ -246,15 +242,24 @@ function truncateSnippet(value, maxLength = 220) {
|
|
|
246
242
|
function buildLintContext(html, options = {}) {
|
|
247
243
|
const rawSource = html || "";
|
|
248
244
|
let source = stripHtmlComments(rawSource);
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
"
|
|
245
|
+
const initialStructure = parseHtmlStructure(source);
|
|
246
|
+
const templateTags = initialStructure.tags.filter(
|
|
247
|
+
(tag) => tag.name === "template" && tag.closeIndex != null
|
|
252
248
|
);
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
249
|
+
let sourceWithoutTemplates = source;
|
|
250
|
+
for (const template2 of [...templateTags].reverse()) {
|
|
251
|
+
const end = template2.endIndex ?? template2.index;
|
|
252
|
+
sourceWithoutTemplates = sourceWithoutTemplates.slice(0, template2.index) + " ".repeat(end - template2.index) + sourceWithoutTemplates.slice(end);
|
|
253
|
+
}
|
|
254
|
+
const template = templateTags[0];
|
|
255
|
+
let structure = initialStructure;
|
|
256
|
+
if (template && !findRootTag(sourceWithoutTemplates)) {
|
|
257
|
+
source = source.slice(template.index + template.raw.length, template.closeIndex);
|
|
258
|
+
structure = parseHtmlStructure(source);
|
|
259
|
+
}
|
|
260
|
+
const tags = structure.tags;
|
|
256
261
|
const styles = [
|
|
257
|
-
...
|
|
262
|
+
...structure.styles,
|
|
258
263
|
...(options.externalStyles ?? []).map((style) => ({
|
|
259
264
|
attrs: `href="${style.href}"`,
|
|
260
265
|
content: style.content,
|
|
@@ -262,9 +267,9 @@ function buildLintContext(html, options = {}) {
|
|
|
262
267
|
index: -1
|
|
263
268
|
}))
|
|
264
269
|
];
|
|
265
|
-
const scripts =
|
|
270
|
+
const scripts = structure.scripts;
|
|
266
271
|
const compositionIds = collectCompositionIds(tags);
|
|
267
|
-
const rootTag = findRootTag(source);
|
|
272
|
+
const rootTag = findRootTag(source, tags);
|
|
268
273
|
const rootCompositionId = readAttr(rootTag?.raw || "", "data-composition-id");
|
|
269
274
|
return {
|
|
270
275
|
source,
|
|
@@ -403,6 +408,23 @@ function findVisibleMarkupCommentLeak(source) {
|
|
|
403
408
|
return null;
|
|
404
409
|
}
|
|
405
410
|
var coreRules = [
|
|
411
|
+
// id_requires_css_escape
|
|
412
|
+
({ tags }) => {
|
|
413
|
+
const findings = [];
|
|
414
|
+
for (const tag of tags) {
|
|
415
|
+
const id = readAttr(tag.raw, "id");
|
|
416
|
+
if (!id || !/^\d/.test(id)) continue;
|
|
417
|
+
findings.push({
|
|
418
|
+
code: "id_requires_css_escape",
|
|
419
|
+
severity: "warning",
|
|
420
|
+
message: `id="${id}" starts with a digit, so the common selector \`#${id}\` throws a SyntaxError in querySelector().`,
|
|
421
|
+
elementId: id,
|
|
422
|
+
fixHint: "Rename the id to start with a letter (recommended), or build selectors with `#${CSS.escape(id)}` at runtime.",
|
|
423
|
+
snippet: truncateSnippet(tag.raw)
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
return findings;
|
|
427
|
+
},
|
|
406
428
|
// root_missing_composition_id + root_missing_dimensions
|
|
407
429
|
({ rootTag }) => {
|
|
408
430
|
const findings = [];
|
|
@@ -457,10 +479,11 @@ var coreRules = [
|
|
|
457
479
|
];
|
|
458
480
|
},
|
|
459
481
|
// missing_timeline_registry + timeline_registry_missing_init
|
|
460
|
-
({ source, rawSource, options }) => {
|
|
482
|
+
({ source, rawSource, rootTag, options }) => {
|
|
461
483
|
if (options.isSubComposition || rawSource.trimStart().toLowerCase().startsWith("<template")) {
|
|
462
484
|
return [];
|
|
463
485
|
}
|
|
486
|
+
if (/(?:^|\s)data-no-timeline(?=[\s=/]|$)/i.test(rootTag?.attrs || "")) return [];
|
|
464
487
|
const findings = [];
|
|
465
488
|
if (!TIMELINE_REGISTRY_INIT_PATTERN.test(source) && !TIMELINE_REGISTRY_ASSIGN_PATTERN.test(source) && !TIMELINE_REGISTRY_OBJECT_LITERAL_PATTERN.test(source)) {
|
|
466
489
|
findings.push({
|
|
@@ -1981,18 +2004,19 @@ ${right.raw}`)
|
|
|
1981
2004
|
async ({ styles, scripts, tags }) => {
|
|
1982
2005
|
const findings = [];
|
|
1983
2006
|
const cssOpacityZeroSelectors = /* @__PURE__ */ new Set();
|
|
2007
|
+
const opacityExactlyZero = /opacity\s*:\s*0(?:\.0+)?\s*(?:;|$)/;
|
|
1984
2008
|
for (const style of styles) {
|
|
1985
2009
|
for (const [, selector, body] of style.content.matchAll(
|
|
1986
2010
|
/([#.][a-zA-Z0-9_-]+)\s*\{([^}]+)\}/g
|
|
1987
2011
|
)) {
|
|
1988
|
-
if (body &&
|
|
2012
|
+
if (body && opacityExactlyZero.test(body)) {
|
|
1989
2013
|
cssOpacityZeroSelectors.add((selector ?? "").trim());
|
|
1990
2014
|
}
|
|
1991
2015
|
}
|
|
1992
2016
|
}
|
|
1993
2017
|
for (const tag of tags) {
|
|
1994
2018
|
const inlineStyle = readAttr(tag.raw, "style");
|
|
1995
|
-
if (!inlineStyle ||
|
|
2019
|
+
if (!inlineStyle || !opacityExactlyZero.test(inlineStyle)) continue;
|
|
1996
2020
|
const id = readAttr(tag.raw, "id");
|
|
1997
2021
|
const classes = readAttr(tag.raw, "class")?.split(/\s+/).filter(Boolean) ?? [];
|
|
1998
2022
|
if (id) cssOpacityZeroSelectors.add(`#${id}`);
|
|
@@ -2460,21 +2484,20 @@ function collectDeclaredVariableIds(htmlTagRaw) {
|
|
|
2460
2484
|
}
|
|
2461
2485
|
return declared;
|
|
2462
2486
|
}
|
|
2463
|
-
function collectAllDeclaredVariableIds(
|
|
2487
|
+
function collectAllDeclaredVariableIds(tags) {
|
|
2464
2488
|
const all = /* @__PURE__ */ new Set();
|
|
2465
|
-
const
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
const ids = collectDeclaredVariableIds(match[0]);
|
|
2489
|
+
for (const tag of tags) {
|
|
2490
|
+
if (!readAttr(tag.raw, "data-composition-variables")) continue;
|
|
2491
|
+
const ids = collectDeclaredVariableIds(tag.raw);
|
|
2469
2492
|
if (ids === null) return null;
|
|
2470
2493
|
for (const id of ids) all.add(id);
|
|
2471
2494
|
}
|
|
2472
2495
|
return all;
|
|
2473
2496
|
}
|
|
2474
|
-
function declaredIdsForBindingCheck(
|
|
2475
|
-
const declared = collectAllDeclaredVariableIds(
|
|
2497
|
+
function declaredIdsForBindingCheck(tags) {
|
|
2498
|
+
const declared = collectAllDeclaredVariableIds(tags);
|
|
2476
2499
|
if (declared === null) return null;
|
|
2477
|
-
if (declared.size === 0 && !findHtmlTag(
|
|
2500
|
+
if (declared.size === 0 && !findHtmlTag(tags)) return null;
|
|
2478
2501
|
return declared;
|
|
2479
2502
|
}
|
|
2480
2503
|
var compositionRules = [
|
|
@@ -2878,8 +2901,8 @@ var compositionRules = [
|
|
|
2878
2901
|
// nothing, so a typo'd binding is invisible until a customer's override
|
|
2879
2902
|
// does nothing. Skipped for fragment files (no <html>): their values come
|
|
2880
2903
|
// from a host's data-variable-values, which this file can't see.
|
|
2881
|
-
({
|
|
2882
|
-
const declared = declaredIdsForBindingCheck(
|
|
2904
|
+
({ tags }) => {
|
|
2905
|
+
const declared = declaredIdsForBindingCheck(tags);
|
|
2883
2906
|
if (!declared) return [];
|
|
2884
2907
|
const findings = [];
|
|
2885
2908
|
for (const tag of tags) {
|
|
@@ -2904,8 +2927,8 @@ var compositionRules = [
|
|
|
2904
2927
|
// catch them at lint time rather than wondering why their `getVariables()`
|
|
2905
2928
|
// defaults aren't applied.
|
|
2906
2929
|
// fallow-ignore-next-line complexity
|
|
2907
|
-
({
|
|
2908
|
-
const htmlTag = findHtmlTag(
|
|
2930
|
+
({ tags }) => {
|
|
2931
|
+
const htmlTag = findHtmlTag(tags);
|
|
2909
2932
|
if (!htmlTag) return [];
|
|
2910
2933
|
const raw = readJsonAttr(htmlTag.raw, "data-composition-variables");
|
|
2911
2934
|
if (!raw) return [];
|
|
@@ -2978,8 +3001,8 @@ var compositionRules = [
|
|
|
2978
3001
|
// fixed top-left-origin screenshot region, which RTL layout can shift the
|
|
2979
3002
|
// actual content away from), only surfaces the already-confirmed footgun
|
|
2980
3003
|
// before someone hits it blind.
|
|
2981
|
-
({
|
|
2982
|
-
const htmlTag = findHtmlTag(
|
|
3004
|
+
({ tags }) => {
|
|
3005
|
+
const htmlTag = findHtmlTag(tags);
|
|
2983
3006
|
if (!htmlTag) return [];
|
|
2984
3007
|
const dir = readAttr(htmlTag.raw, "dir");
|
|
2985
3008
|
if (!dir) return [];
|
|
@@ -3671,11 +3694,8 @@ async function lintHyperframeHtml(html, options = {}) {
|
|
|
3671
3694
|
}
|
|
3672
3695
|
function extractMediaUrls(html) {
|
|
3673
3696
|
const results = [];
|
|
3674
|
-
const
|
|
3675
|
-
|
|
3676
|
-
while ((match = tagRe.exec(html)) !== null) {
|
|
3677
|
-
const tagName = (match[1] ?? "").toLowerCase();
|
|
3678
|
-
const raw = match[0];
|
|
3697
|
+
for (const { name: tagName, raw } of parseHtmlStructure(html).tags) {
|
|
3698
|
+
if (!/^(?:video|audio|img|source)$/.test(tagName)) continue;
|
|
3679
3699
|
const src = readAttr(raw, "src");
|
|
3680
3700
|
if (!src) continue;
|
|
3681
3701
|
if (/^https?:\/\//i.test(src)) {
|