about summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/css/cascade.nim9
-rw-r--r--src/css/selectorparser.nim1
-rw-r--r--src/html/chadombuilder.nim7
-rw-r--r--src/html/dom.nim263
-rw-r--r--src/server/buffer.nim12
5 files changed, 204 insertions, 88 deletions
diff --git a/src/css/cascade.nim b/src/css/cascade.nim
index 6a2172d7..8b0d9574 100644
--- a/src/css/cascade.nim
+++ b/src/css/cascade.nim
@@ -427,6 +427,13 @@ proc applyRulesFrameInvalid(frame: CascadeFrame; ua, user: CSSStylesheet;
       )
       let styledText = styledParent.newStyledReplacement(content, pseudo)
       styledParent.children.add(styledText)
+    of peSVG:
+      let content = CSSContent(
+        t: ContentImage,
+        bmp: SVGSVGElement(styledParent.node).bitmap
+      )
+      let styledText = styledParent.newStyledReplacement(content, pseudo)
+      styledParent.children.add(styledText)
     of peCanvas:
       let bmp = HTMLCanvasElement(styledParent.node).bitmap
       if bmp != nil and bmp.cacheId != 0:
@@ -537,6 +544,8 @@ proc appendChildren(styledStack: var seq[CascadeFrame]; frame: CascadeFrame;
   of TAG_AUDIO: styledStack.stackAppend(frame, styledChild, peAudio, idx)
   of TAG_BR: styledStack.stackAppend(frame, styledChild, peNewline, idx)
   of TAG_CANVAS: styledStack.stackAppend(frame, styledChild, peCanvas, idx)
+  elif element.tagType(Namespace.SVG) == TAG_SVG:
+    styledStack.stackAppend(frame, styledChild, peSVG, idx)
   else:
     for i in countdown(element.childList.high, 0):
       let child = element.childList[i]
diff --git a/src/css/selectorparser.nim b/src/css/selectorparser.nim
index 90da97c9..58a2e4f4 100644
--- a/src/css/selectorparser.nim
+++ b/src/css/selectorparser.nim
@@ -21,6 +21,7 @@ type
     peVideo = "-cha-video"
     peAudio = "-cha-audio"
     peCanvas = "-cha-canvas"
+    peSVG = "-cha-svg"
 
   PseudoClass* = enum
     pcFirstChild = "first-child"
diff --git a/src/html/chadombuilder.nim b/src/html/chadombuilder.nim
index f71850b1..7e33fbd3 100644
--- a/src/html/chadombuilder.nim
+++ b/src/html/chadombuilder.nim
@@ -126,7 +126,7 @@ proc createElementForTokenImpl(builder: ChaDOMBuilder; localName: CAtom;
     namespace: Namespace; intendedParent: Node; htmlAttrs: Table[CAtom, string];
     xmlAttrs: seq[ParsedAttr[CAtom]]): Node =
   let document = builder.document
-  let element = document.newHTMLElement(localName, namespace)
+  let element = document.newElement(localName, namespace)
   for k, v in htmlAttrs:
     element.attr(k, v)
   for attr in xmlAttrs:
@@ -203,6 +203,11 @@ proc elementPoppedImpl(builder: ChaDOMBuilder; element: Node) =
   elif element of HTMLScriptElement:
     assert builder.poppedScript == nil or not builder.document.scriptingEnabled
     builder.poppedScript = HTMLScriptElement(element)
+  elif element of SVGSVGElement:
+    let window = element.document.window
+    if window != nil:
+      let svg = SVGSVGElement(element)
+      window.loadResource(svg)
 
 proc newChaDOMBuilder(url: URL; window: Window; factory: CAtomFactory;
     confidence: CharsetConfidence; charset = DefaultCharset): ChaDOMBuilder =
diff --git a/src/html/dom.nim b/src/html/dom.nim
index 5fbea56e..8c630a47 100644
--- a/src/html/dom.nim
+++ b/src/html/dom.nim
@@ -99,6 +99,7 @@ type
     factory*: CAtomFactory
     pendingResources*: seq[EmptyPromise]
     imageURLCache: Table[string, CachedURLImage]
+    svgCache*: Table[string, SVGSVGElement]
     images*: bool
     styling*: bool
     # ID of the next image
@@ -266,6 +267,13 @@ type
   HTMLElement* = ref object of Element
     dataset {.jsget.}: DOMStringMap
 
+  SVGElement = ref object of Element
+
+  SVGSVGElement* = ref object of SVGElement
+    bitmap*: NetworkBitmap
+    shared: seq[SVGSVGElement] # elements that serialize to the same string
+    fetchStarted: bool
+
   FormAssociatedElement* = ref object of HTMLElement
     form*: HTMLFormElement
     parserInserted*: bool
@@ -441,6 +449,8 @@ jsDestructor(HTMLImageElement)
 jsDestructor(HTMLVideoElement)
 jsDestructor(HTMLAudioElement)
 jsDestructor(HTMLIFrameElement)
+jsDestructor(SVGElement)
+jsDestructor(SVGSVGElement)
 jsDestructor(Node)
 jsDestructor(NodeList)
 jsDestructor(HTMLCollection)
@@ -539,7 +549,9 @@ proc create2DContext(jctx: JSContext; target: HTMLCanvasElement;
   let ps = newPosixStream(pipefd[1])
   let ctlreq = newRequest(newURL("stream:canvas-ctl-" & $imageId).get)
   let ctlres = loader.doRequest(ctlreq)
-  doAssert ctlres.res == 0
+  if ctlres.res != 0: # loader forgot about me :(
+    ps.sclose()
+    return
   let cacheId = loader.addCacheFile(ctlres.outputId, loader.clientPid)
   target.bitmap.cacheId = cacheId
   let request = newRequest(
@@ -555,7 +567,7 @@ proc create2DContext(jctx: JSContext; target: HTMLCanvasElement;
     ctlres.close()
     return
   ctlres.close()
-  response.resume()
+  response.close()
   target.ctx2d = CanvasRenderingContext2D(
     bitmap: target.bitmap,
     canvas: target,
@@ -1074,8 +1086,8 @@ proc toAtom(document: Document; prefix: NamespacePrefix): CAtom =
 func tagTypeNoNS(element: Element): TagType =
   return element.document.toTagType(element.localName)
 
-func tagType*(element: Element): TagType =
-  if element.namespace != Namespace.HTML:
+func tagType*(element: Element; namespace = Namespace.HTML): TagType =
+  if element.namespace != namespace:
     return TAG_UNKNOWN
   return element.tagTypeNoNS
 
@@ -2450,51 +2462,49 @@ func serializesAsVoid(element: Element): bool =
   const Extra = {TAG_BASEFONT, TAG_BGSOUND, TAG_FRAME, TAG_KEYGEN, TAG_PARAM}
   return element.tagType in VoidElements + Extra
 
-func serializeFragment*(node: Node): string
+func serializeFragment(res: var string; node: Node)
 
-func serializeFragmentInner(child: Node; parentType: TagType): string =
-  result = ""
+func serializeFragmentInner(res: var string; child: Node; parentType: TagType) =
   if child of Element:
     let element = Element(child)
     let tags = element.document.toStr(element.localName)
-    result &= '<'
+    res &= '<'
     #TODO qualified name if not HTML, SVG or MathML
-    result &= tags
+    res &= tags
     #TODO custom elements
     for attr in element.attrs:
-      #TODO namespaced attrs
-      let k = element.document.toStr(attr.localName)
-      result &= ' ' & k & "=\"" & attr.value.escapeText(true) & "\""
-    result &= '>'
-    result &= element.serializeFragment()
-    result &= "</"
-    result &= tags
-    result &= '>'
+      let k = element.document.toStr(attr.qualifiedName)
+      res &= ' ' & k & "=\"" & attr.value.escapeText(true) & "\""
+    res &= '>'
+    res.serializeFragment(element)
+    res &= "</"
+    res &= tags
+    res &= '>'
   elif child of Text:
     let text = Text(child)
     const LiteralTags = {
       TAG_STYLE, TAG_SCRIPT, TAG_XMP, TAG_IFRAME, TAG_NOEMBED, TAG_NOFRAMES,
       TAG_PLAINTEXT, TAG_NOSCRIPT
     }
-    result = if parentType in LiteralTags:
-      text.data
+    if parentType in LiteralTags:
+      res &= text.data
     else:
-      text.data.escapeText()
+      res &= text.data.escapeText()
   elif child of Comment:
-    result &= "<!--" & Comment(child).data & "-->"
+    res &= "<!--" & Comment(child).data & "-->"
   elif child of ProcessingInstruction:
     let inst = ProcessingInstruction(child)
-    result &= "<?" & inst.target & " " & inst.data & '>'
+    res &= "<?" & inst.target & " " & inst.data & '>'
   elif child of DocumentType:
-    result &= "<!DOCTYPE " & DocumentType(child).name & '>'
+    res &= "<!DOCTYPE " & DocumentType(child).name & '>'
 
-func serializeFragment*(node: Node): string =
+func serializeFragment(res: var string; node: Node) =
   var node = node
   var parentType = TAG_UNKNOWN
   if node of Element:
     let element = Element(node)
     if element.serializesAsVoid():
-      return ""
+      return
     if element of HTMLTemplateElement:
       node = HTMLTemplateElement(element).content
     else:
@@ -2503,10 +2513,12 @@ func serializeFragment*(node: Node): string =
         # Pretend parentType is not noscript, so we do not append literally
         # in serializeFragmentInner.
         parentType = TAG_UNKNOWN
-  var s = ""
   for child in node.childList:
-    s &= child.serializeFragmentInner(parentType)
-  return s
+    res.serializeFragmentInner(child, parentType)
+
+func serializeFragment*(node: Node): string =
+  result = ""
+  result.serializeFragment(node)
 
 # Element attribute reflection (getters)
 func innerHTML(element: Element): string {.jsfget.} =
@@ -2515,7 +2527,8 @@ func innerHTML(element: Element): string {.jsfget.} =
 
 func outerHTML(element: Element): string {.jsfget.} =
   #TODO xml
-  return element.serializeFragmentInner(TAG_UNKNOWN)
+  result = ""
+  result.serializeFragmentInner(element, TAG_UNKNOWN)
 
 func crossOrigin0(element: HTMLElement): CORSAttribute =
   if not element.attrb(satCrossorigin):
@@ -3023,65 +3036,65 @@ func newComment(ctx: JSContext; data: string = ""): Comment {.jsctor.} =
   return window.document.newComment(data)
 
 #TODO custom elements
-proc newHTMLElement*(document: Document; localName: CAtom;
-    namespace = Namespace.HTML; prefix = NO_PREFIX): HTMLElement =
+proc newElement*(document: Document; localName: CAtom;
+    namespace = Namespace.HTML; prefix = NO_PREFIX): Element =
   let tagType = document.toTagType(localName)
-  case tagType
+  let element: Element = case tagType
   of TAG_INPUT:
-    result = HTMLInputElement()
+    HTMLInputElement()
   of TAG_A:
     let anchor = HTMLAnchorElement()
     let localName = document.toAtom(satRel)
     anchor.relList = DOMTokenList(element: anchor, localName: localName)
-    result = anchor
+    anchor
   of TAG_SELECT:
-    result = HTMLSelectElement()
+    HTMLSelectElement()
   of TAG_OPTGROUP:
-    result = HTMLOptGroupElement()
+    HTMLOptGroupElement()
   of TAG_OPTION:
-    result = HTMLOptionElement()
+    HTMLOptionElement()
   of TAG_H1, TAG_H2, TAG_H3, TAG_H4, TAG_H5, TAG_H6:
-    result = HTMLHeadingElement()
+    HTMLHeadingElement()
   of TAG_BR:
-    result = HTMLBRElement()
+    HTMLBRElement()
   of TAG_SPAN:
-    result = HTMLSpanElement()
+    HTMLSpanElement()
   of TAG_OL:
-    result = HTMLOListElement()
+    HTMLOListElement()
   of TAG_UL:
-    result = HTMLUListElement()
+    HTMLUListElement()
   of TAG_MENU:
-    result = HTMLMenuElement()
+    HTMLMenuElement()
   of TAG_LI:
-    result = HTMLLIElement()
+    HTMLLIElement()
   of TAG_STYLE:
-    result = HTMLStyleElement()
+    HTMLStyleElement()
   of TAG_LINK:
     let link = HTMLLinkElement()
     let localName = document.toAtom(satRel)
     link.relList = DOMTokenList(element: link, localName: localName)
-    result = link
+    link
   of TAG_FORM:
     let form = HTMLFormElement()
     let localName = document.toAtom(satRel)
     form.relList = DOMTokenList(element: form, localName: localName)
-    result = form
+    form
   of TAG_TEMPLATE:
-    result = HTMLTemplateElement(
-      content: DocumentFragment(internalDocument: document, host: result)
-    )
+    let templ = HTMLTemplateElement(content: newDocumentFragment(document))
+    templ.content.host = templ
+    templ
   of TAG_UNKNOWN:
-    result = HTMLUnknownElement()
+    HTMLUnknownElement()
   of TAG_SCRIPT:
-    result = HTMLScriptElement(forceAsync: true)
+    HTMLScriptElement(forceAsync: true)
   of TAG_BASE:
-    result = HTMLBaseElement()
+    HTMLBaseElement()
   of TAG_BUTTON:
-    result = HTMLButtonElement()
+    HTMLButtonElement()
   of TAG_TEXTAREA:
-    result = HTMLTextAreaElement()
+    HTMLTextAreaElement()
   of TAG_LABEL:
-    result = HTMLLabelElement()
+    HTMLLabelElement()
   of TAG_CANVAS:
     let bitmap = if document.scriptingEnabled:
       NetworkBitmap(
@@ -3092,33 +3105,41 @@ proc newHTMLElement*(document: Document; localName: CAtom;
       )
     else:
       nil
-    result = HTMLCanvasElement(bitmap: bitmap)
+    HTMLCanvasElement(bitmap: bitmap)
   of TAG_IMG:
-    result = HTMLImageElement()
+    HTMLImageElement()
   of TAG_VIDEO:
-    result = HTMLVideoElement()
+    HTMLVideoElement()
   of TAG_AUDIO:
-    result = HTMLAudioElement()
+    HTMLAudioElement()
   of TAG_AREA:
     let area = HTMLAreaElement()
     let localName = document.toAtom(satRel)
     area.relList = DOMTokenList(element: area, localName: localName)
-    result = area
+    area
+  elif namespace == Namespace.SVG:
+    if tagType == TAG_SVG:
+      SVGSVGElement()
+    else:
+      SVGElement()
   else:
-    result = HTMLElement()
-  result.localName = localName
-  result.namespace = namespace
-  result.namespacePrefix = prefix
-  result.internalDocument = document
+    HTMLElement()
+  element.localName = localName
+  element.namespace = namespace
+  element.namespacePrefix = prefix
+  element.internalDocument = document
   let localName = document.toAtom(satClassList)
-  result.classList = DOMTokenList(element: result, localName: localName)
-  result.index = -1
-  result.elIndex = -1
-  result.dataset = DOMStringMap(target: result)
+  element.classList = DOMTokenList(element: element, localName: localName)
+  element.index = -1
+  element.elIndex = -1
+  if namespace == Namespace.HTML:
+    let element = HTMLElement(element)
+    element.dataset = DOMStringMap(target: element)
+  return element
 
 proc newHTMLElement*(document: Document; tagType: TagType): HTMLElement =
   let localName = document.toAtom(tagType)
-  return document.newHTMLElement(localName, Namespace.HTML, NO_PREFIX)
+  return HTMLElement(document.newElement(localName, Namespace.HTML, NO_PREFIX))
 
 proc newDocument*(factory: CAtomFactory): Document =
   assert factory != nil
@@ -3421,7 +3442,7 @@ proc loadResource*(window: Window; image: HTMLImageElement) =
       url.scheme = "https"
     let surl = $url
     window.imageURLCache.withValue(surl, p):
-      if p[].expiry > getTime().utc().toTime().toUnix():
+      if p[].expiry > getTime().toUnix():
         image.bitmap = p[].bmp
         return
       elif p[].loading:
@@ -3463,6 +3484,7 @@ proc loadResource*(window: Window; image: HTMLImageElement) =
           body = RequestBody(t: rbtOutput, outputId: response.outputId),
         )
         let r = window.corsFetch(request)
+        response.resume()
         response.close()
         var expiry = -1i64
         if "Cache-Control" in response.headers:
@@ -3483,19 +3505,16 @@ proc loadResource*(window: Window; image: HTMLImageElement) =
           let response = res.get
           # close immediately; all data we're interested in is in the headers.
           response.close()
-          if "Cha-Image-Dimensions" notin response.headers.table:
-            window.console.error("Cha-Image-Dimensions missing in",
-              $response.url)
-            return
-          let dims = response.headers.table["Cha-Image-Dimensions"][0]
-          let width = parseIntP(dims.until('x'))
-          let height = parseIntP(dims.after('x'))
-          if width.get(-1) < 0 or height.get(-1) < 0:
+          let headers = response.headers
+          let dims = headers.getOrDefault("Cha-Image-Dimensions")
+          let width = parseIntP(dims.until('x')).get(-1)
+          let height = parseIntP(dims.after('x')).get(-1)
+          if width < 0 or height < 0:
             window.console.error("wrong Cha-Image-Dimensions in", $response.url)
             return
           let bmp = NetworkBitmap(
-            width: width.get,
-            height: height.get,
+            width: width,
+            height: height,
             cacheId: cacheId,
             imageId: window.getImageId(),
             contentType: contentType
@@ -3510,6 +3529,78 @@ proc loadResource*(window: Window; image: HTMLImageElement) =
       )
     window.pendingResources.add(p)
 
+proc loadResource*(window: Window; svg: SVGSVGElement) =
+  if not window.images:
+    svg.invalid = svg.invalid or svg.bitmap != nil
+    svg.bitmap = nil
+    svg.fetchStarted = false
+    return
+  if svg.fetchStarted:
+    return
+  svg.fetchStarted = true
+  let s = svg.outerHTML
+  if s.len <= 4096: # try to dedupe if the SVG is small enough.
+    window.svgCache.withValue(s, elp):
+      svg.bitmap = elp.bitmap
+      if svg.bitmap != nil: # already decoded
+        svg.setInvalid()
+      else: # tell me when you're done
+        elp.shared.add(svg)
+      return
+    window.svgCache[s] = svg
+  var pipefd {.noinit.}: array[2, cint]
+  if pipe(pipefd) == -1:
+    return
+  let ps = newPosixStream(pipefd[1])
+  let imageId = window.getImageId()
+  let loader = window.loader
+  loader.passFd("svg-" & $imageId, pipefd[0])
+  discard close(pipefd[0])
+  let svgreq = newRequest(newURL("stream:svg-" & $imageId).get)
+  let svgres = loader.doRequest(svgreq)
+  if svgres.res != 0: # loader forgot about me :(
+    ps.sclose()
+    return
+  let cacheId = loader.addCacheFile(svgres.outputId, loader.clientPid)
+  try:
+    ps.sendDataLoop(s)
+  except IOError:
+    return
+  finally:
+    ps.sclose()
+  let request = newRequest(
+    newURL("img-codec+svg+xml:decode").get,
+    httpMethod = hmPost,
+    headers = newHeaders({"Cha-Image-Info-Only": "1"}),
+    body = RequestBody(t: rbtOutput, outputId: svgres.outputId)
+  )
+  let p = loader.fetch(request).then(proc(res: JSResult[Response]) =
+    svgres.close()
+    if res.isNone: # no SVG module; give up
+      return
+    let response = res.get
+    # close immediately; all data we're interested in is in the headers.
+    response.close()
+    let dims = response.headers.getOrDefault("Cha-Image-Dimensions")
+    let width = parseIntP(dims.until('x')).get(-1)
+    let height = parseIntP(dims.after('x')).get(-1)
+    if width < 0 or height < 0:
+      window.console.error("wrong Cha-Image-Dimensions in", $response.url)
+      return
+    svg.bitmap = NetworkBitmap(
+      width: width,
+      height: height,
+      cacheId: cacheId,
+      imageId: imageId,
+      contentType: "image/svg+xml"
+    )
+    for share in svg.shared:
+      share.bitmap = svg.bitmap
+      share.setInvalid()
+    svg.setInvalid()
+  )
+  window.pendingResources.add(p)
+
 proc reflectEvent(element: Element; target: EventTarget; name, ctype: StaticAtom;
     value: string) =
   let document = element.document
@@ -4537,7 +4628,7 @@ proc createElement(document: Document; localName: string):
     Namespace.HTML
   else:
     NO_NAMESPACE
-  return ok(document.newHTMLElement(localName, namespace))
+  return ok(document.newElement(localName, namespace))
 
 #TODO createElementNS
 
@@ -4606,7 +4697,7 @@ proc clone(node: Node; document = none(Document), deep = false): Node =
   let copy = if node of Element:
     #TODO is value
     let element = Element(node)
-    let x = document.newHTMLElement(element.localName, element.namespace,
+    let x = document.newElement(element.localName, element.namespace,
       element.namespacePrefix)
     x.id = element.id
     x.name = element.name
@@ -4962,7 +5053,7 @@ proc outerHTML(element: Element; s: string): Err[DOMException] {.jsfset.} =
     let ex = newDOMException("outerHTML is disallowed for Document children",
       "NoModificationAllowedError")
     return err(ex)
-  let parent = if parent0 of DocumentFragment:
+  let parent: Element = if parent0 of DocumentFragment:
     element.document.newHTMLElement(TAG_BODY)
   else:
     # neither a document, nor a document fragment => parent must be an
@@ -5038,6 +5129,8 @@ proc registerElements(ctx: JSContext; nodeCID: JSClassID) =
   register(HTMLVideoElement, TAG_VIDEO)
   register(HTMLAudioElement, TAG_AUDIO)
   register(HTMLIFrameElement, TAG_IFRAME)
+  let svgElementCID = ctx.registerType(SVGElement, parent = elementCID)
+  ctx.registerType(SVGSVGElement, parent = svgElementCID)
 
 proc addDOMModule*(ctx: JSContext) =
   let eventTargetCID = ctx.getClass("EventTarget")
diff --git a/src/server/buffer.nim b/src/server/buffer.nim
index 501a5d94..508bf66a 100644
--- a/src/server/buffer.nim
+++ b/src/server/buffer.nim
@@ -409,6 +409,10 @@ proc getCachedImageHover(buffer: Buffer; element: Element): string =
     let image = HTMLImageElement(element)
     if image.bitmap != nil and image.bitmap.cacheId != 0:
       return $image.bitmap.cacheId & ' ' & image.bitmap.contentType
+  elif element of SVGSVGElement:
+    let image = SVGSVGElement(element)
+    if image.bitmap != nil and image.bitmap.cacheId != 0:
+      return $image.bitmap.cacheId & ' ' & image.bitmap.contentType
   ""
 
 func getCursorStyledNode(buffer: Buffer; cursorx, cursory: int): StyledNode =
@@ -1753,8 +1757,12 @@ proc markURL*(buffer: Buffer; schemes: seq[string]) {.proxy.} =
 proc toggleImages0(buffer: Buffer): bool =
   buffer.config.images = not buffer.config.images
   buffer.window.images = buffer.config.images
-  for element in buffer.document.elements({TAG_IMG, TAG_IMAGE}):
-    buffer.window.loadResource(HTMLImageElement(element))
+  buffer.window.svgCache.clear()
+  for element in buffer.document.descendants:
+    if element of HTMLImageElement:
+      buffer.window.loadResource(HTMLImageElement(element))
+    elif element of SVGSVGElement:
+      buffer.window.loadResource(SVGSVGElement(element))
   buffer.savetask = true
   buffer.loadResources().then(proc() =
     if buffer.tasks[bcToggleImages] == 0: