Files
Imhotep/packages/imhotep-topology/src/ancestors.ts
T

177 lines
6.0 KiB
TypeScript

// 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);
}
}
}
}