@hyperframes/lint 0.8.65 → 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/index.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/index.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,15 +560,67 @@ 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)}...`;
564
567
  }
565
568
  function mediaSrcTagRe(tagAlternation) {
566
569
  return new RegExp(`<(${tagAlternation})\\b[^>]*\\ssrc\\s*=\\s*["']([^"']+)["'][^>]*>`, "gi");
567
570
  }
568
571
 
572
+ // src/sourceLocations.ts
573
+ function normalized(source) {
574
+ let text = "";
575
+ const offsets = [];
576
+ for (let i = 0; i < source.length; i++) {
577
+ const char = source[i];
578
+ if (/\s/.test(char)) {
579
+ if (text.endsWith(" ")) continue;
580
+ text += " ";
581
+ } else text += char;
582
+ offsets.push(i);
583
+ }
584
+ return { text, offsets };
585
+ }
586
+ function createSourceLocator(source) {
587
+ const tags = parseHtmlStructure(source).tags;
588
+ const searchable = source.split("");
589
+ const parser = new Parser2({
590
+ oncomment() {
591
+ searchable.fill("\0", parser.startIndex, parser.endIndex + 1);
592
+ }
593
+ });
594
+ parser.write(source);
595
+ parser.end();
596
+ const normalizedSource = normalized(searchable.join(""));
597
+ const at = (offset) => {
598
+ const before = source.slice(0, offset);
599
+ return {
600
+ line: before.split(/\r\n|\r|\n/).length,
601
+ column: offset - Math.max(before.lastIndexOf("\n"), before.lastIndexOf("\r"))
602
+ };
603
+ };
604
+ return (finding) => {
605
+ if (finding.line !== void 0) return { line: finding.line, column: finding.column };
606
+ if (finding.snippet) {
607
+ const offset = snippetOffset(normalizedSource, finding.snippet);
608
+ return offset === void 0 ? {} : at(offset);
609
+ }
610
+ const id = finding.elementId ?? /^#([\w-]+)$/.exec(finding.selector ?? "")?.[1];
611
+ if (!id) return {};
612
+ const matches = tags.filter((tag) => readDecodedAttr(tag.raw, "id") === id);
613
+ return matches.length === 1 ? at(matches[0].index) : {};
614
+ };
615
+ }
616
+ function snippetOffset(source, snippet) {
617
+ const needle = snippet.replace(/\s+/g, " ").trim().replace(/\.\.\.$/, "");
618
+ if (!needle) return void 0;
619
+ const index = source.text.indexOf(needle);
620
+ if (index < 0 || source.text.indexOf(needle, index + 1) >= 0) return void 0;
621
+ return source.offsets[index];
622
+ }
623
+
569
624
  // src/context.ts
570
625
  function buildLintContext(html, options = {}) {
571
626
  const rawSource = html || "";
@@ -1366,18 +1421,18 @@ function classNamesFromAttr(classAttr) {
1366
1421
  return classAttr.split(/\s+/).filter(Boolean);
1367
1422
  }
1368
1423
  function selectorTargetsManagedMedia(selector, mediaIndex) {
1369
- const normalized = selector.trim();
1370
- if (!normalized) return false;
1371
- if (mediaIndex.hasVideo && /\bvideo\b/i.test(normalized)) return true;
1372
- if (mediaIndex.hasAudio && /\baudio\b/i.test(normalized)) return true;
1424
+ const normalized2 = selector.trim();
1425
+ if (!normalized2) return false;
1426
+ if (mediaIndex.hasVideo && /\bvideo\b/i.test(normalized2)) return true;
1427
+ if (mediaIndex.hasAudio && /\baudio\b/i.test(normalized2)) return true;
1373
1428
  for (const mediaId of mediaIndex.ids) {
1374
1429
  const escapedId = escapeRegExp(mediaId);
1375
- if (new RegExp(`#${escapedId}(?![\\w-])`).test(normalized) || normalized.includes(`[id="${mediaId}"]`) || normalized.includes(`[id='${mediaId}']`)) {
1430
+ if (new RegExp(`#${escapedId}(?![\\w-])`).test(normalized2) || normalized2.includes(`[id="${mediaId}"]`) || normalized2.includes(`[id='${mediaId}']`)) {
1376
1431
  return true;
1377
1432
  }
1378
1433
  }
1379
1434
  for (const className of mediaIndex.classes) {
1380
- if (new RegExp(`\\.${escapeRegExp(className)}(?![\\w-])`).test(normalized)) {
1435
+ if (new RegExp(`\\.${escapeRegExp(className)}(?![\\w-])`).test(normalized2)) {
1381
1436
  return true;
1382
1437
  }
1383
1438
  }
@@ -2116,8 +2171,8 @@ function readTagTiming(rawTag) {
2116
2171
  }
2117
2172
  function countPhysicalLines(source) {
2118
2173
  if (source.length === 0) return 0;
2119
- const normalized = source.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
2120
- const withoutFinalNewline = normalized.endsWith("\n") ? normalized.slice(0, -1) : normalized;
2174
+ const normalized2 = source.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
2175
+ const withoutFinalNewline = normalized2.endsWith("\n") ? normalized2.slice(0, -1) : normalized2;
2121
2176
  return withoutFinalNewline.split("\n").length;
2122
2177
  }
2123
2178
  function countStructuralLines(source) {
@@ -2130,8 +2185,8 @@ function isCaptionCue(tag) {
2130
2185
  }
2131
2186
  function isRegistrySourceFile(filePath) {
2132
2187
  if (!filePath) return false;
2133
- const normalized = filePath.replace(/\\/g, "/");
2134
- return /(?:^|\/)registry\/blocks\/([^/]+)\/\1\.html$/i.test(normalized);
2188
+ const normalized2 = filePath.replace(/\\/g, "/");
2189
+ return /(?:^|\/)registry\/blocks\/([^/]+)\/\1\.html$/i.test(normalized2);
2135
2190
  }
2136
2191
  function isRegistryInstalledFile(rawSource) {
2137
2192
  return /^\s*<!--\s*hyperframes-registry-item:[^>]*-->/i.test(rawSource.slice(0, 512));
@@ -3081,8 +3136,8 @@ function zeroValue(value) {
3081
3136
  return Number(value.trim()) === 0;
3082
3137
  }
3083
3138
  function isGsapColorProperty(property) {
3084
- const normalized = property.toLowerCase();
3085
- return !normalized.startsWith("--") && (normalized.endsWith("color") || normalized === "fill" || normalized === "stroke");
3139
+ const normalized2 = property.toLowerCase();
3140
+ return !normalized2.startsWith("--") && (normalized2.endsWith("color") || normalized2 === "fill" || normalized2 === "stroke");
3086
3141
  }
3087
3142
  function collectStaticCssVariableDefinitions(tags, styles) {
3088
3143
  const definitions = /* @__PURE__ */ new Set();
@@ -3209,10 +3264,10 @@ function styleHasHiddenInitialState(style) {
3209
3264
  function styleHasOpaqueBackground(style) {
3210
3265
  const background = readStyleProperty(style, "background") || readStyleProperty(style, "background-color");
3211
3266
  if (!background) return false;
3212
- const normalized = background.toLowerCase().replace(/\s+/g, "");
3213
- if (normalized === "transparent" || normalized === "none") return false;
3214
- if (/rgba?\([^)]*,0(?:\.0+)?\)$/.test(normalized)) return false;
3215
- if (/hsla?\([^)]*,0(?:\.0+)?\)$/.test(normalized)) return false;
3267
+ const normalized2 = background.toLowerCase().replace(/\s+/g, "");
3268
+ if (normalized2 === "transparent" || normalized2 === "none") return false;
3269
+ if (/rgba?\([^)]*,0(?:\.0+)?\)$/.test(normalized2)) return false;
3270
+ if (/hsla?\([^)]*,0(?:\.0+)?\)$/.test(normalized2)) return false;
3216
3271
  return true;
3217
3272
  }
3218
3273
  function styleLooksFullFrameOverlay(style) {
@@ -3594,9 +3649,9 @@ function elementLevelTokens(tokens, tagsByToken) {
3594
3649
  return expanded;
3595
3650
  }
3596
3651
  function isMultiComponentDasharray(value) {
3597
- const normalized = value.replace(/!important\s*$/i, "").trim();
3598
- if (!normalized || /^none$/i.test(normalized)) return false;
3599
- return normalized.split(/[\s,]+/).filter(Boolean).length >= 2;
3652
+ const normalized2 = value.replace(/!important\s*$/i, "").trim();
3653
+ if (!normalized2 || /^none$/i.test(normalized2)) return false;
3654
+ return normalized2.split(/[\s,]+/).filter(Boolean).length >= 2;
3600
3655
  }
3601
3656
  function gsapDasharrayValueLooksMultiComponent(valueSource) {
3602
3657
  const literal = valueSource.trim().match(/^(["'`])([\s\S]*)\1$/)?.[2];
@@ -5588,7 +5643,15 @@ function collectFindings(produced, seen, filePath, into) {
5588
5643
  async function lintHyperframeHtml(html, options = {}) {
5589
5644
  const startedAt = performance.now();
5590
5645
  const ctx = buildLintContext(html, options);
5591
- const { findings, timings } = await runRules(ctx, options.filePath);
5646
+ const { findings: rawFindings, timings } = await runRules(ctx, options.filePath);
5647
+ const locate = createSourceLocator(html);
5648
+ const findings = rawFindings.map((finding) => {
5649
+ if (finding.snippet && options.externalStyles?.some(
5650
+ (style) => style.content.replace(/\s+/g, " ").includes(finding.snippet.replace(/\.\.\.$/, ""))
5651
+ ))
5652
+ return finding;
5653
+ return { ...finding, ...locate(finding) };
5654
+ });
5592
5655
  const errorCount = findings.filter((f) => f.severity === "error").length;
5593
5656
  const warningCount = findings.filter((f) => f.severity === "warning").length;
5594
5657
  const infoCount = findings.filter((f) => f.severity === "info").length;