diff --git a/README.md b/README.md
index 2636ae8..8be3713 100644
--- a/README.md
+++ b/README.md
@@ -78,7 +78,7 @@ Following are rules that work or will work soon. Shorthand properties are not li
| margin | `em`, `px`, `%`, `cm` etc, `auto` | ✅ Works |
| max-height, max-width,min-height, min-width | `em`, `px`, `%`, `cm` etc, `auto` | 🚧 Planned |
| padding | `em`, `px`, `%`, `cm` etc | ✅ Works |
-| position | `absolute` | 🚧 Planned |
+| position | `absolute` | ✅ Works |
| position | `fixed` | 🚧 Planned |
| position | `relative` | ✅ Works |
| transform | | 🚧 Planned |
diff --git a/src/layout-box.ts b/src/layout-box.ts
index 91888b2..9c4e005 100644
--- a/src/layout-box.ts
+++ b/src/layout-box.ts
@@ -163,7 +163,10 @@ export abstract class Box extends TreeNode {
// 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
+ // 19: the only bit that also propagates out of an Inline, up to the
+ // containing block: Inline, FormattingBox <- Inline, FormattingBox
+ hasAbsoluteInCb: 1 << 19,
+ // 20..31: if you take them, remove them from PROPAGATES_TO_INLINE_BITS
};
/**
@@ -415,6 +418,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
@@ -555,7 +562,11 @@ export abstract class FormattingBox extends Box {
}
isOutOfFlow() {
- return this.style.float !== 'none'; // TODO: or position === 'absolute'
+ return this.style.float !== 'none' || this.style.position === 'absolute';
+ }
+
+ isAbsolute() {
+ return this.style.position === 'absolute';
}
propagate(parent: Box) {
@@ -564,11 +575,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 {
@@ -606,6 +625,10 @@ export class BoxArea {
return this.box.style.direction;
}
+ getWritingModeAsParticipant() {
+ return this.parent ? this.parent.getEstablishedWritingMode() : 'horizontal-tb';
+ }
+
get x() {
return this.lineLeft;
}
@@ -630,10 +653,49 @@ 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;
}
+ blockSizeForPotentiallyOrthogonal(box: FormattingBox) {
+ if (!this.parent) return this.blockSize; // root area
+ if (!this.box.isBlockContainer()) return this.blockSize; // cannot be orthogonal
+ const cb1 = this.box.getContainingBlock();
+ const cb2 = box.getContainingBlock();
+ if (
+ (this.box.getWritingModeAsParticipant(cb1) === 'horizontal-tb') !==
+ (box.getWritingModeAsParticipant(cb2) === 'horizontal-tb')
+ ) {
+ return this.inlineSize;
+ } else {
+ return this.blockSize;
+ }
+ }
+
inlineSizeForPotentiallyOrthogonal(box: FormattingBox) {
if (!this.parent) return this.inlineSize; // root area
if (!this.box.isBlockContainer()) return this.inlineSize; // cannot be orthogonal
@@ -649,6 +711,39 @@ export class BoxArea {
}
}
+ getLineLeftOfAreaAgainstSelf(area: BoxArea) {
+ const thisWm = this.getEstablishedWritingMode();
+ 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 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;
diff --git a/src/layout-flow.ts b/src/layout-flow.ts
index 217a1fb..0e2cf1c 100644
--- a/src/layout-flow.ts
+++ b/src/layout-flow.ts
@@ -82,7 +82,7 @@ const EMPTY_MAP = new Map();
export class BlockFormattingContext {
public inlineSize: number;
public fctx?: FloatContext;
- public stack: (BlockContainer | {post: BlockContainer})[];
+ public stack: (BlockContainer | {post: BlockContainer} | {abs: BlockLevel})[];
public cbBlockStart: number;
public cbLineLeft: number;
public cbLineRight: number;
@@ -111,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;
@@ -137,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;
@@ -151,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;
@@ -172,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);
@@ -206,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()};
}
@@ -225,13 +225,18 @@ export class BlockFormattingContext {
this.last = 'end';
}
- boxAtomic(layout: Layout, box: BlockLevel) {
+ boxOutOfFlow(box: BlockLevel) {
+ layoutStaticBox(box);
+ this.stack.push({abs: box});
+ }
+
+ 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);
@@ -264,14 +269,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(layout, ctx);
- this.positionBlockContainers();
+ if (box.isAbsolute()) {
+ insetBlockStart = box.style.getInsetBlockStart(containingBlock);
+ insetBlockEnd = box.style.getInsetBlockEnd(containingBlock);
+ }
- if (blockSize === 'auto') {
+ if (blockSize === 'auto' && !(insetBlockStart !== 'auto' && insetBlockEnd !== 'auto')) {
let lineboxHeight = 0;
if (box.isBlockContainerOfInlines()) {
lineboxHeight = box.getContentArea().blockSize;
@@ -279,9 +291,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();
@@ -292,6 +306,18 @@ export class BlockFormattingContext {
this.cbBlockStart += margin;
for (const item of this.stack) {
+ if ('abs' in item) {
+ // Out of flow: it takes a static position but no space, and its margins
+ // collapse with nothing
+ const level = sizeStack.length - 1;
+ const atMarginLevel = passedMarginLevel || this.margin.level === level;
+ const containingBlock = item.abs.getContainingBlock();
+ const {lineLeft} = item.abs.getMarginsAutoIsZero(containingBlock);
+ item.abs.setBlockPosition(sizeStack[level] + (atMarginLevel ? 0 : margin));
+ item.abs.setInlinePosition(lineLeft);
+ continue;
+ }
+
const box = 'post' in item ? item.post : item;
if ('post' in item) {
@@ -305,6 +331,8 @@ export class BlockFormattingContext {
box.setBlockSize(containingBlock, childSize);
}
+ if (!box.isBfcRoot()) finalizeBlockContainer(layout, box, ctx);
+
const blockSize = box.getBorderArea().blockSize;
sizeStack[level] += blockSize;
@@ -813,7 +841,6 @@ export abstract class BlockContainerBase extends FormattingBox {
super.propagate(parent);
if (this.isInlineLevel()) {
- // TODO: and not absolutely positioned
parent.bitfield |= Box.BITS.hasInlineBlocks;
}
}
@@ -955,7 +982,8 @@ export class BlockContainerOfInlines extends BlockContainerBase {
return inline.hasText()
|| inline.hasSizedInline()
|| inline.hasFloatOrReplaced()
- || inline.hasInlineBlocks();
+ || inline.hasInlineBlocks()
+ || inline.hasAbsoluteInCb();
}
doTextLayout(layout: Layout, ctx: LayoutContext) {
@@ -1095,13 +1123,17 @@ function layoutBlockBoxInner(
for (let i = box.treeStart + 1; i <= box.treeFinal; i++) {
const child = layout.tree[i];
if (!child.isFormattingBox()) throw new Error('Assertion failed');
- layoutBlockLevelBox(layout, child, cctx);
+ if (child.isAbsolute()) {
+ cctx.bfc!.boxOutOfFlow(child);
+ } else {
+ layoutBlockLevelBox(layout, child, cctx);
+ }
i = child.treeFinal;
}
}
if (establishedBfc) {
- establishedBfc.finalize(box);
+ establishedBfc.finalize(layout, box, ctx);
if (establishedBfc.fctx) {
if (box.loggingEnabled()) {
console.log('Left floats');
@@ -1113,7 +1145,7 @@ function layoutBlockBoxInner(
}
}
- containingBfc?.boxEnd(layout, box);
+ containingBfc?.boxEnd(layout, box, ctx);
}
function layoutBlockBox(
@@ -1137,7 +1169,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(
@@ -1239,6 +1271,246 @@ export function layoutFloatBox(
}
}
+function getShrinkToFitInlineSize(
+ layout: Layout,
+ box: BlockLevel,
+ available: number
+) {
+ const minContent = layoutContribution(layout, box, 'min-content');
+ const maxContent = layoutContribution(layout, box, 'max-content');
+ return Math.max(minContent, Math.min(maxContent, available));
+}
+
+// § 10.3.7
+function doInlineBoxModelForAbsoluteBox(
+ layout: Layout,
+ staticContainingBlock: BoxArea | null,
+ box: BlockLevel
+) {
+ const containingBlock = box.getContainingBlock();
+ const cInlineSize = containingBlock.inlineSizeForPotentiallyOrthogonal(box);
+ const ltr = box.getDirectionAsParticipant(containingBlock) === 'ltr';
+ const insetLineLeft = box.style.getInsetLineLeft(containingBlock);
+ const insetLineRight = box.style.getInsetLineRight(containingBlock);
+ const styleMarginLineLeft = box.style.getMarginLineLeft(containingBlock);
+ const styleMarginLineRight = box.style.getMarginLineRight(containingBlock);
+ const definiteInlineSize = box.getDefiniteOuterInlineSize(containingBlock);
+ let marginLineLeft = styleMarginLineLeft === 'auto' ? 0 : styleMarginLineLeft;
+ let marginLineRight = styleMarginLineRight === 'auto' ? 0 : styleMarginLineRight;
+ let sizedFromInsets = false;
+ let overConstrained = false;
+ let inlineSize;
+ let lineLeft;
+
+ // 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()) {
+ inlineSize = box.getIntrinsicIsize();
+ } else if (insetLineLeft !== 'auto' && insetLineRight !== 'auto') {
+ // Paragraph 3: both insets are given, so the size is what they leave behind
+ // and auto margins are zero
+ inlineSize = Math.max(0, cInlineSize
+ - insetLineLeft
+ - insetLineRight
+ - marginLineLeft
+ - marginLineRight);
+ sizedFromInsets = true;
+ } else {
+ // Paragraphs 1 and 5: shrink-to-fit over the space the insets leave behind
+ const available = cInlineSize
+ - (insetLineLeft === 'auto' ? 0 : insetLineLeft)
+ - (insetLineRight === 'auto' ? 0 : insetLineRight);
+ inlineSize = getShrinkToFitInlineSize(layout, box, available)
+ - marginLineLeft
+ - 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
+ const rest = cInlineSize - insetLineLeft - insetLineRight - inlineSize;
+
+ if (styleMarginLineLeft === 'auto' && styleMarginLineRight === 'auto') {
+ if (rest < 0) {
+ // Equal margins would be negative, so the line-start one is dropped
+ marginLineLeft = ltr ? 0 : rest;
+ marginLineRight = ltr ? rest : 0;
+ } else {
+ marginLineLeft = marginLineRight = rest / 2;
+ }
+ } else if (styleMarginLineLeft === 'auto') {
+ marginLineLeft = rest - marginLineRight;
+ } else if (styleMarginLineRight === 'auto') {
+ marginLineRight = rest - marginLineLeft;
+ }
+
+ // Otherwise the values are over-constrained
+ overConstrained = styleMarginLineLeft !== 'auto' && styleMarginLineRight !== 'auto';
+ }
+
+ if (insetLineLeft === 'auto' && insetLineRight === 'auto') {
+ // solve:
+ // left = auto, width = auto, right = auto
+ // left = auto, width = px , right = auto
+ lineLeft = box.getBorderArea().lineLeft; // against the static cb
+ let area = staticContainingBlock;
+ while (area && area !== containingBlock) {
+ lineLeft += containingBlock.getLineLeftOfAreaAgainstSelf(area);
+ area = area.parent;
+ }
+ if (!ltr) lineLeft -= inlineSize;
+ } else if (insetLineLeft !== 'auto' && (insetLineRight === 'auto' || ltr || !overConstrained)) {
+ // solve:
+ // left = px , width = auto, right = auto
+ // left = px , width = px , right = auto
+ // left = px , width = auto, right = px
+ // left = px , width = px , right = px , unless over-constrained in rtl
+ lineLeft = insetLineLeft + marginLineLeft;
+ } else if (insetLineRight !== 'auto') {
+ // solve:
+ // left = auto, width = auto, right = px
+ // left = auto, width = px , right = px
+ // left = px , width = px , right = px , over-constrained in rtl
+ lineLeft = cInlineSize - insetLineRight - marginLineRight - inlineSize;
+ } else {
+ throw new Error('Assertion failed');
+ }
+
+ box.setInlineOuterSize(containingBlock, inlineSize);
+ box.setInlinePosition(lineLeft);
+}
+
+// § 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 blockSize = box.getDefiniteInnerBlockSize(containingBlock);
+
+ if (
+ blockSize === undefined &&
+ insetBlockStart !== 'auto' &&
+ insetBlockEnd !== 'auto'
+ ) {
+ // Paragraph 5: an auto size is what the two insets leave behind
+ const borderBlockStartWidth = box.style.getBorderBlockStartWidth(containingBlock);
+ const paddingBlockStart = box.style.getPaddingBlockStart(containingBlock);
+ const paddingBlockEnd = box.style.getPaddingBlockEnd(containingBlock);
+ const borderBlockEndWidth = box.style.getBorderBlockEndWidth(containingBlock);
+
+ blockSize = Math.max(0, cBlockSize
+ - insetBlockStart
+ - insetBlockEnd
+ - (marginBlockStart === 'auto' ? 0 : marginBlockStart)
+ - (marginBlockEnd === 'auto' ? 0 : marginBlockEnd)
+ - borderBlockStartWidth
+ - paddingBlockStart
+ - paddingBlockEnd
+ - borderBlockEndWidth);
+ }
+
+ if (blockSize !== undefined) {
+ box.setBlockSize(containingBlock, blockSize);
+ }
+
+ 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 (marginBlockStart === 'auto' && marginBlockEnd === 'auto') {
+ // Paragraph 6: equal margins center the box, negative values included
+ usedMarginBlockStart = usedMarginBlockEnd = 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 + usedMarginBlockStart);
+ } else if (insetBlockEnd !== 'auto') {
+ box.setBlockPosition(cBlockSize - insetBlockEnd - usedMarginBlockEnd - outerBlockSize);
+ } else {
+ 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,
+ staticContainingBlock: BoxArea | null,
+ box: BlockLevel,
+ ctx: LayoutContext
+) {
+ const cctx: LayoutContext = {...ctx, bfc: undefined};
+
+ // NB: fillAreas was called in layoutStaticBox
+ doInlineBoxModelForAbsoluteBox(layout, staticContainingBlock, box);
+ doBlockBoxModelForAbsoluteBox(staticContainingBlock, box);
+
+ // A replaced box has no inner layout: the box model above sized it
+ if (box.isBlockContainer()) layoutBlockBoxInner(layout, box, cctx);
+}
+
+function finalizeBlockContainer(
+ layout: Layout,
+ box: BlockContainer,
+ ctx: LayoutContext
+) {
+ if (box.isAbsoluteContainingBlock()) {
+ const staticParents: FormattingBox[] = [];
+
+ for (let i = box.treeStart + 1; i <= box.treeFinal; i++) {
+ const item = layout.tree[i];
+
+ if (item.isFormattingBox()) {
+ if (!item.isPositioned()) staticParents.push(item);
+ if (item.isAbsolute()) {
+ const staticParent = staticParents[staticParents.length - 1];
+ const staticContainingBlock = staticParent?.getContentArea() ?? null;
+ layoutAbsoluteBox(layout, staticContainingBlock, item, ctx);
+ }
+ }
+
+ // A positioned descendant finalizes its own absolutes
+ if (item.isBox() && item.isPositioned()) i = item.treeFinal;
+
+ while (
+ staticParents.length &&
+ i >= staticParents[staticParents.length - 1].treeFinal
+ ) staticParents.pop();
+ }
+ }
+}
+
+export function layoutStaticBox(box: FormattingBox) {
+ const containingBlock = box.getContainingBlock();
+ box.fillAreas(containingBlock);
+}
+
export class Break extends TreeNode {
public className = 'break';
@@ -1296,6 +1568,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;
}
}
@@ -1561,12 +1835,8 @@ export function inlineIteratorStateNext(state: InlineIteratorState) {
} else if (item.isBreak()) {
state.buffered.push({state: 'break', index: state.index});
} else {
- if (item.isFloat()) {
- state.buffered.push({state: 'box', item});
- } else {
- state.buffered.push({state: 'box', item});
- state.isInlineBlock = true;
- }
+ state.buffered.push({state: 'box', item});
+ if (!item.isOutOfFlow()) state.isInlineBlock = true;
state.index = item.treeFinal;
}
}
@@ -1830,6 +2100,7 @@ export function generateBlockContainer(tree: InlineLevel[], el: HTMLElement) {
// generatesBreak, etc
if (
el.style.float !== 'none' ||
+ el.style.position === 'absolute' ||
el.style.overflow === 'hidden' ||
el.style.display.inner === 'flow-root' ||
el.parent && writingModeInlineAxis(el) !== writingModeInlineAxis(el.parent)
@@ -1862,7 +2133,15 @@ export function generateBlockContainer(tree: InlineLevel[], el: HTMLElement) {
preBcInlineChild(tree, ctx);
tree.push(new Break(child.style));
} else if (child.style.display.outer === 'block') {
- if (child.style.isOutOfFlow()) {
+ if (child.style.position === 'absolute') {
+ // Joins whichever formatting context is open: the BFC positions it
+ // among blocks, the IFC on the line it would have been on
+ if (ctx.ifcIndex > -1) {
+ preBcInlineChild(tree, ctx);
+ } else {
+ preBcBlockChild(tree, ctx);
+ }
+ } else if (child.style.isOutOfFlow()) {
preBcInlineChild(tree, ctx);
} else {
preBcBlockChild(tree, ctx);
diff --git a/src/layout-text.ts b/src/layout-text.ts
index cd22443..a6359db 100644
--- a/src/layout-text.ts
+++ b/src/layout-text.ts
@@ -6,6 +6,7 @@ import {
IfcVacancy,
Inline,
layoutFloatBox,
+ layoutStaticBox,
layoutContribution,
createInlineIteratorState,
inlineIteratorStateNext,
@@ -2030,8 +2031,9 @@ function createMarkIterator(
inlineIteratorStateNext(inline);
}
- // Consume floats
- if (inline.value?.state === 'box' && inline.value.item.isFloat() && inlineMark === mark.position) {
+ // Consume out-of-flow boxes: floats, which the line has to flow around, and
+ // absolutely positioned boxes, which only need the line they landed on
+ if (inline.value?.state === 'box' && inline.value.item.isOutOfFlow() && inlineMark === mark.position) {
mark.box = inline.value.item;
inlineIteratorStateNext(inline);
return {done: false, value: mark};
@@ -2270,6 +2272,7 @@ class InlineFormattingContext {
lineHasWord: boolean;
/** True when we should append the line */
lineIsDirty: boolean;
+ lineHasAbsolutes: boolean;
/** Inlines to be fragmented; shared across finishLine calls */
inlines: Inline[];
@@ -2296,6 +2299,7 @@ class InlineFormattingContext {
this.blockOffset = this.bfc.cbBlockStart;
this.lineHasWord = false;
this.lineIsDirty = false;
+ this.lineHasAbsolutes = false;
this.inlines = [];
}
}
@@ -2531,9 +2535,10 @@ function positionPhysicalLineItems(
}
} else {
const box = layout.tree[item.treeIndex];
- if (box.isFormattingBox() && !box.isOutOfFlow()) {
+ if (box.isFormattingBox() && !box.isFloat()) {
const {lineLeft} = box.getMarginsAutoIsZero(containingBlock);
box.setInlinePosition(x + lineLeft);
+ if (box.isAbsolute()) box.setBlockPosition(line.blockOffset);
}
}
x += item.inlineSpace + item.endSpace;
@@ -2547,9 +2552,10 @@ function positionPhysicalLineItems(
ifc.block.items[item.itemIndex].x = x;
} else {
const box = layout.tree[item.treeIndex];
- if (box.isFormattingBox() && !box.isOutOfFlow()) {
+ if (box.isFormattingBox() && !box.isFloat()) {
const {lineLeft} = box.getMarginsAutoIsZero(containingBlock);
box.setInlinePosition(x - lineLeft);
+ if (box.isAbsolute()) box.setBlockPosition(line.blockOffset);
}
}
x -= item.endSpace;
@@ -2920,6 +2926,13 @@ export function createIfcLineboxes(
}
}
+ if (mark.box?.isAbsolute()) {
+ // Rides the line with no width, to learn its static position (§ 10.3.7)
+ layoutStaticBox(mark.box);
+ ifc.candidates.addBox(mark.box.treeStart, mark.box.treeFinal, 0);
+ ifc.lineHasAbsolutes = true;
+ }
+
if (mark.inlinePost) {
const inlineSpace = mark.inlinePost.getInlineEndSize(containingBlock);
if (inlineSpace > 0) ifc.candidates.width.addInk(inlineSpace);
@@ -3060,12 +3073,17 @@ export function createIfcLineboxes(
// There could have been floats after the paragraph's final line break
bfc.getLocalVacancyForLine(bfc, ifc.blockOffset, ifc.line.height(), ifc.vacancy);
finishLine(ifc, ifc.line, true);
- } else if (ifc.candidates.width.hasContent()) {
+ } else if (ifc.candidates.width.hasContent() || ifc.lineHasAbsolutes) {
// We never hit a break opportunity because there is no non-whitespace
// text and no inline-blocks, but there is some content on spans (border,
- // padding, or margin). Add everything.
+ // padding, or margin), or an absolute still needs a static position.
+ // Add everything.
+ const forAbsolutesOnly = !ifc.candidates.width.hasContent();
+ const blockOffset = ifc.blockOffset;
ifc.line.concat(ifc.candidates);
finishLine(ifc, ifc.line, true);
+ // Not a line box, so it adds no height
+ if (forAbsolutesOnly) ifc.blockOffset = blockOffset;
} else {
bfc.fctx?.consumeMisfits();
}
diff --git a/src/style.ts b/src/style.ts
index 83b704b..079f072 100644
--- a/src/style.ts
+++ b/src/style.ts
@@ -35,6 +35,10 @@ const LogicalMaps = Object.freeze({
borderLineRightStyle: 'borderRightStyle',
borderInlineStartStyle: Object.freeze({ltr: 'borderLeftStyle', rtl: 'borderRightStyle'}),
borderInlineEndStyle: Object.freeze({ltr: 'borderRightStyle', rtl: 'borderLeftStyle'}),
+ insetBlockStart: 'top',
+ insetBlockEnd: 'bottom',
+ insetLineLeft: 'left',
+ insetLineRight: 'right',
blockSize: 'height',
inlineSize: 'width'
}),
@@ -63,6 +67,10 @@ const LogicalMaps = Object.freeze({
borderLineRightStyle: 'borderBottomStyle',
borderInlineStartStyle: Object.freeze({ltr: 'borderTopStyle', rtl: 'borderBottomStyle'}),
borderInlineEndStyle: Object.freeze({ltr: 'borderBottomStyle', rtl: 'borderTopStyle'}),
+ insetBlockStart: 'left',
+ insetBlockEnd: 'right',
+ insetLineLeft: 'top',
+ insetLineRight: 'bottom',
blockSize: 'width',
inlineSize: 'height'
}),
@@ -91,6 +99,10 @@ const LogicalMaps = Object.freeze({
borderLineRightStyle: 'borderBottomStyle',
borderInlineStartStyle: Object.freeze({ltr: 'borderTopStyle', rtl: 'borderBottomStyle'}),
borderInlineEndStyle: Object.freeze({ltr: 'borderBottomStyle', rtl: 'borderTopStyle'}),
+ insetBlockStart: 'right',
+ insetBlockEnd: 'left',
+ insetLineLeft: 'top',
+ insetLineRight: 'bottom',
blockSize: 'width',
inlineSize: 'height'
})
@@ -503,7 +515,7 @@ export class Style {
}
isOutOfFlow() {
- return this.float !== 'none'; // TODO: or this.position === 'absolute'
+ return this.float !== 'none' || this.position === 'absolute';
}
isWsCollapsible() {
@@ -669,6 +681,44 @@ export class Style {
return resolvePercent(containingBlock, cssWidthVal);
}
+ /**
+ * `top`, `right`, `bottom` and `left` are physical, but layout is logical, so
+ * they get mapped like the rest of the box model. Percentages on the line
+ * axis resolve against the containing block's inline size and percentages on
+ * the block axis against its block size (CSS 2.2 § 10.3.7, § 10.6.4).
+ */
+ private getInset(
+ key: 'insetBlockStart' | 'insetBlockEnd' | 'insetLineLeft' | 'insetLineRight',
+ containingBlock: BoxArea
+ ) {
+ const writingMode = containingBlock.box.style.writingMode;
+ const map = LogicalMaps[writingMode];
+ const cssVal = this[map[key]];
+ if (cssVal === 'auto') return cssVal;
+ if (typeof cssVal === 'object') {
+ const isBlockAxis = key === 'insetBlockStart' || key === 'insetBlockEnd';
+ const size = containingBlock[isBlockAxis ? map.blockSize : map.inlineSize];
+ return cssVal.value / 100 * size;
+ }
+ return cssVal;
+ }
+
+ getInsetBlockStart(containingBlock: BoxArea) {
+ return this.getInset('insetBlockStart', containingBlock);
+ }
+
+ getInsetBlockEnd(containingBlock: BoxArea) {
+ return this.getInset('insetBlockEnd', containingBlock);
+ }
+
+ getInsetLineLeft(containingBlock: BoxArea) {
+ return this.getInset('insetLineLeft', containingBlock);
+ }
+
+ getInsetLineRight(containingBlock: BoxArea) {
+ return this.getInset('insetLineRight', containingBlock);
+ }
+
getBlockSize(containingBlock: BoxArea) {
const writingMode = containingBlock.box.style.writingMode;
let cssVal = this[LogicalMaps[writingMode].blockSize];
@@ -1097,9 +1147,10 @@ function computeStyle(parentStyle: Style, cascadedStyle: DeclaredStyle) {
const style = new Style(computed, parentStyle, cascadedStyle);
- // Blockify floats (TODO: abspos too) (CSS Display §2.7). This drives what
+ // Blockify floats and absolutely positioned boxes (CSS Display §2.7). This
+ // drives what
// type of box is created (-> not an inline), but otherwise has no effect.
- if (computed.float !== 'none') style.blockify();
+ if (computed.float !== 'none' || computed.position === 'absolute') style.blockify();
return style;
}
diff --git a/test/ci.js b/test/ci.js
index cbdaf77..50ddd70 100644
--- a/test/ci.js
+++ b/test/ci.js
@@ -8,6 +8,7 @@ import './api.spec.js';
import './cascade.spec.js';
import './css.spec.js';
import './flow.spec.js';
+import './position-absolute.spec.js';
import './text.spec.js';
import './font.spec.js';
import './itemize.spec.js';
diff --git a/test/position-absolute.spec.js b/test/position-absolute.spec.js
new file mode 100644
index 0000000..e99b1cd
--- /dev/null
+++ b/test/position-absolute.spec.js
@@ -0,0 +1,455 @@
+import '#register-default-environment';
+import {expect} from 'chai';
+import * as flow from 'dropflow';
+import parse from 'dropflow/parse.js';
+import {registerFontAsset, unregisterFontAsset} from '../assets/register.ts';
+import PaintSpy from './paint-spy.js';
+import paint from '../src/paint.ts';
+
+const adaUrl = new URL(import.meta.resolve('#assets/images/ada.png'));
+
+describe('Absolute positioning', function () {
+ before(function () {
+ registerFontAsset('Arimo/Arimo-Regular.ttf');
+ this.reflow = function (html, width = 300, height = 500) {
+ this.rootElement = parse(html);
+ flow.loadSync(this.rootElement);
+ this.layout = flow.layout(this.rootElement);
+ flow.reflow(this.layout, width, height);
+ this.get = selector => this.rootElement.query(selector)?.boxes[0];
+ this.border = selector => {
+ const box = this.get(selector);
+ if (!box) throw new Error(`no box for ${selector}`);
+ const area = box.getBorderArea();
+ return {x: area.x, y: area.y, width: area.width, height: area.height};
+ };
+ };
+ });
+
+ after(function () {
+ unregisterFontAsset('Arimo/Arimo-Regular.ttf');
+ });
+
+ it('places the border box against the containing block padding box', function () {
+ this.reflow(`
+