dropflow
Version:
A small CSS2 document renderer built from specifications
1,261 lines • 61.8 kB
JavaScript
import { binarySearch } from './util.js';
import { HTMLElement, TextNode } from './dom.js';
import { createStyle, EMPTY_STYLE } from './style.js';
import { EmptyInlineMetrics, Run, collapseWhitespace, createEmptyParagraph, createParagraph, getFontMetrics } from './layout-text.js';
import { Box, BoxArea, RenderItem } from './layout-box.js';
function assumePx(v) {
if (typeof v !== 'number') {
throw new TypeError('The value accessed here has not been reduced to a used value in a ' +
'context where a used value is expected. Make sure to perform any ' +
'needed layouts.');
}
}
function writingModeInlineAxis(el) {
if (el.style.writingMode === 'horizontal-tb') {
return 'horizontal';
}
else {
return 'vertical';
}
}
class MarginCollapseCollection {
positive;
negative;
constructor(initialMargin = 0) {
this.positive = 0;
this.negative = 0;
this.add(initialMargin);
}
add(margin) {
if (margin < 0) {
this.negative = Math.max(this.negative, -margin);
}
else {
this.positive = Math.max(this.positive, margin);
}
return this;
}
get() {
return this.positive - this.negative;
}
clone() {
const c = new MarginCollapseCollection();
c.positive = this.positive;
c.negative = this.negative;
return c;
}
}
const EMPTY_MAP = new Map();
export class BlockFormattingContext {
inlineSize;
fctx;
stack;
cbBlockStart;
cbLineLeft;
cbLineRight;
sizeStack;
offsetStack;
last;
level;
hypotheticals;
margin;
constructor(inlineSize) {
this.inlineSize = inlineSize;
this.stack = [];
this.cbBlockStart = 0;
this.cbLineLeft = 0;
this.cbLineRight = 0;
this.sizeStack = [0];
this.offsetStack = [0];
this.last = null;
this.level = 0;
this.margin = { level: 0, collection: new MarginCollapseCollection() };
this.hypotheticals = EMPTY_MAP;
}
boxStart(box, ctx) {
const { lineLeft, lineRight, blockStart } = box.getContainingBlockToContent();
const paddingBlockStart = box.style.getPaddingBlockStart(box);
const borderBlockStartWidth = box.style.getBorderBlockStartWidth(box);
const marginBlockStart = box.style.getMarginBlockStart(box);
let floatBottom = 0;
let clearance = 0;
assumePx(marginBlockStart);
if (this.fctx && (box.style.clear === 'left' || box.style.clear === 'both')) {
floatBottom = Math.max(floatBottom, this.fctx.getLeftBottom());
}
if (this.fctx && (box.style.clear === 'right' || box.style.clear === 'both')) {
floatBottom = Math.max(floatBottom, this.fctx.getRightBottom());
}
if (box.style.clear !== 'none') {
const hypo = this.margin.collection.clone().add(marginBlockStart).get();
clearance = Math.max(clearance, floatBottom - (this.cbBlockStart + hypo));
}
const adjoinsPrevious = clearance === 0;
const adjoinsNext = paddingBlockStart === 0 && borderBlockStartWidth === 0;
if (adjoinsPrevious) {
this.margin.collection.add(marginBlockStart);
}
else {
this.positionBlockContainers();
const c = floatBottom - this.cbBlockStart;
this.margin = { level: this.level, collection: new MarginCollapseCollection(c) };
if (box.canCollapseThrough())
this.margin.clearanceAtLevel = this.level;
}
this.last = 'start';
this.level += 1;
this.cbLineLeft += lineLeft;
this.cbLineRight += lineRight;
this.stack.push(box);
if (box.isBlockContainerOfInlines()) {
this.cbBlockStart += blockStart + this.margin.collection.get();
}
this.fctx?.boxStart();
if (box.isBlockContainerOfInlines()) {
box.doTextLayout(ctx);
this.cbBlockStart -= blockStart + this.margin.collection.get();
}
if (!adjoinsNext) {
this.positionBlockContainers();
this.margin = { level: this.level, collection: new MarginCollapseCollection() };
}
}
boxEnd(box) {
const { lineLeft, lineRight } = box.getContainingBlockToContent();
const paddingBlockEnd = box.style.getPaddingBlockEnd(box);
const borderBlockEndWidth = box.style.getBorderBlockEndWidth(box);
const marginBlockEnd = box.style.getMarginBlockEnd(box);
let adjoins = paddingBlockEnd === 0
&& borderBlockEndWidth === 0
&& (this.margin.clearanceAtLevel == null || this.level > this.margin.clearanceAtLevel);
assumePx(marginBlockEnd);
if (adjoins) {
if (this.last === 'start') {
adjoins = box.canCollapseThrough();
}
else {
const blockSize = box.style.getBlockSize(box);
// Handle the end of a block box that was at the end of its parent
adjoins = blockSize === 'auto';
}
}
this.stack.push({ post: box });
this.level -= 1;
this.cbLineLeft -= lineLeft;
this.cbLineRight -= lineRight;
if (!adjoins) {
this.positionBlockContainers();
this.margin = { level: this.level, collection: new MarginCollapseCollection() };
}
// Collapsing through - need to find the hypothetical position
if (this.last === 'start') {
if (this.hypotheticals === EMPTY_MAP)
this.hypotheticals = new Map();
this.hypotheticals.set(box, this.margin.collection.get());
}
this.margin.collection.add(marginBlockEnd);
// When a box's end adjoins to the previous margin, move the "root" (the
// box which the margin will be placed adjacent to) to the highest-up box
// in the tree, since its siblings need to be shifted.
if (this.level < this.margin.level)
this.margin.level = this.level;
this.last = 'end';
}
getLocalVacancyForLine(bfc, blockOffset, blockSize, vacancy) {
let leftInlineSpace = 0;
let rightInlineSpace = 0;
if (this.fctx) {
leftInlineSpace = this.fctx.leftFloats.getOccupiedSpace(blockOffset, blockSize, -this.cbLineLeft);
rightInlineSpace = this.fctx.rightFloats.getOccupiedSpace(blockOffset, blockSize, -this.cbLineRight);
}
vacancy.leftOffset = this.cbLineLeft + leftInlineSpace;
vacancy.rightOffset = this.cbLineRight + rightInlineSpace;
vacancy.inlineSize = this.inlineSize - vacancy.leftOffset - vacancy.rightOffset;
vacancy.blockOffset = blockOffset - bfc.cbBlockStart;
vacancy.leftOffset -= bfc.cbLineLeft;
vacancy.rightOffset -= bfc.cbLineRight;
}
ensureFloatContext(blockOffset) {
return this.fctx || (this.fctx = new FloatContext(this, blockOffset));
}
finalize(box) {
if (!box.isBfcRoot())
throw new Error('This is for bfc roots only');
const blockSize = box.style.getBlockSize(box);
this.positionBlockContainers();
if (blockSize === 'auto') {
let lineboxHeight = 0;
if (box.isBlockContainerOfInlines()) {
lineboxHeight = box.contentArea.blockSize;
}
box.setBlockSize(Math.max(lineboxHeight, this.cbBlockStart, this.fctx?.getBothBottom() ?? 0));
}
}
positionBlockContainers() {
const sizeStack = this.sizeStack;
const offsetStack = this.offsetStack;
const margin = this.margin.collection.get();
let passedMarginLevel = this.margin.level === offsetStack.length - 1;
let levelNeedsPostOffset = offsetStack.length - 1;
sizeStack[this.margin.level] += margin;
this.cbBlockStart += margin;
for (const item of this.stack) {
const box = 'post' in item ? item.post : item;
if ('post' in item) {
const childSize = sizeStack.pop();
const offset = offsetStack.pop();
const level = sizeStack.length - 1;
const sBlockSize = box.style.getBlockSize(box);
if (sBlockSize === 'auto' && box.isBlockContainerOfBlockContainers() && !box.isBfcRoot()) {
box.setBlockSize(childSize);
}
const blockSize = box.borderArea.blockSize;
sizeStack[level] += blockSize;
this.cbBlockStart = offset + blockSize;
// Each time we go beneath a level that was created by the previous
// positionBlockContainers(), we have to put the margin on the "after"
// side of the block container. ("before" sides are covered at the top)
// ][[]]
if (level < levelNeedsPostOffset) {
--levelNeedsPostOffset;
this.cbBlockStart += margin;
}
}
else {
const hypothetical = this.hypotheticals.get(box);
const level = sizeStack.length - 1;
let blockOffset = sizeStack[level];
if (!passedMarginLevel) {
passedMarginLevel = this.margin.level === level;
}
if (!passedMarginLevel) {
blockOffset += margin;
}
if (hypothetical !== undefined) {
blockOffset -= margin - hypothetical;
}
box.setBlockPosition(blockOffset);
sizeStack.push(0);
offsetStack.push(this.cbBlockStart);
}
}
this.stack = [];
}
}
class FloatSide {
items;
// Moving shelf area (stretches to infinity in the block direction)
shelfBlockOffset;
shelfTrackIndex;
// Tracks
blockOffsets;
inlineSizes;
inlineOffsets;
floatCounts;
constructor(blockOffset) {
this.items = [];
this.shelfBlockOffset = blockOffset;
this.shelfTrackIndex = 0;
this.blockOffsets = [blockOffset];
this.inlineSizes = [0];
this.inlineOffsets = [0];
this.floatCounts = [0];
}
initialize(blockOffset) {
this.shelfBlockOffset = blockOffset;
this.blockOffsets = [blockOffset];
}
repr() {
let row1 = '', row2 = '';
for (let i = 0; i < this.blockOffsets.length; ++i) {
const blockOffset = this.blockOffsets[i];
const inlineOffset = this.inlineOffsets[i];
const size = this.inlineSizes[i];
const count = this.floatCounts[i];
const cell1 = `${blockOffset}`;
const cell2 = `| O:${inlineOffset} S:${size} N:${count} `;
const colSize = Math.max(cell1.length, cell2.length);
row1 += cell1 + ' '.repeat(colSize - cell1.length);
row2 += ' '.repeat(colSize - cell2.length) + cell2;
}
row1 += 'Inf';
row2 += '|';
return row1 + '\n' + row2;
}
getSizeOfTracks(start, end, inlineOffset) {
let max = 0;
for (let i = start; i < end; ++i) {
if (this.floatCounts[i] > 0) {
max = Math.max(max, inlineOffset + this.inlineSizes[i] + this.inlineOffsets[i]);
}
}
return max;
}
getOverflow() {
return this.getSizeOfTracks(0, this.inlineSizes.length, 0);
}
getFloatCountOfTracks(start, end) {
let max = 0;
for (let i = start; i < end; ++i)
max = Math.max(max, this.floatCounts[i]);
return max;
}
getEndTrack(start, blockOffset, blockSize) {
const blockPosition = blockOffset + blockSize;
let end = start + 1;
while (end < this.blockOffsets.length && this.blockOffsets[end] < blockPosition)
end++;
return end;
}
getTrackRange(blockOffset, blockSize = 0) {
let start = binarySearch(this.blockOffsets, blockOffset);
if (this.blockOffsets[start] !== blockOffset)
start -= 1;
return [start, this.getEndTrack(start, blockOffset, blockSize)];
}
getOccupiedSpace(blockOffset, blockSize, inlineOffset) {
if (this.items.length === 0)
return 0;
const [start, end] = this.getTrackRange(blockOffset, blockSize);
return this.getSizeOfTracks(start, end, inlineOffset);
}
boxStart(blockOffset) {
// This seems to violate rule 5 for blocks if the boxStart block has a
// negative margin, but it's what browsers do 🤷♂️
this.shelfBlockOffset = blockOffset;
[this.shelfTrackIndex] = this.getTrackRange(this.shelfBlockOffset);
}
dropShelf(blockOffset) {
if (blockOffset > this.shelfBlockOffset) {
this.shelfBlockOffset = blockOffset;
[this.shelfTrackIndex] = this.getTrackRange(this.shelfBlockOffset);
}
}
getNextTrackOffset() {
if (this.shelfTrackIndex + 1 < this.blockOffsets.length) {
return this.blockOffsets[this.shelfTrackIndex + 1];
}
else {
return this.blockOffsets[this.shelfTrackIndex];
}
}
getBottom() {
return this.blockOffsets[this.blockOffsets.length - 1];
}
splitTrack(trackIndex, blockOffset) {
const size = this.inlineSizes[trackIndex];
const offset = this.inlineOffsets[trackIndex];
const count = this.floatCounts[trackIndex];
this.blockOffsets.splice(trackIndex + 1, 0, blockOffset);
this.inlineSizes.splice(trackIndex, 0, size);
this.inlineOffsets.splice(trackIndex, 0, offset);
this.floatCounts.splice(trackIndex, 0, count);
}
splitIfShelfDropped() {
if (this.blockOffsets[this.shelfTrackIndex] !== this.shelfBlockOffset) {
this.splitTrack(this.shelfTrackIndex, this.shelfBlockOffset);
this.shelfTrackIndex += 1;
}
}
placeFloat(box, vacancy, cbLineLeft, cbLineRight) {
if (box.style.float === 'none') {
throw new Error('Tried to place float:none');
}
if (vacancy.blockOffset !== this.shelfBlockOffset) {
throw new Error('Assertion failed');
}
this.splitIfShelfDropped();
const startTrack = this.shelfTrackIndex;
const margins = box.getMarginsAutoIsZero();
const blockSize = box.borderArea.height + margins.blockStart + margins.blockEnd;
const blockEndOffset = this.shelfBlockOffset + blockSize;
let endTrack;
if (blockSize > 0) {
endTrack = this.getEndTrack(startTrack, this.shelfBlockOffset, blockSize);
if (this.blockOffsets[endTrack] !== blockEndOffset) {
this.splitTrack(endTrack - 1, blockEndOffset);
}
}
else {
endTrack = startTrack;
}
const cbOffset = box.style.float === 'left' ? vacancy.leftOffset : vacancy.rightOffset;
const cbLineSide = box.style.float === 'left' ? cbLineLeft : cbLineRight;
const marginOffset = box.style.float === 'left' ? margins.lineLeft : margins.lineRight;
const marginEnd = box.style.float === 'left' ? margins.lineRight : margins.lineLeft;
if (box.style.float === 'left') {
box.setInlinePosition(cbOffset - cbLineSide + marginOffset);
}
else {
const inlineSize = box.containingBlock.inlineSize;
const size = box.borderArea.inlineSize;
box.setInlinePosition(cbOffset - cbLineSide + inlineSize - marginOffset - size);
}
for (let track = startTrack; track < endTrack; track += 1) {
if (this.floatCounts[track] === 0) {
this.inlineOffsets[track] = cbOffset;
this.inlineSizes[track] = marginOffset + box.borderArea.width + marginEnd;
}
else {
this.inlineSizes[track] = cbOffset - this.inlineOffsets[track] + marginOffset + box.borderArea.width + marginEnd;
}
this.floatCounts[track] += 1;
}
this.items.push(box);
}
}
export class IfcVacancy {
leftOffset;
rightOffset;
inlineSize;
blockOffset;
leftFloatCount;
rightFloatCount;
static EPSILON = 1 / 64;
constructor(leftOffset, rightOffset, blockOffset, inlineSize, leftFloatCount, rightFloatCount) {
this.leftOffset = leftOffset;
this.rightOffset = rightOffset;
this.blockOffset = blockOffset;
this.inlineSize = inlineSize;
this.leftFloatCount = leftFloatCount;
this.rightFloatCount = rightFloatCount;
}
fits(inlineSize) {
return inlineSize - this.inlineSize < IfcVacancy.EPSILON;
}
hasFloats() {
return this.leftFloatCount > 0 || this.rightFloatCount > 0;
}
}
;
export class FloatContext {
bfc;
leftFloats;
rightFloats;
misfits;
constructor(bfc, blockOffset) {
this.bfc = bfc;
this.leftFloats = new FloatSide(blockOffset);
this.rightFloats = new FloatSide(blockOffset);
this.misfits = [];
}
boxStart() {
this.leftFloats.boxStart(this.bfc.cbBlockStart);
this.rightFloats.boxStart(this.bfc.cbBlockStart);
}
getVacancyForLine(blockOffset, blockSize) {
const leftInlineSpace = this.leftFloats.getOccupiedSpace(blockOffset, blockSize, -this.bfc.cbLineLeft);
const rightInlineSpace = this.rightFloats.getOccupiedSpace(blockOffset, blockSize, -this.bfc.cbLineRight);
const leftOffset = this.bfc.cbLineLeft + leftInlineSpace;
const rightOffset = this.bfc.cbLineRight + rightInlineSpace;
const inlineSize = this.bfc.inlineSize - leftOffset - rightOffset;
return new IfcVacancy(leftOffset, rightOffset, blockOffset, inlineSize, 0, 0);
}
getVacancyForBox(box, lineWidth) {
const float = box.style.float;
const floats = float === 'left' ? this.leftFloats : this.rightFloats;
const oppositeFloats = float === 'left' ? this.rightFloats : this.leftFloats;
const inlineOffset = float === 'left' ? -this.bfc.cbLineLeft : -this.bfc.cbLineRight;
const oppositeInlineOffset = float === 'left' ? -this.bfc.cbLineRight : -this.bfc.cbLineLeft;
const blockOffset = floats.shelfBlockOffset;
const blockSize = box.borderArea.height;
const startTrack = floats.shelfTrackIndex;
const endTrack = floats.getEndTrack(startTrack, blockOffset, blockSize);
const inlineSpace = floats.getSizeOfTracks(startTrack, endTrack, inlineOffset);
const [oppositeStartTrack, oppositeEndTrack] = oppositeFloats.getTrackRange(blockOffset, blockSize);
const oppositeInlineSpace = oppositeFloats.getSizeOfTracks(oppositeStartTrack, oppositeEndTrack, oppositeInlineOffset);
const leftOffset = this.bfc.cbLineLeft + (float === 'left' ? inlineSpace : oppositeInlineSpace);
const rightOffset = this.bfc.cbLineRight + (float === 'right' ? inlineSpace : oppositeInlineSpace);
const inlineSize = this.bfc.inlineSize - leftOffset - rightOffset - lineWidth;
const floatCount = floats.getFloatCountOfTracks(startTrack, endTrack);
const oppositeFloatCount = oppositeFloats.getFloatCountOfTracks(oppositeStartTrack, oppositeEndTrack);
const leftFloatCount = float === 'left' ? floatCount : oppositeFloatCount;
const rightFloatCount = float === 'left' ? oppositeFloatCount : floatCount;
return new IfcVacancy(leftOffset, rightOffset, blockOffset, inlineSize, leftFloatCount, rightFloatCount);
}
getLeftBottom() {
return this.leftFloats.getBottom();
}
getRightBottom() {
return this.rightFloats.getBottom();
}
getBothBottom() {
return Math.max(this.leftFloats.getBottom(), this.rightFloats.getBottom());
}
findLinePosition(blockOffset, blockSize, inlineSize) {
let [leftShelfIndex] = this.leftFloats.getTrackRange(blockOffset, blockSize);
let [rightShelfIndex] = this.rightFloats.getTrackRange(blockOffset, blockSize);
while (leftShelfIndex < this.leftFloats.inlineSizes.length ||
rightShelfIndex < this.rightFloats.inlineSizes.length) {
let leftOffset, rightOffset;
if (leftShelfIndex < this.leftFloats.inlineSizes.length) {
leftOffset = this.leftFloats.blockOffsets[leftShelfIndex];
}
else {
leftOffset = Infinity;
}
if (rightShelfIndex < this.rightFloats.inlineSizes.length) {
rightOffset = this.rightFloats.blockOffsets[rightShelfIndex];
}
else {
rightOffset = Infinity;
}
blockOffset = Math.max(blockOffset, Math.min(leftOffset, rightOffset));
const vacancy = this.getVacancyForLine(blockOffset, blockSize);
if (inlineSize <= vacancy.inlineSize)
return vacancy;
if (leftOffset <= rightOffset)
leftShelfIndex += 1;
if (rightOffset <= leftOffset)
rightShelfIndex += 1;
}
return this.getVacancyForLine(blockOffset, blockSize);
}
placeFloat(lineWidth, lineIsEmpty, box) {
if (box.style.float === 'none') {
throw new Error('Attempted to place float: none');
}
if (this.misfits.length) {
this.misfits.push(box);
}
else {
const side = box.style.float === 'left' ? this.leftFloats : this.rightFloats;
const oppositeSide = box.style.float === 'left' ? this.rightFloats : this.leftFloats;
if (box.style.clear === 'left' || box.style.clear === 'both') {
side.dropShelf(this.leftFloats.getBottom());
}
if (box.style.clear === 'right' || box.style.clear === 'both') {
side.dropShelf(this.rightFloats.getBottom());
}
const vacancy = this.getVacancyForBox(box, lineWidth);
const margins = box.getMarginsAutoIsZero();
const inlineSize = box.borderArea.width + margins.lineLeft + margins.lineRight;
if (vacancy.fits(inlineSize) || lineIsEmpty && !vacancy.hasFloats()) {
box.setBlockPosition(side.shelfBlockOffset + margins.blockStart - this.bfc.cbBlockStart);
side.placeFloat(box, vacancy, this.bfc.cbLineLeft, this.bfc.cbLineRight);
}
else {
const vacancy = this.getVacancyForBox(box, 0);
if (!vacancy.fits(inlineSize)) {
const count = box.style.float === 'left' ? vacancy.leftFloatCount : vacancy.rightFloatCount;
const oppositeCount = box.style.float === 'left' ? vacancy.rightFloatCount : vacancy.leftFloatCount;
if (count > 0) {
side.dropShelf(side.getNextTrackOffset());
}
else if (oppositeCount > 0) {
const [, trackIndex] = oppositeSide.getTrackRange(side.shelfBlockOffset);
if (trackIndex === oppositeSide.blockOffsets.length)
throw new Error('assertion failed');
side.dropShelf(oppositeSide.blockOffsets[trackIndex]);
} // else both counts are 0 so it will fit next time the line is empty
}
this.misfits.push(box);
}
}
}
consumeMisfits() {
while (this.misfits.length) {
const misfits = this.misfits;
this.misfits = [];
for (const box of misfits)
this.placeFloat(0, true, box);
}
}
dropShelf(blockOffset) {
this.leftFloats.dropShelf(blockOffset);
this.rightFloats.dropShelf(blockOffset);
}
postLine(line, didBreak) {
if (didBreak || this.misfits.length) {
this.dropShelf(this.bfc.cbBlockStart + line.blockOffset + line.height());
}
this.consumeMisfits();
}
// Float processing happens after every line, but some floats may be before
// all lines
preTextContent() {
this.consumeMisfits();
}
}
export class BlockContainer extends Box {
children;
borderArea;
paddingArea;
contentArea;
static ATTRS = {
...Box.ATTRS,
isInline: Box.BITS.isInline,
isBfcRoot: Box.BITS.isBfcRoot
};
constructor(style, children, attrs) {
super(style, children, attrs);
this.children = children;
const area = new BoxArea(this);
this.borderArea = area;
this.paddingArea = area;
this.contentArea = area;
if (this.style.hasBorder()) {
this.contentArea = this.paddingArea = this.borderArea.clone();
}
if (this.style.hasPadding()) {
this.contentArea = this.paddingArea.clone();
}
}
fillAreas() {
if (this.style.hasBorder()) {
const borderBlockStartWidth = this.style.getBorderBlockStartWidth(this);
const borderLineLeftWidth = this.style.getBorderLineLeftWidth(this);
this.paddingArea.blockStart = borderBlockStartWidth;
this.paddingArea.lineLeft = borderLineLeftWidth;
this.paddingArea.setParent(this.borderArea);
}
if (this.style.hasPadding()) {
const paddingBlockStart = this.style.getPaddingBlockStart(this);
const paddingLineLeft = this.style.getPaddingLineLeft(this);
this.contentArea.blockStart = paddingBlockStart;
this.contentArea.lineLeft = paddingLineLeft;
this.contentArea.setParent(this.paddingArea);
}
}
getLogSymbol() {
if (this.isFloat()) {
return '○︎';
}
else if (this.isInlineLevel()) {
return '▬';
}
else {
return '◼︎';
}
}
logName(log) {
if (this.isAnonymous())
log.dim();
if (this.isBfcRoot())
log.underline();
log.text(`Block ${this.id}`);
log.reset();
}
get writingModeAsParticipant() {
return this.containingBlock.writingMode;
}
get directionAsParticipant() {
return this.containingBlock.direction;
}
setBlockPosition(position) {
this.borderArea.blockStart = position;
}
setBlockSize(size) {
this.contentArea.blockSize = size;
if (this.style.hasPadding()) {
const paddingBlockStart = this.style.getPaddingBlockStart(this);
const paddingBlockEnd = this.style.getPaddingBlockEnd(this);
const paddingSize = size + paddingBlockStart + paddingBlockEnd;
this.paddingArea.blockSize = paddingSize;
}
if (this.style.hasBorder()) {
const borderBlockStartWidth = this.style.getBorderBlockStartWidth(this);
const borderBlockEndWidth = this.style.getBorderBlockEndWidth(this);
const borderSize = this.paddingArea.blockSize + borderBlockStartWidth + borderBlockEndWidth;
this.borderArea.blockSize = borderSize;
}
}
setInlinePosition(lineLeft) {
this.borderArea.lineLeft = lineLeft;
}
setInlineOuterSize(size) {
this.borderArea.inlineSize = size;
if (this.style.hasBorder()) {
const borderLineLeftWidth = this.style.getBorderLineLeftWidth(this);
const borderLineRightWidth = this.style.getBorderLineRightWidth(this);
const paddingSize = size - borderLineLeftWidth - borderLineRightWidth;
this.paddingArea.inlineSize = paddingSize;
}
if (this.style.hasPadding()) {
const paddingLineLeft = this.style.getPaddingLineLeft(this);
const paddingLineRight = this.style.getPaddingLineRight(this);
const contentSize = this.paddingArea.inlineSize - paddingLineLeft - paddingLineRight;
this.contentArea.inlineSize = contentSize;
}
}
getContainingBlockToContent() {
const inlineSize = this.containingBlock.inlineSizeForPotentiallyOrthogonal(this);
const borderBlockStartWidth = this.style.getBorderBlockStartWidth(this);
const paddingBlockStart = this.style.getPaddingBlockStart(this);
const bLineLeft = this.borderArea.lineLeft;
const blockStart = borderBlockStartWidth + paddingBlockStart;
const cInlineSize = this.contentArea.inlineSize;
const borderLineLeftWidth = this.style.getBorderLineLeftWidth(this);
const paddingLineLeft = this.style.getPaddingLineLeft(this);
const lineLeft = bLineLeft + borderLineLeftWidth + paddingLineLeft;
const lineRight = inlineSize - lineLeft - cInlineSize;
return { blockStart, lineLeft, lineRight };
}
getDefiniteInlineSize() {
const inlineSize = this.style.getInlineSize(this);
if (inlineSize !== 'auto') {
const marginLineLeft = this.style.getMarginLineLeft(this);
const borderLineLeftWidth = this.style.getBorderLineLeftWidth(this);
const paddingLineLeft = this.style.getPaddingLineLeft(this);
const paddingLineRight = this.style.getPaddingLineRight(this);
const borderLineRightWidth = this.style.getBorderLineRightWidth(this);
const marginLineRight = this.style.getMarginLineRight(this);
return (marginLineLeft === 'auto' ? 0 : marginLineLeft)
+ borderLineLeftWidth
+ paddingLineLeft
+ inlineSize
+ paddingLineRight
+ borderLineRightWidth
+ (marginLineRight === 'auto' ? 0 : marginLineRight);
}
}
getMarginsAutoIsZero() {
let marginLineLeft = this.style.getMarginLineLeft(this);
let marginLineRight = this.style.getMarginLineRight(this);
let marginBlockStart = this.style.getMarginBlockStart(this);
let marginBlockEnd = this.style.getMarginBlockEnd(this);
if (marginBlockStart === 'auto')
marginBlockStart = 0;
if (marginLineRight === 'auto')
marginLineRight = 0;
if (marginBlockEnd === 'auto')
marginBlockEnd = 0;
if (marginLineLeft === 'auto')
marginLineLeft = 0;
return {
blockStart: marginBlockStart,
lineRight: marginLineRight,
blockEnd: marginBlockEnd,
lineLeft: marginLineLeft
};
}
getLastBaseline() {
const stack = [{ block: this, offset: 0 }];
while (stack.length) {
const { block, offset } = stack.pop();
if (block.isBlockContainerOfInlines()) {
const [ifc] = block.children;
const linebox = ifc.paragraph.lineboxes.at(-1);
if (linebox)
return offset + linebox.blockOffset + linebox.ascender;
}
if (block.isBlockContainerOfBlockContainers()) {
const parentOffset = offset;
for (const child of block.children) {
const offset = parentOffset
+ child.borderArea.blockStart
+ child.style.getBorderBlockStartWidth(child);
+child.style.getPaddingBlockStart(child);
stack.push({ block: child, offset });
}
}
}
}
assignContainingBlocks(ctx) {
// CSS2.2 10.1
if (this.style.position === 'absolute') {
this.containingBlock = ctx.lastPositionedArea;
}
else {
this.containingBlock = ctx.lastBlockContainerArea;
}
this.fillAreas();
this.borderArea.setParent(this.containingBlock);
ctx.lastBlockContainerArea = this.contentArea;
if (this.style.position !== 'static') {
ctx.lastPositionedArea = this.paddingArea;
}
}
isBlockContainer() {
return true;
}
isInlineLevel() {
return Boolean(this.bitfield & Box.BITS.isInline);
}
isBfcRoot() {
return Boolean(this.bitfield & Box.BITS.isBfcRoot);
}
isFloat() {
return this.style.float !== 'none';
}
isInlineBlock() {
return this.isInlineLevel() && this.style.float === 'none';
}
loggingEnabled() {
return Boolean(this.bitfield & Box.BITS.enableLogging);
}
isBlockContainerOfInlines() {
return Boolean(this.children.length && this.children[0].isIfcInline());
}
canCollapseThrough() {
const blockSize = this.style.getBlockSize(this);
if (blockSize !== 'auto' && blockSize !== 0)
return false;
if (this.isBlockContainerOfInlines()) {
const [ifc] = this.children;
return !ifc.hasText();
}
else {
return this.children.length === 0;
}
}
isBlockContainerOfBlockContainers() {
return !this.isBlockContainerOfInlines();
}
propagate(parent) {
super.propagate(parent);
if (this.isFloat()) {
parent.bitfield |= Box.BITS.hasFloats;
}
if (this.isInlineBlock()) {
// TODO: and not absolutely positioned
parent.bitfield |= Box.BITS.hasInlineBlocks;
}
}
postlayoutPreorder() {
if (this.style.position === 'relative') {
this.borderArea.x += this.getRelativeHorizontalShift();
this.borderArea.y += this.getRelativeVerticalShift();
}
this.borderArea.absolutify();
if (this.paddingArea !== this.borderArea)
this.paddingArea.absolutify();
if (this.contentArea !== this.paddingArea)
this.contentArea.absolutify();
}
postlayoutPostorder() {
this.borderArea.snapPixels();
if (this.paddingArea !== this.borderArea)
this.paddingArea.snapPixels();
if (this.contentArea !== this.paddingArea)
this.contentArea.snapPixels();
}
doTextLayout(ctx) {
if (!this.isBlockContainerOfInlines())
throw new Error('Children are block containers');
const [ifc] = this.children;
const blockSize = this.style.getBlockSize(this);
ifc.doTextLayout(ctx);
if (blockSize === 'auto')
this.setBlockSize(ifc.paragraph.height);
}
hasBackground() {
return this.style.backgroundColor.a > 0
|| this.style.borderTopWidth > 0 && this.style.borderTopColor.a > 0
|| this.style.borderRightWidth > 0 && this.style.borderRightColor.a > 0
|| this.style.borderBottomWidth > 0 && this.style.borderBottomColor.a > 0
|| this.style.borderLeftWidth > 0 && this.style.borderLeftColor.a > 0;
}
}
function preBlockContainer(box, ctx) {
// Containing blocks first, for absolute positioning later
box.assignContainingBlocks(ctx);
if (box.isBlockContainerOfInlines()) {
const [inline] = box.children;
inline.assignContainingBlocks(ctx);
}
}
// §10.3.3
function doInlineBoxModelForBlockBox(box) {
const cInlineSize = box.containingBlock.inlineSizeForPotentiallyOrthogonal(box);
const inlineSize = box.style.getInlineSize(box);
let marginLineLeft = box.style.getMarginLineLeft(box);
let marginLineRight = box.style.getMarginLineRight(box);
// Paragraphs 2 and 3
if (inlineSize !== 'auto') {
const borderLineLeftWidth = box.style.getBorderLineLeftWidth(box);
const paddingLineLeft = box.style.getPaddingLineLeft(box);
const paddingLineRight = box.style.getPaddingLineRight(box);
const borderLineRightWidth = box.style.getBorderLineRightWidth(box);
const specifiedInlineSize = inlineSize
+ borderLineLeftWidth
+ paddingLineLeft
+ paddingLineRight
+ borderLineRightWidth
+ (marginLineLeft === 'auto' ? 0 : marginLineLeft)
+ (marginLineRight === 'auto' ? 0 : marginLineRight);
// Paragraph 2: zero out auto margins if specified values sum to a length
// greater than the containing block's width.
if (specifiedInlineSize > cInlineSize) {
if (marginLineLeft === 'auto')
marginLineLeft = 0;
if (marginLineRight === 'auto')
marginLineRight = 0;
}
if (marginLineLeft !== 'auto' && marginLineRight !== 'auto') {
// Paragraph 3: check over-constrained values. This expands the right
// margin in LTR documents to fill space, or, if the above scenario was
// hit, it makes the right margin negative.
if (box.directionAsParticipant === 'ltr') {
marginLineRight = cInlineSize - (specifiedInlineSize - marginLineRight);
}
else {
marginLineLeft = cInlineSize - (specifiedInlineSize - marginLineRight);
}
}
else { // one or both of the margins is auto, specifiedWidth < cb width
if (marginLineLeft === 'auto' && marginLineRight !== 'auto') {
// Paragraph 4: only auto value is margin-left
marginLineLeft = cInlineSize - specifiedInlineSize;
}
else if (marginLineRight === 'auto' && marginLineLeft !== 'auto') {
// Paragraph 4: only auto value is margin-right
marginLineRight = cInlineSize - specifiedInlineSize;
}
else {
// Paragraph 6: two auto values, center the content
const margin = (cInlineSize - specifiedInlineSize) / 2;
marginLineLeft = marginLineRight = margin;
}
}
}
// Paragraph 5: auto width
if (inlineSize === 'auto') {
if (marginLineLeft === 'auto')
marginLineLeft = 0;
if (marginLineRight === 'auto')
marginLineRight = 0;
}
assumePx(marginLineLeft);
assumePx(marginLineRight);
box.setInlinePosition(marginLineLeft);
box.setInlineOuterSize(cInlineSize - marginLineLeft - marginLineRight);
}
// §10.6.3
function doBlockBoxModelForBlockBox(box) {
const blockSize = box.style.getBlockSize(box);
if (blockSize === 'auto') {
if (box.children.length === 0) {
box.setBlockSize(0); // Case 4
}
else {
// Cases 1-4 should be handled by doBoxPositioning, where margin
// calculation happens. These bullet points seem to be re-phrasals of
// margin collapsing in CSS 2.2 § 8.3.1 at the very end. If I'm wrong,
// more might need to happen here.
}
}
else {
box.setBlockSize(blockSize);
}
}
export function layoutBlockBox(box, ctx) {
const bfc = ctx.bfc;
const cctx = { ...ctx };
preBlockContainer(box, cctx);
doInlineBoxModelForBlockBox(box);
doBlockBoxModelForBlockBox(box);
if (box.isBfcRoot()) {
const inlineSize = box.contentArea.inlineSize;
cctx.bfc = new BlockFormattingContext(inlineSize);
}
bfc.boxStart(box, cctx); // Assign block position if it's an IFC
// Child flow is now possible
if (box.isBlockContainerOfInlines()) {
// text layout happens in bfc.boxStart
}
else if (box.isBlockContainerOfBlockContainers()) {
for (const child of box.children) {
layoutBlockBox(child, cctx);
}
}
else {
throw new Error(`Unknown box type: ${box.id}`);
}
if (box.isBfcRoot()) {
cctx.bfc.finalize(box);
if (cctx.bfc.fctx) {
if (box.loggingEnabled()) {
console.log('Left floats');
console.log(cctx.bfc.fctx.leftFloats.repr());
console.log('Right floats');
console.log(cctx.bfc.fctx.rightFloats.repr());
console.log();
}
}
}
bfc.boxEnd(box);
}
function doInlineBoxModelForFloatBox(box, inlineSize) {
const marginLineLeft = box.style.getMarginLineLeft(box);
const marginLineRight = box.style.getMarginLineRight(box);
box.setInlineOuterSize(inlineSize -
(marginLineLeft === 'auto' ? 0 : marginLineLeft) -
(marginLineRight === 'auto' ? 0 : marginLineRight));
}
function layoutContribution(box, ctx, mode) {
const cctx = { ...ctx };
let intrinsicSize = 0;
cctx.mode = mode;
preBlockContainer(box, cctx);
const definiteSize = box.getDefiniteInlineSize();
if (definiteSize !== undefined)
return definiteSize;
if (box.isBfcRoot())
cctx.bfc = new BlockFormattingContext(mode === 'min-content' ? 0 : Infinity);
ctx.bfc.boxStart(box, cctx);
if (box.isBlockContainerOfInlines()) {
const [ifc] = box.children;
for (const line of ifc.paragraph.lineboxes) {
intrinsicSize = Math.max(intrinsicSize, line.width);
}
}
else if (box.isBlockContainerOfBlockContainers()) {
for (const child of box.children) {
intrinsicSize = Math.max(intrinsicSize, layoutContribution(child, cctx, mode));
}
}
else {
throw new Error(`Unknown box type: ${box.id}`);
}
if (box.isBfcRoot()) {
cctx.bfc.finalize(box);
if (cctx.bfc.fctx) {
if (mode === 'max-content') {
intrinsicSize += cctx.bfc.fctx.leftFloats.getOverflow();
intrinsicSize += cctx.bfc.fctx.rightFloats.getOverflow();
}
else {
intrinsicSize = Math.max(intrinsicSize, cctx.bfc.fctx.leftFloats.getOverflow());
intrinsicSize = Math.max(intrinsicSize, cctx.bfc.fctx.rightFloats.getOverflow());
}
}
}
ctx.bfc.boxEnd(box);
const marginLineLeft = box.style.getMarginLineLeft(box);
const marginLineRight = box.style.getMarginLineRight(box);
const borderLineLeftWidth = box.style.getBorderLineLeftWidth(box);
const paddingLineLeft = box.style.getPaddingLineLeft(box);
const paddingLineRight = box.style.getPaddingLineRight(box);
const borderLineRightWidth = box.style.getBorderLineRightWidth(box);
intrinsicSize += (marginLineLeft === 'auto' ? 0 : marginLineLeft)
+ borderLineLeftWidth
+ paddingLineLeft
+ paddingLineRight
+ borderLineRightWidth
+ (marginLineRight === 'auto' ? 0 : marginLineRight);
return intrinsicSize;
}
export function layoutFloatBox(box, ctx) {
if (!box.isBfcRoot()) {
throw new Error(`Box ${box.id} is float but not BFC root, that should be impossible`);
}
const cctx = { ...ctx };
preBlockContainer(box, cctx);
let inlineSize = box.getDefiniteInlineSize();
if (inlineSize === undefined) {
const cctx = { ...ctx };
cctx.bfc = new BlockFormattingContext(0); // Not used, but children call it
if (ctx.mode === 'min-content') {
inlineSize = layoutContribution(box, cctx, 'min-content');
}
else if (ctx.mode === 'max-content') {
inlineSize = layoutContribution(box, cctx, 'max-content');
}
else {
const minContent = layoutContribution(box, cctx, 'min-content');
const maxContent = layoutContribution(box, cctx, 'max-content');
const availableSpace = box.containingBlock.inlineSize;
inlineSize = Math.max(minContent, Math.min(maxContent, availableSpace));
}
}
doInlineBoxModelForFloatBox(box, inlineSize);
doBlockBoxModelForBlockBox(box);
const cInlineSize = box.contentArea.inlineSize;
cctx.bfc = new BlockFormattingContext(cInlineSize);
if (box.isBlockContainerOfInlines()) {
box.doTextLayout(cctx);
}
else if (box.isBlockContainerOfBlockContainers()) {
for (const child of box.children) {
layoutBlockBox(child, cctx);
}
}
else {
throw new Error(`Unknown box type: ${box.id}`);
}
cctx.bfc.finalize(box);
}
export class Break extends RenderItem {
className = 'break';
isBreak() {
return true;
}
getLogSymbol() {
return '⏎';
}
logName(log) {
log.text('BR');
}
propagate(parent) {
parent.bitfield |= Box.BITS.hasBreaks;
}
}
export class Inline extends Box {
children;
nshaped;
metrics;
start;
end;
constructor(start, end, style, children, attrs) {
super(style, children, attrs);
this.start = start;
this.end = end;
this.children = children;
this.nshaped = 0;
this.metrics = EmptyInlineMetrics;
}
prelayout() {
this.metrics = getFontMetrics(this);
}
propagate(parent) {
super.propagate(parent);
if (parent.isInline()) {
parent.bitfield |= Box.BITS.hasInlines;
if (this.style.backgroundColor.a !== 0 || this.style.hasBorder()) {
parent.bitfield |= Box.BITS.hasPaintedInlines;
}
if (this.style.position === 'relative') {
parent.bitfield |= Box.BITS.hasPositionedInline;
}
if (!parent.hasSizedInline() &&
(this.hasLineLeftGap() || this.hasLineRightGap())) {
parent.bitfield |= Box.BITS.hasSizedInline;
}
if (!parent.hasColoredInline() && (this.style.color.r !== parent.style.color.r ||
this.style.color.g !== parent.style.color.g ||
this.style.color.b !== parent.style.color.b ||
this.style.color.a !== parent.style.color.a)) {
parent.bitfield |= Box.BITS.hasColoredInline;
}
// Bits that propagate to Inline propagate again if the parent is Inline
parent.bitfield |= (this.bitfield & Box.PROPAGATES_TO_INLINE_BITS);
}
}
hasText() {
return this.bitfield & Box.BITS.hasText;
}
hasSoftWrap() {
return this.bitfield & Box.BITS.hasSoftWrap;
}
hasCollapsibleWs() {
return this.bitfield & Box.BITS.hasCollapsibleWs;
}
hasFloats() {
return this.bitfield & Box.BITS.hasFloats;
}
hasInlines() {
return this.bitfield & Box.BITS.hasInlines;
}
hasBreaks() {
return this.bitfield & Box.BITS.hasBreaks;
}
hasComplexText() {
return this.bitfield & Box.BITS.hasComplexText;
}
hasSoftHyphen() {
return this.bitfield & Box.BITS.hasSoftHyphen;
}
hasNewlines() {
return this.bitfield & Box.BITS.hasNewlines;
}
hasPaintedInlines() {
return this.bitfield & Box.BITS.hasPaintedInlines;
}
hasPositionedInline() {
return this.bitfield & Box.BITS.hasPositionedInline;
}
hasInlineBlocks() {
return this.bitfield & Box.BITS.hasInlineBlocks;
}
hasSizedInline() {
return this.bitfield & Box.BITS.hasSizedInline;
}
hasColoredInline() {
return this.bitfield & Box.BITS.hasColoredInline;
}
hasLineLeftGap() {
return this.style.hasLineLeftGap();
}
hasLineRightGap() {
return this.style.hasLineRightGap();
}
getLineLeftMarginBorderPadding(ifc) {
const marginLineLeft = this.style.getMarginLineLeft(ifc);
return (marginLineLeft === 'auto' ? 0 : marginLineLeft)
+ this.style.getBorderLineLeftWidth(ifc)
+ this.style.getPaddingLineLeft(ifc);
}
getLineRightMarginBorderPadding(ifc) {
const marginLineRight = this.style.getMarginLineRight(ifc);
return (marginLineRight === 'auto' ? 0 : marginLineRight)
+ this.style.getBorderLineRightWidth(ifc)
+ this.style.getPaddingLineRight(ifc);
}
isInline() {
return true;
}
getLogSymbol() {
return '▭';
}
logName(log) {
if (this.isAnonymous())
log.dim();
if (this.isIfcInline())
log.underline();
log.text(`Inline ${this.id}`);
log.reset();
}
assignContainingBlocks(ctx) {
this.containingBlock = ctx.lastBlockContainerArea;
for (const child of this.children) {
if (child.isInline())
child.assignContainingBlocks(ctx);
}
}
absolutify() {
// noop: inlines are painted in a different way than block containers
}
hasForeground() {
return Boolean(this.hasLineLeftGap() || this.hasLineRightGap());
}
}
export class IfcInline extends Inline {
children;
text;
paragraph;
constructor(style, text, children, attrs) {
super(0, text.length, style, children, Box.ATTRS.isAnonymous | attrs);
this.children = children;
this.text = text;
this.paragraph = createEmptyParagraph(this);
}
isIfcInline() {
return true;
}
get writingModeAsParticipant() {
return this.containingBlock.writingMode;
}
loggingEnabled() {
return Boolean(this.bitfield & Box.BITS.enableLogging);
}
prelayout() {
super.prelayout();
if (this.shouldLayoutContent()) {
if (this.hasCollapsibleWs())
collapseWhitespace(this);
this.paragraph.destroy();
this.paragraph = createParagraph(this);
this.paragraph.shape();
}
}
positionItemsPostlayout() {
const inlineShifts = new Map();
const stack = [this];
let dx = 0;
let dy = 0;
let itemIndex = 0;
while (stack.length) {
const box = stack.pop();
if ('sentinel' in box) {
while (ite