(html: string, options: HyperframeLinterOptions = {})
| 29 | export type { HyperframeLintFinding }; |
| 30 | |
| 31 | export function buildLintContext(html: string, options: HyperframeLinterOptions = {}): LintContext { |
| 32 | const rawSource = html || ""; |
| 33 | // Strip HTML comments before scanning so a commented-out <template> or tag can't |
| 34 | // hijack the boundary match below. Linear + fixpoint (see stripHtmlComments) to |
| 35 | // stay ReDoS-free and catch markers that re-form when a comment is removed. |
| 36 | let source = stripHtmlComments(rawSource); |
| 37 | const sourceWithoutTemplates = source.replace( |
| 38 | /<template\b[^>]*>[\s\S]*?<\/template(?:\s[^>]*)?>/gi, |
| 39 | " ", |
| 40 | ); |
| 41 | const templateMatch = source.match(/<template[^>]*>([\s\S]*)<\/template>/i); |
| 42 | // Some sub-composition files are HTML shells whose real root lives inside a |
| 43 | // <template>. Keep nested templates intact when the visible document already |
| 44 | // has a composition root; only unwrap when no root exists outside templates. |
| 45 | if (templateMatch?.[1] && !findRootTag(sourceWithoutTemplates)) source = templateMatch[1]; |
| 46 | |
| 47 | const tags = extractOpenTags(source); |
| 48 | const styles = [ |
| 49 | ...extractBlocks(source, STYLE_BLOCK_PATTERN), |
| 50 | ...(options.externalStyles ?? []).map((style) => ({ |
| 51 | attrs: `href="${style.href}"`, |
| 52 | content: style.content, |
| 53 | raw: style.content, |
| 54 | index: -1, |
| 55 | })), |
| 56 | ]; |
| 57 | const scripts = extractBlocks(source, SCRIPT_BLOCK_PATTERN); |
| 58 | const compositionIds = collectCompositionIds(tags); |
| 59 | const rootTag = findRootTag(source); |
| 60 | const rootCompositionId = readAttr(rootTag?.raw || "", "data-composition-id"); |
| 61 | |
| 62 | return { |
| 63 | source, |
| 64 | rawSource, |
| 65 | tags, |
| 66 | styles, |
| 67 | scripts, |
| 68 | compositionIds, |
| 69 | rootTag, |
| 70 | rootCompositionId, |
| 71 | options, |
| 72 | }; |
| 73 | } |
no test coverage detected