@hyperframes/lint 0.8.156 → 0.8.158

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 CHANGED
@@ -2577,9 +2577,9 @@ function countStructuralLines(source) {
2577
2577
  return countPhysicalLines(source.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, "<style></style>"));
2578
2578
  }
2579
2579
  function isCaptionCue(tag) {
2580
- const classTokens = (readAttr(tag.raw, "class") || "").split(/\s+/).filter(Boolean);
2580
+ const classTokens2 = (readAttr(tag.raw, "class") || "").split(/\s+/).filter(Boolean);
2581
2581
  const id = readAttr(tag.raw, "id");
2582
- return classTokens.some((token) => CAPTION_CUE_TOKEN.test(token)) || Boolean(id && CAPTION_CUE_TOKEN.test(id));
2582
+ return classTokens2.some((token) => CAPTION_CUE_TOKEN.test(token)) || Boolean(id && CAPTION_CUE_TOKEN.test(id));
2583
2583
  }
2584
2584
  function isRegistrySourceFile(filePath) {
2585
2585
  if (!filePath) return false;
@@ -2879,6 +2879,58 @@ function elementSelector(tag) {
2879
2879
  const elementId = readAttr(tag.raw, "id");
2880
2880
  return elementId ? `#${elementId}` : void 0;
2881
2881
  }
2882
+ var STACKING_CONTEXT_DECLARATION = /(?:^|[;{])\s*(?:isolation\s*:\s*isolate|opacity\s*:\s*(?:0\b|0?\.\d+)|transform\s*:\s*(?!none\b)[^;]+|filter\s*:\s*(?!none\b)[^;]+|backdrop-filter\s*:\s*(?!none\b)[^;]+|-webkit-backdrop-filter\s*:\s*(?!none\b)[^;]+|contain\s*:[^;]*\bpaint\b|will-change\s*:[^;]*(?:transform|opacity|filter))/i;
2883
+ function stackingContextSelectors(styles) {
2884
+ const selectors = /* @__PURE__ */ new Set();
2885
+ for (const style of styles) {
2886
+ const content = stripCssComments(style.content);
2887
+ const rule = /([^{}]+)\{([^{}]*)\}/g;
2888
+ let match;
2889
+ while ((match = rule.exec(content)) !== null) {
2890
+ if (!STACKING_CONTEXT_DECLARATION.test(match[2] ?? "")) continue;
2891
+ for (const part of (match[1] ?? "").split(",")) {
2892
+ const selector = part.trim();
2893
+ if (/^[.#][\w-]+$/.test(selector)) selectors.add(selector);
2894
+ }
2895
+ }
2896
+ }
2897
+ return selectors;
2898
+ }
2899
+ function classTokens(tag) {
2900
+ return (readAttr(tag.raw, "class") ?? "").split(/\s+/).filter(Boolean);
2901
+ }
2902
+ function tagMatchesSimpleSelector(tag, selector) {
2903
+ if (selector.startsWith("#")) return readAttr(tag.raw, "id") === selector.slice(1);
2904
+ if (selector.startsWith(".")) return classTokens(tag).includes(selector.slice(1));
2905
+ return false;
2906
+ }
2907
+ function nearestParentTag(tag, tags) {
2908
+ let parent;
2909
+ for (const candidate of tags) {
2910
+ if (candidate.closeIndex == null) continue;
2911
+ if (tag.index <= candidate.index || tag.index >= candidate.closeIndex) continue;
2912
+ if (!parent || candidate.index > parent.index) parent = candidate;
2913
+ }
2914
+ return parent;
2915
+ }
2916
+ function parentHasStackingContext(tag, tags, stacking) {
2917
+ const parent = nearestParentTag(tag, tags);
2918
+ if (!parent) return false;
2919
+ return elementFormsStackingContext(parent, stacking);
2920
+ }
2921
+ function elementFormsStackingContext(tag, stacking) {
2922
+ const id = readAttr(tag.raw, "id");
2923
+ if (id && stacking.has(`#${id}`)) return true;
2924
+ if (classTokens(tag).some((name) => stacking.has(`.${name}`))) return true;
2925
+ const inline = readAttr(tag.raw, "style");
2926
+ return Boolean(inline && STACKING_CONTEXT_DECLARATION.test(inline));
2927
+ }
2928
+ function negativeZIndexIsContained(selector, tags, stacking) {
2929
+ if (!selector || !/^[.#][\w-]+$/.test(selector)) return false;
2930
+ const matched = tags.filter((tag) => tagMatchesSimpleSelector(tag, selector));
2931
+ if (matched.length === 0) return false;
2932
+ return matched.every((tag) => parentHasStackingContext(tag, tags, stacking));
2933
+ }
2882
2934
  function negativeZIndexFinding(match, selector) {
2883
2935
  const level = match[1] ?? "";
2884
2936
  return {
@@ -3206,9 +3258,9 @@ var compositionRules = [
3206
3258
  // is in the DOM, laid out, and invisible. The message therefore names the CAUSE
3207
3259
  // and the remedy rather than implying the tool failed.
3208
3260
  //
3209
- // THE CONDITION IS LOAD-BEARING AND THE MESSAGE STATES IT. The element is only
3210
- // dropped when its nearest ancestor stacking context is the composition root,
3211
- // which is the shape #4366 reports. Give any ancestor a stacking context and it
3261
+ // The element is only dropped when its nearest ancestor stacking context is
3262
+ // the composition root,
3263
+ // which is the shape #4366 reports. Give its parent a stacking context and it
3212
3264
  // renders correctly: measured at 0.8.72 on ONE frame carrying the same
3213
3265
  // `z-index: -1` band under six triggers -- isolation:isolate, transform,
3214
3266
  // opacity below 1, filter, contain:paint, will-change -- all six PRESENT at full
@@ -3216,19 +3268,22 @@ var compositionRules = [
3216
3268
  // That is ordinary CSS painting order: a negative-z child paints above its
3217
3269
  // stacking context root's own background, so only the root case is hidden.
3218
3270
  //
3219
- // This rule matches CSS TEXT and does not resolve the cascade, so it cannot know
3220
- // whether an ancestor forms a stacking context and fires on both shapes. That is
3221
- // why the message is CONDITIONAL rather than an assertion of absence: an
3222
- // unconditional "silently dropped" is false for every isolated case, and a lint
3223
- // message that overclaims is how authors learn to disregard the rule.
3271
+ // A simple class or id parent that this file itself sets as a stacking
3272
+ // context (isolation, transform, opacity below 1, filter, contain:paint,
3273
+ // will-change) is enough to know the element paints. Anything the cascade
3274
+ // could still hide stays a finding, and that message stays CONDITIONAL:
3275
+ // an unconditional "silently dropped" is false for every isolated case.
3224
3276
  ({ tags, styles }) => {
3225
3277
  const findings = [];
3278
+ const stacking = stackingContextSelectors(styles);
3226
3279
  for (const style of styles) {
3227
3280
  const content = stripCssComments(style.content);
3228
3281
  NEGATIVE_Z_INDEX.lastIndex = 0;
3229
3282
  let match;
3230
3283
  while ((match = NEGATIVE_Z_INDEX.exec(content)) !== null) {
3231
- findings.push(negativeZIndexFinding(match, cssOwnerSelector(content, match.index)));
3284
+ const selector = cssOwnerSelector(content, match.index);
3285
+ if (negativeZIndexIsContained(selector, tags, stacking)) continue;
3286
+ findings.push(negativeZIndexFinding(match, selector));
3232
3287
  }
3233
3288
  }
3234
3289
  for (const tag of tags) {
@@ -3236,7 +3291,9 @@ var compositionRules = [
3236
3291
  if (!inline) continue;
3237
3292
  NEGATIVE_Z_INDEX.lastIndex = 0;
3238
3293
  const match = NEGATIVE_Z_INDEX.exec(inline);
3239
- if (match) findings.push(negativeZIndexFinding(match, elementSelector(tag)));
3294
+ if (!match) continue;
3295
+ if (parentHasStackingContext(tag, tags, stacking)) continue;
3296
+ findings.push(negativeZIndexFinding(match, elementSelector(tag)));
3240
3297
  }
3241
3298
  return findings;
3242
3299
  },
@@ -3715,6 +3772,7 @@ async function loadGsapScriptMotionPathFirstUseIndex() {
3715
3772
  return mod.gsapScriptMotionPathFirstUseIndex;
3716
3773
  }
3717
3774
  var SCENE_BOUNDARY_EPSILON_SECONDS = 0.05;
3775
+ var TWEEN_ENDPOINT_EPSILON_SECONDS = 1e-3;
3718
3776
  var UNRESOLVED_TARGET = "__unresolved__";
3719
3777
  function targetHasNoStableIdentity(selector, identity) {
3720
3778
  if (identity) return false;
@@ -3793,6 +3851,16 @@ function stringValue(value) {
3793
3851
  if (typeof value === "number") return String(value);
3794
3852
  return null;
3795
3853
  }
3854
+ function fromToGroupSharesFromProperty(windows) {
3855
+ if (windows.some((win) => win.fromPropertyValues === void 0)) return true;
3856
+ const counts = /* @__PURE__ */ new Map();
3857
+ for (const win of windows) {
3858
+ for (const key of Object.keys(win.fromPropertyValues ?? {})) {
3859
+ counts.set(key, (counts.get(key) ?? 0) + 1);
3860
+ }
3861
+ }
3862
+ return [...counts.values()].some((count) => count >= 2);
3863
+ }
3796
3864
  function zeroValue(value) {
3797
3865
  if (typeof value === "number") return value === 0;
3798
3866
  if (typeof value !== "string") return false;
@@ -4445,7 +4513,7 @@ var gsapRules = [
4445
4513
  if (leftIdentity !== rightIdentity) continue;
4446
4514
  const overlapStart = Math.max(left.position, right.position);
4447
4515
  const overlapEnd = Math.min(left.end, right.end);
4448
- if (overlapEnd <= overlapStart) continue;
4516
+ if (overlapEnd - overlapStart <= TWEEN_ENDPOINT_EPSILON_SECONDS) continue;
4449
4517
  if (left.overwriteAuto || right.overwriteAuto) continue;
4450
4518
  const sharedProperties = left.properties.filter(
4451
4519
  (prop) => right.properties.includes(prop)
@@ -4471,7 +4539,7 @@ ${right.raw}`)
4471
4539
  fromToWindowsBySelector.set(win.targetSelector, windows);
4472
4540
  }
4473
4541
  const repeatedFromToGroups = [...fromToWindowsBySelector.values()].filter(
4474
- (windows) => windows.length >= 2
4542
+ (windows) => windows.length >= 2 && fromToGroupSharesFromProperty(windows)
4475
4543
  );
4476
4544
  for (const fromToWindows of repeatedFromToGroups) {
4477
4545
  const firstFromTo = fromToWindows[0];
package/dist/index.js CHANGED
@@ -2583,9 +2583,9 @@ function countStructuralLines(source) {
2583
2583
  return countPhysicalLines(source.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, "<style></style>"));
2584
2584
  }
2585
2585
  function isCaptionCue(tag) {
2586
- const classTokens = (readAttr(tag.raw, "class") || "").split(/\s+/).filter(Boolean);
2586
+ const classTokens2 = (readAttr(tag.raw, "class") || "").split(/\s+/).filter(Boolean);
2587
2587
  const id = readAttr(tag.raw, "id");
2588
- return classTokens.some((token) => CAPTION_CUE_TOKEN.test(token)) || Boolean(id && CAPTION_CUE_TOKEN.test(id));
2588
+ return classTokens2.some((token) => CAPTION_CUE_TOKEN.test(token)) || Boolean(id && CAPTION_CUE_TOKEN.test(id));
2589
2589
  }
2590
2590
  function isRegistrySourceFile(filePath) {
2591
2591
  if (!filePath) return false;
@@ -2885,6 +2885,58 @@ function elementSelector(tag) {
2885
2885
  const elementId = readAttr(tag.raw, "id");
2886
2886
  return elementId ? `#${elementId}` : void 0;
2887
2887
  }
2888
+ var STACKING_CONTEXT_DECLARATION = /(?:^|[;{])\s*(?:isolation\s*:\s*isolate|opacity\s*:\s*(?:0\b|0?\.\d+)|transform\s*:\s*(?!none\b)[^;]+|filter\s*:\s*(?!none\b)[^;]+|backdrop-filter\s*:\s*(?!none\b)[^;]+|-webkit-backdrop-filter\s*:\s*(?!none\b)[^;]+|contain\s*:[^;]*\bpaint\b|will-change\s*:[^;]*(?:transform|opacity|filter))/i;
2889
+ function stackingContextSelectors(styles) {
2890
+ const selectors = /* @__PURE__ */ new Set();
2891
+ for (const style of styles) {
2892
+ const content = stripCssComments(style.content);
2893
+ const rule = /([^{}]+)\{([^{}]*)\}/g;
2894
+ let match;
2895
+ while ((match = rule.exec(content)) !== null) {
2896
+ if (!STACKING_CONTEXT_DECLARATION.test(match[2] ?? "")) continue;
2897
+ for (const part of (match[1] ?? "").split(",")) {
2898
+ const selector = part.trim();
2899
+ if (/^[.#][\w-]+$/.test(selector)) selectors.add(selector);
2900
+ }
2901
+ }
2902
+ }
2903
+ return selectors;
2904
+ }
2905
+ function classTokens(tag) {
2906
+ return (readAttr(tag.raw, "class") ?? "").split(/\s+/).filter(Boolean);
2907
+ }
2908
+ function tagMatchesSimpleSelector(tag, selector) {
2909
+ if (selector.startsWith("#")) return readAttr(tag.raw, "id") === selector.slice(1);
2910
+ if (selector.startsWith(".")) return classTokens(tag).includes(selector.slice(1));
2911
+ return false;
2912
+ }
2913
+ function nearestParentTag(tag, tags) {
2914
+ let parent;
2915
+ for (const candidate of tags) {
2916
+ if (candidate.closeIndex == null) continue;
2917
+ if (tag.index <= candidate.index || tag.index >= candidate.closeIndex) continue;
2918
+ if (!parent || candidate.index > parent.index) parent = candidate;
2919
+ }
2920
+ return parent;
2921
+ }
2922
+ function parentHasStackingContext(tag, tags, stacking) {
2923
+ const parent = nearestParentTag(tag, tags);
2924
+ if (!parent) return false;
2925
+ return elementFormsStackingContext(parent, stacking);
2926
+ }
2927
+ function elementFormsStackingContext(tag, stacking) {
2928
+ const id = readAttr(tag.raw, "id");
2929
+ if (id && stacking.has(`#${id}`)) return true;
2930
+ if (classTokens(tag).some((name) => stacking.has(`.${name}`))) return true;
2931
+ const inline = readAttr(tag.raw, "style");
2932
+ return Boolean(inline && STACKING_CONTEXT_DECLARATION.test(inline));
2933
+ }
2934
+ function negativeZIndexIsContained(selector, tags, stacking) {
2935
+ if (!selector || !/^[.#][\w-]+$/.test(selector)) return false;
2936
+ const matched = tags.filter((tag) => tagMatchesSimpleSelector(tag, selector));
2937
+ if (matched.length === 0) return false;
2938
+ return matched.every((tag) => parentHasStackingContext(tag, tags, stacking));
2939
+ }
2888
2940
  function negativeZIndexFinding(match, selector) {
2889
2941
  const level = match[1] ?? "";
2890
2942
  return {
@@ -3212,9 +3264,9 @@ var compositionRules = [
3212
3264
  // is in the DOM, laid out, and invisible. The message therefore names the CAUSE
3213
3265
  // and the remedy rather than implying the tool failed.
3214
3266
  //
3215
- // THE CONDITION IS LOAD-BEARING AND THE MESSAGE STATES IT. The element is only
3216
- // dropped when its nearest ancestor stacking context is the composition root,
3217
- // which is the shape #4366 reports. Give any ancestor a stacking context and it
3267
+ // The element is only dropped when its nearest ancestor stacking context is
3268
+ // the composition root,
3269
+ // which is the shape #4366 reports. Give its parent a stacking context and it
3218
3270
  // renders correctly: measured at 0.8.72 on ONE frame carrying the same
3219
3271
  // `z-index: -1` band under six triggers -- isolation:isolate, transform,
3220
3272
  // opacity below 1, filter, contain:paint, will-change -- all six PRESENT at full
@@ -3222,19 +3274,22 @@ var compositionRules = [
3222
3274
  // That is ordinary CSS painting order: a negative-z child paints above its
3223
3275
  // stacking context root's own background, so only the root case is hidden.
3224
3276
  //
3225
- // This rule matches CSS TEXT and does not resolve the cascade, so it cannot know
3226
- // whether an ancestor forms a stacking context and fires on both shapes. That is
3227
- // why the message is CONDITIONAL rather than an assertion of absence: an
3228
- // unconditional "silently dropped" is false for every isolated case, and a lint
3229
- // message that overclaims is how authors learn to disregard the rule.
3277
+ // A simple class or id parent that this file itself sets as a stacking
3278
+ // context (isolation, transform, opacity below 1, filter, contain:paint,
3279
+ // will-change) is enough to know the element paints. Anything the cascade
3280
+ // could still hide stays a finding, and that message stays CONDITIONAL:
3281
+ // an unconditional "silently dropped" is false for every isolated case.
3230
3282
  ({ tags, styles }) => {
3231
3283
  const findings = [];
3284
+ const stacking = stackingContextSelectors(styles);
3232
3285
  for (const style of styles) {
3233
3286
  const content = stripCssComments(style.content);
3234
3287
  NEGATIVE_Z_INDEX.lastIndex = 0;
3235
3288
  let match;
3236
3289
  while ((match = NEGATIVE_Z_INDEX.exec(content)) !== null) {
3237
- findings.push(negativeZIndexFinding(match, cssOwnerSelector(content, match.index)));
3290
+ const selector = cssOwnerSelector(content, match.index);
3291
+ if (negativeZIndexIsContained(selector, tags, stacking)) continue;
3292
+ findings.push(negativeZIndexFinding(match, selector));
3238
3293
  }
3239
3294
  }
3240
3295
  for (const tag of tags) {
@@ -3242,7 +3297,9 @@ var compositionRules = [
3242
3297
  if (!inline) continue;
3243
3298
  NEGATIVE_Z_INDEX.lastIndex = 0;
3244
3299
  const match = NEGATIVE_Z_INDEX.exec(inline);
3245
- if (match) findings.push(negativeZIndexFinding(match, elementSelector(tag)));
3300
+ if (!match) continue;
3301
+ if (parentHasStackingContext(tag, tags, stacking)) continue;
3302
+ findings.push(negativeZIndexFinding(match, elementSelector(tag)));
3246
3303
  }
3247
3304
  return findings;
3248
3305
  },
@@ -3721,6 +3778,7 @@ async function loadGsapScriptMotionPathFirstUseIndex() {
3721
3778
  return mod.gsapScriptMotionPathFirstUseIndex;
3722
3779
  }
3723
3780
  var SCENE_BOUNDARY_EPSILON_SECONDS = 0.05;
3781
+ var TWEEN_ENDPOINT_EPSILON_SECONDS = 1e-3;
3724
3782
  var UNRESOLVED_TARGET = "__unresolved__";
3725
3783
  function targetHasNoStableIdentity(selector, identity) {
3726
3784
  if (identity) return false;
@@ -3799,6 +3857,16 @@ function stringValue(value) {
3799
3857
  if (typeof value === "number") return String(value);
3800
3858
  return null;
3801
3859
  }
3860
+ function fromToGroupSharesFromProperty(windows) {
3861
+ if (windows.some((win) => win.fromPropertyValues === void 0)) return true;
3862
+ const counts = /* @__PURE__ */ new Map();
3863
+ for (const win of windows) {
3864
+ for (const key of Object.keys(win.fromPropertyValues ?? {})) {
3865
+ counts.set(key, (counts.get(key) ?? 0) + 1);
3866
+ }
3867
+ }
3868
+ return [...counts.values()].some((count) => count >= 2);
3869
+ }
3802
3870
  function zeroValue(value) {
3803
3871
  if (typeof value === "number") return value === 0;
3804
3872
  if (typeof value !== "string") return false;
@@ -4451,7 +4519,7 @@ var gsapRules = [
4451
4519
  if (leftIdentity !== rightIdentity) continue;
4452
4520
  const overlapStart = Math.max(left.position, right.position);
4453
4521
  const overlapEnd = Math.min(left.end, right.end);
4454
- if (overlapEnd <= overlapStart) continue;
4522
+ if (overlapEnd - overlapStart <= TWEEN_ENDPOINT_EPSILON_SECONDS) continue;
4455
4523
  if (left.overwriteAuto || right.overwriteAuto) continue;
4456
4524
  const sharedProperties = left.properties.filter(
4457
4525
  (prop) => right.properties.includes(prop)
@@ -4477,7 +4545,7 @@ ${right.raw}`)
4477
4545
  fromToWindowsBySelector.set(win.targetSelector, windows);
4478
4546
  }
4479
4547
  const repeatedFromToGroups = [...fromToWindowsBySelector.values()].filter(
4480
- (windows) => windows.length >= 2
4548
+ (windows) => windows.length >= 2 && fromToGroupSharesFromProperty(windows)
4481
4549
  );
4482
4550
  for (const fromToWindows of repeatedFromToGroups) {
4483
4551
  const firstFromTo = fromToWindows[0];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/lint",
3
- "version": "0.8.156",
3
+ "version": "0.8.158",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/heygen-com/hyperframes",
@@ -35,7 +35,7 @@
35
35
  "linkedom": "^0.18.12",
36
36
  "postcss": "^8.5.8",
37
37
  "postcss-selector-parser": "^7.1.4",
38
- "@hyperframes/parsers": "0.8.156"
38
+ "@hyperframes/parsers": "0.8.158"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/node": "^25.0.10",