From 05c3b5503faf5da1efe5d64985b63db3f762f765 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Sat, 11 Jul 2026 00:03:07 -0400 Subject: [PATCH] fix(lint): parse HTML structure without regex (#2223) * fix(lint): ignore scripts inside quoted attributes * Address PR review feedback (#2223) - replace repeated attribute scanner with quoted tag ranges - remove the Fallow complexity finding * refactor(lint): parse HTML structure with htmlparser2 * fix(lint): traverse template style sources * test(lint): cover nested template style sources --- bun.lock | 1 + packages/lint/package.json | 1 + packages/lint/src/context.ts | 35 ++++-- packages/lint/src/hyperframeLinter.test.ts | 29 ++++- packages/lint/src/hyperframeLinter.ts | 9 +- packages/lint/src/project.test.ts | 26 ++++ packages/lint/src/project.ts | 95 ++++++++------- packages/lint/src/rules/composition.ts | 29 +++-- packages/lint/src/rules/core.test.ts | 40 +++++++ packages/lint/src/utils.ts | 131 +++++++++++---------- 10 files changed, 252 insertions(+), 144 deletions(-) diff --git a/bun.lock b/bun.lock index 379cf361b..c6eb53154 100644 --- a/bun.lock +++ b/bun.lock @@ -169,6 +169,7 @@ "version": "0.7.45", "dependencies": { "@hyperframes/parsers": "workspace:*", + "htmlparser2": "^10.1.0", "linkedom": "^0.18.12", "postcss": "^8.5.8", }, diff --git a/packages/lint/package.json b/packages/lint/package.json index 10f2d5283..58b3456fa 100644 --- a/packages/lint/package.json +++ b/packages/lint/package.json @@ -54,6 +54,7 @@ }, "dependencies": { "@hyperframes/parsers": "workspace:*", + "htmlparser2": "^10.1.0", "linkedom": "^0.18.12", "postcss": "^8.5.8" }, diff --git a/packages/lint/src/context.ts b/packages/lint/src/context.ts index 3e71b29a3..9638f3ac5 100644 --- a/packages/lint/src/context.ts +++ b/packages/lint/src/context.ts @@ -1,13 +1,10 @@ import type { HyperframeLintFinding, HyperframeLinterOptions } from "./types"; import { - extractBlocks, - extractOpenTags, + parseHtmlStructure, findRootTag, collectCompositionIds, readAttr, stripHtmlComments, - STYLE_BLOCK_PATTERN, - SCRIPT_BLOCK_PATTERN, } from "./utils"; import type { OpenTag, ExtractedBlock } from "./utils"; @@ -34,19 +31,31 @@ export function buildLintContext(html: string, options: HyperframeLinterOptions // hijack the boundary match below. Linear + fixpoint (see stripHtmlComments) to // stay ReDoS-free and catch markers that re-form when a comment is removed. let source = stripHtmlComments(rawSource); - const sourceWithoutTemplates = source.replace( - /]*>[\s\S]*?<\/template(?:\s[^>]*)?>/gi, - " ", + const initialStructure = parseHtmlStructure(source); + const templateTags = initialStructure.tags.filter( + (tag) => tag.name === "template" && tag.closeIndex != null, ); - const templateMatch = source.match(/]*>([\s\S]*)<\/template>/i); + let sourceWithoutTemplates = source; + for (const template of [...templateTags].reverse()) { + const end = template.endIndex ?? template.index; + sourceWithoutTemplates = + sourceWithoutTemplates.slice(0, template.index) + + " ".repeat(end - template.index) + + sourceWithoutTemplates.slice(end); + } // Some sub-composition files are HTML shells whose real root lives inside a //