about summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2024-06-01 23:57:28 +0200
committerbptato <nincsnevem662@gmail.com>2024-06-01 23:57:43 +0200
commitf451c6f5b0e9c672e2281090f1b75b55a2d8bab1 (patch)
treecb4835de2b46cff203dcfded9219bd71577fe8fe /src
parent7c1fa0ab00e48f1e1d551b48736e1281501f1b38 (diff)
downloadchawan-f451c6f5b0e9c672e2281090f1b75b55a2d8bab1.tar.gz
layout: clean up inline tree construction
Much cleaner than the previous solution.

Should also be somewhat less wasteful, as we no longer constantly
rebuild the same tree with new branches.
Diffstat (limited to 'src')
-rw-r--r--src/layout/box.nim23
-rw-r--r--src/layout/engine.nim569
-rw-r--r--src/layout/renderdocument.nim9
3 files changed, 309 insertions, 292 deletions
diff --git a/src/layout/box.nim b/src/layout/box.nim
index ff11c906..501cdc78 100644
--- a/src/layout/box.nim
+++ b/src/layout/box.nim
@@ -54,16 +54,25 @@ type
     areas*: seq[Area] # background that should be painted by fragment
     atoms*: seq[InlineAtom]
 
+  InlineFragmentType* = enum
+    iftParent, iftText, iftNewline, iftBitmap, iftBox
+
   InlineFragment* = ref object
-    children*: seq[InlineFragment]
+    state*: InlineFragmentState
     computed*: CSSComputedValues
     node*: StyledNode
     splitType*: set[SplitType]
-    state*: InlineFragmentState
-    text*: seq[string]
-    newline*: bool #TODO enumify
-    bmp*: Bitmap
-    box*: BlockBox
+    case t*: InlineFragmentType
+    of iftParent:
+      children*: seq[InlineFragment]
+    of iftText:
+      text*: string
+    of iftNewline:
+      discard
+    of iftBitmap:
+      bmp*: Bitmap
+    of iftBox:
+      box*: BlockBox
 
   Span* = object
     start*: LayoutUnit
@@ -86,11 +95,11 @@ type
     baseline*: LayoutUnit
 
   BlockBox* = ref object
+    state*: BlockBoxLayoutState
     computed*: CSSComputedValues
     node*: StyledNode
     inline*: RootInlineFragment
     nested*: seq[BlockBox]
-    state*: BlockBoxLayoutState
 
 func offset*(x, y: LayoutUnit): Offset =
   return [dtHorizontal: x, dtVertical: y]
diff --git a/src/layout/engine.nim b/src/layout/engine.nim
index 12b11256..722d589a 100644
--- a/src/layout/engine.nim
+++ b/src/layout/engine.nim
@@ -230,7 +230,7 @@ type
     word: InlineAtom
     wordstate: InlineAtomState
     wrappos: int # position of last wrapping opportunity, or -1
-    firstTextFragment: InlineFragment
+    textFragmentSeen: bool
     lastTextFragment: InlineFragment
 
   InlineState = object
@@ -581,8 +581,9 @@ proc addBackgroundAreas(ictx: var InlineContext; rootFragment: InlineFragment) =
                 ))
             continue
           traverseStack.add(nil) # sentinel
-          for i in countdown(thisNode.children.high, 0):
-            traverseStack.add(thisNode.children[i])
+          if thisNode.t == iftParent:
+            for i in countdown(thisNode.children.high, 0):
+              traverseStack.add(thisNode.children[i])
           thisNode.state.areas.add(Area(
             offset: offset(x = atom.offset.x, y = line.offsety),
             size: size(w = atom.size.w, h = line.height)
@@ -828,28 +829,22 @@ proc layoutTextLoop(ictx: var InlineContext; state: var InlineState;
   let shift = ictx.computeShift(state)
   ictx.currentLine.widthAfterWhitespace = ictx.currentLine.size.w + shift
 
-iterator transform(text: seq[string]; v: CSSTextTransform): string {.inline.} =
-  if v == TextTransformNone:
-    for str in text:
-      yield str
+proc layoutText(ictx: var InlineContext; state: var InlineState; s: string) =
+  ictx.flushWhitespace(state)
+  ictx.newWord(state)
+  let transform = state.fragment.computed{"text-transform"}
+  if transform == TextTransformNone:
+    ictx.layoutTextLoop(state, s)
   else:
-    for str in text:
-      let str = case v
-      of TextTransformCapitalize: str.capitalizeLU()
-      of TextTransformUppercase: str.toUpperLU()
-      of TextTransformLowercase: str.toLowerLU()
-      of TextTransformFullWidth: str.fullwidth()
-      of TextTransformFullSizeKana: str.fullsize()
-      of TextTransformChaHalfWidth: str.halfwidth()
-      else: ""
-      yield str
-
-proc layoutText(ictx: var InlineContext; state: var InlineState;
-    text: seq[string]) =
-  for str in text.transform(state.fragment.computed{"text-transform"}):
-    ictx.flushWhitespace(state)
-    ictx.newWord(state)
-    ictx.layoutTextLoop(state, str)
+    let s = case transform
+    of TextTransformCapitalize: s.capitalizeLU()
+    of TextTransformUppercase: s.toUpperLU()
+    of TextTransformLowercase: s.toLowerLU()
+    of TextTransformFullWidth: s.fullwidth()
+    of TextTransformFullSizeKana: s.fullsize()
+    of TextTransformChaHalfWidth: s.halfwidth()
+    else: ""
+    ictx.layoutTextLoop(state, s)
 
 func spx(l: CSSLength; lctx: LayoutContext; p: SizeConstraint;
     computed: CSSComputedValues; padding: LayoutUnit): LayoutUnit =
@@ -1357,9 +1352,9 @@ proc layoutListItem(bctx: var BlockContext; box: BlockBox;
     bctx.layoutFlow(box, sizes)
 
 proc addInlineBlock(ictx: var InlineContext; state: var InlineState;
-    box: BlockBox; space: AvailableSpace) =
+    box: BlockBox) =
   let lctx = ictx.lctx
-  let sizes = lctx.resolveFloatSizes(space, preserveHeight = false,
+  let sizes = lctx.resolveFloatSizes(ictx.space, preserveHeight = false,
     box.computed)
   box.state = BlockBoxLayoutState(
     margin: sizes.margin,
@@ -1400,6 +1395,20 @@ proc addInlineBlock(ictx: var InlineContext; state: var InlineState;
   discard ictx.addAtom(state, iastate, iblock)
   ictx.whitespacenum = 0
 
+proc addInlineImage(ictx: var InlineContext; state: var InlineState;
+    bmp: Bitmap) =
+  let h = int(bmp.height).toLayoutUnit().ceilTo(ictx.cellHeight)
+  let iastate = InlineAtomState(
+    vertalign: state.fragment.computed{"vertical-align"},
+    baseline: h
+  )
+  let atom = InlineAtom(
+    t: iatImage,
+    bmp: bmp,
+    size: size(w = int(bmp.width), h = h), #TODO overflow
+  )
+  discard ictx.addAtom(state, iastate, atom)
+
 func calcLineHeight(computed: CSSComputedValues; lctx: LayoutContext):
     LayoutUnit =
   if computed{"line-height"}.auto: # ergo normal
@@ -1407,27 +1416,13 @@ func calcLineHeight(computed: CSSComputedValues; lctx: LayoutContext):
   # Percentage: refers to the font size of the element itself.
   return computed{"line-height"}.px(lctx, lctx.cellHeight)
 
-proc layoutChildren(ictx: var InlineContext; state: var InlineState;
-    children: seq[InlineFragment]) =
-  for child in children:
-    if child.box != nil:
-      child.state = InlineFragmentState()
-      var state = InlineState(
-        fragment: child,
-        lineHeight: child.computed.calcLineHeight(ictx.lctx)
-      )
-      let space = availableSpace(w = fitContent(ictx.space.w), h = ictx.space.h)
-      ictx.addInlineBlock(state, child.box, space)
-    else:
-      ictx.layoutInline(child)
-
 proc layoutInline(ictx: var InlineContext; fragment: InlineFragment) =
   let lctx = ictx.lctx
+  let computed = fragment.computed
   fragment.state = InlineFragmentState()
   if stSplitStart in fragment.splitType:
-    let marginLeft = fragment.computed{"margin-left"}.px(lctx, ictx.space.w)
-    ictx.currentLine.size.w += marginLeft
-  let computed = fragment.computed
+    ictx.currentLine.size.w += computed{"margin-left"}.px(lctx, ictx.space.w)
+    ictx.currentLine.size.w += computed{"padding-left"}.px(lctx, ictx.space.w)
   var state = InlineState(
     fragment: fragment,
     firstLine: true,
@@ -1437,37 +1432,22 @@ proc layoutInline(ictx: var InlineContext; fragment: InlineFragment) =
     ),
     lineHeight: computed.calcLineHeight(lctx)
   )
-  if fragment.newline:
-    ictx.flushLine(state)
-  if stSplitStart in fragment.splitType:
-    let paddingLeft = computed{"padding-left"}.px(lctx, ictx.space.w)
-    ictx.currentLine.size.w += paddingLeft
-  assert fragment.children.len == 0 or fragment.text.len == 0
   ictx.applyLineHeight(ictx.currentLine, computed)
-  if ictx.firstTextFragment == nil:
-    ictx.firstTextFragment = fragment
-  ictx.lastTextFragment = fragment
-  if fragment.bmp != nil:
-    let h = int(fragment.bmp.height).toLayoutUnit().ceilTo(ictx.cellHeight)
-    let iastate = InlineAtomState(
-      vertalign: computed{"vertical-align"},
-      baseline: h
-    )
-    let atom = InlineAtom(
-      t: iatImage,
-      bmp: fragment.bmp,
-      size: size(w = int(fragment.bmp.width), h = h), #TODO overflow
-    )
-    discard ictx.addAtom(state, iastate, atom)
-  else:
+  case fragment.t
+  of iftNewline:
+    ictx.flushLine(state)
+  of iftBox:
+    ictx.addInlineBlock(state, fragment.box)
+  of iftBitmap:
+    ictx.addInlineImage(state, fragment.bmp)
+  of iftText:
     ictx.layoutText(state, fragment.text)
-    ictx.layoutChildren(state, fragment.children)
-  assert fragment.children.len == 0 or fragment.state.atoms.len == 0
+  of iftParent:
+    for child in fragment.children:
+      ictx.layoutInline(child)
   if stSplitEnd in fragment.splitType:
-    let paddingRight = computed{"padding-right"}.px(lctx, ictx.space.w)
-    ictx.currentLine.size.w += paddingRight
-    let marginRight = computed{"margin-right"}.px(lctx, ictx.space.w)
-    ictx.currentLine.size.w += marginRight
+    ictx.currentLine.size.w += computed{"padding-right"}.px(lctx, ictx.space.w)
+    ictx.currentLine.size.w += computed{"margin-right"}.px(lctx, ictx.space.w)
   if state.firstLine:
     fragment.state.startOffset = offset(
       x = state.startOffsetTop.x,
@@ -1475,6 +1455,11 @@ proc layoutInline(ictx: var InlineContext; fragment: InlineFragment) =
     )
   else:
     fragment.state.startOffset = offset(x = 0, y = ictx.currentLine.offsety)
+  if fragment.t != iftParent:
+    if not ictx.textFragmentSeen:
+      ictx.textFragmentSeen = true
+      ictx.root.fragment.state.startOffset = fragment.state.startOffset
+    ictx.lastTextFragment = fragment
 
 proc layoutRootInline(bctx: var BlockContext; root: RootInlineFragment;
     space: AvailableSpace; computed: CSSComputedValues;
@@ -1482,8 +1467,6 @@ proc layoutRootInline(bctx: var BlockContext; root: RootInlineFragment;
   root.state = RootInlineFragmentState(offset: offset)
   var ictx = bctx.initInlineContext(space, bfcOffset, root)
   ictx.layoutInline(root.fragment)
-  if ictx.firstTextFragment != nil:
-    root.fragment.state.startOffset = ictx.firstTextFragment.state.startOffset
   if ictx.lastTextFragment != nil:
     let fragment = ictx.lastTextFragment
     var state = InlineState(
@@ -2479,8 +2462,9 @@ proc newMarkerBox(computed: CSSComputedValues; listItemCounter: int):
   # Use pre, so the space at the end of the default markers isn't ignored.
   computed{"white-space"} = WhitespacePre
   return InlineFragment(
+    t: iftText,
     computed: computed,
-    text: @[computed{"list-style-type"}.listMarker(listItemCounter)]
+    text: computed{"list-style-type"}.listMarker(listItemCounter)
   )
 
 type BlockGroup = object
@@ -2491,10 +2475,8 @@ type BlockGroup = object
 
 type InnerBlockContext = object
   styledNode: StyledNode
-  blockgroup: BlockGroup
+  blockGroup: BlockGroup
   lctx: LayoutContext
-  ibox: InlineFragment
-  iroot: InlineFragment
   anonRow: BlockBox
   anonTableWrapper: BlockBox
   quoteLevel: int
@@ -2502,32 +2484,33 @@ type InnerBlockContext = object
   listItemReset: bool
   parent: ptr InnerBlockContext
   inlineStack: seq[StyledNode]
+  inlineStackFragments: seq[InlineFragment]
 
-proc add(blockgroup: var BlockGroup; box: InlineFragment) =
+proc add(blockGroup: var BlockGroup; box: InlineFragment) =
   assert box.computed{"display"} == DisplayInline
-  if blockgroup.inline == nil:
-    blockgroup.inline = RootInlineFragment(
-      fragment: InlineFragment(computed: blockgroup.lctx.myRootProperties)
+  if blockGroup.inline == nil:
+    blockGroup.inline = RootInlineFragment(
+      fragment: InlineFragment(
+        t: iftParent,
+        computed: blockGroup.lctx.myRootProperties
+      )
     )
-  blockgroup.inline.fragment.children.add(box)
+  blockGroup.inline.fragment.children.add(box)
 
-proc flush(blockgroup: var BlockGroup) =
-  if blockgroup.inline != nil:
-    assert blockgroup.parent.computed{"display"} != DisplayInline
-    let computed = blockgroup.parent.computed.inheritProperties()
+proc flush(blockGroup: var BlockGroup) =
+  if blockGroup.inline != nil:
+    assert blockGroup.parent.computed{"display"} != DisplayInline
+    let computed = blockGroup.parent.computed.inheritProperties()
     computed{"display"} = DisplayBlock
-    let box = BlockBox(
-      computed: computed,
-      inline: blockgroup.inline
-    )
-    blockgroup.parent.nested.add(box)
-    blockgroup.inline = nil
+    let box = BlockBox(computed: computed, inline: blockGroup.inline)
+    blockGroup.parent.nested.add(box)
+    blockGroup.inline = nil
 
 # Don't build empty anonymous inline blocks between block boxes
-func canBuildAnonymousInline(blockgroup: BlockGroup;
+func canBuildAnonymousInline(blockGroup: BlockGroup;
     computed: CSSComputedValues; str: string): bool =
-  return blockgroup.inline != nil and
-      blockgroup.inline.fragment.children.len > 0 or
+  let inline = blockGroup.inline
+  return inline != nil and inline.fragment.children.len > 0 or
     computed.whitespacepre or not str.onlyWhitespace()
 
 proc buildTable(parent: var InnerBlockContext; styledNode: StyledNode): BlockBox
@@ -2573,8 +2556,8 @@ proc createAnonTable(ctx: var InnerBlockContext; computed: CSSComputedValues) =
 
 proc flushTableRow(ctx: var InnerBlockContext) =
   if ctx.anonRow != nil:
-    if ctx.blockgroup.parent.computed{"display"} in ProperTableRowParent:
-      ctx.blockgroup.parent.nested.add(ctx.anonRow)
+    if ctx.blockGroup.parent.computed{"display"} in ProperTableRowParent:
+      ctx.blockGroup.parent.nested.add(ctx.anonRow)
     else:
       ctx.createAnonTable(ctx.styledNode.computed)
       ctx.anonTableWrapper.nested[0].nested.add(ctx.anonRow)
@@ -2583,19 +2566,10 @@ proc flushTableRow(ctx: var InnerBlockContext) =
 proc flushTable(ctx: var InnerBlockContext) =
   ctx.flushTableRow()
   if ctx.anonTableWrapper != nil:
-    ctx.blockgroup.parent.nested.add(ctx.anonTableWrapper)
+    ctx.blockGroup.parent.nested.add(ctx.anonTableWrapper)
 
 proc iflush(ctx: var InnerBlockContext) =
-  if ctx.iroot != nil:
-    assert ctx.iroot.computed{"display"} in {DisplayInline, DisplayInlineBlock,
-      DisplayInlineTable, DisplayInlineFlex}
-    ctx.blockgroup.add(ctx.iroot)
-    ctx.iroot = nil
-    ctx.ibox = nil
-
-proc bflush(ctx: var InnerBlockContext) =
-  ctx.iflush()
-  ctx.blockgroup.flush()
+  ctx.inlineStackFragments.setLen(0)
 
 proc flushInherit(ctx: var InnerBlockContext) =
   if ctx.parent != nil:
@@ -2604,128 +2578,171 @@ proc flushInherit(ctx: var InnerBlockContext) =
     ctx.parent.quoteLevel = ctx.quoteLevel
 
 proc flush(ctx: var InnerBlockContext) =
-  ctx.blockgroup.flush()
+  ctx.blockGroup.flush()
   ctx.flushTableRow()
   ctx.flushTable()
   ctx.flushInherit()
 
-proc reconstructInlineParents(ctx: var InnerBlockContext): InlineFragment =
-  let rootNode = ctx.inlineStack[0]
-  var parent = InlineFragment(
-    computed: rootNode.computed,
-    node: rootNode
+proc reconstructInlineParents(ctx: var InnerBlockContext) =
+  if ctx.inlineStackFragments.len == 0:
+    var parent = InlineFragment(
+      t: iftParent,
+      computed: ctx.inlineStack[0].computed,
+      node: ctx.inlineStack[0]
+    )
+    ctx.inlineStackFragments.add(parent)
+    ctx.blockGroup.add(parent)
+    for i in 1 ..< ctx.inlineStack.len:
+      let node = ctx.inlineStack[i]
+      let child = InlineFragment(
+        t: iftParent,
+        computed: node.computed,
+        node: node
+      )
+      parent.children.add(child)
+      ctx.inlineStackFragments.add(child)
+      parent = child
+
+proc buildSomeBlock(ctx: var InnerBlockContext; styledNode: StyledNode):
+    BlockBox =
+  return case styledNode.computed{"display"}
+  of DisplayBlock, DisplayFlowRoot, DisplayInlineBlock:
+    ctx.buildBlock(styledNode)
+  of DisplayFlex, DisplayInlineFlex: ctx.buildFlex(styledNode)
+  of DisplayTable, DisplayInlineTable: ctx.buildTable(styledNode)
+  else: nil
+
+# Note: these also pop
+proc pushBlock(ctx: var InnerBlockContext; styledNode: StyledNode) =
+  ctx.iflush()
+  ctx.flush()
+  let box = ctx.buildSomeBlock(styledNode)
+  ctx.blockGroup.parent.nested.add(box)
+
+proc pushInline(ctx: var InnerBlockContext; fragment: InlineFragment) =
+  if ctx.inlineStack.len == 0:
+    ctx.blockGroup.add(fragment)
+  else:
+    ctx.reconstructInlineParents()
+    ctx.inlineStackFragments[^1].children.add(fragment)
+
+proc pushInlineText(ctx: var InnerBlockContext; computed: CSSComputedValues;
+    styledNode: StyledNode; text: string) =
+  let box = InlineFragment(
+    t: iftText,
+    computed: computed,
+    node: styledNode,
+    text: text
   )
-  ctx.iroot = parent
-  for i in 1 ..< ctx.inlineStack.len:
-    let node = ctx.inlineStack[i]
-    let nbox = InlineFragment(computed: node.computed, node: node)
-    assert nbox.computed{"display"} != DisplayTableCell
-    parent.children.add(nbox)
-    parent = nbox
-  return parent
+  ctx.pushInline(box)
+
+proc pushInlineBlock(ctx: var InnerBlockContext; styledNode: StyledNode) =
+  let wrapper = InlineFragment(
+    t: iftBox,
+    computed: styledNode.computed.inheritProperties(),
+    node: styledNode,
+    box: ctx.buildSomeBlock(styledNode)
+  )
+  ctx.pushInline(wrapper)
+
+proc pushListItem(ctx: var InnerBlockContext; styledNode: StyledNode) =
+  ctx.iflush()
+  ctx.flush()
+  inc ctx.listItemCounter
+  let marker = newMarkerBox(styledNode.computed, ctx.listItemCounter)
+  let position = styledNode.computed{"list-style-position"}
+  let content = case position
+  of ListStylePositionOutside: ctx.buildBlock(styledNode)
+  of ListStylePositionInside: ctx.buildBlock(styledNode, marker)
+  let box = case position
+  of ListStylePositionOutside:
+    content.computed = content.computed.copyProperties()
+    content.computed{"display"} = DisplayBlock
+    let markerComputed = marker.computed.copyProperties()
+    markerComputed{"display"} = DisplayBlock
+    let marker = BlockBox(
+      computed: marker.computed,
+      inline: RootInlineFragment(fragment: marker)
+    )
+    let child = BlockBox(
+      computed: styledNode.computed,
+      nested: @[marker, content]
+    )
+    child
+  of ListStylePositionInside:
+    content
+  ctx.blockGroup.parent.nested.add(box)
+
+proc pushTableRow(ctx: var InnerBlockContext; styledNode: StyledNode) =
+  let box = ctx.blockGroup.parent
+  ctx.iflush()
+  ctx.blockGroup.flush()
+  ctx.flushTableRow()
+  let child = ctx.buildTableRow(styledNode)
+  if box.computed{"display"} in ProperTableRowParent:
+    box.nested.add(child)
+  else:
+    ctx.createAnonTable(box.computed)
+    ctx.anonTableWrapper.nested[0].nested.add(child)
+
+proc pushTableRowGroup(ctx: var InnerBlockContext; styledNode: StyledNode) =
+  let box = ctx.blockGroup.parent
+  ctx.iflush()
+  ctx.blockGroup.flush()
+  ctx.flushTableRow()
+  let child = ctx.buildTableRowGroup(styledNode)
+  if box.computed{"display"} in {DisplayTable, DisplayInlineTable}:
+    box.nested.add(child)
+  else:
+    ctx.createAnonTable(box.computed)
+    ctx.anonTableWrapper.nested[0].nested.add(child)
+
+proc pushTableCell(ctx: var InnerBlockContext; styledNode: StyledNode) =
+  let box = ctx.blockGroup.parent
+  ctx.iflush()
+  ctx.blockGroup.flush()
+  let child = ctx.buildTableCell(styledNode)
+  if box.computed{"display"} == DisplayTableRow:
+    box.nested.add(child)
+  else:
+    if ctx.anonRow == nil:
+      let wrapperVals = box.computed.inheritProperties()
+      wrapperVals{"display"} = DisplayTableRow
+      ctx.anonRow = BlockBox(computed: wrapperVals)
+    ctx.anonRow.nested.add(child)
+
+proc pushTableCaption(ctx: var InnerBlockContext; styledNode: StyledNode) =
+  let box = ctx.blockGroup.parent
+  ctx.iflush()
+  ctx.blockGroup.flush()
+  ctx.flushTableRow()
+  let child = ctx.buildTableCaption(styledNode)
+  if box.computed{"display"} in {DisplayTable, DisplayInlineTable}:
+    box.nested.add(child)
+  else:
+    ctx.createAnonTable(box.computed)
+    # only add first caption
+    if ctx.anonTableWrapper.nested.len == 1:
+      ctx.anonTableWrapper.nested.add(child)
 
 proc buildFromElem(ctx: var InnerBlockContext; styledNode: StyledNode) =
-  let box = ctx.blockgroup.parent
   case styledNode.computed{"display"}
-  of DisplayBlock, DisplayFlowRoot:
-    ctx.iflush()
-    ctx.flush()
-    box.nested.add(ctx.buildBlock(styledNode))
-  of DisplayFlex:
-    ctx.iflush()
-    ctx.flush()
-    box.nested.add(ctx.buildFlex(styledNode))
+  of DisplayBlock, DisplayFlowRoot, DisplayFlex, DisplayTable:
+    ctx.pushBlock(styledNode)
+  of DisplayInlineBlock, DisplayInlineTable, DisplayInlineFlex:
+    ctx.pushInlineBlock(styledNode)
   of DisplayListItem:
-    ctx.flush()
-    inc ctx.listItemCounter
-    let marker = newMarkerBox(styledNode.computed, ctx.listItemCounter)
-    let position = styledNode.computed{"list-style-position"}
-    let content = case position
-    of ListStylePositionOutside: ctx.buildBlock(styledNode)
-    of ListStylePositionInside: ctx.buildBlock(styledNode, marker)
-    case position
-    of ListStylePositionOutside:
-      content.computed = content.computed.copyProperties()
-      content.computed{"display"} = DisplayBlock
-      let markerComputed = marker.computed.copyProperties()
-      markerComputed{"display"} = DisplayBlock
-      let marker = BlockBox(
-        computed: marker.computed,
-        inline: RootInlineFragment(fragment: marker)
-      )
-      let child = BlockBox(
-        computed: styledNode.computed,
-        nested: @[marker, content]
-      )
-      box.nested.add(child)
-    of ListStylePositionInside:
-      box.nested.add(content)
+    ctx.pushListItem(styledNode)
   of DisplayInline:
     ctx.buildInlineBoxes(styledNode)
-  of DisplayInlineBlock, DisplayInlineTable, DisplayInlineFlex:
-    # create a new inline box that we can safely put our inline block into
-    ctx.iflush()
-    let computed = styledNode.computed.inheritProperties()
-    ctx.ibox = InlineFragment(computed: computed, node: styledNode)
-    if ctx.inlineStack.len > 0:
-      let iparent = ctx.reconstructInlineParents()
-      iparent.children.add(ctx.ibox)
-      ctx.iroot = iparent
-    else:
-      ctx.iroot = ctx.ibox
-    let childBox = case styledNode.computed{"display"}
-    of DisplayInlineBlock: ctx.buildBlock(styledNode)
-    of DisplayInlineTable: ctx.buildTable(styledNode)
-    of DisplayInlineFlex: ctx.buildFlex(styledNode)
-    else: nil
-    let wrapper = InlineFragment(computed: computed, box: childBox)
-    ctx.ibox.children.add(wrapper)
-    ctx.iflush()
-  of DisplayTable:
-    #TODO why no ctx.iflush()?
-    ctx.flush()
-    let child = ctx.buildTable(styledNode)
-    box.nested.add(child)
   of DisplayTableRow:
-    ctx.bflush()
-    ctx.flushTableRow()
-    let child = ctx.buildTableRow(styledNode)
-    if box.computed{"display"} in ProperTableRowParent:
-      box.nested.add(child)
-    else:
-      ctx.createAnonTable(box.computed)
-      ctx.anonTableWrapper.nested[0].nested.add(child)
+    ctx.pushTableRow(styledNode)
   of DisplayTableRowGroup, DisplayTableHeaderGroup, DisplayTableFooterGroup:
-    ctx.bflush()
-    ctx.flushTableRow()
-    let child = ctx.buildTableRowGroup(styledNode)
-    if box.computed{"display"} in {DisplayTable, DisplayInlineTable}:
-      box.nested.add(child)
-    else:
-      ctx.createAnonTable(box.computed)
-      ctx.anonTableWrapper.nested[0].nested.add(child)
+    ctx.pushTableRowGroup(styledNode)
   of DisplayTableCell:
-    ctx.bflush()
-    let child = ctx.buildTableCell(styledNode)
-    if box.computed{"display"} == DisplayTableRow:
-      box.nested.add(child)
-    else:
-      if ctx.anonRow == nil:
-        let wrapperVals = box.computed.inheritProperties()
-        wrapperVals{"display"} = DisplayTableRow
-        ctx.anonRow = BlockBox(computed: wrapperVals)
-      ctx.anonRow.nested.add(child)
+    ctx.pushTableCell(styledNode)
   of DisplayTableCaption:
-    ctx.bflush()
-    ctx.flushTableRow()
-    let child = ctx.buildTableCaption(styledNode)
-    if box.computed{"display"} in {DisplayTable, DisplayInlineTable}:
-      box.nested.add(child)
-    else:
-      ctx.createAnonTable(box.computed)
-      # only add first caption
-      if ctx.anonTableWrapper.nested.len == 1:
-        ctx.anonTableWrapper.nested.add(child)
+    ctx.pushTableCaption(styledNode)
   of DisplayTableColumn:
     discard #TODO
   of DisplayTableColumnGroup:
@@ -2734,21 +2751,8 @@ proc buildFromElem(ctx: var InnerBlockContext; styledNode: StyledNode) =
   of DisplayTableWrapper, DisplayInlineTableWrapper:
     assert false
 
-proc buildAnonymousInlineText(ctx: var InnerBlockContext; text: string;
-    styledNode: StyledNode; bmp: Bitmap = nil) =
-  if ctx.iroot == nil:
-    let computed = styledNode.computed.inheritProperties()
-    ctx.ibox = InlineFragment(computed: computed, node: styledNode)
-    if ctx.inlineStack.len > 0:
-      let iparent = ctx.reconstructInlineParents()
-      iparent.children.add(ctx.ibox)
-      ctx.iroot = iparent
-    else:
-      ctx.iroot = ctx.ibox
-  ctx.ibox.bmp = bmp
-  ctx.ibox.text.add(text)
-
-proc buildReplacement(ctx: var InnerBlockContext; child, parent: StyledNode) =
+proc buildReplacement(ctx: var InnerBlockContext; child, parent: StyledNode;
+    computed: CSSComputedValues) =
   case child.content.t
   of ContentOpenQuote:
     let quotes = parent.computed{"quotes"}
@@ -2758,7 +2762,7 @@ proc buildReplacement(ctx: var InnerBlockContext; child, parent: StyledNode) =
     elif quotes.auto:
       text = quoteStart(ctx.quoteLevel)
     else: return
-    ctx.buildAnonymousInlineText(text, parent)
+    ctx.pushInlineText(computed, parent, text)
     inc ctx.quoteLevel
   of ContentCloseQuote:
     if ctx.quoteLevel > 0: dec ctx.quoteLevel
@@ -2769,64 +2773,69 @@ proc buildReplacement(ctx: var InnerBlockContext; child, parent: StyledNode) =
     elif quotes.auto:
       text = quoteEnd(ctx.quoteLevel)
     else: return
-    ctx.buildAnonymousInlineText(text, parent)
+    ctx.pushInlineText(computed, parent, text)
   of ContentNoOpenQuote:
     inc ctx.quoteLevel
   of ContentNoCloseQuote:
     if ctx.quoteLevel > 0: dec ctx.quoteLevel
   of ContentString:
-    #TODO canBuildAnonymousInline?
-    ctx.buildAnonymousInlineText(child.content.s, parent)
+    ctx.pushInlineText(computed, parent, child.content.s)
   of ContentImage:
     #TODO idk
-    ctx.buildAnonymousInlineText("[img]", parent, child.content.bmp)
+    if child.content.bmp != nil:
+      let wrapper = InlineFragment(
+        t: iftBitmap,
+        computed: computed,
+        node: parent,
+        bmp: child.content.bmp
+      )
+      ctx.pushInline(wrapper)
+    else:
+      ctx.pushInlineText(computed, parent, "[img]")
   of ContentVideo:
-    ctx.buildAnonymousInlineText("[video]", parent)
+    ctx.pushInlineText(computed, parent, "[video]")
   of ContentAudio:
-    ctx.buildAnonymousInlineText("[audio]", parent)
+    ctx.pushInlineText(computed, parent, "[audio]")
   of ContentNewline:
-    ctx.iflush()
-    #TODO ??
-    # this used to set ibox (before we had iroot), now I'm not sure if we
-    # should reconstruct here first
-    ctx.iroot = InlineFragment(
-      computed: parent.computed.inheritProperties(),
-      newline: true
+    let fragment = InlineFragment(
+      t: iftNewline,
+      computed: computed,
+      node: child
     )
-    ctx.iflush()
+    ctx.pushInline(fragment)
 
 proc buildInlineBoxes(ctx: var InnerBlockContext; styledNode: StyledNode) =
-  ctx.iflush()
+  let parent = InlineFragment(
+    t: iftParent,
+    computed: styledNode.computed,
+    splitType: {stSplitStart}
+  )
+  if ctx.inlineStack.len == 0:
+    ctx.blockGroup.add(parent)
+  else:
+    ctx.reconstructInlineParents()
+    ctx.inlineStackFragments[^1].children.add(parent)
   ctx.inlineStack.add(styledNode)
-  var lbox = ctx.reconstructInlineParents()
-  lbox.splitType.incl(stSplitStart)
-  ctx.ibox = lbox
+  ctx.inlineStackFragments.add(parent)
   for child in styledNode.children:
     case child.t
     of stElement:
       ctx.buildFromElem(child)
     of stText:
-      if ctx.ibox != lbox:
-        ctx.iflush()
-        lbox = ctx.reconstructInlineParents()
-        ctx.ibox = lbox
-      lbox.text.add(child.textData)
+      ctx.pushInlineText(styledNode.computed, styledNode, child.textData)
     of stReplacement:
-      ctx.buildReplacement(child, styledNode)
-  if ctx.ibox != lbox:
-    ctx.iflush()
-    lbox = ctx.reconstructInlineParents()
-    ctx.ibox = lbox
-  lbox.splitType.incl(stSplitEnd)
-  ctx.inlineStack.setLen(ctx.inlineStack.len - 1)
-  ctx.iflush()
+      ctx.buildReplacement(child, styledNode, styledNode.computed)
+  ctx.reconstructInlineParents()
+  let fragment = ctx.inlineStackFragments.pop()
+  fragment.splitType.incl(stSplitEnd)
+  ctx.inlineStack.setLen(ctx.inlineStack.high)
 
 proc newInnerBlockContext(styledNode: StyledNode; box: BlockBox;
     lctx: LayoutContext; parent: ptr InnerBlockContext): InnerBlockContext =
   assert box.computed{"display"} != DisplayInline
   var ctx = InnerBlockContext(
     styledNode: styledNode,
-    blockgroup: BlockGroup(parent: box, lctx: lctx),
+    blockGroup: BlockGroup(parent: box, lctx: lctx),
     lctx: lctx,
     parent: parent
   )
@@ -2840,19 +2849,19 @@ proc newInnerBlockContext(styledNode: StyledNode; box: BlockBox;
   return ctx
 
 proc buildInnerBlockBox(ctx: var InnerBlockContext) =
-  let box = ctx.blockgroup.parent
+  let box = ctx.blockGroup.parent
   assert box.computed{"display"} != DisplayInline
+  let inlineComputed = box.computed.inheritProperties()
   for child in ctx.styledNode.children:
     case child.t
     of stElement:
-      ctx.iflush()
       ctx.buildFromElem(child)
     of stText:
       let text = child.textData
-      if canBuildAnonymousInline(ctx.blockgroup, box.computed, text):
-        ctx.buildAnonymousInlineText(text, ctx.styledNode)
+      if ctx.blockGroup.canBuildAnonymousInline(box.computed, text):
+        ctx.pushInlineText(inlineComputed, ctx.styledNode, text)
     of stReplacement:
-      ctx.buildReplacement(child, ctx.styledNode)
+      ctx.buildReplacement(child, ctx.styledNode, inlineComputed)
   ctx.iflush()
 
 proc buildBlock(styledNode: StyledNode; lctx: LayoutContext;
@@ -2861,8 +2870,7 @@ proc buildBlock(styledNode: StyledNode; lctx: LayoutContext;
   let box = BlockBox(computed: styledNode.computed, node: styledNode)
   var ctx = newInnerBlockContext(styledNode, box, lctx, parent)
   if marker != nil:
-    ctx.iroot = marker
-    ctx.iflush()
+    ctx.pushInline(marker)
   ctx.buildInnerBlockBox()
   # Flush anonymous tables here, to avoid setting inline layout with tables.
   ctx.flushTableRow()
@@ -2870,16 +2878,17 @@ proc buildBlock(styledNode: StyledNode; lctx: LayoutContext;
   # (flush here, because why not)
   ctx.flushInherit()
   # Avoid unnecessary anonymous block boxes. This also helps set our layout to
-  # inline even if no inner anonymous block was buildd.
+  # inline even if no inner anonymous block was built.
   if box.nested.len == 0:
-    box.inline = if ctx.blockgroup.inline != nil:
-      ctx.blockgroup.inline
+    box.inline = if ctx.blockGroup.inline != nil:
+      ctx.blockGroup.inline
     else:
       RootInlineFragment(fragment: InlineFragment(
+        t: iftParent,
         computed: lctx.myRootProperties
       ))
-    ctx.blockgroup.inline = nil
-  ctx.blockgroup.flush()
+    ctx.blockGroup.inline = nil
+  ctx.blockGroup.flush()
   return box
 
 proc buildFlex(styledNode: StyledNode; lctx: LayoutContext;
@@ -2887,10 +2896,10 @@ proc buildFlex(styledNode: StyledNode; lctx: LayoutContext;
   let box = BlockBox(computed: styledNode.computed, node: styledNode)
   var ctx = newInnerBlockContext(styledNode, box, lctx, parent)
   assert box.computed{"display"} != DisplayInline
+  let inlineComputed = box.computed.inheritProperties()
   for child in ctx.styledNode.children:
     case child.t
     of stElement:
-      ctx.iflush()
       let display = child.computed{"display"}.blockify()
       if display != child.computed{"display"}:
         #TODO this is a hack.
@@ -2905,17 +2914,17 @@ proc buildFlex(styledNode: StyledNode; lctx: LayoutContext;
         ctx.buildFromElem(child)
     of stText:
       let text = child.textData
-      if ctx.blockgroup.canBuildAnonymousInline(box.computed, text):
-        ctx.buildAnonymousInlineText(text, ctx.styledNode)
+      if ctx.blockGroup.canBuildAnonymousInline(box.computed, text):
+        ctx.pushInlineText(inlineComputed, ctx.styledNode, text)
     of stReplacement:
-      ctx.buildReplacement(child, ctx.styledNode)
+      ctx.buildReplacement(child, ctx.styledNode, inlineComputed)
   ctx.iflush()
   # Flush anonymous tables here, to avoid setting inline layout with tables.
   ctx.flushTableRow()
   ctx.flushTable()
   # (flush here, because why not)
   ctx.flushInherit()
-  ctx.blockgroup.flush()
+  ctx.blockGroup.flush()
   assert box.inline == nil
   const FlexReverse = {FlexDirectionRowReverse, FlexDirectionColumnReverse}
   if box.computed{"flex-direction"} in FlexReverse:
diff --git a/src/layout/renderdocument.nim b/src/layout/renderdocument.nim
index 1e987c41..e62455e4 100644
--- a/src/layout/renderdocument.nim
+++ b/src/layout/renderdocument.nim
@@ -350,12 +350,14 @@ proc paintInlineFragment(grid: var FlexibleGrid; state: var RenderState;
 
 proc renderInlineFragment(grid: var FlexibleGrid; state: var RenderState;
     fragment: InlineFragment; offset: Offset) =
-  assert fragment.state.atoms.len == 0 or fragment.children.len == 0
   let bgcolor = fragment.computed{"background-color"}
   if bgcolor.t == ctANSI or bgcolor.t == ctRGB and bgcolor.argbcolor.a > 0:
     #TODO color blending
     grid.paintInlineFragment(state, fragment, offset, bgcolor)
-  if fragment.state.atoms.len > 0:
+  if fragment.t == iftParent:
+    for child in fragment.children:
+      grid.renderInlineFragment(state, child, offset)
+  else:
     let format = fragment.computed.toFormat()
     for atom in fragment.state.atoms:
       case atom.t
@@ -371,9 +373,6 @@ proc renderInlineFragment(grid: var FlexibleGrid; state: var RenderState;
           y: (offset.y div state.attrs.ppl).toInt,
           bmp: atom.bmp
         ))
-  else:
-    for child in fragment.children:
-      grid.renderInlineFragment(state, child, offset)
   if fragment.computed{"position"} != PositionStatic:
     if fragment.splitType != {stSplitStart, stSplitEnd}:
       if stSplitStart in fragment.splitType: