@hyperframes/lint 0.8.49 → 0.8.51
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 +2 -0
- package/dist/browser.js +210 -1
- package/dist/browser.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +216 -4
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/browser.d.ts
CHANGED
|
@@ -45,6 +45,8 @@ type HyperframeLinterOptions = {
|
|
|
45
45
|
* render time — the font will silently fall back to whatever the OS provides.
|
|
46
46
|
*/
|
|
47
47
|
distributed?: boolean;
|
|
48
|
+
/** Who is running the lint: Studio raises the structure rules to errors, the CLI keeps them warnings. */
|
|
49
|
+
host?: "studio" | "cli";
|
|
48
50
|
};
|
|
49
51
|
|
|
50
52
|
declare function lintHyperframeHtml(html: string, options?: HyperframeLinterOptions): Promise<HyperframeLintResult>;
|
package/dist/browser.js
CHANGED
|
@@ -1538,6 +1538,7 @@ function findRuntimeMediaSrcMutationFindings(ctx) {
|
|
|
1538
1538
|
}
|
|
1539
1539
|
var mediaRules = [
|
|
1540
1540
|
findNestedMediaStartBasisFindings,
|
|
1541
|
+
findSpeedRampOnNonMediaFindings,
|
|
1541
1542
|
// duplicate_media_id + duplicate_media_discovery_risk
|
|
1542
1543
|
({ tags }) => {
|
|
1543
1544
|
const findings = [];
|
|
@@ -1959,6 +1960,18 @@ function findVolumeDoubleAutomationFindings(ctx) {
|
|
|
1959
1960
|
}
|
|
1960
1961
|
return findings;
|
|
1961
1962
|
}
|
|
1963
|
+
function findSpeedRampOnNonMediaFindings(ctx) {
|
|
1964
|
+
return ctx.tags.filter((tag) => tag.name !== "video" && tag.name !== "audio").filter(
|
|
1965
|
+
(tag) => /"target"\s*:\s*"rate"/.test(readDecodedAttr(tag.raw, "data-automation") ?? "")
|
|
1966
|
+
).map((tag) => ({
|
|
1967
|
+
code: "speed_ramp_on_non_media",
|
|
1968
|
+
severity: "warning",
|
|
1969
|
+
message: `<${tag.name}> has a speed-ramp lane, but only <video> and <audio> clips can be retimed. The lane does nothing here.`,
|
|
1970
|
+
elementId: readAttr(tag.raw, "id") || void 0,
|
|
1971
|
+
fixHint: "Move the rate lane onto the <video> or <audio> clip, or retime an animation with a GSAP timeline instead.",
|
|
1972
|
+
snippet: truncateSnippet(tag.raw)
|
|
1973
|
+
}));
|
|
1974
|
+
}
|
|
1962
1975
|
function findCarveUngroupedSourcesFindings(ctx) {
|
|
1963
1976
|
const groupIds = new Set(
|
|
1964
1977
|
ctx.tags.filter((tag) => tag.name === "hf-audio-group").map((tag) => readAttr(tag.raw, "id"))
|
|
@@ -2065,6 +2078,11 @@ function findAudioGroupCarveAttrFindings(ctx) {
|
|
|
2065
2078
|
// src/rules/composition.ts
|
|
2066
2079
|
import { COMPOSITION_VARIABLE_TYPES, isSafeMediaUrl } from "@hyperframes/parsers/composition";
|
|
2067
2080
|
import { COMPOSITION_ATTRIBUTES, readClipTiming } from "@hyperframes/parsers/composition-contract";
|
|
2081
|
+
import { resolveCompositionDuration } from "@hyperframes/parsers/composition-duration";
|
|
2082
|
+
import {
|
|
2083
|
+
readAuthoredDurationSeconds,
|
|
2084
|
+
resolveMediaDuration
|
|
2085
|
+
} from "@hyperframes/parsers/media-duration";
|
|
2068
2086
|
var MAX_COMPOSITION_LINES = 300;
|
|
2069
2087
|
var MAX_TIMED_ELEMENTS_PER_TRACK = 3;
|
|
2070
2088
|
var TRACK_DENSITY_EXEMPT_TAGS = /* @__PURE__ */ new Set(["audio", "script", "style", "video"]);
|
|
@@ -2840,6 +2858,18 @@ ${allInlineStyles}`.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
|
2840
2858
|
const hasInfiniteCssAnimation = /\banimation(?:-iteration-count)?\s*:[^;{}]*(?<![\w-])infinite(?![\w-])/.test(combinedCss);
|
|
2841
2859
|
const hasAnyNonGsapSignal = usesLottie || usesThree || usesWaapi || hasCssAnimationName;
|
|
2842
2860
|
if (!hasAnyNonGsapSignal) {
|
|
2861
|
+
const derived = deriveDurationFromClips(tags, rootTag);
|
|
2862
|
+
if (derived.source === "derived") {
|
|
2863
|
+
return [
|
|
2864
|
+
{
|
|
2865
|
+
code: "root_composition_duration_derived",
|
|
2866
|
+
severity: "warning",
|
|
2867
|
+
message: `Root composition has no data-duration and no GSAP timeline, so its length is taken from its clips: at least ${derived.seconds}s` + (derived.pendingClips > 0 ? `, and ${derived.pendingClips} clip(s) whose length is only known at runtime` : "") + ".",
|
|
2868
|
+
fixHint: 'Add data-duration="<seconds>" to the root element to set the length yourself.',
|
|
2869
|
+
snippet: truncateSnippet(rootTag.raw)
|
|
2870
|
+
}
|
|
2871
|
+
];
|
|
2872
|
+
}
|
|
2843
2873
|
return [
|
|
2844
2874
|
{
|
|
2845
2875
|
code: "root_composition_missing_duration_source",
|
|
@@ -2924,6 +2954,35 @@ ${allInlineStyles}`.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
|
2924
2954
|
];
|
|
2925
2955
|
}
|
|
2926
2956
|
];
|
|
2957
|
+
var CLIP_MEDIA_TAGS = /* @__PURE__ */ new Set(["img", "video", "audio"]);
|
|
2958
|
+
function deriveDurationFromClips(tags, rootTag) {
|
|
2959
|
+
const clipEnds = [];
|
|
2960
|
+
for (const tag of tags) {
|
|
2961
|
+
if (tag === rootTag) continue;
|
|
2962
|
+
const startRaw = readAttr(tag.raw, "data-start");
|
|
2963
|
+
if (startRaw === null) continue;
|
|
2964
|
+
const start = Number(startRaw);
|
|
2965
|
+
if (!Number.isFinite(start)) {
|
|
2966
|
+
clipEnds.push(null);
|
|
2967
|
+
continue;
|
|
2968
|
+
}
|
|
2969
|
+
const getAttr = (name) => readAttr(tag.raw, name);
|
|
2970
|
+
const authored = readAuthoredDurationSeconds(getAttr, start);
|
|
2971
|
+
if (CLIP_MEDIA_TAGS.has(tag.name)) {
|
|
2972
|
+
const { seconds } = resolveMediaDuration({
|
|
2973
|
+
tag: tag.name,
|
|
2974
|
+
authoredDurationSeconds: authored,
|
|
2975
|
+
sourceDurationSeconds: null,
|
|
2976
|
+
mediaStartSeconds: 0,
|
|
2977
|
+
playbackRate: 1
|
|
2978
|
+
});
|
|
2979
|
+
clipEnds.push(seconds === null ? null : start + seconds);
|
|
2980
|
+
} else if (authored !== null && authored > 0) {
|
|
2981
|
+
clipEnds.push(start + authored);
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
return resolveCompositionDuration({ authoredDurationSeconds: null, clipEndsSeconds: clipEnds });
|
|
2985
|
+
}
|
|
2927
2986
|
|
|
2928
2987
|
// src/rules/gsap.ts
|
|
2929
2988
|
async function loadParseGsapScript() {
|
|
@@ -5319,6 +5378,155 @@ var slideshowRules = [
|
|
|
5319
5378
|
}
|
|
5320
5379
|
];
|
|
5321
5380
|
|
|
5381
|
+
// src/rules/structure.ts
|
|
5382
|
+
import {
|
|
5383
|
+
isSubCompositionHost,
|
|
5384
|
+
topLevelElements,
|
|
5385
|
+
trackKindOf
|
|
5386
|
+
} from "@hyperframes/parsers/top-level-elements";
|
|
5387
|
+
var INLINE_TEXT_TAGS = /* @__PURE__ */ new Set([
|
|
5388
|
+
"br",
|
|
5389
|
+
"wbr",
|
|
5390
|
+
"b",
|
|
5391
|
+
"i",
|
|
5392
|
+
"u",
|
|
5393
|
+
"s",
|
|
5394
|
+
"em",
|
|
5395
|
+
"strong",
|
|
5396
|
+
"small",
|
|
5397
|
+
"sub",
|
|
5398
|
+
"sup",
|
|
5399
|
+
"mark",
|
|
5400
|
+
"span",
|
|
5401
|
+
"a",
|
|
5402
|
+
"code",
|
|
5403
|
+
"abbr"
|
|
5404
|
+
]);
|
|
5405
|
+
var OPAQUE_TAGS = /* @__PURE__ */ new Set([
|
|
5406
|
+
"svg",
|
|
5407
|
+
"canvas",
|
|
5408
|
+
"picture",
|
|
5409
|
+
"video",
|
|
5410
|
+
"audio",
|
|
5411
|
+
"select",
|
|
5412
|
+
"textarea",
|
|
5413
|
+
"iframe",
|
|
5414
|
+
"object"
|
|
5415
|
+
]);
|
|
5416
|
+
var NON_LAYOUT_TAGS = /* @__PURE__ */ new Set(["style", "script", "template", "noscript"]);
|
|
5417
|
+
var MEDIA_TAGS = /* @__PURE__ */ new Set(["video", "audio", "img"]);
|
|
5418
|
+
var NODE_ATTRS = [
|
|
5419
|
+
"id",
|
|
5420
|
+
"class",
|
|
5421
|
+
"data-start",
|
|
5422
|
+
"data-duration",
|
|
5423
|
+
"data-end",
|
|
5424
|
+
"data-track",
|
|
5425
|
+
"data-track-index",
|
|
5426
|
+
"data-track-kind",
|
|
5427
|
+
"data-composition-id",
|
|
5428
|
+
"data-composition-src"
|
|
5429
|
+
];
|
|
5430
|
+
function toNode(open) {
|
|
5431
|
+
const attrs = {};
|
|
5432
|
+
for (const name of NODE_ATTRS) attrs[name] = readDecodedAttr(open.raw, name) ?? void 0;
|
|
5433
|
+
return { tag: open.name, attrs, children: [], open };
|
|
5434
|
+
}
|
|
5435
|
+
function enclosing(stack, root, index) {
|
|
5436
|
+
let top = stack.at(-1);
|
|
5437
|
+
while (top && stack.length > 1 && index >= top.end) {
|
|
5438
|
+
stack.pop();
|
|
5439
|
+
top = stack.at(-1);
|
|
5440
|
+
}
|
|
5441
|
+
return top?.node ?? root;
|
|
5442
|
+
}
|
|
5443
|
+
function buildTree(ctx) {
|
|
5444
|
+
const { rootTag } = ctx;
|
|
5445
|
+
const rootEnd = rootTag?.closeIndex;
|
|
5446
|
+
if (!rootTag || rootEnd == null) return null;
|
|
5447
|
+
const root = toNode(rootTag);
|
|
5448
|
+
const stack = [{ node: root, end: rootEnd }];
|
|
5449
|
+
for (const tag of ctx.tags.filter((t) => t.index > rootTag.index && t.index < rootEnd)) {
|
|
5450
|
+
const node = toNode(tag);
|
|
5451
|
+
enclosing(stack, root, tag.index).children.push(node);
|
|
5452
|
+
if (tag.closeIndex != null) stack.push({ node, end: tag.closeIndex });
|
|
5453
|
+
}
|
|
5454
|
+
return root;
|
|
5455
|
+
}
|
|
5456
|
+
function hasNestedStructure(node) {
|
|
5457
|
+
if (OPAQUE_TAGS.has(node.tag)) return null;
|
|
5458
|
+
for (const child of node.children) {
|
|
5459
|
+
if (NON_LAYOUT_TAGS.has(child.tag)) continue;
|
|
5460
|
+
if (!INLINE_TEXT_TAGS.has(child.tag)) return child;
|
|
5461
|
+
const deeper = hasNestedStructure(child);
|
|
5462
|
+
if (deeper) return deeper;
|
|
5463
|
+
}
|
|
5464
|
+
return null;
|
|
5465
|
+
}
|
|
5466
|
+
var describe = (node) => `<${node.tag}${node.attrs.id ? ` id="${node.attrs.id}"` : ""}>`;
|
|
5467
|
+
function nestedStructureFindings(rows, severity) {
|
|
5468
|
+
return rows.flatMap((row) => {
|
|
5469
|
+
const nested = isSubCompositionHost(row) ? null : hasNestedStructure(row);
|
|
5470
|
+
if (!nested) return [];
|
|
5471
|
+
return [
|
|
5472
|
+
{
|
|
5473
|
+
code: "nested_structure_needs_subcomposition",
|
|
5474
|
+
severity,
|
|
5475
|
+
message: `${describe(row)} is a timeline element that contains nested ${describe(nested)}. The timeline shows one row per top-level element, and the root composition is built only from sub-compositions.`,
|
|
5476
|
+
elementId: row.attrs.id,
|
|
5477
|
+
fixHint: `Move ${describe(row)} and its contents into a sub-composition file and mount it with data-composition-src.`
|
|
5478
|
+
}
|
|
5479
|
+
];
|
|
5480
|
+
});
|
|
5481
|
+
}
|
|
5482
|
+
function missingDurationFindings(rows, severity) {
|
|
5483
|
+
return rows.filter(
|
|
5484
|
+
(row) => !MEDIA_TAGS.has(row.tag) && row.attrs["data-duration"] === void 0 && row.attrs["data-end"] === void 0 && !isSubCompositionHost(row)
|
|
5485
|
+
).map((row) => ({
|
|
5486
|
+
code: "timeline_element_missing_timing",
|
|
5487
|
+
severity,
|
|
5488
|
+
message: `${describe(row)} is a timeline element without data-duration, so the timeline cannot draw where it ends.`,
|
|
5489
|
+
elementId: row.attrs.id,
|
|
5490
|
+
fixHint: `Add data-duration (in seconds) to ${describe(row)}.`
|
|
5491
|
+
}));
|
|
5492
|
+
}
|
|
5493
|
+
function captionFindings(rows, severity) {
|
|
5494
|
+
const captionRows = rows.filter((row) => trackKindOf(row).kind === "captions");
|
|
5495
|
+
const findings = captionRows.filter((row) => trackKindOf(row).source === "legacy-captions").map((row) => ({
|
|
5496
|
+
code: "caption_track_kind_missing",
|
|
5497
|
+
severity,
|
|
5498
|
+
message: `${describe(row)} looks like captions but is not marked as such, so the timeline cannot group it on the caption track.`,
|
|
5499
|
+
elementId: row.attrs.id,
|
|
5500
|
+
fixHint: `Add data-track-kind="captions" to ${describe(row)}.`
|
|
5501
|
+
}));
|
|
5502
|
+
const lanes = new Set(
|
|
5503
|
+
captionRows.flatMap((row) => row.attrs["data-track-index"] ?? row.attrs["data-track"] ?? [])
|
|
5504
|
+
);
|
|
5505
|
+
if (lanes.size > 1) {
|
|
5506
|
+
findings.push({
|
|
5507
|
+
code: "multiple_caption_tracks",
|
|
5508
|
+
severity,
|
|
5509
|
+
message: `Captions are spread across ${lanes.size} timeline tracks. Keep all captions on one track.`,
|
|
5510
|
+
fixHint: "Give every caption element the same data-track-index."
|
|
5511
|
+
});
|
|
5512
|
+
}
|
|
5513
|
+
return findings;
|
|
5514
|
+
}
|
|
5515
|
+
var structureRules = [
|
|
5516
|
+
(ctx) => {
|
|
5517
|
+
if (ctx.options.isSubComposition) return [];
|
|
5518
|
+
const root = buildTree(ctx);
|
|
5519
|
+
if (!root) return [];
|
|
5520
|
+
const severity = ctx.options.host === "studio" ? "error" : "warning";
|
|
5521
|
+
const rows = topLevelElements(root);
|
|
5522
|
+
return [
|
|
5523
|
+
...nestedStructureFindings(rows, severity),
|
|
5524
|
+
...missingDurationFindings(rows, severity),
|
|
5525
|
+
...captionFindings(rows, severity)
|
|
5526
|
+
];
|
|
5527
|
+
}
|
|
5528
|
+
];
|
|
5529
|
+
|
|
5322
5530
|
// src/hyperframeLinter.ts
|
|
5323
5531
|
var RULE_GROUPS = [
|
|
5324
5532
|
{ group: "core", rules: coreRules },
|
|
@@ -5329,7 +5537,8 @@ var RULE_GROUPS = [
|
|
|
5329
5537
|
{ group: "adapters", rules: adapterRules },
|
|
5330
5538
|
{ group: "textures", rules: textureRules },
|
|
5331
5539
|
{ group: "fonts", rules: fontRules },
|
|
5332
|
-
{ group: "slideshow", rules: slideshowRules }
|
|
5540
|
+
{ group: "slideshow", rules: slideshowRules },
|
|
5541
|
+
{ group: "structure", rules: structureRules }
|
|
5333
5542
|
];
|
|
5334
5543
|
var LINT_RULE_COUNT = RULE_GROUPS.reduce((n, g) => n + g.rules.length, 0);
|
|
5335
5544
|
var LINT_RULE_GROUP_COUNTS = Object.fromEntries(
|