@hyperframes/lint 0.8.66 → 0.8.67

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 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;
package/dist/browser.js CHANGED
@@ -1,3 +1,6 @@
1
+ // src/sourceLocations.ts
2
+ import { Parser as Parser2 } from "htmlparser2";
3
+
1
4
  // src/utils.ts
2
5
  import { Parser } from "htmlparser2";
3
6
  var COMPOSITION_ID_IN_CSS_PATTERN = /\[data-composition-id=["']([^"']+)["']\]/g;
@@ -557,10 +560,62 @@ function hasCaptionStyles(styles) {
557
560
  return styles.some((s) => /\.caption[-_]?(?:group|word)/i.test(s.content));
558
561
  }
559
562
  function truncateSnippet(value, maxLength = 220) {
560
- const normalized = value.replace(/\s+/g, " ").trim();
561
- if (!normalized) return void 0;
562
- if (normalized.length <= maxLength) return normalized;
563
- return `${normalized.slice(0, maxLength - 3)}...`;
563
+ const normalized2 = value.replace(/\s+/g, " ").trim();
564
+ if (!normalized2) return void 0;
565
+ if (normalized2.length <= maxLength) return normalized2;
566
+ return `${normalized2.slice(0, maxLength - 3)}...`;
567
+ }
568
+
569
+ // src/sourceLocations.ts
570
+ function normalized(source) {
571
+ let text = "";
572
+ const offsets = [];
573
+ for (let i = 0; i < source.length; i++) {
574
+ const char = source[i];
575
+ if (/\s/.test(char)) {
576
+ if (text.endsWith(" ")) continue;
577
+ text += " ";
578
+ } else text += char;
579
+ offsets.push(i);
580
+ }
581
+ return { text, offsets };
582
+ }
583
+ function createSourceLocator(source) {
584
+ const tags = parseHtmlStructure(source).tags;
585
+ const searchable = source.split("");
586
+ const parser = new Parser2({
587
+ oncomment() {
588
+ searchable.fill("\0", parser.startIndex, parser.endIndex + 1);
589
+ }
590
+ });
591
+ parser.write(source);
592
+ parser.end();
593
+ const normalizedSource = normalized(searchable.join(""));
594
+ const at = (offset) => {
595
+ const before = source.slice(0, offset);
596
+ return {
597
+ line: before.split(/\r\n|\r|\n/).length,
598
+ column: offset - Math.max(before.lastIndexOf("\n"), before.lastIndexOf("\r"))
599
+ };
600
+ };
601
+ return (finding) => {
602
+ if (finding.line !== void 0) return { line: finding.line, column: finding.column };
603
+ if (finding.snippet) {
604
+ const offset = snippetOffset(normalizedSource, finding.snippet);
605
+ return offset === void 0 ? {} : at(offset);
606
+ }
607
+ const id = finding.elementId ?? /^#([\w-]+)$/.exec(finding.selector ?? "")?.[1];
608
+ if (!id) return {};
609
+ const matches = tags.filter((tag) => readDecodedAttr(tag.raw, "id") === id);
610
+ return matches.length === 1 ? at(matches[0].index) : {};
611
+ };
612
+ }
613
+ function snippetOffset(source, snippet) {
614
+ const needle = snippet.replace(/\s+/g, " ").trim().replace(/\.\.\.$/, "");
615
+ if (!needle) return void 0;
616
+ const index = source.text.indexOf(needle);
617
+ if (index < 0 || source.text.indexOf(needle, index + 1) >= 0) return void 0;
618
+ return source.offsets[index];
564
619
  }
565
620
 
566
621
  // src/context.ts
@@ -1363,18 +1418,18 @@ function classNamesFromAttr(classAttr) {
1363
1418
  return classAttr.split(/\s+/).filter(Boolean);
1364
1419
  }
1365
1420
  function selectorTargetsManagedMedia(selector, mediaIndex) {
1366
- const normalized = selector.trim();
1367
- if (!normalized) return false;
1368
- if (mediaIndex.hasVideo && /\bvideo\b/i.test(normalized)) return true;
1369
- if (mediaIndex.hasAudio && /\baudio\b/i.test(normalized)) return true;
1421
+ const normalized2 = selector.trim();
1422
+ if (!normalized2) return false;
1423
+ if (mediaIndex.hasVideo && /\bvideo\b/i.test(normalized2)) return true;
1424
+ if (mediaIndex.hasAudio && /\baudio\b/i.test(normalized2)) return true;
1370
1425
  for (const mediaId of mediaIndex.ids) {
1371
1426
  const escapedId = escapeRegExp(mediaId);
1372
- if (new RegExp(`#${escapedId}(?![\\w-])`).test(normalized) || normalized.includes(`[id="${mediaId}"]`) || normalized.includes(`[id='${mediaId}']`)) {
1427
+ if (new RegExp(`#${escapedId}(?![\\w-])`).test(normalized2) || normalized2.includes(`[id="${mediaId}"]`) || normalized2.includes(`[id='${mediaId}']`)) {
1373
1428
  return true;
1374
1429
  }
1375
1430
  }
1376
1431
  for (const className of mediaIndex.classes) {
1377
- if (new RegExp(`\\.${escapeRegExp(className)}(?![\\w-])`).test(normalized)) {
1432
+ if (new RegExp(`\\.${escapeRegExp(className)}(?![\\w-])`).test(normalized2)) {
1378
1433
  return true;
1379
1434
  }
1380
1435
  }
@@ -2113,8 +2168,8 @@ function readTagTiming(rawTag) {
2113
2168
  }
2114
2169
  function countPhysicalLines(source) {
2115
2170
  if (source.length === 0) return 0;
2116
- const normalized = source.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
2117
- const withoutFinalNewline = normalized.endsWith("\n") ? normalized.slice(0, -1) : normalized;
2171
+ const normalized2 = source.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
2172
+ const withoutFinalNewline = normalized2.endsWith("\n") ? normalized2.slice(0, -1) : normalized2;
2118
2173
  return withoutFinalNewline.split("\n").length;
2119
2174
  }
2120
2175
  function countStructuralLines(source) {
@@ -2127,8 +2182,8 @@ function isCaptionCue(tag) {
2127
2182
  }
2128
2183
  function isRegistrySourceFile(filePath) {
2129
2184
  if (!filePath) return false;
2130
- const normalized = filePath.replace(/\\/g, "/");
2131
- return /(?:^|\/)registry\/blocks\/([^/]+)\/\1\.html$/i.test(normalized);
2185
+ const normalized2 = filePath.replace(/\\/g, "/");
2186
+ return /(?:^|\/)registry\/blocks\/([^/]+)\/\1\.html$/i.test(normalized2);
2132
2187
  }
2133
2188
  function isRegistryInstalledFile(rawSource) {
2134
2189
  return /^\s*<!--\s*hyperframes-registry-item:[^>]*-->/i.test(rawSource.slice(0, 512));
@@ -3078,8 +3133,8 @@ function zeroValue(value) {
3078
3133
  return Number(value.trim()) === 0;
3079
3134
  }
3080
3135
  function isGsapColorProperty(property) {
3081
- const normalized = property.toLowerCase();
3082
- return !normalized.startsWith("--") && (normalized.endsWith("color") || normalized === "fill" || normalized === "stroke");
3136
+ const normalized2 = property.toLowerCase();
3137
+ return !normalized2.startsWith("--") && (normalized2.endsWith("color") || normalized2 === "fill" || normalized2 === "stroke");
3083
3138
  }
3084
3139
  function collectStaticCssVariableDefinitions(tags, styles) {
3085
3140
  const definitions = /* @__PURE__ */ new Set();
@@ -3206,10 +3261,10 @@ function styleHasHiddenInitialState(style) {
3206
3261
  function styleHasOpaqueBackground(style) {
3207
3262
  const background = readStyleProperty(style, "background") || readStyleProperty(style, "background-color");
3208
3263
  if (!background) return false;
3209
- const normalized = background.toLowerCase().replace(/\s+/g, "");
3210
- if (normalized === "transparent" || normalized === "none") return false;
3211
- if (/rgba?\([^)]*,0(?:\.0+)?\)$/.test(normalized)) return false;
3212
- if (/hsla?\([^)]*,0(?:\.0+)?\)$/.test(normalized)) return false;
3264
+ const normalized2 = background.toLowerCase().replace(/\s+/g, "");
3265
+ if (normalized2 === "transparent" || normalized2 === "none") return false;
3266
+ if (/rgba?\([^)]*,0(?:\.0+)?\)$/.test(normalized2)) return false;
3267
+ if (/hsla?\([^)]*,0(?:\.0+)?\)$/.test(normalized2)) return false;
3213
3268
  return true;
3214
3269
  }
3215
3270
  function styleLooksFullFrameOverlay(style) {
@@ -3591,9 +3646,9 @@ function elementLevelTokens(tokens, tagsByToken) {
3591
3646
  return expanded;
3592
3647
  }
3593
3648
  function isMultiComponentDasharray(value) {
3594
- const normalized = value.replace(/!important\s*$/i, "").trim();
3595
- if (!normalized || /^none$/i.test(normalized)) return false;
3596
- return normalized.split(/[\s,]+/).filter(Boolean).length >= 2;
3649
+ const normalized2 = value.replace(/!important\s*$/i, "").trim();
3650
+ if (!normalized2 || /^none$/i.test(normalized2)) return false;
3651
+ return normalized2.split(/[\s,]+/).filter(Boolean).length >= 2;
3597
3652
  }
3598
3653
  function gsapDasharrayValueLooksMultiComponent(valueSource) {
3599
3654
  const literal = valueSource.trim().match(/^(["'`])([\s\S]*)\1$/)?.[2];
@@ -5585,7 +5640,15 @@ function collectFindings(produced, seen, filePath, into) {
5585
5640
  async function lintHyperframeHtml(html, options = {}) {
5586
5641
  const startedAt = performance.now();
5587
5642
  const ctx = buildLintContext(html, options);
5588
- const { findings, timings } = await runRules(ctx, options.filePath);
5643
+ const { findings: rawFindings, timings } = await runRules(ctx, options.filePath);
5644
+ const locate = createSourceLocator(html);
5645
+ const findings = rawFindings.map((finding) => {
5646
+ if (finding.snippet && options.externalStyles?.some(
5647
+ (style) => style.content.replace(/\s+/g, " ").includes(finding.snippet.replace(/\.\.\.$/, ""))
5648
+ ))
5649
+ return finding;
5650
+ return { ...finding, ...locate(finding) };
5651
+ });
5589
5652
  const errorCount = findings.filter((f) => f.severity === "error").length;
5590
5653
  const warningCount = findings.filter((f) => f.severity === "warning").length;
5591
5654
  const infoCount = findings.filter((f) => f.severity === "info").length;