import unicode
import options
import layout/box
import html/tags
import html/dom
import css/values
import utils/twtstr
import io/term
func cells_in(l: CSSLength, state: Viewport, d: int, p: Option[int], o: bool): int =
return cells(l, d, state.term, p, o)
func cells_w(l: CSSLength, state: Viewport, p: int): int =
return l.cells_in(state, state.term.ppc, p.some, true)
func cells_h(l: CSSLength, state: Viewport, p: Option[int]): int =
return l.cells_in(state, state.term.ppl, p, false)
func cells_h(l: CSSLength, state: Viewport, p: int): int =
return l.cells_in(state, state.term.ppl, p.some, false)
func px(l: CSSLength, state: Viewport, p = 0): int {.inline.} =
return px(l, state.term, p)
type InlineState = object
ictx: InlineContext
skip: bool
node: Node
word: InlineWord
maxwidth: int
specified: CSSSpecifiedValues
func whitespacepre(specified: CSSSpecifiedValues): bool {.inline.} =
specified{"white-space"} in {WHITESPACE_PRE, WHITESPACE_PRE_WRAP}
func cellwidth(ictx: InlineContext): int {.inline.} =
ictx.viewport.term.ppc
func cellheight(ictx: InlineContext): int {.inline.} =
ictx.viewport.term.ppl
# Whitespace between words
func computeShift(ictx: InlineContext, specified: CSSSpecifiedValues): int =
if ictx.whitespace:
if ictx.thisrow.atoms.len > 0 or specified.whitespacepre:
let spacing = specified{"word-spacing"}
if spacing.auto:
return ictx.cellwidth
#return spacing.cells_w(ictx.viewport, 0)
return spacing.px(ictx.viewport)
return 0
proc newWord(state: var InlineState) =
let word = InlineWord()
let specified = state.specified
word.color = specified{"color"}
word.fontstyle = specified{"font-style"}
word.fontweight = specified{"font-weight"}
word.textdecoration = specified{"text-decoration"}
word.node = state.node
state.word = word
proc finishRow(ictx: InlineContext) =
if ictx.thisrow.height != 0:
let oldrow = ictx.thisrow
ictx.rows.add(oldrow)
ictx.height += oldrow.height
ictx.width = max(ictx.width, oldrow.width)
ictx.thisrow = InlineRow(rely: oldrow.rely + oldrow.height)
proc addAtom(ictx: InlineContext, atom: InlineAtom, maxwidth: int, specified: CSSSpecifiedValues) =
var shift = ictx.computeShift(specified)
ictx.whitespace = false
# Line wrapping
if specified{"white-space"} notin {WHITESPACE_NOWRAP, WHITESPACE_PRE}:
if ictx.thisrow.width + atom.width + shift > maxwidth:
ictx.finishRow()
# Recompute on newline
shift = ictx.computeShift(specified)
ictx.whitespace = false
if atom.width > 0 and atom.height > 0:
if shift > 0:
let spacing = InlineSpacing(width: shift, height: atom.height)
spacing.relx = ictx.thisrow.width
ictx.thisrow.width += spacing.width
ictx.thisrow.atoms.add(spacing)
atom.relx += ictx.thisrow.width
ictx.thisrow.width += atom.width
ictx.thisrow.height = max(ictx.thisrow.height, atom.height)
ictx.thisrow.atoms.add(atom)
proc addWord(state: var InlineState) =
if state.word.str != "":
var word = state.word
word.height = state.ictx.cellheight
state.ictx.addAtom(word, state.maxwidth, state.specified)
state.newWord()
# Start a new line, even if the previous one is empty
proc flushLine(ictx: InlineContext) =
ictx.thisrow.height = max(ictx.thisrow.height, ictx.cellheight)
ictx.finishRow()
proc checkWrap(state: var InlineState, r: Rune) =
if state.specified{"white-space"} in {WHITESPACE_NOWRAP, WHITESPACE_PRE}:
return
let shift = state.ictx.computeShift(state.specified)
case state.specified{"word-break"}
of WORD_BREAK_BREAK_ALL:
if state.ictx.thisrow.width + state.word.width + shift + r.width() * state.ictx.cellwidth > state.maxwidth:
state.addWord()
state.ictx.finishRow()
state.ictx.whitespace = false
of WORD_BREAK_KEEP_ALL:
if state.ictx.thisrow.width + state.word.width + shift + r.width() * state.ictx.cellwidth > state.maxwidth:
state.ictx.finishRow()
state.ictx.whitespace = false
else: discard
proc processWhitespace(state: var InlineState, c: char) =
state.addWord()
case state.specified{"white-space"}
of WHITESPACE_NORMAL, WHITESPACE_NOWRAP:
state.ictx.whitespace = true
of WHITESPACE_PRE_LINE, WHITESPACE_PRE, WHITESPACE_PRE_WRAP:
if c == '\n':
state.ictx.flushLine()
else:
state.ictx.whitespace = true
proc renderText*(ictx: InlineContext, str: string, maxwidth: int, specified: CSSSpecifiedValues, node: Node) =
var state: InlineState
state.specified = specified
state.ictx = ictx
state.maxwidth = maxwidth
state.node = node
state.newWord()
#if str.strip().len > 0:
#eprint "start", str.strip()
var i = 0
while i < str.len:
if str[i].isWhitespace():
state.processWhitespace(str[i])
inc i
else:
var r: Rune
fastRuneAt(str, i, r)
state.checkWrap(r)
state.word.str &= r
state.word.width += r.width() * state.ictx.cellwidth
state.addWord()
proc finish(ictx: InlineContext) =
ictx.finishRow()
proc computedDimensions(bctx: BlockContext, width: int, height: Option[int]) =
let pwidth = bctx.specified{"width"}
if pwidth.auto:
bctx.compwidth = width
else:
#bctx.compwidth = pwidth.cells_w(bctx.viewport, width)
bctx.compwidth = pwidth.px(bctx.viewport, width)
#let mlef = bctx.specified{"margin-left"}.cells_w(bctx.viewport, width)
#let mrig = bctx.specified{"margin-right"}.cells_w(bctx.viewport, width)
let mlef = bctx.specified{"margin-left"}.px(bctx.viewport, width)
let mrig = bctx.specified{"margin-right"}.px(bctx.viewport, width)
bctx.relx = mlef
bctx.compwidth -= mlef
bctx.compwidth -= mrig
let pheight = bctx.specified{"height"}
if not pheight.auto:
#bctx.compheight = pheight.cells_h(bctx.viewport, height).some
if pheight.unit != UNIT_PERC:
bctx.compheight = pheight.px(bctx.viewport).some
elif height.issome:
bctx.compheight = pheight.px(bctx.viewport, height.get).some
proc newBlockContext_common(parent: BlockContext, box: CSSBox): BlockContext {.inline.} =
new(result)
result.viewport = parent.viewport
result.specified = box.specified
result.computedDimensions(parent.compwidth, parent.compheight)
proc newBlockContext(parent: BlockContext, box: BlockBox): BlockContext =
result = newBlockContext_common(parent, box)
proc newInlineBlockContext(parent: BlockContext, box: InlineBlockBox): BlockContext =
newBlockContext_common(parent, box)
# Anonymous block box.
proc newBlockContext(parent: BlockContext): BlockContext =
new(result)
result.specified = parent.specified.inheritProperties()
result.viewport = parent.viewport
result.computedDimensions(parent.compwidth, parent.compheight)
# Anonymous block box (root).
proc newBlockContext(viewport: Viewport): BlockContext =
new(result)
result.specified = rootProperties()
result.viewport = viewport
result.computedDimensions(viewport.term.width_px, none(int))
proc newInlineContext(bctx: BlockContext): InlineContext =
new(result)
result.thisrow = InlineRow()
result.viewport = bctx.viewport
bctx.inline = result
# Blocks' positions do not have to be arranged if alignBlocks is called with
# children, whence the separate procedure.
proc arrangeBlocks(bctx: BlockContext) =
var y = 0
var margin_todo = 0
template apply_child(child: BlockContext) =
child.rely = y
y += child.height
bctx.height += child.height
bctx.width = max(bctx.width, child.width)
margin_todo = child.margin_bottom
var i = 0
if i < bctx.nested.len:
let child = bctx.nested[i]
bctx.margin_top = child.margin_top
#let mtop = bctx.specified{"margin-top"}.cells_h(bctx.viewport, bctx.compwidth)
let mtop = bctx.specified{"margin-top"}.px(bctx.viewport, bctx.compwidth)
if mtop > bctx.margin_top or mtop < 0:
bctx.margin_top = mtop - bctx.margin_top
apply_child(child)
inc i
while i < bctx.nested.len:
let child = bctx.nested[i]
if child.margin_top > margin_todo or child.margin_top < 0:
margin_todo += child.margin_top - margin_todo
y += margin_todo
bctx.height += margin_todo
apply_child(child)
inc i
bctx.margin_bottom = margin_todo
#let mbot = bctx.specified{"margin-bottom"}.cells_h(bctx.viewport, bctx.compwidth)
let mbot = bctx.specified{"margin-bottom"}.px(bctx.viewport, bctx.compwidth)
if mbot > bctx.margin_bottom or mbot < 0:
bctx.margin_bottom = mbot - bctx.margin_bottom
if bctx.compheight.issome:
bctx.height = bctx.compheight.get
proc alignBlock(box: BlockBox)
proc alignInlineBlock(bctx: BlockContext, box: InlineBlockBox, parentcss: CSSSpecifiedValues) =
if box.bctx.done:
return
alignBlock(box)
box.bctx.rely += box.bctx.margin_top
box.bctx.height += box.bctx.margin_top
box.bctx.height += box.bctx.margin_bottom
box.ictx.addAtom(box.bctx, bctx.compwidth, parentcss)
box.ictx.whitespace = false
proc alignInline(bctx: BlockContext, box: InlineBox) =
assert box.ictx != nil
if box.newline:
box.ictx.flushLine()
for text in box.text:
assert box.children.len == 0
box.ictx.renderText(text, bctx.compwidth, box.specified, box.node)
for child in box.children:
case child.t
of DISPLAY_INLINE:
let child = InlineBox(child)
child.ictx = box.ictx
bctx.alignInline(child)
of DISPLAY_INLINE_BLOCK:
let child = InlineBlockBox(child)
child.ictx = box.ictx
bctx.alignInlineBlock(child, box.specified)
else:
assert false, "child.t is " & $child.t
proc alignInlines(bctx: BlockContext, inlines: seq[CSSBox]) =
let ictx = bctx.newInlineContext()
for child in inlines:
case child.t
of DISPLAY_INLINE:
let child = InlineBox(child)
child.ictx = ictx
bctx.alignInline(child)
of DISPLAY_INLINE_BLOCK:
let child = InlineBlockBox(child)
child.ictx = ictx
bctx.alignInlineBlock(child, bctx.specified)
else:
assert false, "child.t is " & $child.t
ictx.finish()
bctx.height += ictx.height
if bctx.compheight.issome:
bctx.height = bctx.compheight.get
bctx.width = max(ictx.width, ictx.width)
#bctx.margin_top = bctx.specified{"margin-top"}.cells_h(bctx.viewport, bctx.compwidth)
#bctx.margin_bottom = bctx.specified{"margin-bottom"}.cells_h(bctx.viewport, bctx.compwidth)
bctx.margin_top = bctx.specified{"margin-top"}.px(bctx.viewport, bctx.compwidth)
bctx.margin_bottom = bctx.specified{"margin-bottom"}.px(bctx.viewport, bctx.compwidth)
template flush_group() =
if blockgroup.len > 0:
let gctx = newBlockContext(bctx)
gctx.alignInlines(blockgroup)
blockgroup.setLen(0)
bctx.nested.add(gctx)
proc alignBlocks(bctx: BlockContext, blocks: seq[CSSBox], blockgroup: var seq[CSSBox], node: Node) =
# Box contains block boxes.
# If present, group inline boxes together in anonymous block boxes. Place
# block boxes inbetween these.
for child in blocks:
case child.t
of DISPLAY_BLOCK, DISPLAY_LIST_ITEM:
let child = BlockBox(child)
flush_group()
bctx.nested.add(child.bctx)
alignBlock(child)
of DISPLAY_INLINE:
if child.inlinelayout:
blockgroup.add(child)
else:
bctx.alignBlocks(child.children, blockgroup, child.node)
of DISPLAY_INLINE_BLOCK:
blockgroup.add(child)
else: discard #TODO
proc alignBlock(box: BlockBox) =
if box.bctx.done:
return
if box.inlinelayout:
# Box only contains inline boxes.
box.bctx.alignInlines(box.children)
else:
var blockgroup: seq[CSSBox]
box.bctx.alignBlocks(box.children, blockgroup, box.node)
let bctx = box.bctx
flush_group()
box.bctx.arrangeBlocks()
box.bctx.done = true
proc getBox(specified: CSSSpecifiedValues): CSSBox =
case specified{"display"}
of DISPLAY_BLOCK:
result = BlockBox()
of DISPLAY_INLINE_BLOCK:
result = InlineBlockBox()
of DISPLAY_INLINE:
result = InlineBox()
of DISPLAY_LIST_ITEM:
result = ListItemBox()
of DISPLAY_NONE: return nil
else: return nil
result.t = specified{"display"}
result.specified = specified
# Returns a block box, disregarding the specified value
proc getBlockBox(specified: CSSSpecifiedValues): BlockBox =
new(result)
result.t = DISPLAY_BLOCK
result.specified = specified.copyProperties()
result.specified{"display"} = DISPLAY_BLOCK
proc getTextBox(box: CSSBox): InlineBox =
new(result)
result.t = DISPLAY_INLINE
result.inlinelayout = true
result.specified = box.specified.inheritProperties()
proc getPseudoBox(bctx: BlockContext, specified: CSSSpecifiedValues): CSSBox =
let box = getBox(specified)
if box == nil:
return nil
case box.specified{"display"}
of DISPLAY_BLOCK, DISPLAY_LIST_ITEM:
let box = BlockBox(box)
box.bctx = bctx.newBlockContext(box)
of DISPLAY_INLINE_BLOCK:
let box = InlineBlockBox(box)
box.bctx = bctx.newInlineBlockContext(box)
else:
discard
box.inlinelayout = true
if specified{"content"}.len > 0:
let content = getTextBox(box)
content.text.add($specified{"content"})
box.children.add(content)
return box
proc generateBox(elem: Element, viewport: Viewport, bctx: BlockContext = nil): CSSBox =
elem.rendered = true
if viewport.map[elem.uid] != nil:
let box = viewport.map[elem.uid]
var bctx = bctx
if box.specified{"display"} in {DISPLAY_BLOCK, DISPLAY_LIST_ITEM, DISPLAY_INLINE_BLOCK}:
let box = BlockBox(box)
if bctx == nil:
box.bctx = viewport.newBlockContext()
else:
box.bctx = bctx.newBlockContext(box)
bctx = box.bctx
var i = 0
while i < box.children.len:
let child = box.children[i]
if child.element != nil:
box.children[i] = generateBox(child.element, viewport, bctx)
inc i
return viewport.map[elem.uid]
let box = if bctx != nil:
getBox(elem.css)
else:
getBlockBox(elem.css)
if box == nil:
return nil
box.node = elem
box.element = elem
var bctx = bctx
case box.specified{"display"}
of DISPLAY_BLOCK, DISPLAY_LIST_ITEM:
let box = BlockBox(box)
if bctx == nil:
box.bctx = viewport.newBlockContext()
else:
box.bctx = bctx.newBlockContext(box)
bctx = box.bctx
of DISPLAY_INLINE_BLOCK:
let box = InlineBlockBox(box)
box.bctx = bctx.newInlineBlockContext(box)
bctx = box.bctx
else: discard
var ibox: InlineBox
template add_ibox() =
if ibox != nil:
box.children.add(ibox)
ibox = nil
template add_box(child: CSSBox) =
add_ibox()
box.children.add(child)
if child.t notin {DISPLAY_INLINE, DISPLAY_INLINE_BLOCK} or not child.inlinelayout:
box.inlinelayout = false
box.inlinelayout = true
if box.t == DISPLAY_LIST_ITEM:
var ordinalvalue = 1
if elem.tagType == TAG_LI:
ordinalvalue = HTMLLIElement(elem).ordinalvalue
let marker = box.getTextBox()
marker.node = elem
marker.text.add(elem.css{"list-style-type"}.listMarker(ordinalvalue))
add_box(marker)
let before = elem.pseudo[PSEUDO_BEFORE]
if before != nil:
let bbox = bctx.getPseudoBox(before)
if bbox != nil:
bbox.node = elem
add_box(bbox)
for child in elem.childNodes:
case child.nodeType
of ELEMENT_NODE:
let elem = Element(child)
if elem.tagType == TAG_BR:
add_ibox()
ibox = box.getTextBox()
ibox.newline = true
let cbox = elem.generateBox(viewport, bctx)
if cbox != nil:
add_box(cbox)
of TEXT_NODE:
let text = Text(child)
# Don't generate empty anonymous inline blocks between block boxes
if box.specified{"display"} == DISPLAY_INLINE or
box.children.len > 0 and box.children[^1].specified{"display"} == DISPLAY_INLINE or
box.specified{"white-space"} in {WHITESPACE_PRE_LINE, WHITESPACE_PRE, WHITESPACE_PRE_WRAP} or
not text.data.onlyWhitespace():
if ibox == nil:
ibox = box.getTextBox()
ibox.node = elem
ibox.text.add(text.data)
else: discard
add_ibox()
let after = elem.pseudo[PSEUDO_AFTER]
if after != nil:
let abox = bctx.getPseudoBox(after)
if abox != nil:
abox.node = elem
add_box(abox)
viewport.map[elem.uid] = box
return box
proc renderLayout*(viewport: var Viewport, document: Document) =
if viewport.root == nil or document.all_elements.len != viewport.map.len:
viewport.map = newSeq[CSSBox](document.all_elements.len)
else:
var i = 0
while i < viewport.map.len:
if not document.all_elements[i].rendered:
viewport.map[i] = nil
inc i
viewport.root = BlockBox(document.root.generateBox(viewport))
alignBlock(viewport.root)