about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--src/buffer/buffer.nim70
-rw-r--r--src/config/config.nim12
-rw-r--r--src/css/cascade.nim3
-rw-r--r--src/css/values.nim12
-rw-r--r--src/display/pager.nim3
-rw-r--r--src/html/dom.nim34
-rw-r--r--src/img/png.nim6
-rw-r--r--src/layout/box.nim13
-rw-r--r--src/layout/engine.nim168
-rw-r--r--src/render/renderdocument.nim26
-rw-r--r--src/types/mime.nim1
11 files changed, 212 insertions, 136 deletions
diff --git a/src/buffer/buffer.nim b/src/buffer/buffer.nim
index acfbad19..f587c8ee 100644
--- a/src/buffer/buffer.nim
+++ b/src/buffer/buffer.nim
@@ -12,29 +12,30 @@ when defined(posix):
   import posix
 
 import buffer/cell
+import config/config
 import css/cascade
 import css/cssparser
 import css/mediaquery
 import css/sheet
 import css/stylednode
 import css/values
-import config/config
 import data/charset
 import html/dom
 import html/env
 import html/htmlparser
 import html/tags
+import img/png
 import io/loader
-import io/request
 import io/posixstream
 import io/promise
+import io/request
 import io/teestream
+import io/window
 import ips/serialize
 import ips/serversocket
 import ips/socketstream
 import js/regex
 import js/timeout
-import io/window
 import layout/box
 import render/renderdocument
 import render/rendertext
@@ -491,9 +492,10 @@ proc do_reshape(buffer: Buffer) =
   of "text/html":
     if buffer.viewport == nil:
       buffer.viewport = Viewport(window: buffer.attrs)
-    let ret = renderDocument(buffer.document, buffer.attrs, buffer.userstyle, buffer.viewport, buffer.prevstyled)
-    buffer.lines = ret[0]
-    buffer.prevstyled = ret[1]
+    let ret = renderDocument(buffer.document, buffer.userstyle,
+      buffer.viewport, buffer.prevstyled)
+    buffer.lines = ret.grid
+    buffer.prevstyled = ret.styledRoot
   else:
     buffer.lines.renderStream(buffer.srenderer, buffer.available)
     buffer.available = 0
@@ -545,30 +547,50 @@ proc updateHover*(buffer: Buffer, cursorx, cursory: int): UpdateHoverResult {.pr
 
   buffer.prevnode = thisnode
 
-proc loadResource(buffer: Buffer, document: Document, elem: HTMLLinkElement): EmptyPromise =
+proc loadResource(buffer: Buffer, elem: HTMLLinkElement): EmptyPromise =
+  let document = buffer.document
   let href = elem.attr("href")
   if href == "": return
   let url = parseURL(href, document.url.some)
   if url.isSome:
     let url = url.get
-    if url.scheme == buffer.url.scheme:
-      let media = elem.media
-      if media != "":
-        let media = parseMediaQueryList(parseListOfComponentValues(newStringStream(media)))
-        if not media.applies(document.window): return
-      return buffer.loader.fetch(newRequest(url)).then(proc(res: Response) =
-        if res.contenttype == "text/css":
-          elem.sheet = parseStylesheet(res.body))
-
-proc loadResources(buffer: Buffer, document: Document): EmptyPromise =
+    let media = elem.media
+    if media != "":
+      let media = parseMediaQueryList(parseListOfComponentValues(newStringStream(media)))
+      if not media.applies(document.window): return
+    return buffer.loader.fetch(newRequest(url)).then(proc(res: Response) =
+      if res.contenttype == "text/css":
+        elem.sheet = parseStylesheet(res.body))
+
+proc loadResource(buffer: Buffer, elem: HTMLImageElement): EmptyPromise =
+  let document = buffer.document
+  let src = elem.attr("src")
+  if src == "": return
+  let url = parseURL(src, document.url.some)
+  if url.isSome:
+    let url = url.get
+    return buffer.loader.fetch(newRequest(url)).then(proc(res: Response) =
+      if res.contenttype == "image/png":
+        let pngData = res.body.readAll()
+        elem.bitmap = fromPNG(toOpenArrayByte(pngData, 0, pngData.high)))
+
+proc loadResources(buffer: Buffer): EmptyPromise =
+  let document = buffer.document
   var promises: seq[EmptyPromise]
   if document.html != nil:
-    for elem in document.html.elements(TAG_LINK):
-      let elem = HTMLLinkElement(elem)
-      if elem.rel == "stylesheet":
-        let p = buffer.loadResource(document, elem)
-        if p != nil:
-          promises.add(p)
+    for elem in document.html.elements({TAG_LINK, TAG_IMG}):
+      var p: EmptyPromise = nil
+      case elem.tagType
+      of TAG_LINK:
+        let elem = HTMLLinkElement(elem)
+        if elem.rel == "stylesheet":
+          p = buffer.loadResource(elem)
+      of TAG_IMG:
+        let elem = HTMLImageElement(elem)
+        p = buffer.loadResource(elem)
+      else: discard
+      if p != nil:
+        promises.add(p)
   return all(promises)
 
 type ConnectResult* = object
@@ -655,7 +677,7 @@ proc finishLoad(buffer: Buffer): EmptyPromise =
       window = buffer.window, url = buffer.url)
     buffer.document = doc
     buffer.state = LOADING_RESOURCES
-    p = buffer.loadResources(buffer.document)
+    p = buffer.loadResources()
   else:
     p = EmptyPromise()
     p.resolve()
diff --git a/src/config/config.nim b/src/config/config.nim
index d242c37d..2b81460c 100644
--- a/src/config/config.nim
+++ b/src/config/config.nim
@@ -35,6 +35,7 @@ type
     referer_from*: Option[bool]
     scripting: Option[bool]
     document_charset: seq[Charset]
+    images: Option[bool]
 
   StaticOmniRule = object
     match: string
@@ -50,6 +51,7 @@ type
     referer_from*: Option[bool]
     scripting*: Option[bool]
     document_charset*: seq[Charset]
+    images*: Option[bool]
 
   OmniRule* = object
     match*: Regex
@@ -115,6 +117,7 @@ type
     referrerpolicy*: ReferrerPolicy
     scripting*: bool
     charsets*: seq[Charset]
+    images*: bool
 
   ForkServerConfig* = object
     tmpdir*: string
@@ -136,7 +139,8 @@ func getForkServerConfig*(config: Config): ForkServerConfig =
 
 proc getBufferConfig*(config: Config, location: URL, cookiejar: CookieJar = nil,
       headers: Headers = nil, referer_from = false, scripting = false,
-      charsets = config.encoding.document_charset): BufferConfig =
+      charsets = config.encoding.document_charset,
+      images = false): BufferConfig =
   result = BufferConfig(
     userstyle: config.css.stylesheet,
     filter: newURLFilter(scheme = some(location.scheme), default = true),
@@ -144,7 +148,8 @@ proc getBufferConfig*(config: Config, location: URL, cookiejar: CookieJar = nil,
     headers: headers,
     referer_from: referer_from,
     scripting: scripting,
-    charsets: charsets
+    charsets: charsets,
+    images: images
   )
   new(result.headers)
   result.headers[] = DefaultHeaders
@@ -156,7 +161,8 @@ proc getSiteConfig*(config: Config, jsctx: JSContext): seq[SiteConfig] =
       scripting: sc.scripting,
       share_cookie_jar: sc.share_cookie_jar,
       referer_from: sc.referer_from,
-      document_charset: sc.document_charset
+      document_charset: sc.document_charset,
+      images: sc.images
     )
     if sc.url.isSome:
       conf.url = compileRegex(sc.url.get, 0)
diff --git a/src/css/cascade.nim b/src/css/cascade.nim
index 397dda90..c4bdc0c0 100644
--- a/src/css/cascade.nim
+++ b/src/css/cascade.nim
@@ -314,7 +314,8 @@ proc applyRules(document: Document, ua, user: CSSStylesheet, cachedTree: StyledN
             styledText.pseudo = pseudo
             styledParent.children.add(styledText)
         of PSEUDO_IMAGE:
-          let content = CSSContent(t: CONTENT_IMAGE, s: "[img]")
+          let src = Element(styledParent.node).attr("src")
+          let content = CSSContent(t: CONTENT_IMAGE, s: src)
           let styledText = styledParent.newStyledReplacement(content)
           styledText.pseudo = pseudo
           styledParent.children.add(styledText)
diff --git a/src/css/values.nim b/src/css/values.nim
index 99d1bc76..dd479509 100644
--- a/src/css/values.nim
+++ b/src/css/values.nim
@@ -6,6 +6,7 @@ import unicode
 
 import css/cssparser
 import css/selectorparser
+import img/bitmap
 import io/window
 import types/color
 import utils/twtstr
@@ -137,6 +138,7 @@ type
   CSSContent* = object
     t*: CSSContentType
     s*: string
+    bmp*: Bitmap
 
   CSSQuotes* = object
     auto*: bool
@@ -519,9 +521,10 @@ func cssColor*(val: CSSComponentValue): RGBAColor =
     let f = CSSFunction(val)
     var i = 0
     var commaMode = false
-    template check_err =
+    template check_err(slash: bool) =
       #TODO calc, percentages, etc (cssnumber function or something)
-      if i >= f.value.len or f.value[i] != CSS_NUMBER_TOKEN:
+      if not slash and i >= f.value.len or i < f.value.len and
+          f.value[i] != CSS_NUMBER_TOKEN:
         raise newException(CSSValueError, "Invalid color")
     template next_value(first = false, slash = false) =
       inc i
@@ -539,12 +542,11 @@ func cssColor*(val: CSSComponentValue): RGBAColor =
             raise newException(CSSValueError, "Invalid color")
           inc i
           f.value.skipWhitespace(i)
-      if not slash:
-        check_err
+      check_err slash
     case f.name
     of "rgb", "rgba":
       f.value.skipWhitespace(i)
-      check_err
+      check_err false
       let r = CSSToken(f.value[i]).nvalue
       next_value true
       let g = CSSToken(f.value[i]).nvalue
diff --git a/src/display/pager.nim b/src/display/pager.nim
index adfe2f87..bed2b4b5 100644
--- a/src/display/pager.nim
+++ b/src/display/pager.nim
@@ -557,6 +557,7 @@ proc applySiteconf(pager: Pager, request: Request): BufferConfig =
   var cookiejar: CookieJar
   var headers: Headers
   var scripting: bool
+  var images: bool
   var charsets = pager.config.encoding.document_charset
   for sc in pager.siteconf:
     if sc.url.isSome and not sc.url.get.match(url):
@@ -583,6 +584,8 @@ proc applySiteconf(pager: Pager, request: Request): BufferConfig =
       referer_from = sc.referer_from.get
     if sc.document_charset.len > 0:
       charsets = sc.document_charset
+    if sc.images.isSome:
+      images = sc.images.get
   return pager.config.getBufferConfig(request.url, cookiejar, headers,
     referer_from, scripting, charsets)
 
diff --git a/src/html/dom.nim b/src/html/dom.nim
index b022d858..3d901eb9 100644
--- a/src/html/dom.nim
+++ b/src/html/dom.nim
@@ -359,6 +359,9 @@ type
     alphabeticBaseline {.jsget.}: float64
     ideographicBaseline {.jsget.}: float64
 
+  HTMLImageElement* = ref object of HTMLElement
+    bitmap*: Bitmap
+
 proc parseColor(element: Element, s: string): RGBAColor
 
 proc resetTransform(state: var DrawingState) =
@@ -671,7 +674,8 @@ func makes(name: string, ts: set[TagType]): ReflectEntry =
     tags: ts
   )
 
-func makes(attrname: string, funcname: string, ts: set[TagType]): ReflectEntry =
+func makes(attrname: string, funcname: string, ts: set[TagType]):
+    ReflectEntry =
   ReflectEntry(
     attrname: attrname,
     funcname: funcname,
@@ -685,14 +689,18 @@ func makes(name: string, ts: varargs[TagType]): ReflectEntry =
 func makes(attrname, funcname: string, ts: varargs[TagType]): ReflectEntry =
   makes(attrname, funcname, toset(ts))
 
-template makeb(name: string, ts: varargs[TagType]): ReflectEntry =
+func makeb(attrname, funcname: string, ts: varargs[TagType]):
+    ReflectEntry =
   ReflectEntry(
-    attrname: name,
-    funcname: name,
+    attrname: attrname,
+    funcname: funcname,
     t: REFLECT_BOOL,
     tags: toset(ts)
   )
 
+func makeb(name: string, ts: varargs[TagType]): ReflectEntry =
+  makeb(name, name, ts)
+
 template makeul(name: string, ts: varargs[TagType], default = 0u32): ReflectEntry =
   ReflectEntry(
     attrname: name,
@@ -729,6 +737,13 @@ const ReflectTable0 = [
   makeulgz("size", TAG_INPUT, 20u32),
   makeul("width", TAG_CANVAS, 300u32),
   makeul("height", TAG_CANVAS, 150u32),
+  makes("alt", TAG_IMG),
+  makes("src", TAG_IMG, TAG_SCRIPT),
+  makes("srcset", TAG_IMG),
+  makes("sizes", TAG_IMG),
+  #TODO can we add crossOrigin here?
+  makes("usemap", "useMap", TAG_IMG),
+  makeb("ismap", "isMap", TAG_IMG),
   # "super-global" attributes
   makes("slot", AllTagTypes),
   makes("class", "className", AllTagTypes)
@@ -1429,7 +1444,7 @@ func innerHTML*(element: Element): string {.jsfget.} =
 func outerHTML*(element: Element): string {.jsfget.} =
   return $element
 
-func crossorigin(element: HTMLScriptElement): CORSAttribute =
+func crossOrigin0(element: HTMLElement): CORSAttribute =
   if not element.attrb("crossorigin"):
     return NO_CORS
   case element.attr("crossorigin")
@@ -1439,6 +1454,12 @@ func crossorigin(element: HTMLScriptElement): CORSAttribute =
     return USE_CREDENTIALS
   return ANONYMOUS
 
+func crossOrigin(element: HTMLScriptElement): CORSAttribute {.jsfget.} =
+  return element.crossOrigin0
+
+func crossOrigin(element: HTMLImageElement): CORSAttribute {.jsfget.} =
+  return element.crossOrigin0
+
 func referrerpolicy(element: HTMLScriptElement): Option[ReferrerPolicy] =
   getReferrerPolicy(element.attr("referrerpolicy"))
 
@@ -1739,6 +1760,8 @@ func newHTMLElement*(document: Document, tagType: TagType,
     result = new(HTMLLabelElement)
   of TAG_CANVAS:
     result = new(HTMLCanvasElement)
+  of TAG_IMG:
+    result = new(HTMLImageElement)
   else:
     result = new(HTMLElement)
   result.nodeType = ELEMENT_NODE
@@ -2778,6 +2801,7 @@ proc registerElements(ctx: JSContext, nodeCID: JSClassID) =
   register(HTMLTextAreaElement, TAG_TEXTAREA)
   register(HTMLLabelElement, TAG_LABEL)
   register(HTMLCanvasElement, TAG_CANVAS)
+  register(HTMLImageElement, TAG_IMG)
 
 proc addDOMModule*(ctx: JSContext) =
   let eventTargetCID = ctx.registerType(EventTarget)
diff --git a/src/img/png.nim b/src/img/png.nim
index 50b0e980..3de395f2 100644
--- a/src/img/png.nim
+++ b/src/img/png.nim
@@ -138,6 +138,7 @@ func scanlen(reader: PNGReader): int {.inline.} =
   return (w * reader.spp * int(reader.bitDepth) + 7) div 8
 
 proc handleError(reader: var PNGReader, msg: string) =
+  eprint msg
   reader.bmp = nil
   if reader.hasstrm:
     discard inflateEnd(addr reader.strm)
@@ -281,7 +282,6 @@ proc unfilter(reader: var PNGReader, irow: openArray[uint8], bpp: int) =
   of 4u8: # paeth
     reader.err "paeth not implemented yet"
   else:
-    eprint fil
     reader.err "got invalid filter"
 
 proc writepxs(reader: var PNGReader, crow: var openArray[RGBAColor]) =
@@ -414,8 +414,8 @@ proc zlibFree(opaque: pointer, address: pointer) {.cdecl.} =
 
 proc initZStream(reader: var PNGReader) =
   let bps = max(int(reader.bitDepth) div 8, 1)
-  reader.idatBuf = newSeq[uint8](reader.scanlen * reader.height * bps)
-  reader.uprow = newSeq[uint8](reader.width * bps)
+  reader.idatBuf = newSeq[uint8](reader.scanlen * reader.height)
+  reader.uprow = newSeq[uint8](reader.scanlen)
   reader.strm = z_stream(
     zalloc: zlibAlloc,
     zfree: zlibFree
diff --git a/src/layout/box.nim b/src/layout/box.nim
index 5acc7ef6..1167ca7b 100644
--- a/src/layout/box.nim
+++ b/src/layout/box.nim
@@ -23,7 +23,6 @@ type
 
   Viewport* = ref object
     window*: WindowAttributes
-    root*: seq[BlockBox]
     positioned*: seq[BlockBox]
 
   BoxBuilder* = ref object of RootObj
@@ -100,7 +99,7 @@ type
     width*: int
     contentWidth*: int
     contentHeight*: Option[int]
-    maxContentWidth*: int
+    contentWidthInfinite*: bool
 
     charwidth*: int
     whitespacenum*: int
@@ -139,14 +138,8 @@ type
     contentWidth*: int
     contentHeight*: Option[int]
     shrink*: bool
-    # The sole purpose of maxContentWidth is to stretch children of table
-    # cells to infinity in its maximum width calculation pass.
-    # For blocks with a specified width, maxContentWidth does nothing.
-    # This should never be used for anything other than setting width to
-    # min(maxContentWidth, width)! Failure to do so will almost certainly
-    # result in overflow errors (because maxContentWidth may be set to
-    # high(int).)
-    maxContentWidth*: int
+    # Whether to stretch content to infinity.
+    contentWidthInfinite*: bool
 
     positioned*: bool
     x_positioned*: bool
diff --git a/src/layout/engine.nim b/src/layout/engine.nim
index b5da155e..c3e6c3e0 100644
--- a/src/layout/engine.nim
+++ b/src/layout/engine.nim
@@ -255,8 +255,8 @@ proc addAtom(ictx: InlineContext, atom: InlineAtom, pcomputed: CSSComputedValues
   var shift = ictx.computeShift(pcomputed)
   ictx.whitespacenum = 0
   # Line wrapping
-  if not pcomputed.nowrap:
-    if ictx.currentLine.width + atom.width + shift > ictx.maxContentWidth:
+  if not pcomputed.nowrap and not ictx.contentWidthInfinite:
+    if ictx.currentLine.width + atom.width + shift > ictx.contentWidth:
       ictx.finishLine(pcomputed, false)
       # Recompute on newline
       shift = ictx.computeShift(pcomputed)
@@ -307,26 +307,30 @@ proc flushLine(ictx: InlineContext, computed: CSSComputedValues) =
 proc checkWrap(state: var InlineState, r: Rune) =
   if state.computed{"white-space"} in {WHITESPACE_NOWRAP, WHITESPACE_PRE}:
     return
+  if state.ictx.contentWidthInfinite:
+    return
   let shift = state.ictx.computeShift(state.computed)
   let rw = r.width()
+  let currWidth = state.ictx.currentLine.width + state.word.width + shift +
+    rw * state.ictx.cellwidth
   case state.computed{"word-break"}
   of WORD_BREAK_NORMAL:
     if rw == 2 or state.wrappos != -1: # break on cjk and wrap opportunities
-      if state.ictx.currentLine.width + state.word.width + shift + rw * state.ictx.cellwidth > state.ictx.maxContentWidth:
+      if currWidth > state.ictx.contentWidth:
         let l = state.ictx.currentLine
         state.addWordEOL()
         if l == state.ictx.currentLine: # no line wrapping occured in addAtom
           state.ictx.finishLine(state.computed)
           state.ictx.whitespacenum = 0
   of WORD_BREAK_BREAK_ALL:
-    if state.ictx.currentLine.width + state.word.width + shift + rw * state.ictx.cellwidth > state.ictx.maxContentWidth:
+    if currWidth > state.ictx.contentWidth:
       let l = state.ictx.currentLine
       state.addWordEOL()
       if l == state.ictx.currentLine: # no line wrapping occured in addAtom
         state.ictx.finishLine(state.computed)
         state.ictx.whitespacenum = 0
   of WORD_BREAK_KEEP_ALL:
-    if state.ictx.currentLine.width + state.word.width + shift + rw * state.ictx.cellwidth > state.ictx.maxContentWidth:
+    if currWidth > state.ictx.contentWidth:
       state.ictx.finishLine(state.computed)
       state.ictx.whitespacenum = 0
 
@@ -410,8 +414,10 @@ proc resolveContentWidth(box: BlockBox, widthpx, availableWidth: int, isauto = f
         box.margin_right = underflow div 2
 
 # Resolve percentage-based dimensions.
-# availableWidth: width of the containing box. availableHeight: ditto, but with height.
-proc resolveDimensions(box: BlockBox, availableWidth: int, availableHeight: Option[int], maxContentWidth: Option[int]) =
+# availableWidth: width of the containing box
+# availableHeight: ditto, but with height.
+proc resolveDimensions(box: BlockBox, availableWidth: int,
+    availableHeight: Option[int]) =
   let viewport = box.viewport
   let computed = box.computed
 
@@ -464,10 +470,8 @@ proc resolveDimensions(box: BlockBox, availableWidth: int, availableHeight: Opti
     if min_height.isSome and box.contentHeight.isSome and
         min_height.get > box.contentHeight.get:
       box.contentHeight = min_height
-  # if no max content width is supplied, just use regular content width.
-  box.maxContentWidth = maxContentWidth.get(box.contentWidth)
 
-proc resolveTableCellDimensions(box: BlockBox, availableWidth: int, availableHeight: Option[int], maxContentWidth: Option[int]) =
+proc resolveTableCellDimensions(box: BlockBox, availableWidth: int, availableHeight: Option[int]) =
   let viewport = box.viewport
   let computed = box.computed
 
@@ -490,9 +494,6 @@ proc resolveTableCellDimensions(box: BlockBox, availableWidth: int, availableHei
   let pheight = computed{"height"}
   if not pheight.auto and pheight.unit != UNIT_PERC:
     box.contentHeight = some(pheight.px(viewport))
-  # if no max content width is supplied, just use regular content width.
-  box.maxContentWidth = maxContentWidth.get(box.contentWidth)
-
 
 # Whether a width was specified on this block box.
 func isWidthSpecified(box: BlockBox): bool =
@@ -535,50 +536,72 @@ func isShrink(box: BlockBox, parent: BlockBox = nil, override = false): bool =
       return parent.shrink and not box.isWidthSpecified()
   else: discard
 
-proc newTableCellBox(viewport: Viewport, builder: BoxBuilder, parentWidth: int, parentHeight = none(int), shrink = true, maxContentWidth = none(int)): BlockBox {.inline.} =
+proc newTableCellBox(viewport: Viewport, builder: BoxBuilder,
+    parentWidth: int, parentHeight = none(int), shrink = true,
+    contentWidthInfinite = false): BlockBox =
   let box = BlockBox(
     viewport: viewport,
     computed: builder.computed,
     node: builder.node
   )
   box.shrink = box.isShrink(nil, shrink)
-  box.resolveTableCellDimensions(parentWidth, parentHeight, maxContentWidth)
+  box.contentWidthInfinite = contentWidthInfinite
+  box.resolveTableCellDimensions(parentWidth, parentHeight)
   return box
 
-proc newFlowRootBox(viewport: Viewport, builder: BoxBuilder, parentWidth: int, parentHeight = none(int), shrink = true, maxContentWidth = none(int)): BlockBox {.inline.} =
-  new(result)
-  result.viewport = viewport
-  result.computed = builder.computed
-  result.node = builder.node
-  result.positioned = builder.computed{"position"} != POSITION_STATIC
-  result.shrink = result.isShrink(nil, shrink)
-  result.resolveDimensions(parentWidth, parentHeight, maxContentWidth)
+proc newFlowRootBox(viewport: Viewport, builder: BoxBuilder,
+    parentWidth: int, parentHeight = none(int), shrink = true,
+    contentWidthInfinite = false): BlockBox =
+  let box = BlockBox(
+    viewport: viewport,
+    computed: builder.computed,
+    node: builder.node,
+    positioned: builder.computed{"position"} != POSITION_STATIC,
+    contentWidthInfinite: contentWidthInfinite
+  )
+  box.shrink = box.isShrink(nil, shrink)
+  box.resolveDimensions(parentWidth, parentHeight)
+  return box
 
 proc newBlockBox(parent: BlockBox, builder: BoxBuilder): BlockBox =
-  new(result)
-  result.viewport = parent.viewport
-  result.computed = builder.computed
-  result.shrink = result.isShrink(parent)
-  result.positioned = builder.computed{"position"} != POSITION_STATIC
-  let maxContentWidth = if result.shrink:
-    some(parent.maxContentWidth)
+  let box = BlockBox(
+    viewport: parent.viewport,
+    computed: builder.computed,
+    positioned: builder.computed{"position"} != POSITION_STATIC,
+    node: builder.node
+  )
+  box.shrink = box.isShrink(parent)
+  box.contentWidthInfinite = parent.contentWidthInfinite
+  let parentWidth = if box.positioned:
+    parent.viewport.positioned[^1].contentWidth
+  else:
+    parent.contentWidth
+  let parentHeight = if box.positioned:
+    parent.viewport.positioned[^1].contentHeight
   else:
-    none(int)
-  result.node = builder.node
-  result.resolveDimensions(parent.contentWidth, parent.contentHeight, maxContentWidth)
+    parent.contentHeight
+  box.resolveDimensions(parentWidth, parentHeight)
+  return box
 
 proc newListItem(parent: BlockBox, builder: ListItemBoxBuilder): ListItemBox =
-  new(result)
-  result.viewport = parent.viewport
-  result.computed = builder.content.computed
-  result.positioned = builder.computed{"position"} != POSITION_STATIC
-  result.shrink = result.isShrink(parent)
-  let maxContentWidth = if result.shrink:
-    some(parent.maxContentWidth)
+  let box = ListItemBox(
+    viewport: parent.viewport,
+    computed: builder.computed,
+    positioned: builder.computed{"position"} != POSITION_STATIC,
+    node: builder.node
+  )
+  box.shrink = box.isShrink(parent)
+  box.contentWidthInfinite = parent.contentWidthInfinite
+  let parentWidth = if box.positioned:
+    parent.viewport.positioned[^1].contentWidth
   else:
-    none(int)
-  result.node = builder.node
-  result.resolveDimensions(parent.contentWidth, parent.contentHeight, maxContentWidth)
+    parent.contentWidth
+  let parentHeight = if box.positioned:
+    parent.viewport.positioned[^1].contentHeight
+  else:
+    parent.contentHeight
+  box.resolveDimensions(parentWidth, parentHeight)
+  return box
 
 proc newInlineBlock(viewport: Viewport, builder: BoxBuilder, parentWidth: int, parentHeight = none(int)): InlineBlockBox =
   new(result)
@@ -592,7 +615,7 @@ proc newInlineContext(parent: BlockBox): InlineContext =
     shrink: parent.shrink,
     contentHeight: parent.contentHeight,
     contentWidth: parent.contentWidth,
-    maxContentWidth: parent.maxContentWidth
+    contentWidthInfinite: parent.contentWidthInfinite
   )
 
 proc buildBlock(builder: BlockBoxBuilder, parent: BlockBox): BlockBox
@@ -614,7 +637,10 @@ proc applyInlineDimensions(box: BlockBox) =
   box.width = if not box.isWidthSpecified():
     # We can make the box as small/large as the content's width.
     if box.shrink:
-      min(box.width, box.maxContentWidth)
+      if box.contentWidthInfinite:
+        box.width
+      else:
+        min(box.width, box.contentWidth)
     else:
       max(box.width, box.contentWidth)
   else:
@@ -761,27 +787,14 @@ proc buildListItem(builder: ListItemBoxBuilder, parent: BlockBox): ListItemBox =
     result.marker = buildMarker(builder.marker, result)
   result.buildLayout(builder.content)
 
-proc positionAbsolute(box: BlockBox, last: BlockBox = box.viewport.root[0]) =
-  # we use the viewport's dimensions if not parentPositioned.
-  let parentPositioned = box.viewport.positioned.len > 0
-  let last = if parentPositioned:
-    box.viewport.positioned[^1]
-  else:
-    box.viewport.root[0]
+proc positionAbsolute(box: BlockBox) =
+  let last = box.viewport.positioned[^1]
   let left = box.computed{"left"}
   let right = box.computed{"right"}
   let top = box.computed{"top"}
   let bottom = box.computed{"bottom"}
-  let parentHeight = if parentPositioned:
-    last.height
-  else:
-    box.viewport.window.height_px
-  let parentWidth = if parentPositioned:
-    last.width
-  else:
-    box.viewport.window.width_px
-  #TODO TODO TODO we should use parentWidth/parentHeight for size calculations
-  # too
+  let parentHeight = last.contentHeight.get(box.viewport.window.height_px)
+  let parentWidth = last.contentWidth
   box.x_positioned = not (left.auto and right.auto)
   box.y_positioned = not (top.auto and bottom.auto)
   if not left.auto:
@@ -824,7 +837,10 @@ proc applyChildPosition(parent, child: BlockBox, spec: bool, x, y: var int, marg
     y += child.height
     parent.height += child.height
     if not spec:
-      parent.width = min(parent.maxContentWidth, max(child.width, parent.width))
+      parent.width = if parent.contentWidthInfinite:
+        max(child.width, parent.width)
+      else:
+        min(parent.contentWidth, max(child.width, parent.width))
     parent.xminwidth = max(parent.xminwidth, child.xminwidth)
     margin_todo = Strut()
     margin_todo.append(child.margin_bottom)
@@ -846,7 +862,7 @@ proc positionBlocks(box: BlockBox) =
 
   # If content width has been specified, use it.
   # Otherwise, contentWidth is just the maximum width we can take up, so
-  # set width to min(maxContentWidth, box.contentWidth)
+  # set width to box.contentWidth
   let spec = box.isWidthSpecified()
   if spec:
     box.width = box.contentWidth
@@ -916,8 +932,11 @@ proc buildTableCaption(viewport: Viewport, builder: TableCaptionBoxBuilder, maxw
   result = viewport.newFlowRootBox(builder, maxwidth, maxheight, shrink)
   result.buildLayout(builder)
 
-proc buildTableCell(viewport: Viewport, builder: TableCellBoxBuilder, parentWidth: int, parentHeight: Option[int], shrink: bool, max_width = none(int)): BlockBox =
-  result = viewport.newTableCellBox(builder, parentWidth, parentHeight, shrink, max_width)
+proc buildTableCell(viewport: Viewport, builder: TableCellBoxBuilder,
+    parentWidth: int, parentHeight: Option[int], shrink: bool,
+    contentWidthInfinite = false): BlockBox =
+  result = viewport.newTableCellBox(builder, parentWidth, parentHeight,
+    shrink, contentWidthInfinite)
   result.buildLayout(builder)
 
 proc preBuildTableRow(pctx: var TableContext, box: TableRowBoxBuilder, parent: BlockBox, i: int): RowContext =
@@ -931,11 +950,8 @@ proc preBuildTableRow(pctx: var TableContext, box: TableRowBoxBuilder, parent: B
     let rowspan = cellbuilder.computed{"-cha-rowspan"}
     let computedWidth = cellbuilder.computed{"width"}
     let spec = (not computedWidth.auto) and computedWidth.unit != UNIT_PERC
-    let max_width = if spec:
-      none(int)
-    else:
-      some(high(int))
-    let box = parent.viewport.buildTableCell(cellbuilder, parent.contentWidth, parent.contentHeight, not spec, max_width)
+    let box = parent.viewport.buildTableCell(cellbuilder, parent.contentWidth,
+        parent.contentHeight, not spec, not spec)
     let wrapper = CellWrapper(box: box, builder: cellbuilder, colspan: colspan, rowspan: rowspan, rowi: i, coli: n)
     ctx.cells[i] = wrapper
     if rowspan != 1:
@@ -1211,14 +1227,15 @@ proc buildBlock(builder: BlockBoxBuilder, parent: BlockBox): BlockBox =
   result.buildLayout(builder)
 
 # Establish a new flow-root context and build a block box.
-proc buildRootBlock(viewport: Viewport, builder: BlockBoxBuilder) =
+proc buildRootBlock(viewport: Viewport, builder: BlockBoxBuilder): BlockBox =
   let box = viewport.newFlowRootBox(builder, viewport.window.width_px, shrink = false)
-  viewport.root.add(box)
+  viewport.positioned.add(box)
   box.buildLayout(builder)
   # Normally margin-top would be used by positionBlock, but the root block
   # doesn't get positioned by the parent, so we have to do it manually here.
   #TODO this is kind of ugly.
   box.offset.y += box.margin_top
+  return box
 
 # Generation phase
 
@@ -1656,8 +1673,7 @@ proc generateTableBox(styledNode: StyledNode, viewport: Viewport, parent: var In
   box.generateTableChildWrappers()
   return box
 
-proc renderLayout*(viewport: var Viewport, root: StyledNode) =
-  viewport.root.setLen(0)
+proc renderLayout*(viewport: var Viewport, root: StyledNode): BlockBox =
   viewport.positioned.setLen(0)
   let builder = root.generateBlockBox(viewport)
-  viewport.buildRootBlock(builder)
+  return viewport.buildRootBlock(builder)
diff --git a/src/render/renderdocument.nim b/src/render/renderdocument.nim
index 0cdd8075..d18dbdd5 100644
--- a/src/render/renderdocument.nim
+++ b/src/render/renderdocument.nim
@@ -366,15 +366,23 @@ const css = staticRead"res/ua.css"
 let uastyle = css.parseStylesheet()
 const quirk = css & staticRead"res/quirk.css"
 let quirkstyle = quirk.parseStylesheet()
-proc renderDocument*(document: Document, window: WindowAttributes, userstyle: CSSStylesheet, layout: var Viewport, previousStyled: StyledNode): (FlexibleGrid, StyledNode) =
+type RenderedDocument* = object
+  grid*: FlexibleGrid
+  styledRoot*: StyledNode
+  images*: seq[StyledNode]
+
+proc renderDocument*(document: Document, userstyle: CSSStylesheet,
+    layout: var Viewport, previousStyled: StyledNode): RenderedDocument =
+  var grid: FlexibleGrid
   var uastyle = uastyle
   if document.mode == QUIRKS:
     uastyle = quirkstyle
-  let styledNode = document.applyStylesheets(uastyle, userstyle, previousStyled)
-  result[1] = styledNode
-  layout.renderLayout(styledNode)
-  result[0].setLen(0)
-  for root in layout.root:
-    result[0].renderBlockBox(root, 0, 0, window)
-  if result[0].len == 0:
-    result[0].addLine()
+  let styledRoot = document.applyStylesheets(uastyle, userstyle, previousStyled)
+  let rootBox = layout.renderLayout(styledRoot)
+  grid.renderBlockBox(rootBox, 0, 0, document.window.attrs)
+  if grid.len == 0:
+    grid.addLine()
+  return RenderedDocument(
+    grid: grid,
+    styledRoot: styledRoot
+  )
diff --git a/src/types/mime.nim b/src/types/mime.nim
index cd4c2d51..96742337 100644
--- a/src/types/mime.nim
+++ b/src/types/mime.nim
@@ -9,6 +9,7 @@ const DefaultGuess = [
   ("xht", "application/xhtml+xml"),
   ("txt", "text/plain"),
   ("css", "text/css"),
+  ("png", "image/png"),
   ("", "text/plain")
 ].toTable()