177 lines
6.0 KiB
TypeScript
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);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|