v1.1.0: pooled runtime, 959 tests, production hardening (0 squash)
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user