// Ancestor resolution for Imhotep topology engine. // Computes nearest positioned ancestor and containing block for every node. import { type DomTree, type StyleFacts, type TopologyGraph, Position, Display, ConfidenceLevel, INVALID_ID, setConfidence, } from './graph.js'; // --------------------------------------------------------------------------- // Positioned element predicate // --------------------------------------------------------------------------- /** Returns true if the element's computed position is not static. */ export function isPositioned(position: number): boolean { return position !== Position.STATIC; } // --------------------------------------------------------------------------- // Containing block resolution (CSS 2.1 Section 10.1) // --------------------------------------------------------------------------- /** * For a given subject, walk up the DOM tree and return the ID of the node * that acts as its containing block. * * Rules (simplified): * - For fixed positioned elements, the containing block is the viewport * (represented here as INVALID_ID because it lives outside the DOM tree). * - For absolute positioned elements, the containing block is the nearest * ancestor that is positioned (not static). * - For other elements, the containing block is the nearest block container * ancestor (a block, inline-block, flex item, grid item, or table cell). */ export function resolveContainingBlock( subjectId: number, dom: DomTree, styles: StyleFacts, ): number { const pos = styles.position[subjectId]; if (pos === Position.FIXED) { // Viewport is outside the node tree; caller maps INVALID_ID to a synthetic frame. return INVALID_ID; } if (pos === Position.ABSOLUTE) { return walkUpForPositionedAncestor(subjectId, dom, styles); } // For static, relative, and sticky, the containing block is the nearest // block-container ancestor. In a simplified model we treat every ancestor // that is display:block, inline-block, flex, grid, or table as a containing // block. If we reach the root without finding one, the root itself is the // containing block. let cursor = dom.parentNodeId[subjectId]; while (cursor !== INVALID_ID) { const disp = styles.display[cursor]; if ( disp === Display.BLOCK || disp === Display.INLINE_BLOCK || disp === Display.FLEX || disp === Display.GRID || disp === Display.TABLE ) { return cursor; } cursor = dom.parentNodeId[cursor]; } return INVALID_ID; } // --------------------------------------------------------------------------- // Nearest positioned ancestor // --------------------------------------------------------------------------- /** * Walks up the parent chain until it finds a node whose position value is * not STATIC. Returns INVALID_ID if none is found. */ export function walkUpForPositionedAncestor( subjectId: number, dom: DomTree, styles: StyleFacts, ): number { let cursor = dom.parentNodeId[subjectId]; while (cursor !== INVALID_ID) { if (isPositioned(styles.position[cursor])) { return cursor; } cursor = dom.parentNodeId[cursor]; } return INVALID_ID; } // --------------------------------------------------------------------------- // Batch ancestor resolution for the whole tree // --------------------------------------------------------------------------- export interface AncestorTables { containingBlockOf: Uint32Array; nearestPositionedAncestorOf: Uint32Array; } /** * Builds the containing-block and nearest-positioned-ancestor tables for * every node in the DOM tree. * * This is a single upward-walk per node. Complexity: O(N * depth). * For very deep or wide trees the caller may memoize. */ export function buildAncestorTables( dom: DomTree, styles: StyleFacts, ): AncestorTables { const containingBlockOf = new Uint32Array(dom.nodeCount).fill(INVALID_ID); const nearestPositionedAncestorOf = new Uint32Array(dom.nodeCount).fill(INVALID_ID); for (let i = 0; i < dom.nodeCount; i++) { containingBlockOf[i] = resolveContainingBlock(i, dom, styles); nearestPositionedAncestorOf[i] = walkUpForPositionedAncestor(i, dom, styles); } return { containingBlockOf, nearestPositionedAncestorOf }; } // --------------------------------------------------------------------------- // Write ancestor tables into the topology graph (with confidence) // --------------------------------------------------------------------------- /** * Computes ancestor tables and writes them into `graph`, tagging each node * with the appropriate confidence level. * * Positioned-ancestor resolution is deterministic (DERIVED) when styles are * present. Containing-block resolution may be APPROXIMATE when we lack full // CSS box-model data (e.g. table-cell detection). */ export function computeAncestors( dom: DomTree, styles: StyleFacts, graph: TopologyGraph, ): void { const tables = buildAncestorTables(dom, styles); for (let i = 0; i < dom.nodeCount; i++) { graph.containingBlockOf[i] = tables.containingBlockOf[i]; graph.nearestPositionedAncestorOf[i] = tables.nearestPositionedAncestorOf[i]; // Positioned ancestors are exact when styles are exact. setConfidence(graph, i, ConfidenceLevel.DERIVED, 1); // Containing block is exact for absolute/fixed (purely style-based). // For static/relative/sticky it depends on display value; if display is // missing or approximated we downgrade confidence. const pos = styles.position[i]; if (pos === Position.ABSOLUTE || pos === Position.FIXED) { setConfidence(graph, i, ConfidenceLevel.DERIVED, 2); } else { // Normal flow containing block: confidence depends on display accuracy. // If display is Display.NONE (missing / not extracted) we mark low. const disp = styles.display[i]; if (disp === Display.NONE || disp === Display.OTHER) { setConfidence(graph, i, ConfidenceLevel.MEDIUM, 3); } else { setConfidence(graph, i, ConfidenceLevel.DERIVED, 4); } } } }