about summary refs log tree commit diff stats
path: root/src/html
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2024-03-14 20:57:45 +0100
committerbptato <nincsnevem662@gmail.com>2024-03-14 21:05:16 +0100
commitd26766c4c4015990703e84e8136f96d222edbc97 (patch)
tree7f412f8ca98d2b04323da5cf2fd607efbd6c408d /src/html
parenta8f05f18fdd64485c26b453e62e8073b50e271ef (diff)
downloadchawan-d26766c4c4015990703e84e8136f96d222edbc97.tar.gz
Move around some modules
* extern -> gone, runproc absorbed by pager, others moved into io/
* display -> local/ (where else would we display?)
* xhr -> html/
* move out WindowAttributes from term, so we don't depend on local
  from server
Diffstat (limited to 'src/html')
-rw-r--r--src/html/dom.nim2
-rw-r--r--src/html/env.nim6
-rw-r--r--src/html/formdata.nim177
-rw-r--r--src/html/xmlhttprequest.nim118
4 files changed, 299 insertions, 4 deletions
diff --git a/src/html/dom.nim b/src/html/dom.nim
index 2f36d81a..163e4a66 100644
--- a/src/html/dom.nim
+++ b/src/html/dom.nim
@@ -10,7 +10,6 @@ import css/cssparser
 import css/mediaquery
 import css/sheet
 import css/values
-import display/term
 import html/catom
 import html/enums
 import html/event
@@ -37,6 +36,7 @@ import types/matrix
 import types/referrer
 import types/url
 import types/vector
+import types/winattrs
 import utils/mimeguess
 import utils/strwidth
 import utils/twtstr
diff --git a/src/html/env.nim b/src/html/env.nim
index 29104707..3ffff116 100644
--- a/src/html/env.nim
+++ b/src/html/env.nim
@@ -2,12 +2,13 @@ import std/selectors
 import std/streams
 
 import bindings/quickjs
-import display/term
 import html/catom
 import html/chadombuilder
 import html/dom
 import html/event
+import html/formdata
 import html/script
+import html/xmlhttprequest
 import io/promise
 import js/base64
 import js/console
@@ -24,8 +25,7 @@ import loader/request
 import loader/response
 import types/blob
 import types/url
-import xhr/formdata
-import xhr/xmlhttprequest
+import types/winattrs
 
 # NavigatorID
 proc appCodeName(navigator: ptr Navigator): string {.jsfget.} = "Mozilla"
diff --git a/src/html/formdata.nim b/src/html/formdata.nim
new file mode 100644
index 00000000..bbf9a843
--- /dev/null
+++ b/src/html/formdata.nim
@@ -0,0 +1,177 @@
+import std/base64
+import std/streams
+
+import html/catom
+import html/dom
+import html/enums
+import js/domexception
+import js/javascript
+import js/tojs
+import types/blob
+import types/formdata
+import utils/twtstr
+
+import chame/tags
+
+proc constructEntryList*(form: HTMLFormElement, submitter: Element = nil,
+    encoding = "UTF-8"): seq[FormDataEntry]
+
+proc generateBoundary(): string =
+  let urandom = newFileStream("/dev/urandom")
+  let s = urandom.readStr(32)
+  urandom.close()
+  # 32 * 4 / 3 (padded) = 44 + prefix string is 22 bytes = 66 bytes
+  return "----WebKitFormBoundary" & base64.encode(s)
+
+proc newFormData0*(): FormData =
+  return FormData(boundary: generateBoundary())
+
+proc newFormData*(form: HTMLFormElement = nil,
+    submitter: HTMLElement = nil): DOMResult[FormData] {.jsctor.} =
+  let this = newFormData0()
+  if form != nil:
+    if submitter != nil:
+      if not submitter.isSubmitButton():
+        return errDOMException("Submitter must be a submit button",
+          "InvalidStateError")
+      if FormAssociatedElement(submitter).form != form:
+        return errDOMException("Submitter's form owner is not form",
+          "InvalidStateError")
+    if not form.constructingEntryList:
+      this.entries = constructEntryList(form, submitter)
+  return ok(this)
+
+#TODO filename should not be allowed for string entries
+# in other words, this should be an overloaded function, not just an or type
+proc append*[T: string|Blob](this: FormData, name: string, value: T,
+    filename = opt(string)) {.jsfunc.} =
+  when T is Blob:
+    let filename = if filename.isSome:
+      filename.get
+    elif value of WebFile:
+      WebFile(value).name
+    else:
+      "blob"
+    this.entries.add(FormDataEntry(
+      name: name,
+      isstr: false,
+      value: value,
+      filename: filename
+    ))
+  else: # string
+    this.entries.add(FormDataEntry(
+      name: name,
+      isstr: true,
+      svalue: value
+    ))
+
+proc delete(this: FormData, name: string) {.jsfunc.} =
+  for i in countdown(this.entries.high, 0):
+    if this.entries[i].name == name:
+      this.entries.delete(i)
+
+proc get(ctx: JSContext, this: FormData, name: string): JSValue {.jsfunc.} =
+  for entry in this.entries:
+    if entry.name == name:
+      if entry.isstr:
+        return toJS(ctx, entry.svalue)
+      else:
+        return toJS(ctx, entry.value)
+  return JS_NULL
+
+proc getAll(ctx: JSContext, this: FormData, name: string): seq[JSValue]
+    {.jsfunc.} =
+  for entry in this.entries:
+    if entry.name == name:
+      if entry.isstr:
+        result.add(toJS(ctx, entry.svalue))
+      else:
+        result.add(toJS(ctx, entry.value))
+
+proc add(list: var seq[FormDataEntry], entry: tuple[name, value: string]) =
+  list.add(FormDataEntry(
+    name: entry.name,
+    isstr: true,
+    svalue: entry.value
+  ))
+
+func toNameValuePairs*(list: seq[FormDataEntry]):
+    seq[tuple[name, value: string]] =
+  for entry in list:
+    if entry.isstr:
+      result.add((entry.name, entry.svalue))
+    else:
+      result.add((entry.name, entry.name))
+
+# https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#constructing-the-form-data-set
+# Warning: we skip the first "constructing entry list" check; the caller must
+# do it.
+proc constructEntryList*(form: HTMLFormElement, submitter: Element = nil,
+    encoding = "UTF-8"): seq[FormDataEntry] =
+  assert not form.constructingEntryList
+  form.constructingEntryList = true
+  var entrylist: seq[FormDataEntry] = @[]
+  for field in form.controls:
+    if field.findAncestor({TAG_DATALIST}) != nil or
+        field.attrb(atDisabled) or
+        field.isButton() and Element(field) != submitter:
+      continue
+    if field of HTMLInputElement:
+      let field = HTMLInputElement(field)
+      if field.inputType in {INPUT_CHECKBOX, INPUT_RADIO} and not field.checked:
+        continue
+      if field.inputType == INPUT_IMAGE:
+        var name = field.attr(atName)
+        if name != "":
+          name &= '.'
+        entrylist.add((name & 'x', $field.xcoord))
+        entrylist.add((name & 'y', $field.ycoord))
+        continue
+    #TODO custom elements
+    let name = field.attr(atName)
+    if name == "":
+      continue
+    if field of HTMLSelectElement:
+      let field = HTMLSelectElement(field)
+      for option in field.options:
+        if option.selected and not option.isDisabled:
+          entrylist.add((name, option.value))
+    elif field of HTMLInputElement:
+      let field = HTMLInputElement(field)
+      case field.inputType
+      of INPUT_CHECKBOX, INPUT_RADIO:
+        let v = field.attr(atValue)
+        let value = if v != "":
+          v
+        else:
+          "on"
+        entrylist.add((name, value))
+      of INPUT_FILE:
+        #TODO file
+        discard
+      of INPUT_HIDDEN:
+        if name.equalsIgnoreCase("_charset_"):
+          entrylist.add((name, encoding))
+        else:
+          entrylist.add((name, field.value))
+      else:
+        entrylist.add((name, field.value))
+    elif field of HTMLButtonElement:
+      entrylist.add((name, HTMLButtonElement(field).value))
+    elif field of HTMLTextAreaElement:
+      entrylist.add((name, HTMLTextAreaElement(field).value))
+    else:
+      assert false, "Tag type " & $field.tagType &
+        " not accounted for in constructEntryList"
+    if field of HTMLTextAreaElement or
+        field of HTMLInputElement and
+        HTMLInputElement(field).inputType in AutoDirInput:
+      let dirname = field.attr(atDirname)
+      if dirname != "":
+        let dir = "ltr" #TODO bidi
+        entrylist.add((dirname, dir))
+  form.constructingEntryList = false
+  return entrylist
+
+proc addFormDataModule*(ctx: JSContext) =
+  ctx.registerType(FormData)
diff --git a/src/html/xmlhttprequest.nim b/src/html/xmlhttprequest.nim
new file mode 100644
index 00000000..e9905760
--- /dev/null
+++ b/src/html/xmlhttprequest.nim
@@ -0,0 +1,118 @@
+import std/options
+import std/strutils
+
+import bindings/quickjs
+import html/dom
+import html/event
+import js/domexception
+import js/fromjs
+import js/javascript
+import loader/headers
+import loader/request
+import loader/response
+import types/url
+
+type
+  XMLHttpRequestResponseType = enum
+    TYPE_UNKNOWN = ""
+    TYPE_ARRAYBUFFER = "arraybuffer"
+    TYPE_BLOB = "blob"
+    TYPE_DOCUMENT = "document"
+    TYPE_JSON = "json"
+    TYPE_TEXT = "text"
+
+  XMLHttpRequestState = enum
+    UNSENT = 0u16
+    OPENED = 1u16
+    HEADERS_RECEIVED = 2u16
+    LOADING = 3u16
+    DONE = 4u16
+
+  XMLHttpRequestFlag = enum
+    SEND_FLAG, UPLOAD_LISTENER_FLAG, SYNC_FLAG
+
+  XMLHttpRequestEventTarget = ref object of EventTarget
+    onloadstart {.jsgetset.}: EventHandler
+    onprogress {.jsgetset.}: EventHandler
+    onabort {.jsgetset.}: EventHandler
+    onerror {.jsgetset.}: EventHandler
+    onload {.jsgetset.}: EventHandler
+    ontimeout {.jsgetset.}: EventHandler
+    onloadend {.jsgetset.}: EventHandler
+
+  XMLHttpRequestUpload = ref object of XMLHttpRequestEventTarget
+
+  XMLHttpRequest = ref object of XMLHttpRequestEventTarget
+    onreadystatechange {.jsgetset.}: EventHandler
+    readyState: XMLHttpRequestState
+    upload {.jsget.}: XMLHttpRequestUpload
+    flags: set[XMLHttpRequestFlag]
+    requestMethod: HttpMethod
+    requestURL: URL
+    authorRequestHeaders: Headers
+    response: Response
+    responseType {.jsgetset.}: XMLHttpRequestResponseType
+
+jsDestructor(XMLHttpRequestEventTarget)
+jsDestructor(XMLHttpRequestUpload)
+jsDestructor(XMLHttpRequest)
+
+func newXMLHttpRequest(): XMLHttpRequest {.jsctor.} =
+  let upload = XMLHttpRequestUpload()
+  return XMLHttpRequest(
+    upload: upload,
+    authorRequestHeaders: newHeaders()
+  )
+
+func readyState(this: XMLHttpRequest): uint16 {.jsfget.} =
+  return uint16(this.readyState)
+
+proc parseMethod(s: string): DOMResult[HttpMethod] =
+  return case s.toLowerAscii()
+  of "get": ok(HTTP_GET)
+  of "delete": ok(HTTP_DELETE)
+  of "head": ok(HTTP_HEAD)
+  of "options": ok(HTTP_OPTIONS)
+  of "patch": ok(HTTP_PATCH)
+  of "post": ok(HTTP_POST)
+  of "put": ok(HTTP_PUT)
+  of "connect", "trace", "track":
+    errDOMException("Forbidden method", "SecurityError")
+  else:
+    errDOMException("Invalid method", "SyntaxError")
+
+proc open(ctx: JSContext, this: XMLHttpRequest, httpMethod, url: string):
+    Err[DOMException] {.jsfunc.} =
+  let httpMethod = ?parseMethod(httpMethod)
+  let global = JS_GetGlobalObject(ctx)
+  let window = fromJS[Window](ctx, global)
+  JS_FreeValue(ctx, global)
+  let x = if window.isSome:
+    parseURL(url, some(window.get.document.baseURL))
+  else:
+    parseURL(url)
+  if x.isNone:
+    return errDOMException("Invalid URL", "SyntaxError")
+  let parsedURL = x.get
+  #TODO async, username, password arguments
+  let async = true
+  #TODO if async is false... probably just throw.
+  #TODO terminate fetch controller
+  this.flags.excl(SEND_FLAG)
+  this.flags.excl(UPLOAD_LISTENER_FLAG)
+  if async:
+    this.flags.excl(SYNC_FLAG)
+  else:
+    this.flags.incl(SYNC_FLAG)
+  this.requestMethod = httpMethod
+  this.authorRequestHeaders = newHeaders()
+  this.response = makeNetworkError()
+  this.requestURL = parsedURL
+  return ok()
+
+proc addXMLHttpRequestModule*(ctx: JSContext) =
+  let eventTargetCID = ctx.getClass("EventTarget")
+  let xhretCID = ctx.registerType(XMLHttpRequestEventTarget, eventTargetCID)
+  ctx.registerType(XMLHttpRequestUpload, xhretCID)
+  let xhrCID = ctx.registerType(XMLHttpRequest, xhretCID)
+  ctx.defineConsts(xhrCID, XMLHttpRequestState, uint16)