Skip to content

implement position: absolute - #34

Open
Endika wants to merge 7 commits into
chearon:masterfrom
Endika:position-absolute
Open

implement position: absolute#34
Endika wants to merge 7 commits into
chearon:masterfrom
Endika:position-absolute

Conversation

@Endika

@Endika Endika commented Jul 29, 2026

Copy link
Copy Markdown

Closes #6.

The support matrix listed position: absolute as planned, and #6 notes that the blocker was containing
blocks being assigned during layout rather than before it. That blocker is gone since "assign the containing
blocks in the prelayout step", so this fills in the rest.

What it does

Absolutely positioned boxes are laid out against the padding box of their nearest positioned ancestor, or
the initial containing block when there is none, and they leave normal flow: they do not contribute to their
parent's block size, do not appear on a line, do not add to a line's width and do not interact with floats.
They establish a block formatting context of their own, so floats inside them are contained, and a
positioned inline is blockified the way a float is.

Sizing and placement follow CSS 2.1 §10.3.7 and §10.6.4: a size from a pair of insets, shrink-to-fit for an
auto inline size, auto-margin centering, a single auto margin taking the remainder, and the over-constrained
rules — the line-right inset is dropped in ltr, the line-left one in rtl, and the block-end one always.
Percentages in insets and sizes resolve against the containing block's padding box. When both insets on an
axis are auto the box stays at its static position, taken as the start of the line box it would have
appeared on.

The part worth reviewing closely

Layout here is logical while top/right/bottom/left are physical, so the insets are mapped through the
containing block's writing mode exactly the way margins, padding and borders already are — the logical maps
gain four inset entries per mode, with insetBlockStart being top in horizontal-tb, left in
vertical-lr and right in vertical-rl. The static position is resolved during postlayout, when every
ancestor area is already physical, so a chain that crosses writing modes costs nothing; resolving it during
layout by summing logical offsets is wrong as soon as an orthogonal block intervenes.

One thing that is easy to miss: the mark iterator only consumed a box mark when it was a float or
inline-level, so making a positioned box out-of-flow without touching it turns line building into an
infinite loop.

Deliberately out of scope

position: fixed (still listed as planned); a relatively positioned inline as a containing block, since only
block containers establish one today; and the static position accounting for content earlier on the line —
the start of the line box is used instead, which is enough for the placements the tests cover.

Tests

test/position-absolute.spec.js, wired into test/ci.js: 27 cases over containing-block resolution,
out-of-flow behaviour, sizing from insets, shrink-to-fit, auto margins, the over-constrained rules,
percentages against the padding box, static positions including rtl and vertical writing modes, replaced
boxes, formatting-context containment, blockification and paint order. npm test is 470 passing and
npm run tsc is clean.

Provenance

This implementation was written as the reference solution for a coding-challenge submission that was
rejected for being too easy — three of four independent blind solvers reproduced it. That is a reason for
confidence in the design rather than against it: four independent implementations converged on the same
architecture. The code is unencumbered.

Two pre-existing bugs surfaced while those implementations were being graded. Both reproduce on unmodified
master and I have kept them out of this PR; I will file them separately:

  1. "Unknown" line items use treeIndex === 0 as a sentinel, which resolves to the root box, so
    positionPhysicalLineItems moves the whole root. Repro: <span><div style="float:left"></div></span>.
  2. An out-of-flow box as the last child of a <span> with a line-right gap throws Assertion failed in
    LineCandidates.inlinePost. Repro: <span style="padding:4px">ab<div style="float:left"></div></span>.

@chearon

chearon commented Jul 29, 2026

Copy link
Copy Markdown
Owner

This looks promising! Before I dive into this, can you disclose if you used AI or not?

@Endika

Endika commented Jul 29, 2026

Copy link
Copy Markdown
Author

This looks promising! Before I dive into this, can you disclose if you used AI or not?

I leverage AI models for automated unit test generation, technical writing, and code documentation.

@chearon

chearon commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Cool. I got absolute positioning 90% done a year or two ago but I think I got stuck on where to save the static positions (more on that below). It'll take me a while to re-familiarize myself and finish what I'm working on, but it looks good. The other fixes you mentioned sound like a quicker review.

One thing that stands out is that my end goal for the Layout class is to only have {tree, fragments}, where fragments are glyphs/run fragments and inline fragments. I use dropflow to hold hundreds of thousands of layouts, so Layout needs to retain only what's necessary. Is it not possible to set the static position on the box areas temporarily instead of needing that map?

@Endika

Endika commented Jul 29, 2026

Copy link
Copy Markdown
Author

Apologies if you already had work in progress. I didn't see anything in the repository. Having spent some time testing it, I just thought it would be helpful to share what I've found so far.

@Endika

Endika commented Jul 29, 2026

Copy link
Copy Markdown
Author

On the map: it's only alive between line building and postlayout, and reflow() spans both, so it
can be a local there rather than a field on Layout. The box areas don't have a free slot for it layoutAbsoluteBox overwrites blockStart/lineLeft after the flow pass records them, but the
record can drop to two numbers, since the flags are just "both insets on this axis are auto" and the
area is always the nearest block container ancestor. I can push that here if you want it, or leave
it for your branch.

@chearon

chearon commented Jul 30, 2026

Copy link
Copy Markdown
Owner

I'm much more interested in your work here vs mine. Sorry, I re-read my last comment and realized it was ambiguous: the things I'm working on right now are totally unrelated to absolute positioning.

Since position: absolute adjustments happen in post-layout, pre-order traversal, shouldn't it be possible for the position: absolute box areas to get their static positions (positioned against the inline containing block) during layout, then that gets adjusted to go against the real containing block in postlayout, preorder?

I agree with all the other changes you mentioned, though.

I'll give this a full review soon, only had an hour to play around with it yesterday.

@Endika

Endika commented Jul 31, 2026

Copy link
Copy Markdown
Author

Yeah, that's exactly what it does, the problem was only where to keep the number until then.

By the time postlayout gets there, layout has already put the inset/margin position in blockStart/lineLeft, and the static position is measured from a different ancestor (the in-flow parent), so postlayout has to add the two together. One slot per axis, two values.

But it's just two numbers and none of it had to stick around, so I pushed that: Layout is back to {tree} and the record is a Map<Box, [number, number]> that lives in reflow() and is gone when it returns. The area is derived from the parents stack postlayout already keeps, it's always the nearest block container's content area, and the flags from both insets on the axis being auto. Tests still green.

@chearon chearon left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implementation was written as the reference solution for a coding-challenge submission that was rejected for being too easy — three of four independent blind solvers reproduced it. That is a reason for confidence in the design rather than against it: four independent implementations converged on the same architecture. The code is unencumbered.

Can you elaborate on what you mean here? What is a "blind solver"?

This is good, but it's not the best solution. In particular, it walks the whole layout tree even if there aren't absolutes. It should only re-walk where necessary. It doesn't need to use extra memory for the static position, and it double-wraps position: absolute when it's among blocks. It leaves inline static positioning totally unsolved.

That said, If you're in it for the long haul, this is undoubtedly a great start.

Here's my reference implementation. It passes all of your tests, and isn't much more code. I'm not sure if I even removed unused functions.

all design changes except the more efficient box tree (patch is against ac41203)
diff --git a/src/api.ts b/src/api.ts
index 3eac20e..f8fbc14 100644
--- a/src/api.ts
+++ b/src/api.ts
@@ -2,14 +2,13 @@ import '#register-default-environment';
 import {HTMLElement, TextNode} from './dom.ts';
 import {DeclaredStyle, getOriginStyle, computeElementStyle} from './style.ts';
 import {fonts, FontFace, createFaceFromTables, createFaceFromTablesSync, onLoadWalkerTextNodeForFonts, onLoadWalkerElementForFonts} from './text-font.ts';
-import {generateBlockContainer, layoutBlockLevelBox, layoutAbsolutes} from './layout-flow.ts';
+import {generateBlockContainer, layoutBlockLevelBox} from './layout-flow.ts';
 import HtmlPaintBackend from './paint-html.ts';
 import SvgPaintBackend from './paint-svg.ts';
 import CanvasPaintBackend from './paint-canvas.ts';
 
 import paint from './paint.ts';
 import {BoxArea, Layout, prelayout, postlayout} from './layout-box.ts';
-import type {Box, StaticPosition} from './layout-box.ts';
 import {onLoadWalkerElementForImage} from './layout-image.ts';
 import {id, uuid} from './util.ts';
 
@@ -51,13 +50,9 @@ export function layout(rootElement: HTMLElement): Layout {
 export function reflow(layout: Layout, width = 640, height = 480) {
   const initialContainingBlock = new BoxArea(layout.root(), 0, 0, width, height);
 
-  // Only alive for the length of this reflow, so nothing is retained on the Layout
-  const staticPositions = new Map<Box, StaticPosition>();
-
   prelayout(layout, initialContainingBlock);
-  layoutBlockLevelBox(layout, layout.root(), {staticPositions});
-  layoutAbsolutes(layout, {staticPositions});
-  postlayout(layout, staticPositions);
+  layoutBlockLevelBox(layout, layout.root(), {});
+  postlayout(layout);
 }
 
 /**
diff --git a/src/layout-box.ts b/src/layout-box.ts
index 1d53958..564c291 100644
--- a/src/layout-box.ts
+++ b/src/layout-box.ts
@@ -148,22 +148,23 @@ export abstract class Box extends TreeNode {
     // block) because anonymously created boxes cannot invoke those modes.
     isInline:                  1 << 8,
     isBfcRoot:                 1 << 9,
-    // 8..13: propagation bits: Inline <- Run
+    // 8..13: propagation bits: Inline+ <- Run
     hasText:                   1 << 8,
     hasComplexText:            1 << 9,
     hasSoftHyphen:             1 << 10,
     hasNewlines:               1 << 11,
     hasSoftWrap:               1 << 12,
     hasWordSpacing:            1 << 13,
-    // 14..15: propagation bits: Inline <- Inline
+    // 14..15: propagation bits: Inline+ <- Inline
     hasPaintedInlines:         1 << 14,
     hasSizedInline:            1 << 15,
-    // 16: propagation bits: Inline <- Break, Inline, ReplacedBox
+    // 16: propagation bits: Inline+ <- Break, Inline, ReplacedBox
     hasBreakInlineOrReplaced:  1 << 16,
-    // 17..18: propagation bits: Inline <- FormattingBox
+    // 17..18: propagation bits: Inline+ <- FormattingBox
     hasFloatOrReplaced:        1 << 17,
     hasInlineBlocks:           1 << 18,
-    // 19..31: if you take them, remove them from PROPAGATES_TO_INLINE_BITS
+    hasAbsoluteInCb:           1 << 19,
+    // 20..31: if you take them, remove them from PROPAGATES_TO_INLINE_BITS
   };
 
   /**
@@ -415,6 +416,10 @@ export abstract class Box extends TreeNode {
     return Boolean(this.bitfield & Box.BITS.hasForegroundInDescendent);
   }
 
+  hasAbsoluteInCb() {
+    return Boolean(this.bitfield & Box.BITS.hasAbsoluteInCb);
+  }
+
   postlayoutPreorder(layout: Layout) {
     // TODO: Inlines don't use this yet. Get rid of paragraph's backgroundBoxes
     // and use normal inline areas instead, with fragmentation
@@ -568,11 +573,19 @@ export abstract class FormattingBox extends Box {
     if (this.isFloat()) {
       parent.bitfield |= Box.BITS.hasFloatOrReplaced;
     }
+
+    if (this.isAbsolute() || this.hasAbsoluteInCb() && !this.isPositioned()) {
+      parent.bitfield |= Box.BITS.hasAbsoluteInCb;
+    }
   }
 
   isInlineLevel() {
     return this.style.display.outer === 'inline';
   }
+
+  isAbsoluteContainingBlock() {
+    return (this.isPositioned() || this.treeStart === 0) && this.hasAbsoluteInCb();
+  }
 }
 
 export class BoxArea {
@@ -610,6 +623,14 @@ export class BoxArea {
     return this.box.style.direction;
   }
 
+  getWritingModeAsParticipant() {
+    return this.parent ? this.parent.getEstablishedWritingMode() : 'horizontal-tb';
+  }
+
+  getDirectionAsParticipant() {
+    return this.parent ? this.parent.getEstablishedDirection() : 'ltr';
+  }
+
   get x() {
     return this.lineLeft;
   }
@@ -634,6 +655,30 @@ export class BoxArea {
     return this.blockSize;
   }
 
+  getBlockEndInset() {
+    if (!this.parent) return 0;
+    if (
+      (this.getWritingModeAsParticipant() === 'horizontal-tb') !==
+      (this.parent.getWritingModeAsParticipant() === 'horizontal-tb')
+    ) {
+      return this.parent.inlineSize - this.blockStart - this.blockSize;
+    } else {
+      return this.parent.blockSize - this.blockStart - this.blockSize;
+    }
+  }
+
+  getLineRightInset() {
+    if (!this.parent) return 0;
+    if (
+      (this.getWritingModeAsParticipant() === 'horizontal-tb') !==
+      (this.parent.getWritingModeAsParticipant() === 'horizontal-tb')
+    ) {
+      return this.parent.blockSize - this.lineLeft - this.inlineSize;
+    } else {
+      return this.parent.inlineSize - this.lineLeft - this.inlineSize;
+    }
+  }
+
   setParent(p: BoxArea) {
     this.parent = p;
   }
@@ -668,6 +713,41 @@ export class BoxArea {
     }
   }
 
+  getLineLeftOfAreaAgainstSelf(area: BoxArea) {
+    const thisCb = this.box.getContainingBlock();
+    const thisWm = this.box.getWritingModeAsParticipant(thisCb);
+    const areaCb = area.box.getContainingBlock();
+    const areaWm = area.box.getWritingModeAsParticipant(areaCb);
+
+    if (thisWm === 'horizontal-tb') {
+      if (areaWm === 'vertical-rl') return area.getBlockEndInset();
+      if (areaWm === 'vertical-lr') return area.blockStart;
+    } else { // 'vertical-rl', 'vertical-lr'
+      if (areaWm === 'horizontal-tb') return area.blockStart;
+    }
+
+    return area.lineLeft;
+  }
+
+  getBlockStartOfAreaAgainstSelf(area: BoxArea) {
+    const thisCb = this.box.getContainingBlock();
+    const thisWm = this.getEstablishedWritingMode();
+    const areaCb = area.box.getContainingBlock();
+    const areaWm = area.box.getWritingModeAsParticipant(areaCb);
+
+    if (thisWm === 'horizontal-tb') {
+      if (areaWm !== 'horizontal-tb') return area.lineLeft;
+    } else if (thisWm === 'vertical-rl') {
+      if (areaWm === 'horizontal-tb') return area.getLineRightInset();
+      if (areaWm === 'vertical-lr') return area.getBlockEndInset();
+    } else { // 'vertical-lr'
+      if (areaWm === 'horizontal-tb') return area.lineLeft;
+      if (areaWm === 'vertical-rl') return area.getBlockEndInset();
+    }
+
+    return area.blockStart;
+  }
+
   absolutify() {
     let x, y, width, height;
 
@@ -734,56 +814,6 @@ export class BoxArea {
   }
 }
 
-/**
- * An absolutely positioned box whose insets are `auto` on an axis sits at the
- * position it would have had in flow, which is known in the axes of its in-flow
- * parent, not of its containing block. Both of those are ancestors, so both are
- * already absolute when postlayout reaches this box, and the offset can be
- * mapped through physical coordinates.
- *
- * `area` is the content area of the in-flow parent, which is the nearest block
- * container ancestor: the only boxes that can sit between them are inlines.
- */
-function shiftToStaticPosition(box: Box, staticPosition: StaticPosition, area: BoxArea) {
-  const [blockOffset, inlineOffset] = staticPosition;
-  const borderArea = box.getBorderArea();
-  const containingBlock = borderArea.parent;
-  if (!containingBlock) throw new Error('Assertion failed');
-  // Layout only leaves room for the static position when it had no inset to
-  // position against, which is the same question the box model asked
-  const needsBlock = box.style.getInsetBlockStart(containingBlock) === 'auto' &&
-    box.style.getInsetBlockEnd(containingBlock) === 'auto';
-  const needsLineLeft = box.style.getInsetLineLeft(containingBlock) === 'auto' &&
-    box.style.getInsetLineRight(containingBlock) === 'auto';
-  if (!needsBlock && !needsLineLeft) return;
-  const parentWritingMode = area.getEstablishedWritingMode();
-  let x, y;
-
-  if (parentWritingMode === 'vertical-lr') {
-    x = area.x + blockOffset;
-    y = area.y + inlineOffset;
-  } else if (parentWritingMode === 'vertical-rl') {
-    x = area.x + area.width - blockOffset;
-    y = area.y + inlineOffset;
-  } else { // 'horizontal-tb'
-    x = area.x + inlineOffset;
-    y = area.y + blockOffset;
-  }
-
-  const writingMode = containingBlock.getEstablishedWritingMode();
-
-  if (writingMode === 'vertical-lr') {
-    if (needsBlock) borderArea.blockStart += x - containingBlock.x;
-    if (needsLineLeft) borderArea.lineLeft += y - containingBlock.y;
-  } else if (writingMode === 'vertical-rl') {
-    if (needsBlock) borderArea.blockStart += containingBlock.x + containingBlock.width - x;
-    if (needsLineLeft) borderArea.lineLeft += y - containingBlock.y;
-  } else { // 'horizontal-tb'
-    if (needsBlock) borderArea.blockStart += y - containingBlock.y;
-    if (needsLineLeft) borderArea.lineLeft += x - containingBlock.x;
-  }
-}
-
 export function prelayout(layout: Layout, icb: BoxArea) {
   const parents: (BlockContainer | Inline)[] = [];
   const ifcs: BlockContainerOfInlines[] = [];
@@ -841,26 +871,11 @@ export function prelayout(layout: Layout, icb: BoxArea) {
   }
 }
 
-export function postlayout(layout: Layout, staticPositions: Map<Box, StaticPosition>) {
+export function postlayout(layout: Layout) {
   const parents: (BlockContainer | Inline)[] = [];
 
   for (let i = 0; i < layout.tree.length; i++) {
     const item = layout.tree[i];
-
-    if (item.isFormattingBox() && item.isAbsolute()) {
-      // The offset was recorded in the axes of the in-flow parent, which the
-      // preorder walk has already absolutified
-      const staticPosition = staticPositions.get(item);
-      if (staticPosition) {
-        let inflowParent;
-        for (let j = parents.length - 1; j >= 0 && !inflowParent; j--) {
-          if (parents[j].isBlockContainer()) inflowParent = parents[j];
-        }
-        if (!inflowParent) throw new Error('Assertion failed');
-        shiftToStaticPosition(item, staticPosition, inflowParent.getContentArea());
-      }
-    }
-
     item.postlayoutPreorder(layout);
     if (item.isBlockContainer() || item.isInline()) {
       parents.push(item);
@@ -920,17 +935,6 @@ export function log(layout: Layout, logger?: Logger, options?: TreeLogOptions) {
   logger.flush();
 }
 
-/**
- * Where an absolutely positioned box would have been if it were in flow, in the
- * logical axes of the content area of its in-flow parent. Only used when both
- * insets on that axis are `auto` (CSS 2.2 § 10.3.7, § 10.6.4).
- *
- * Only needed between line building and postlayout, so it is not stored on the
- * `Layout`: it rides along on the layout context and is gone once `reflow`
- * returns.
- */
-export type StaticPosition = [blockOffset: number, inlineOffset: number];
-
 export class Layout {
   tree: InlineLevel[];
 
diff --git a/src/layout-flow.ts b/src/layout-flow.ts
index 02af535..b9eedce 100644
--- a/src/layout-flow.ts
+++ b/src/layout-flow.ts
@@ -17,7 +17,7 @@ import {getImage} from './layout-image.ts';
 import {Box, FormattingBox, TreeNode, Layout} from './layout-box.ts';
 
 import type {InlineMetrics, ShapedItem, InlineFragment} from './layout-text.ts';
-import type {BoxArea, PrelayoutContext, StaticPosition} from './layout-box.ts';
+import type {BoxArea, PrelayoutContext} from './layout-box.ts';
 import type {AllocatedUint16Array} from './text-harfbuzz.ts';
 
 function assumePx(v: any): asserts v is number {
@@ -44,12 +44,6 @@ export interface LayoutContext {
    * This is only undefined for the root box or when an element is out of flow.
    */
   bfc?: BlockFormattingContext
-  /**
-   * Where the out-of-flow boxes seen so far would have been in flow, for the
-   * ones that need it. Read by the absolute layout pass and by postlayout, then
-   * dropped.
-   */
-  staticPositions: Map<Box, StaticPosition>
 }
 
 class MarginCollapseCollection {
@@ -117,7 +111,7 @@ export class BlockFormattingContext {
     this.hypotheticals = EMPTY_MAP;
   }
 
-  collapseStart(layout: Layout, box: BlockLevel) {
+  collapseStart(layout: Layout, box: BlockLevel, ctx: LayoutContext) {
     const containingBlock = box.getContainingBlock();
     const marginBlockStart = box.style.getMarginBlockStart(containingBlock);
     let floatBottom = 0;
@@ -143,7 +137,7 @@ export class BlockFormattingContext {
     if (adjoinsPrevious) {
       this.margin.collection.add(marginBlockStart);
     } else {
-      this.positionBlockContainers();
+      this.positionBlockContainers(layout, ctx);
       const c = floatBottom - this.cbBlockStart;
       this.margin = {level: this.level, collection: new MarginCollapseCollection(c)};
       if (box.canCollapseThrough(layout)) this.margin.clearanceAtLevel = this.level;
@@ -157,7 +151,7 @@ export class BlockFormattingContext {
     const borderBlockStartWidth = box.style.getBorderBlockStartWidth(containingBlock);
     const adjoinsNext = paddingBlockStart === 0 && borderBlockStartWidth === 0;
 
-    this.collapseStart(layout, box);
+    this.collapseStart(layout, box, ctx);
 
     this.last = 'start';
     this.level += 1;
@@ -178,12 +172,12 @@ export class BlockFormattingContext {
     }
 
     if (!adjoinsNext) {
-      this.positionBlockContainers();
+      this.positionBlockContainers(layout, ctx);
       this.margin = {level: this.level, collection: new MarginCollapseCollection()};
     }
   }
 
-  boxEnd(layout: Layout, box: BlockContainer) {
+  boxEnd(layout: Layout, box: BlockContainer, ctx: LayoutContext) {
     const containingBlock = box.getContainingBlock();
     const {lineLeft, lineRight} = box.getContainingBlockToContent(containingBlock);
     const paddingBlockEnd = box.style.getPaddingBlockEnd(containingBlock);
@@ -212,7 +206,7 @@ export class BlockFormattingContext {
     this.cbLineRight -= lineRight;
 
     if (!adjoins) {
-      this.positionBlockContainers();
+      this.positionBlockContainers(layout, ctx);
       this.margin = {level: this.level, collection: new MarginCollapseCollection()};
     }
 
@@ -231,13 +225,13 @@ export class BlockFormattingContext {
     this.last = 'end';
   }
 
-  boxAtomic(layout: Layout, box: BlockLevel) {
+  boxAtomic(layout: Layout, box: BlockLevel, ctx: LayoutContext) {
     const containingBlock = box.getContainingBlock();
     const marginBlockEnd = box.style.getMarginBlockEnd(containingBlock);
     assumePx(marginBlockEnd);
-    this.collapseStart(layout, box);
+    this.collapseStart(layout, box, ctx);
     this.fctx?.boxStart();
-    this.positionBlockContainers();
+    this.positionBlockContainers(layout, ctx);
     box.setBlockPosition(this.cbBlockStart);
     this.margin.collection = new MarginCollapseCollection();
     this.margin.collection.add(marginBlockEnd);
@@ -270,14 +264,21 @@ export class BlockFormattingContext {
     return this.fctx || (this.fctx = new FloatContext(this, blockOffset));
   }
 
-  finalize(box: BlockContainer) {
+  finalize(layout: Layout, box: BlockContainer, ctx: LayoutContext) {
     if (!box.isBfcRoot()) throw new Error('This is for bfc roots only');
     const containingBlock = box.getContainingBlock();
     const blockSize = box.style.getBlockSize(containingBlock);
+    let insetBlockStart: number | 'auto' = 'auto';
+    let insetBlockEnd: number | 'auto' = 'auto';
 
-    this.positionBlockContainers();
+    this.positionBlockContainers(layout, ctx);
 
-    if (blockSize === 'auto') {
+    if (box.isAbsolute()) {
+      insetBlockStart = box.style.getInsetBlockStart(containingBlock);
+      insetBlockEnd = box.style.getInsetBlockEnd(containingBlock);
+    }
+
+    if (blockSize === 'auto' && !(insetBlockStart !== 'auto' && insetBlockEnd !== 'auto')) {
       let lineboxHeight = 0;
       if (box.isBlockContainerOfInlines()) {
         lineboxHeight = box.getContentArea().blockSize;
@@ -285,9 +286,11 @@ export class BlockFormattingContext {
       const blockSize = Math.max(lineboxHeight, this.cbBlockStart, this.fctx?.getBothBottom() ?? 0);
       box.setBlockSize(containingBlock, blockSize);
     }
+
+    finalizeBlockContainer(layout, box, ctx);
   }
 
-  positionBlockContainers() {
+  positionBlockContainers(layout: Layout, ctx: LayoutContext) {
     const sizeStack = this.sizeStack;
     const offsetStack = this.offsetStack;
     const margin = this.margin.collection.get();
@@ -311,6 +314,8 @@ export class BlockFormattingContext {
           box.setBlockSize(containingBlock, childSize);
         }
 
+        if (!box.isBfcRoot()) finalizeBlockContainer(layout, box, ctx);
+
         const blockSize = box.getBorderArea().blockSize;
 
         sizeStack[level] += blockSize;
@@ -960,7 +965,8 @@ export class BlockContainerOfInlines extends BlockContainerBase {
     return inline.hasText()
       || inline.hasSizedInline()
       || inline.hasFloatOrReplaced()
-      || inline.hasInlineBlocks();
+      || inline.hasInlineBlocks()
+      || inline.hasAbsoluteInCb();
   }
 
   doTextLayout(layout: Layout, ctx: LayoutContext) {
@@ -1072,28 +1078,6 @@ function doBlockBoxModelForBlockBox(layout: Layout, box: BlockContainer) {
   }
 }
 
-/**
- * An inline formatting context with nothing to lay out still has to answer for
- * the absolutely positioned boxes inside it, which would have started at its
- * content edge (CSS 2.2 § 10.3.7, § 10.6.4).
- */
-function setStaticPositionsWithoutLines(
-  layout: Layout,
-  box: BlockContainerOfInlines,
-  ctx: LayoutContext
-) {
-  const contentArea = box.getContentArea();
-  const ltr = box.style.direction === 'ltr';
-
-  for (let i = box.treeStart + 1; i <= box.treeFinal; i++) {
-    const item = layout.tree[i];
-    if (item.isFormattingBox() && item.isAbsolute()) {
-      ctx.staticPositions.set(item, [0, ltr ? 0 : contentArea.inlineSize]);
-      i = item.treeFinal;
-    }
-  }
-}
-
 function layoutBlockBoxInner(
   layout: Layout,
   box: BlockContainer,
@@ -1113,11 +1097,7 @@ function layoutBlockBoxInner(
   // Child flow is now possible
 
   if (box.isBlockContainerOfInlines()) {
-    if (!box.shouldLayoutContent(layout)) {
-      // No lines will be built, so the boxes taken out of this flow would all
-      // have started at the content edge
-      setStaticPositionsWithoutLines(layout, box, cctx);
-    } else if (containingBfc) {
+    if (containingBfc) {
       // text layout happens in bfc.boxStart
     } else {
       box.doTextLayout(layout, cctx);
@@ -1132,7 +1112,7 @@ function layoutBlockBoxInner(
   }
 
   if (establishedBfc) {
-    establishedBfc.finalize(box);
+    establishedBfc.finalize(layout, box, ctx);
     if (establishedBfc.fctx) {
       if (box.loggingEnabled()) {
         console.log('Left floats');
@@ -1144,7 +1124,7 @@ function layoutBlockBoxInner(
     }
   }
 
-  containingBfc?.boxEnd(layout, box);
+  containingBfc?.boxEnd(layout, box, ctx);
 }
 
 function layoutBlockBox(
@@ -1168,7 +1148,7 @@ function layoutReplacedBox(
   box.fillAreas(containingBlock);
   doInlineBoxModelForBlockBox(box);
   box.setBlockSize(containingBlock, box.getDefiniteInnerBlockSize());
-  ctx.bfc!.boxAtomic(layout, box);
+  ctx.bfc!.boxAtomic(layout, box, ctx);
 }
 
 export function layoutBlockLevelBox(
@@ -1270,22 +1250,6 @@ export function layoutFloatBox(
   }
 }
 
-/**
- * Absolutely positioned boxes are laid out after the flow they were taken out
- * of, because their containing block - the padding area of the nearest
- * positioned ancestor, or the initial containing block - only has a size once
- * that ancestor is done. Tree order means an ancestor is always resolved before
- * a box it contains.
- */
-export function layoutAbsolutes(layout: Layout, ctx: LayoutContext) {
-  for (let i = 1; i < layout.tree.length; i++) {
-    const item = layout.tree[i];
-    if (item.isFormattingBox() && item.isAbsolute()) {
-      layoutAbsoluteBox(layout, item, ctx);
-    }
-  }
-}
-
 function getShrinkToFitInlineSize(
   layout: Layout,
   box: BlockLevel,
@@ -1299,8 +1263,8 @@ function getShrinkToFitInlineSize(
 // § 10.3.7
 function doInlineBoxModelForAbsoluteBox(
   layout: Layout,
-  box: BlockLevel,
-  staticPosition: StaticPosition | undefined
+  staticContainingBlock: BoxArea | null,
+  box: BlockLevel
 ) {
   const containingBlock = box.getContainingBlock();
   const cInlineSize = containingBlock.inlineSizeForPotentiallyOrthogonal(box);
@@ -1314,7 +1278,12 @@ function doInlineBoxModelForAbsoluteBox(
   let marginLineRight = styleMarginLineRight === 'auto' ? 0 : styleMarginLineRight;
   let sizedFromInsets = false;
   let inlineSize;
+  let lineLeft = insetLineLeft === 'auto' ? 0 : insetLineLeft;
 
+  // solve:
+  // left = px  , width = auto, right = px
+  // left = px  , width = auto, right = auto
+  // left = px  , width = px  , right = auto
   if (definiteInlineSize !== undefined) {
     inlineSize = definiteInlineSize;
   } else if (box.isReplacedBox()) {
@@ -1338,6 +1307,8 @@ function doInlineBoxModelForAbsoluteBox(
       - marginLineRight;
   }
 
+  // solve:
+  // left = px  , width = px  , right = px
   if (!sizedFromInsets && insetLineLeft !== 'auto' && insetLineRight !== 'auto') {
     // Paragraph 2: the equation is solvable, so what is left over goes to the
     // margins that are auto
@@ -1356,44 +1327,54 @@ function doInlineBoxModelForAbsoluteBox(
     } else if (styleMarginLineRight === 'auto') {
       marginLineRight = rest - marginLineLeft;
     }
+
     // Otherwise the values are over-constrained. The line-right inset is the
     // one ignored in ltr and the line-left one in rtl, which is what falls out
     // of positioning against the retained side below
+    if (styleMarginLineLeft !== 'auto' && styleMarginLineRight !== 'auto' && !ltr) {
+      lineLeft = cInlineSize - insetLineRight - marginLineRight - inlineSize;
+    } else {
+      lineLeft += marginLineLeft;
+    }
   }
 
-  box.setInlineOuterSize(containingBlock, inlineSize);
+  // solve:
+  // left = auto, width = auto, right = auto
+  // left = auto, width = px  , right = auto
+  if (insetLineLeft === 'auto' && insetLineRight === 'auto') {
+    lineLeft = box.getBorderArea().lineLeft; // against static cb
+    let area = staticContainingBlock;
+    while (area && area !== containingBlock) {
+      lineLeft += containingBlock.getLineLeftOfAreaAgainstSelf(area);
+      area = area.parent;
+    }
+    if (!ltr) lineLeft -= inlineSize;
+  }
 
-  if (insetLineLeft !== 'auto' && (insetLineRight === 'auto' || ltr)) {
-    box.setInlinePosition(insetLineLeft + marginLineLeft);
-  } else if (insetLineRight !== 'auto') {
-    box.setInlinePosition(cInlineSize - insetLineRight - marginLineRight - inlineSize);
-  } else if (staticPosition) {
-    // Paragraphs 1 and 4: both insets are auto, so the box stays where it would
-    // have been. In rtl it is the line-right margin edge that was recorded
-    box.setInlinePosition(ltr ? marginLineLeft : -(inlineSize + marginLineRight));
-  } else {
-    box.setInlinePosition(marginLineLeft);
+  // solve:
+  // left = auto, width = auto, right = px
+  // left = auto, width = px  , right = px
+  if (insetLineLeft === 'auto' && insetLineRight !== 'auto') {
+    lineLeft = cInlineSize - insetLineRight - marginLineRight - inlineSize;
   }
-}
 
-// § 10.6.4
-interface AbsoluteBlockAxis {
-  usesContentBlockSize: boolean;
-  blockSize: number | undefined;
-  insetBlockStart: number | 'auto';
-  insetBlockEnd: number | 'auto';
-  cBlockSize: number;
+  box.setInlineOuterSize(containingBlock, inlineSize);
+  box.setInlinePosition(lineLeft);
 }
 
-function doBlockBoxModelForAbsoluteBox(box: BlockLevel): AbsoluteBlockAxis {
+// § 10.6.4
+function doBlockBoxModelForAbsoluteBox(
+  staticContainingBlock: BoxArea | null,
+  box: BlockLevel
+) {
   const containingBlock = box.getContainingBlock();
+  const borderArea = box.getBorderArea();
   const cBlockSize = containingBlock.blockSizeForPotentiallyOrthogonal(box);
   const insetBlockStart = box.style.getInsetBlockStart(containingBlock);
   const insetBlockEnd = box.style.getInsetBlockEnd(containingBlock);
-  const marginBlockStart = box.style.getMarginBlockStart(containingBlock);
-  const marginBlockEnd = box.style.getMarginBlockEnd(containingBlock);
+  let marginBlockStart = box.style.getMarginBlockStart(containingBlock);
+  let marginBlockEnd = box.style.getMarginBlockEnd(containingBlock);
   let blockSize = box.getDefiniteInnerBlockSize(containingBlock);
-  let usesContentBlockSize = blockSize === undefined;
 
   if (
     blockSize === undefined &&
@@ -1415,69 +1396,100 @@ function doBlockBoxModelForAbsoluteBox(box: BlockLevel): AbsoluteBlockAxis {
       - paddingBlockStart
       - paddingBlockEnd
       - borderBlockEndWidth);
-    usesContentBlockSize = false;
   }
 
-  if (blockSize !== undefined) box.setBlockSize(containingBlock, blockSize);
-
-  return {usesContentBlockSize, blockSize, insetBlockStart, insetBlockEnd, cBlockSize};
-}
+  if (blockSize !== undefined) {
+    box.setBlockSize(containingBlock, blockSize);
+  }
 
-function setBlockPositionForAbsoluteBox(box: BlockLevel, axis: AbsoluteBlockAxis) {
-  const containingBlock = box.getContainingBlock();
-  const {insetBlockStart, insetBlockEnd, cBlockSize} = axis;
-  const outerBlockSize = box.getBorderArea().blockSize;
-  const styleMarginBlockStart = box.style.getMarginBlockStart(containingBlock);
-  const styleMarginBlockEnd = box.style.getMarginBlockEnd(containingBlock);
-  let marginBlockStart = styleMarginBlockStart === 'auto' ? 0 : styleMarginBlockStart;
-  let marginBlockEnd = styleMarginBlockEnd === 'auto' ? 0 : styleMarginBlockEnd;
+  const outerBlockSize = borderArea.blockSize;
+  let usedMarginBlockStart = marginBlockStart === 'auto' ? 0 : marginBlockStart;
+  let usedMarginBlockEnd = marginBlockEnd === 'auto' ? 0 : marginBlockEnd;
 
   if (insetBlockStart !== 'auto' && insetBlockEnd !== 'auto') {
     const rest = cBlockSize - insetBlockStart - insetBlockEnd - outerBlockSize;
 
-    if (styleMarginBlockStart === 'auto' && styleMarginBlockEnd === 'auto') {
+    if (marginBlockStart === 'auto' && marginBlockEnd === 'auto') {
       // Paragraph 6: equal margins center the box, negative values included
-      marginBlockStart = marginBlockEnd = rest / 2;
-    } else if (styleMarginBlockStart === 'auto') {
-      marginBlockStart = rest - marginBlockEnd;
-    } else if (styleMarginBlockEnd === 'auto') {
-      marginBlockEnd = rest - marginBlockStart;
+      usedMarginBlockStart = marginBlockEnd = rest / 2;
+    } else if (marginBlockStart === 'auto') {
+      usedMarginBlockStart = rest - usedMarginBlockEnd;
+    } else if (marginBlockEnd === 'auto') {
+      usedMarginBlockEnd = rest - marginBlockStart;
     }
     // Otherwise over-constrained, and the block-end inset is the one ignored
   }
 
   if (insetBlockStart !== 'auto') {
-    box.setBlockPosition(insetBlockStart + marginBlockStart);
+    box.setBlockPosition(insetBlockStart + usedMarginBlockStart);
   } else if (insetBlockEnd !== 'auto') {
-    box.setBlockPosition(cBlockSize - insetBlockEnd - marginBlockEnd - outerBlockSize);
+    box.setBlockPosition(cBlockSize - insetBlockEnd - usedMarginBlockEnd - outerBlockSize);
   } else {
-    // Both insets are auto, so the box stays where it would have been. Postlayout
-    // shifts it there once the in-flow parent has absolute coordinates
-    box.setBlockPosition(marginBlockStart);
+    let blockStart = borderArea.blockStart + usedMarginBlockStart;
+    let area = staticContainingBlock;
+    while (area && area !== containingBlock) {
+      blockStart += containingBlock.getBlockStartOfAreaAgainstSelf(area);
+      area = area.parent;
+    }
+    box.setBlockPosition(blockStart);
   }
 }
 
-function layoutAbsoluteBox(layout: Layout, box: BlockLevel, ctx: LayoutContext) {
+function layoutAbsoluteBox(
+  layout: Layout,
+  staticContainingBlock: BoxArea | null,
+  box: BlockLevel,
+  ctx: LayoutContext
+) {
   const cctx: LayoutContext = {...ctx, bfc: undefined};
-  const containingBlock = box.getContainingBlock();
-  const staticPosition = ctx.staticPositions.get(box);
 
-  box.fillAreas(containingBlock);
-  doInlineBoxModelForAbsoluteBox(layout, box, staticPosition);
-  const axis = doBlockBoxModelForAbsoluteBox(box);
+  // NB: fillAreas was called in layoutStaticBox
+  doInlineBoxModelForAbsoluteBox(layout, staticContainingBlock, box);
+  doBlockBoxModelForAbsoluteBox(staticContainingBlock, box);
 
   if (box.isBlockContainer()) {
     layoutBlockBoxInner(layout, box, cctx);
-    // The formatting context sizes an auto block size from the content, which is
-    // only the used value when an inset on that axis is auto
-    if (!axis.usesContentBlockSize && axis.blockSize !== undefined) {
-      box.setBlockSize(containingBlock, axis.blockSize);
+  } else {
+    // replaced boxes have no layout. they were sized by doInline/Block above
+  }
+}
+
+function finalizeBlockContainer(
+  layout: Layout,
+  box: BlockContainer,
+  ctx: LayoutContext
+) {
+  if ((box.isPositioned() || box.treeStart === 0) && box.hasAbsoluteInCb()) {
+    const staticParents: FormattingBox[] = [];
+
+    for (let i = box.treeStart + 1; i <= box.treeFinal; i++) {
+      const box = layout.tree[i];
+
+      if (box.isFormattingBox()) {
+        if (!box.isPositioned()) staticParents.push(box);
+        if (box.isAbsolute()) {
+          let staticContainingBlock = null;
+          if (staticParents.length) {
+            const box = staticParents[staticParents.length - 1];
+            staticContainingBlock = box.getContentArea();
+          }
+          layoutAbsoluteBox(layout, staticContainingBlock, box, ctx);
+        }
+      }
+
+      while (
+        staticParents.length &&
+        i === staticParents[staticParents.length - 1].treeFinal
+      ) staticParents.pop();
+
+      if (box.isBox() && box.isPositioned()) i = box.treeFinal;
     }
-  } else if (axis.usesContentBlockSize) {
-    box.setBlockSize(containingBlock, box.getDefiniteInnerBlockSize());
   }
+}
 
-  setBlockPositionForAbsoluteBox(box, axis);
+export function layoutStaticBox(box: FormattingBox) {
+  const containingBlock = box.getContainingBlock();
+  box.fillAreas(containingBlock);
 }
 
 export class Break extends TreeNode {
@@ -1537,6 +1549,8 @@ export class Inline extends Box {
 
       // Bits that propagate to Inline propagate again if the parent is Inline
       parent.bitfield |= (this.bitfield & Box.PROPAGATES_TO_INLINE_BITS);
+    } else if (parent.isFormattingBox()) {
+      if (this.hasAbsoluteInCb()) parent.bitfield |= Box.BITS.hasAbsoluteInCb;
     }
   }
 
@@ -1802,7 +1816,7 @@ export function inlineIteratorStateNext(state: InlineIteratorState) {
         } else if (item.isBreak()) {
           state.buffered.push({state: 'break', index: state.index});
         } else {
-          if (item.isFloat()) {
+          if (item.isOutOfFlow()) {
             state.buffered.push({state: 'box', item});
           } else {
             state.buffered.push({state: 'box', item});
diff --git a/src/layout-text.ts b/src/layout-text.ts
index cf9be11..6731380 100644
--- a/src/layout-text.ts
+++ b/src/layout-text.ts
@@ -6,6 +6,7 @@ import {
   IfcVacancy,
   Inline,
   layoutFloatBox,
+  layoutStaticBox,
   layoutContribution,
   createInlineIteratorState,
   inlineIteratorStateNext,
@@ -2926,11 +2927,11 @@ export function createIfcLineboxes(
       // line is where it would have been if it were in flow, which is the
       // position it uses when its insets are auto (CSS 2.2 § 10.3.7, § 10.6.4)
       const contentArea = ifc.block.getContentArea();
+      const {lineLeft, lineRight} = mark.box.getMarginsAutoIsZero(containingBlock);
       const ltr = ifc.block.style.direction === 'ltr';
-      ctx.staticPositions.set(mark.box, [
-        ifc.vacancy.blockOffset,
-        ltr ? 0 : contentArea.inlineSize
-      ]);
+      layoutStaticBox(mark.box);
+      mark.box.setBlockPosition(ifc.vacancy.blockOffset);
+      mark.box.setInlinePosition(ltr ? lineLeft : contentArea.inlineSize - lineRight);
     }
 
     if (mark.inlinePost) {

Comment thread src/layout-flow.ts Outdated
* that ancestor is done. Tree order means an ancestor is always resolved before
* a box it contains.
*/
export function layoutAbsolutes(layout: Layout, ctx: LayoutContext) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Walking the whole layout tree all over again isn't acceptable, especially because there may not even be any absolutes. If there are, they can be laid out when their containing block is done being sized. That way, we're only re-walking a subset of the tree, which is important for performance (cache locality).

The way this should be done is by adding a new propagation bit: hasAbsoluteInCb. It propagates from an absolute and goes no further than a positioned box or the root box. When we finish sizing a positioned box or the root box, we can check if it has the bit set and if so, only walk descendants and without crossing into other positioned descendants.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, with your bit. layoutAbsolutes is gone. hasAbsoluteInCb propagates from an absolute and stops at a positioned box or the root, finalizeBlockContainer runs off isAbsoluteContainingBlock(), and the walk jumps to treeFinal at any positioned descendant so it never crosses into another containing block.

One reorder on top of your version: the loop that pops static parents ran before the i = item.treeFinal skip, so a static parent whose treeFinal coincided with that of a positioned descendant stayed on the stack and became the static parent of the next absolute. It is masked as long as every absolute among blocks gets an anonymous IFC wrapper, which is why the suite didn't see it; once the wrapper goes away (your last comment) it reproduces — <div rel><div style="padding:5px"><div rel>x</div></div><div abs></div></div> put the box at x: 5 instead of 0. Test: takes a static position from a static parent that already ended.

Comment thread src/layout-flow.ts Outdated

box.fillAreas(containingBlock);
doInlineBoxModelForAbsoluteBox(layout, box, staticPosition);
const axis = doBlockBoxModelForAbsoluteBox(box);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any reason why this object has to be created and returned. Can you merge the setBlockSize parts below and everything setBlockPositionForAbsoluteBox does into doBlockBoxModelForAbsoluteBox? That would make it mirror the other layout functions like layoutFloatBox

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merged. doBlockBoxModelForAbsoluteBox sizes and positions and returns nothing, like layoutFloatBox. AbsoluteBlockAxis and setBlockPositionForAbsoluteBox are gone.

Comment thread src/layout-text.ts
ifc.vacancy.blockOffset,
ltr ? 0 : contentArea.inlineSize
]);
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't setting the position correctly for inline absolutes. Here's a test case that fails on this PR:

hello <span style="position: absolute;">__</span>is there anybody out there?

The __ should appear underneath "is".

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed __ sits under is now.

Instead of accumulating the width by hand, the box goes on the line as a zero-advance item (addBox(treeStart, treeFinal, 0)) and positionPhysicalLineItems stops skipping it. That way line.inlineOffset (floats and text-align), the bidi reorder, and the case where the box ends up on a later line all come for free, and nothing is stored anywhere.

Two things fell out of it:

  • An IFC whose only content is an absolute never finished a line, so nothing positioned the box. It finishes one now, but a line built only for out-of-flow boxes is not a line box, so ifc.blockOffset is restored afterwards and it adds no block size. That is what took the place of setStaticPositionsWithoutLines.
  • rtl with inline content before the box I could not pin down, so there is no test for it. An in-flow zero-width reference doesn't help: aaa<span style="display:inline-block;width:10px"> in an rtl block puts the inline-block at the line-right edge, to the right of aaa, which is not where I'd expect it after a strong-LTR run in an RTL paragraph. I didn't want to freeze my guess into a test on top of that. In ltr the absolute matches such a reference exactly. Is the in-flow rtl placement what you expect there?

Comment thread src/layout-box.ts Outdated
* `Layout`: it rides along on the layout context and is gone once `reflow`
* returns.
*/
export type StaticPosition = [blockOffset: number, inlineOffset: number];

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to allocate extra memory for this. The absolute box can be positioned using the normal lineLeft and blockStart offsets of its principal box when the parent layout visits it.

When the absolute is actually laid out, those offsets can be interpreted as relative to the static containing block. The hard part is that they have to get converted from the writing mode and direction of the static containing block to the real containing block. Similar to but different from shiftToStaticPosition above.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done: no map, no StaticPosition, postlayout back to one argument and reflow allocating nothing. The box records the static position in the lineLeft/blockStart of its own border area and layout maps those two numbers from the static containing block's axes into the real one's.

Two fixes inside that mapping:

  • getLineLeftOfAreaAgainstSelf used the writing mode the containing block participates in while getBlockStartOfAreaAgainstSelf uses the one it establishes. With position: relative; writing-mode: vertical-rl inside a horizontal root and all four insets auto, the box landed at y: 180 inside a 150px tall containing block. Established is the right one on both axes. Test: maps a fully auto static position through a vertical-rl containing block — my earlier vertical-rl case had top: 6px, so that axis was never auto, which is why it went unnoticed.
  • doInlineBoxModelForAbsoluteBox dropped marginLineLeft everywhere except the over-constrained arm, so left: 10px; margin-left: 5px positioned the box at 10, and so did left: 10px; right: 30px; margin-left: 5px (which subtracted the margin from the size but not from the position). The solve arms are one chain now, with the over-constrained rtl case as its own branch.

Comment thread src/layout-flow.ts Outdated
i = item.treeFinal;
}
}
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function isn't necessary if you adjust shouldLayoutContent to account for "has absolutes". That's how we should do it since that's how floats are treated.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done: hasAbsoluteInCb() sits next to hasFloatOrReplaced() in shouldLayoutContent, and setStaticPositionsWithoutLines is gone.

One wrinkle, which I've also noted on the layout-text.ts thread: an IFC whose only content is an absolute never finished a line, so nothing positioned the box. It finishes one now, and since a line built only for out-of-flow boxes is not a line box, ifc.blockOffset is restored afterwards so it adds no block size.

Comment thread src/layout-flow.ts
preBcInlineChild(tree, ctx);
tree.push(new Break(childEl.style));
} else if (childEl.style.display.outer === 'block') {
if (childEl.style.isOutOfFlow()) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if this is the right path for abspos because it generates kind of a messy box tree:

<div style="position:absolute;">:D</div>

Produces:

◼︎ Block 0
  ▭ Inline 1
    ◼︎ Block 2
      ▭ Inline 3
        Ͳ 0,2 ":D"

But it could just be:

◼︎ Block 0
  ◼︎ Block 2
    ▭ Inline 3
      Ͳ 0,2 ":D"

In other words, shouldn't an abspos among block-level content be positioned by the BFC, not by an IFC? It will be more code, and the BFC will have to handle abspos specially, for example, by never collapsing its margins with any in the BFC.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and done. <div style="position:absolute">:D</div> is Block > Inline > ":D" under the root now, with no wrapper.

Generation: an absolute block-level child joins whichever formatting context is already open preBcInlineChild only when an IFC is, preBcBlockChild otherwise. Floats are untouched.

BFC: block positions are deferred, so cbBlockStart is stale at the child, and calling positionBlockContainers() there would consume the pending margin and break collapsing across the box. So boxOutOfFlow pushes an {abs} marker onto this.stack, and positionBlockContainers gives it the offset a box start would get without pushing to sizeStack/offsetStack and without adding its size: it takes no space and collapses with nothing. Test: siblings with margin-bottom: 20px and margin-top: 30px around it still collapse to 30.

One corner I'd like your call on: with margin: 100px on the absolute between those two siblings, the static position lands after the whole collapsed 30, so y: 140. Placing it after only the first sibling's own 20 would give 130. Both readings fit "its margins never collapse with any in the BFC"; I took the collapsed value because that is what the margin collection holds when the marker is resolved.

@Endika
Endika force-pushed the position-absolute branch from 1b75136 to 98b240d Compare August 14, 2026 10:34
@Endika

Endika commented Aug 14, 2026

Copy link
Copy Markdown
Author

Sorry, that paragraph was written for a different audience and does not apply to the description of a code challenge. By “blind solver,” I meant an independent attempt to solve the same problem using only the problem statement and this repository; the goal is for students to be able to research and solve the problem on their own.

@Endika

Endika commented Aug 14, 2026

Copy link
Copy Markdown
Author

I hope you don't mind if I use your repository for a collaboration with my university for a contest. I thought it would be a good idea to collaborate by presenting it, since it was a real-world implementation.

@Endika
Endika requested a review from chearon August 14, 2026 12:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

position: absolute

2 participants