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)