mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(lint): anchor the opacity-zero probe so 0.98 stops matching
gsap_from_opacity_noop matched 'opacity: 0' as a prefix — an authored 'opacity: 0.98' triggered the rule. One boundary-anchored predicate now owns the exactly-zero test for both block and inline declarations, including a block's last declaration without a trailing semicolon. domEditingDom now imports the grading contract attribute from core instead of re-declaring the literal.
This commit is contained in:
@@ -1194,6 +1194,63 @@ describe("GSAP rules", () => {
|
||||
expect(finding).toBeDefined();
|
||||
});
|
||||
|
||||
it("errors when a style block's LAST declaration is opacity:0 without a semicolon", async () => {
|
||||
const html = `
|
||||
<html><body>
|
||||
<div data-composition-id="c1" data-width="1920" data-height="1080">
|
||||
<div id="hero">Hello</div>
|
||||
</div>
|
||||
<style>
|
||||
#hero { font-size: 200px; opacity: 0 }
|
||||
</style>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
tl.from("#hero", { opacity: 0, duration: 0.25 }, 0.1);
|
||||
window.__timelines["c1"] = tl;
|
||||
</script>
|
||||
</body></html>`;
|
||||
const result = await lintHyperframeHtml(html);
|
||||
const finding = result.findings.find((f) => f.code === "gsap_from_opacity_noop");
|
||||
expect(finding).toBeDefined();
|
||||
});
|
||||
|
||||
it("does NOT error for inline opacity: 0.98 + gsap.from({opacity:0}) — fractional is not zero", async () => {
|
||||
const html = `
|
||||
<html><body>
|
||||
<div data-composition-id="c1" data-width="1920" data-height="1080">
|
||||
<img id="image-clip" style="opacity: 0.98; filter: blur(23px);" src="x.png">
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
tl.from("#image-clip", { opacity: 0, duration: 0.8 }, 0.2);
|
||||
window.__timelines["c1"] = tl;
|
||||
</script>
|
||||
</body></html>`;
|
||||
const result = await lintHyperframeHtml(html);
|
||||
const finding = result.findings.find((f) => f.code === "gsap_from_opacity_noop");
|
||||
expect(finding).toBeUndefined();
|
||||
});
|
||||
|
||||
it("still errors for inline opacity: 0 without a trailing semicolon", async () => {
|
||||
const html = `
|
||||
<html><body>
|
||||
<div data-composition-id="c1" data-width="1920" data-height="1080">
|
||||
<div id="title" style="font-size: 120px; opacity: 0">Hello</div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
tl.from("#title", { opacity: 0, duration: 0.5 }, 0.2);
|
||||
window.__timelines["c1"] = tl;
|
||||
</script>
|
||||
</body></html>`;
|
||||
const result = await lintHyperframeHtml(html);
|
||||
const finding = result.findings.find((f) => f.code === "gsap_from_opacity_noop");
|
||||
expect(finding).toBeDefined();
|
||||
});
|
||||
|
||||
it("does NOT error when gsap.from({opacity:0}) and CSS has no opacity:0", async () => {
|
||||
const html = `
|
||||
<html><body>
|
||||
|
||||
@@ -1128,11 +1128,18 @@ export const gsapRules: LintRule<LintContext>[] = [
|
||||
const findings: HyperframeLintFinding[] = [];
|
||||
const cssOpacityZeroSelectors = new Set<string>();
|
||||
|
||||
// Single owner of "this declaration list sets opacity to EXACTLY zero" —
|
||||
// boundary-anchored so `opacity: 0.98` never matches. Works for both a CSS
|
||||
// block body (brace already stripped by the block regex) and an inline
|
||||
// style attribute: the declaration ends at `;` or at end of input, which
|
||||
// also catches a final declaration without a trailing semicolon.
|
||||
const opacityExactlyZero = /opacity\s*:\s*0(?:\.0+)?\s*(?:;|$)/;
|
||||
|
||||
for (const style of styles) {
|
||||
for (const [, selector, body] of style.content.matchAll(
|
||||
/([#.][a-zA-Z0-9_-]+)\s*\{([^}]+)\}/g,
|
||||
)) {
|
||||
if (body && /opacity\s*:\s*0\s*[;}]/.test(body)) {
|
||||
if (body && opacityExactlyZero.test(body)) {
|
||||
cssOpacityZeroSelectors.add((selector ?? "").trim());
|
||||
}
|
||||
}
|
||||
@@ -1140,7 +1147,7 @@ export const gsapRules: LintRule<LintContext>[] = [
|
||||
|
||||
for (const tag of tags) {
|
||||
const inlineStyle = readAttr(tag.raw, "style");
|
||||
if (!inlineStyle || !/opacity\s*:\s*0/.test(inlineStyle)) continue;
|
||||
if (!inlineStyle || !opacityExactlyZero.test(inlineStyle)) continue;
|
||||
const id = readAttr(tag.raw, "id");
|
||||
const classes = readAttr(tag.raw, "class")?.split(/\s+/).filter(Boolean) ?? [];
|
||||
if (id) cssOpacityZeroSelectors.add(`#${id}`);
|
||||
|
||||
@@ -28,7 +28,7 @@ export function isTextBearingTag(tagName: string): boolean {
|
||||
return ["div", "span", "p", "strong", "h1", "h2", "h3", "h4", "h5", "h6"].includes(tagName);
|
||||
}
|
||||
|
||||
const COLOR_GRADING_SOURCE_HIDDEN_ATTR = "data-hf-color-grading-source-hidden";
|
||||
import { COLOR_GRADING_SOURCE_HIDDEN_ATTR } from "@hyperframes/core/color-grading";
|
||||
|
||||
export function isElementVisibleThroughAncestors(el: HTMLElement): boolean {
|
||||
const win = el.ownerDocument.defaultView;
|
||||
|
||||
Reference in New Issue
Block a user