mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 10:14:30 +00:00
* fix(lint): stop CSS comments in <style> from manufacturing phantom root tags
extractOpenTags scans raw source text with a flat regex that has no
concept of <style>/<script> block boundaries, so a CSS comment like
`/* <g> wrapper */` inside a <style> block reads as a real open tag.
findRootTag consumes that flat tag list and only skips tags literally
named script/style/meta/link/title, so the phantom <g> tag (not in
that skip list) wins the "first non-ignored body tag" search and gets
returned as the composition root instead of the real one that follows.
This manufactured root_missing_composition_id and root_missing_dimensions
(the phantom tag has neither) plus head_leaked_text (the leaked-text
scan slices up to the phantom tag's position, landing inside the
<style> block before its real closing tag, so the raw CSS text reads
as leaked markup) on an otherwise valid sub-composition — reported
with an exact bisected repro: a <template>-wrapped SVG sub-composition
whose <style> block comments reference an inner <g> element.
Fix: compute <style>/<script> content spans up front (reusing the
existing extractBlocks + STYLE_BLOCK_PATTERN/SCRIPT_BLOCK_PATTERN) and
skip any TAG_PATTERN match that falls inside one, before it ever
reaches findRootTag or any other extractOpenTags consumer. Same shape
as the prior fix for a leading <svg> defs block being mistaken for the
root (8ee4b7df) — this closes a sibling gap in the same function.
Test: new regression case with a <style> block containing a `/* <g> */`
comment ahead of an <svg data-composition-id> root, asserting none of
the three findings fire. Full lint package suite (318 tests) passes.
* feat(lint): flag duplicate data-composition-id values
Declaring data-composition-id on more than one element (commonly the <meta>
tag from the quickstart template AND the root <div> added to satisfy
root_missing_composition_id) is a silent collision: `compositions --json`
returns two entries for the same id (one duration:0) and inspect/snapshot
crash with "Cannot read properties of undefined (reading totalDuration)".
Lint passed clean through all of it.
New rule `duplicate_composition_id`: group elements by data-composition-id
value and error on any value shared by 2+ elements, naming the id and calling
out the meta-vs-root collision in the fixHint. 3 tests: dup fires, single id
passes, two distinct ids don't collide. (Implemented via Codex; verified
independently: 111 lint tests pass, oxfmt/oxlint clean.)
* fix(audits): avoid caption false positives
* fix(lint): ignore proxy-label tween overlaps
* fix(cli): preserve the five-percent text audit floor
* fix(lint): preserve proxy identity across lexical scopes
* fix(cli): audit only directly painted text
* fix(lint): compare live composition ids canonically
* fix(lint): preserve expanded proxy identities
* fix(cli): measure directly painted text geometry
* fix(lint): preserve first duplicate attribute value
* fix(lint): keep shared proxy identity across helpers
* fix(parsers): preserve expanded proxy identity
* fix(lint): decode composition IDs consistently
83 lines
2.7 KiB
TypeScript
83 lines
2.7 KiB
TypeScript
import type { HyperframeLintFinding, HyperframeLinterOptions } from "./types";
|
|
import {
|
|
parseHtmlStructure,
|
|
findRootTag,
|
|
collectCompositionIds,
|
|
readDecodedAttr,
|
|
stripHtmlComments,
|
|
} from "./utils";
|
|
import type { OpenTag, ExtractedBlock } from "./utils";
|
|
|
|
export type { OpenTag, ExtractedBlock };
|
|
|
|
export type LintContext = {
|
|
source: string;
|
|
rawSource: string;
|
|
tags: OpenTag[];
|
|
styles: ExtractedBlock[];
|
|
scripts: ExtractedBlock[];
|
|
compositionIds: Set<string>;
|
|
rootTag: OpenTag | null;
|
|
rootCompositionId: string | null;
|
|
options: HyperframeLinterOptions;
|
|
};
|
|
|
|
// Re-export for convenience so rule modules only need one import for the finding type
|
|
export type { HyperframeLintFinding };
|
|
|
|
export function buildLintContext(html: string, options: HyperframeLinterOptions = {}): LintContext {
|
|
const rawSource = html || "";
|
|
// Strip HTML comments before scanning so a commented-out <template> or tag can't
|
|
// 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 initialStructure = parseHtmlStructure(source);
|
|
const templateTags = initialStructure.tags.filter(
|
|
(tag) => tag.name === "template" && tag.closeIndex != null,
|
|
);
|
|
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
|
|
// <template>. Keep nested templates intact when the visible document already
|
|
// has a composition root; only unwrap when no root exists outside templates.
|
|
const template = templateTags[0];
|
|
let structure = initialStructure;
|
|
if (template && !findRootTag(sourceWithoutTemplates)) {
|
|
source = source.slice(template.index + template.raw.length, template.closeIndex);
|
|
structure = parseHtmlStructure(source);
|
|
}
|
|
|
|
const tags = structure.tags;
|
|
const styles = [
|
|
...structure.styles,
|
|
...(options.externalStyles ?? []).map((style) => ({
|
|
attrs: `href="${style.href}"`,
|
|
content: style.content,
|
|
raw: style.content,
|
|
index: -1,
|
|
})),
|
|
];
|
|
const scripts = structure.scripts;
|
|
const compositionIds = collectCompositionIds(tags);
|
|
const rootTag = findRootTag(source, tags);
|
|
const rootCompositionId = readDecodedAttr(rootTag?.raw || "", "data-composition-id");
|
|
|
|
return {
|
|
source,
|
|
rawSource,
|
|
tags,
|
|
styles,
|
|
scripts,
|
|
compositionIds,
|
|
rootTag,
|
|
rootCompositionId,
|
|
options,
|
|
};
|
|
}
|