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: LayoutState, d: int, p: Option[int], o: bool): int =
return cells(l, d, state.term.width_px, state.term.height_px, p, o)
func cells_w(l: CSSLength, state: LayoutState, p: int): int =
return l.cells_in(state, state.term.ppc, p.some, true)
func cells_h(l: CSSLength, state: LayoutState, p: Option[int]): int =
return l.cells_in(state, state.term.ppl, p, false)
func cells_h(l: CSSLength, state: LayoutState, p: int): int =
return l.cells_in(state, state.term.ppl, p.some, false)
func newInlineContext*(): InlineContext =
new(result)
result.whitespace = true
result.ws_initial = true
func newBlockContext(): BlockContext =
new(result)
proc putRows(icontext: InlineContext) =
var i = 0
while i < icontext.rows.len:
icontext.rows[i].bottom = icontext.fromy
inc i
proc flushRows(icontext: InlineContext) =
if icontext.thisrow.len == 0: return
icontext.putRows()
var y = 0
var re = false
y = icontext.thisrow[0].bottom
var i = 1
while i < icontext.thisrow.len:
let ry = icontext.thisrow[i].bottom
if y != ry:
re = true
if y < ry:
y = ry
inc i
if re:
i = 0
while i < icontext.thisrow.len:
icontext.thisrow[i].y = y + icontext.thisrow[i].y - icontext.thisrow[i].bottom
inc i
icontext.rows.add(icontext.thisrow)
icontext.thisrow.setLen(0)
proc flushConty(box: CSSBox) =
box.icontext.flushRows()
box.icontext.fromx = box.x
inc box.icontext.fromy
inc box.bcontext.fromy
box.icontext.conty = false
box.icontext.whitespace = true
box.icontext.ws_initial = true
proc flushMargins(box: CSSBox) =
box.icontext.fromy += box.bcontext.margin_todo
box.bcontext.margin_done += box.bcontext.margin_todo
box.bcontext.margin_todo = 0
proc applyBlockStart(state: LayoutState, box, parent: CSSBox, vals: CSSSpecifiedValues) =
parent.flushMargins()
box.bcontext = newBlockContext()
box.x += vals{"margin-left"}.cells_w(state, parent.bcontext.width)
let mtop = vals{"margin-top"}.cells_h(state, parent.bcontext.width)
if mtop > parent.bcontext.margin_done or mtop < 0:
let diff = mtop - parent.bcontext.margin_done
parent.icontext.fromy += diff
parent.bcontext.margin_done += diff
box.y = parent.icontext.fromy
box.bcontext.margin_done = parent.bcontext.margin_done
let pwidth = vals{"width"}
if pwidth.auto:
box.bcontext.width = parent.bcontext.width
else:
box.bcontext.width = pwidth.cells_w(state, parent.bcontext.width)
let pheight = vals{"height"}
if not pheight.auto:
if pheight.unit != UNIT_PERC or parent.bcontext.height.issome:
box.bcontext.height = pheight.cells_h(state, parent.bcontext.height).some
box.cssvalues = vals
func newBlockBox(state: var LayoutState, parent: CSSBox, vals: CSSSpecifiedValues): CSSBlockBox =
new(result)
result.t = BLOCK
if parent.icontext.conty:
parent.flushConty()
result.x = parent.x
state.applyBlockStart(result, parent, vals)
result.icontext = newInlineContext()
result.icontext.fromy = result.y
result.icontext.fromx = result.x
func newInlineBlockBox*(state: LayoutState, parent: CSSBox, vals: CSSSpecifiedValues): CSSInlineBlockBox =
new(result)
result.t = INLINE_BLOCK
result.x = parent.icontext.fromx
state.applyBlockStart(result, parent, vals)
result.icontext = newInlineContext()
result.icontext.fromy = result.y
result.icontext.fromx = result.x
func newInlineBox*(state: LayoutState, parent: CSSBox, vals: CSSSpecifiedValues): CSSInlineBox =
new(result)
result.t = INLINE
result.x = parent.x
result.y = parent.icontext.fromy
result.icontext = parent.icontext
result.bcontext = parent.bcontext
result.cssvalues = vals
result.icontext.fromx += vals{"margin-left"}.cells_w(state, parent.bcontext.width)
type InlineState = object
icontext: InlineContext
bcontext: BlockContext
rowbox: CSSRowBox
word: seq[Rune]
ww: int
skip: bool
nodes: seq[Node]
cssvalues: CSSSpecifiedValues
x: int
proc newRowBox(state: var InlineState) =
state.rowbox = CSSRowBox()
state.rowbox.x = state.icontext.fromx
state.rowbox.y = state.icontext.fromy
state.rowbox.bottom = state.rowbox.y
let cssvalues = state.cssvalues
state.rowbox.color = cssvalues{"color"}
state.rowbox.fontstyle = cssvalues{"font-style"}
state.rowbox.fontweight = cssvalues{"font-weight"}
state.rowbox.textdecoration = cssvalues{"text-decoration"}
state.rowbox.nodes = state.nodes
proc addRowBox(state: var InlineState) =
state.icontext.thisrow.add(state.rowbox)
proc inlineWrap(state: var InlineState) =
state.addRowBox()
state.icontext.flushRows()
inc state.icontext.fromy
state.icontext.fromx = state.x
if state.word.len == 0:
state.icontext.whitespace = true
state.icontext.ws_initial = true
state.icontext.conty = false
else:
if state.word[^1] == Rune(' '):
state.icontext.whitespace = true
state.icontext.ws_initial = false
state.icontext.conty = true
state.newRowBox()
proc addWord(state: var InlineState) =
state.rowbox.str &= $state.word
state.rowbox.width += state.ww
state.word.setLen(0)
state.ww = 0
proc wrapNormal(state: var InlineState, r: Rune) =
if state.icontext.fromx + state.rowbox.width + state.ww == state.bcontext.width and r == Rune(' '):
state.addWord()
if state.word.len == 0:
if r == Rune(' '):
state.skip = true
elif state.word[0] == Rune(' '):
state.word = state.word.substr(1)
dec state.ww
state.inlineWrap()
if not state.skip and r == Rune(' '):
state.icontext.whitespace = true
state.icontext.ws_initial = false
proc checkWrap(state: var InlineState, r: Rune) =
if state.cssvalues{"white-space"} in {WHITESPACE_NOWRAP, WHITESPACE_PRE}:
return
case state.cssvalues{"word-break"}
of WORD_BREAK_NORMAL:
if state.icontext.fromx + state.rowbox.width > state.x and
state.icontext.fromx + state.rowbox.width + state.ww + r.width() > state.x + state.bcontext.width:
state.wrapNormal(r)
of WORD_BREAK_BREAK_ALL:
if state.icontext.fromx + state.rowbox.width + state.ww + r.width() > state.x + state.bcontext.width:
var pl: seq[Rune]
var i = 0
var w = 0
while i < state.word.len and
state.icontext.fromx + state.rowbox.width + w <
state.bcontext.width:
pl &= state.word[i]
w += state.word[i].width()
inc i
if pl.len > 0:
state.rowbox.str &= $pl
state.rowbox.width += w
state.word = state.word.substr(pl.len)
state.ww = state.word.width()
if r == Rune(' '):
state.skip = true
state.inlineWrap()
of WORD_BREAK_KEEP_ALL:
if state.icontext.fromx + state.rowbox.width > state.x and
state.icontext.fromx + state.rowbox.width + state.ww + r.width() > state.x + state.bcontext.width:
state.wrapNormal(r)
proc preWrap(state: var InlineState) =
state.inlineWrap()
state.icontext.whitespace = false
state.icontext.ws_initial = true
state.skip = true
proc processInlineText(str: string, icontext: InlineContext,
bcontext: BlockContext, cssvalues: CSSSpecifiedValues,
x: int, nodes: seq[Node]) =
var state: InlineState
state.icontext = icontext
state.bcontext = bcontext
state.cssvalues = cssvalues
state.x = x
state.nodes = nodes
var i = 0
state.newRowBox()
var r: Rune
while i < str.len:
var rw = 0
case str[i]
of ' ', '\n', '\t':
rw = 1
r = Rune(str[i])
inc i
state.addWord()
case state.cssvalues{"white-space"}
of WHITESPACE_NORMAL, WHITESPACE_NOWRAP:
if state.icontext.whitespace:
if state.icontext.ws_initial:
state.icontext.ws_initial = false
state.skip = true
else:
state.skip = true
state.icontext.whitespace = true
of WHITESPACE_PRE_LINE:
if state.icontext.whitespace:
state.skip = true
state.icontext.ws_initial = false
if r == Rune('\n'):
state.preWrap()
of WHITESPACE_PRE, WHITESPACE_PRE_WRAP:
state.icontext.ws_initial = false
if r == Rune('\n'):
state.preWrap()
r = Rune(' ')
else:
state.icontext.whitespace = false
fastRuneAt(str, i, r)
rw = r.width()
# TODO a better line wrapping algorithm would be nice... especially because
# this one doesn't even work
if rw > 1 or state.cssvalues{"word-break"} == WORD_BREAK_BREAK_ALL:
state.addWord()
state.checkWrap(r)
if state.skip:
state.skip = false
continue
state.word &= r
state.ww += rw
state.addWord()
if state.rowbox.str.len > 0:
state.addRowBox()
state.icontext.fromx += state.rowbox.width
state.icontext.conty = true
state.bcontext.margin_todo = 0
state.bcontext.margin_done = 0
proc processInlineContext(ibox: CSSInlineBox, str: string, nodes: seq[Node]) =
processInlineText(str, ibox.icontext, ibox.bcontext, ibox.cssvalues, ibox.x, nodes)
proc processInlineBox(state: var LayoutState, parent: CSSBox, str: string): CSSInlineBox =
if str.len == 0:
return nil
#TODO this doesn't really belong in here
parent.flushMargins()
if parent of CSSInlineBox:
let ibox = CSSInlineBox(parent)
ibox.processInlineContext(str, state.nodes)
return nil
let ibox = state.newInlineBox(parent, parent.cssvalues.inheritProperties())
ibox.processInlineContext(str, state.nodes)
return ibox
proc applyBlockEnd(state: var LayoutState, parent, box: CSSBox) =
box.flushMargins()
let mbot = box.cssvalues{"margin-bottom"}.cells_h(state, parent.bcontext.width)
parent.bcontext.margin_todo += mbot
parent.bcontext.margin_done = box.bcontext.margin_done
parent.bcontext.margin_todo = max(parent.bcontext.margin_todo - box.bcontext.margin_done, 0)
if box.bcontext.height.isnone:
parent.icontext.fromy = box.icontext.fromy
else:
parent.icontext.fromy += box.bcontext.height.get
proc add(state: var LayoutState, parent: CSSBox, box: CSSBlockBox) =
parent.icontext.fromx = parent.x
if box.icontext.conty:
box.flushConty()
state.applyBlockEnd(parent, box)
parent.children.add(box)
proc add(state: var LayoutState, parent: CSSBox, box: CSSInlineBox) =
parent.icontext.fromx += box.cssvalues{"margin-right"}.cells_w(state, parent.bcontext.width)
parent.icontext.fromy = box.icontext.fromy
parent.children.add(box)
proc add(state: var LayoutState, parent: CSSBox, box: CSSInlineBlockBox) =
parent.icontext.fromx = max(box.icontext.fromx, box.bcontext.width)
parent.icontext.fromx += box.cssvalues{"margin-right"}.cells_w(state, parent.bcontext.width)
parent.icontext.conty = box.icontext.conty
box.icontext.putRows()
parent.icontext.thisrow.add(box.icontext.rows)
parent.icontext.thisrow.add(box.icontext.thisrow)
box.icontext.rows.setLen(0)
box.icontext.thisrow.setLen(0)
state.applyBlockEnd(parent, box)
parent.children.add(box)
proc add(state: var LayoutState, parent: CSSBox, box: CSSBox) =
case box.t
of BLOCK: state.add(parent, CSSBlockBox(box))
of INLINE: state.add(parent, CSSInlineBox(box))
of INLINE_BLOCK: state.add(parent, CSSInlineBlockBox(box))
proc processComputedValueBox(state: var LayoutState, parent: CSSBox, values: CSSSpecifiedValues): CSSBox =
case values{"display"}
of DISPLAY_BLOCK:
result = state.newBlockBox(parent, values)
of DISPLAY_INLINE_BLOCK:
result = state.newInlineBlockBox(parent, values)
of DISPLAY_INLINE:
result = state.newInlineBox(parent, values)
of DISPLAY_LIST_ITEM:
result = state.newBlockBox(parent, values)
of DISPLAY_NONE:
return nil
else:
return nil
proc processBr(state: var LayoutState, parent: CSSBox, vals: CSSSpecifiedValues) =
if vals{"display"} == DISPLAY_INLINE:
if parent.icontext.conty:
parent.flushConty()
else:
inc parent.bcontext.fromy
inc parent.icontext.fromy
parent.icontext.fromx = parent.x
proc processElemBox(state: var LayoutState, parent: CSSBox, elem: Element): CSSBox =
if elem.tagType == TAG_BR:
state.processBr(parent, elem.css)
result = state.processComputedValueBox(parent, elem.css)
if result != nil:
result.node = elem
proc processElemChildren(state: var LayoutState, parent: CSSBox, elem: Element)
proc processNode(state: var LayoutState, parent: CSSBox, node: Node): CSSBox =
case node.nodeType
of ELEMENT_NODE:
let elem = Element(node)
result = state.processElemBox(parent, Element(node))
if result == nil:
return
state.processElemChildren(result, elem)
of TEXT_NODE:
let text = Text(node)
result = state.processInlineBox(parent, text.data)
if result != nil:
result.node = node
else: discard
proc processBeforePseudoElem(state: var LayoutState, parent: CSSBox, elem: Element) =
if elem.pseudo[PSEUDO_BEFORE] != nil:
let box = state.processComputedValueBox(parent, elem.pseudo[PSEUDO_BEFORE])
if box == nil: return
box.node = elem
let text = elem.pseudo[PSEUDO_BEFORE]{"content"}
var inline = state.processInlineBox(box, $text)
if inline != nil:
inline.node = elem
state.add(box, inline)
state.add(parent, box)
proc processAfterPseudoElem(state: var LayoutState, parent: CSSBox, elem: Element) =
if elem.pseudo[PSEUDO_AFTER] != nil:
let box = state.processComputedValueBox(parent, elem.pseudo[PSEUDO_AFTER])
if box == nil: return
box.node = elem
let text = elem.pseudo[PSEUDO_AFTER]{"content"}
var inline = state.processInlineBox(box, $text)
if inline != nil:
inline.node = elem
state.add(box, inline)
state.add(parent, box)
proc processMarker(state: var LayoutState, parent: CSSBox, elem: Element) =
if elem.css{"display"} == DISPLAY_LIST_ITEM:
var ordinalvalue = 1
if elem.tagType == TAG_LI:
ordinalvalue = HTMLLIElement(elem).ordinalvalue
let text = elem.css{"list-style-type"}.listMarker(ordinalvalue)
let tlen = text.width()
parent.icontext.fromx -= tlen
let marker = state.processInlineBox(parent, text)
if marker != nil:
state.add(parent, marker)
proc processNodes(state: var LayoutState, parent: CSSBox, nodes: seq[Node]) =
for node in nodes:
let box = state.processNode(parent, node)
if box != nil:
state.add(parent, box)
proc processElemChildren(state: var LayoutState, parent: CSSBox, elem: Element) =
state.nodes.add(elem)
state.processBeforePseudoElem(parent, elem)
state.processMarker(parent, elem)
state.processNodes(parent, elem.childNodes)
state.processAfterPseudoElem(parent, elem)
discard state.nodes.pop()
proc alignBoxes*(document: Document, term: TermAttributes): CSSBox =
var state: LayoutState
state.term = term
var rootbox = CSSBlockBox(x: 0, y: 0)
rootbox.cssvalues = rootProperties()
rootbox.bcontext = newBlockContext()
rootbox.icontext = newInlineContext()
rootbox.bcontext.width = term.width
state.nodes.add(document.root)
state.processElemChildren(rootbox, document.root)
return rootbox