diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client.nim | 23 | ||||
-rw-r--r-- | src/css/parser.nim | 6 | ||||
-rw-r--r-- | src/html/dom.nim | 227 | ||||
-rw-r--r-- | src/html/parser.nim | 23 | ||||
-rw-r--r-- | src/io/buffer.nim | 274 | ||||
-rw-r--r-- | src/io/lineedit.nim | 12 | ||||
-rw-r--r-- | src/io/loader.nim | 10 | ||||
-rw-r--r-- | src/layout/box.nim | 3 | ||||
-rw-r--r-- | src/layout/engine.nim | 85 | ||||
-rw-r--r-- | src/render/renderdocument.nim | 5 | ||||
-rw-r--r-- | src/types/url.nim | 2 | ||||
-rw-r--r-- | src/utils/twtstr.nim | 38 |
12 files changed, 580 insertions, 128 deletions
diff --git a/src/client.nim b/src/client.nim index 330b04aa..bba2911d 100644 --- a/src/client.nim +++ b/src/client.nim @@ -99,12 +99,15 @@ proc readPipe(client: Client, ctype: string) = client.buffer.drawBuffer() var g_client: Client -proc gotoUrl(client: Client, url: Url, prevurl = none(Url), force = false, newbuf = true, ctype = "") = +proc gotoUrl(client: Client, url: Url, click = none(ClickAction), prevurl = none(Url), force = false, newbuf = true, ctype = "") = setControlCHook(proc() {.noconv.} = raise newException(InterruptError, "Interrupted")) if force or prevurl.issome or not prevurl.get.equals(url, true): try: - let page = client.loader.getPage(url) + let page = if click.isnone: + client.loader.getPage(url) + else: + client.loader.getPage(url, click.get.smethod, click.get.mimetype, click.get.body, click.get.multipart) if page.s != nil: if newbuf: client.addBuffer() @@ -126,29 +129,29 @@ proc gotoUrl(client: Client, url: Url, prevurl = none(Url), force = false, newbu client.buffer.location = url client.setupBuffer() -proc gotoUrl(client: Client, url: string, prevurl = none(Url), force = false, newbuf = true, ctype = "") = +proc gotoUrl(client: Client, url: string, click = none(ClickAction), prevurl = none(Url), force = false, newbuf = true, ctype = "") = var oldurl = prevurl if oldurl.isnone and client.buffer != nil: oldurl = client.buffer.location.some let newurl = parseUrl(url, oldurl) if newurl.isnone: loadError("Invalid URL " & url) - client.gotoUrl(newurl.get, oldurl, force, newbuf, ctype) + client.gotoUrl(newurl.get, click, oldurl, force, newbuf, ctype) proc loadUrl(client: Client, url: string, ctype = "") = let firstparse = parseUrl(url) if firstparse.issome: - client.gotoUrl(url, none(Url), true, true, ctype) + client.gotoUrl(url, none(ClickAction), none(Url), true, true, ctype) else: try: let cdir = parseUrl("file://" & getCurrentDir() & DirSep) - client.gotoUrl(url, cdir, true, true, ctype) + client.gotoUrl(url, none(ClickAction), cdir, true, true, ctype) except LoadError: - client.gotoUrl("http://" & url, none(Url), true, true, ctype) + client.gotoUrl("http://" & url, none(ClickAction), none(Url), true, true, ctype) proc reloadPage(client: Client) = let pbuffer = client.buffer - client.gotoUrl(pbuffer.location, none(Url), true, false) + client.gotoUrl(pbuffer.location, none(ClickAction), none(Url), true, false) client.buffer.setCursorXY(pbuffer.cursorx, pbuffer.cursory) client.buffer.setFromXY(pbuffer.fromx, pbuffer.fromy) client.buffer.contenttype = pbuffer.contenttype @@ -164,8 +167,8 @@ proc changeLocation(client: Client) = proc click(client: Client) = let s = client.buffer.click() - if s != "": - client.gotoUrl(s) + if s.issome and s.get.url != "": + client.gotoUrl(s.get.url, s) proc toggleSource*(client: Client) = let buffer = client.buffer diff --git a/src/css/parser.nim b/src/css/parser.nim index 0277bf41..1db94958 100644 --- a/src/css/parser.nim +++ b/src/css/parser.nim @@ -374,6 +374,8 @@ proc consumeComments(state: var CSSTokenizerState) = proc consumeToken(state: var CSSTokenizerState): CSSToken = state.consumeComments() + if not state.has(): + return let r = state.consume() case r of Rune('\n'), Rune('\t'), Rune(' '), Rune('\f'), Rune('\r'): @@ -456,7 +458,9 @@ proc tokenizeCSS*(inputStream: Stream): seq[CSSParsedItem] = state.stream = inputStream state.buf = state.stream.readLine().toRunes() while state.has(): - result.add(state.consumeToken()) + let tok = state.consumeToken() + if tok != nil: + result.add(tok) inputStream.close() diff --git a/src/html/dom.nim b/src/html/dom.nim index 22020c58..51bec09a 100644 --- a/src/html/dom.nim +++ b/src/html/dom.nim @@ -6,6 +6,7 @@ import css/values import css/sheet import html/tags import types/url +import utils/twtstr type EventTarget* = ref EventTargetObj @@ -77,11 +78,16 @@ type HTMLElement* = ref object of ElementObj HTMLInputElement* = ref object of HTMLElement - itype*: InputType + inputType*: InputType autofocus*: bool required*: bool value*: string size*: int + checked*: bool + xcoord*: int + ycoord*: int + file*: Option[Url] + form*: HTMLFormElement HTMLAnchorElement* = ref object of HTMLElement href*: string @@ -121,17 +127,49 @@ type href*: string rel*: string + HTMLFormElement* = ref object of HTMLElement + name*: string + smethod*: string + enctype*: string + target*: string + novalidate*: bool + constructingentrylist*: bool + inputs*: seq[HTMLInputElement] + # For debugging -template `$`*(node: Node): string = +func `$`*(node: Node): string = case node.nodeType of ELEMENT_NODE: let element = Element(node) - return "Element of " & $element.tagType & ", children: {\n" & $element.childNodes & "\n}" + "Element of " & $element.tagType & ", children: {\n" & $element.childNodes & "\n}" of TEXT_NODE: let text = Text(node) - return "Text: " & text.data + "Text: " & text.data + else: + "Node of " & $node.nodeType + +iterator elements*(document: Document, tag: TagType): Element {.inline.} = + for element in document.type_elements[tag]: + yield element + +iterator radiogroup(form: HTMLFormElement): HTMLInputElement {.inline.} = + for input in form.inputs: + if input.inputType == INPUT_RADIO: + yield input + +iterator radiogroup(document: Document): HTMLInputElement {.inline.} = + for input in document.elements(TAG_INPUT): + let input = HTMLInputElement(input) + if input.form == nil and input.inputType == INPUT_RADIO: + yield input + +iterator radiogroup*(input: HTMLInputElement): HTMLInputElement {.inline.} = + if input.form != nil: + for input in input.form.radiogroup: + yield input else: - return "Node of " & $node.nodeType + for input in input.ownerDocument.radiogroup: + yield input iterator textNodes*(node: Node): Text {.inline.} = for node in node.childNodes: @@ -219,6 +257,21 @@ func firstNode*(node: Node): bool = func lastNode*(node: Node): bool = return node.parentElement != nil and node.parentElement.childNodes[^1] == node +func attr*(element: Element, s: string): string = + return element.attributes.getOrDefault(s, "") + +func attri*(element: Element, s: string): Option[int] = + let a = element.attr(s) + try: + return some(parseInt(a)) + except ValueError: + return none(int) + +func attrb*(element: Element, s: string): bool = + if s in element.attributes: + return true + return false + func toInputType*(str: string): InputType = case str of "button": INPUT_BUTTON @@ -245,46 +298,92 @@ func toInputType*(str: string): InputType = of "week": INPUT_WEEK else: INPUT_UNKNOWN +func inputString*(input: HTMLInputElement): string = + var text = case input.inputType + of INPUT_CHECKBOX, INPUT_RADIO: + if input.checked: "*" + else: " " + of INPUT_SEARCH, INPUT_TEXT: + if input.size > 0: input.value.padToWidth(input.size) + else: input.value + of INPUT_PASSWORD: + '*'.repeat(input.value.len).padToWidth(input.size) + of INPUT_RESET: + if input.value != "": input.value + else: "RESET" + of INPUT_SUBMIT, INPUT_BUTTON: + if input.value != "": input.value + else: "SUBMIT" + of INPUT_FILE: + if input.file.isnone: "".padToWidth(input.size) + else: input.file.get.path.serialize_unicode().padToWidth(input.size) + else: + input.value + return text + +func isButton*(element: Element): bool = + if element.tagType == TAG_BUTTON: + return true + if element.tagType == TAG_INPUT: + let element = HTMLInputElement(element) + return element.inputType in {INPUT_SUBMIT, INPUT_BUTTON, INPUT_RESET, INPUT_IMAGE} + return false + +func isSubmitButton*(element: Element): bool = + if element.tagType == TAG_BUTTON: + return element.attr("type") == "submit" + elif element.tagType == TAG_INPUT: + let element = HTMLInputElement(element) + return element.inputType in {INPUT_SUBMIT, INPUT_IMAGE} + return false + +func action*(element: Element): string = + if element.isSubmitButton(): + if element.attrb("formaction"): + return element.attr("formaction") + if element.tagType == TAG_INPUT: + let element = HTMLInputElement(element) + if element.form != nil: + if element.form.attrb("action"): + return element.form.attr("action") + return "" + +func enctype*(element: Element): string = + if element.isSubmitButton(): + if element.attrb("formenctype"): + return element.attr("formenctype") + if element.tagType == TAG_INPUT: + let element = HTMLInputElement(element) + if element.form != nil: + if element.form.attrb("enctype"): + return element.form.attr("enctype") + return "application/x-www-form-urlencoded" + +func smethod*(element: Element): string = + if element.isSubmitButton(): + if element.attrb("formmethod"): + return element.attr("formmethod") + if element.tagType == TAG_INPUT: + let element = HTMLInputElement(element) + if element.form != nil: + if element.form.attrb("method"): + return element.form.attr("method") + return "GET" + +func target*(element: Element): string = + if element.attrb("target"): + return element.attr("target") + for base in element.ownerDocument.elements(TAG_BASE): + if base.attrb("target"): + return base.attr("target") + return "" + func findAncestor*(node: Node, tagTypes: set[TagType]): Element = for element in node.ancestors: if element.tagType in tagTypes: return element return nil -func attr*(element: Element, s: string): string = - return element.attributes.getOrDefault(s, "") - -func attri*(element: Element, s: string): Option[int] = - let a = element.attr(s) - try: - return some(parseInt(a)) - except ValueError: - return none(int) - -proc applyOrdinal*(elem: HTMLLIElement) = - let val = elem.attri("value") - if val.issome: - elem.ordinalvalue = val.get - else: - let owner = elem.findAncestor({TAG_OL, TAG_UL, TAG_MENU}) - if owner == nil: - elem.ordinalvalue = 1 - else: - case owner.tagType - of TAG_OL: - let ol = HTMLOListElement(owner) - elem.ordinalvalue = ol.ordinalcounter - inc ol.ordinalcounter - of TAG_UL: - let ul = HTMLUListElement(owner) - elem.ordinalvalue = ul.ordinalcounter - inc ul.ordinalcounter - of TAG_MENU: - let menu = HTMLMenuElement(owner) - elem.ordinalvalue = menu.ordinalcounter - inc menu.ordinalcounter - else: discard - func newText*(): Text = new(result) result.nodeType = TEXT_NODE @@ -323,6 +422,8 @@ func newHtmlElement*(document: Document, tagType: TagType): HTMLElement = result = new(HTMLStyleElement) of TAG_LINK: result = new(HTMLLinkElement) + of TAG_FORM: + result = new(HTMLFormElement) else: result = new(HTMLElement) @@ -351,5 +452,53 @@ func getElementById*(document: Document, id: string): Element = return nil return document.id_elements[id][0] +func baseUrl*(document: Document): Url = + var href = "" + for base in document.elements(TAG_BASE): + if base.attr("href") != "": + href = base.attr("href") + if href == "": + return document.location + let url = parseUrl(href, document.location.some) + if url.isnone: + return document.location + return url.get + func getElementsByTag*(document: Document, tag: TagType): seq[Element] = return document.type_elements[tag] + +proc applyOrdinal*(elem: HTMLLIElement) = + let val = elem.attri("value") + if val.issome: + elem.ordinalvalue = val.get + else: + let owner = elem.findAncestor({TAG_OL, TAG_UL, TAG_MENU}) + if owner == nil: + elem.ordinalvalue = 1 + else: + case owner.tagType + of TAG_OL: + let ol = HTMLOListElement(owner) + elem.ordinalvalue = ol.ordinalcounter + inc ol.ordinalcounter + of TAG_UL: + let ul = HTMLUListElement(owner) + elem.ordinalvalue = ul.ordinalcounter + inc ul.ordinalcounter + of TAG_MENU: + let menu = HTMLMenuElement(owner) + elem.ordinalvalue = menu.ordinalcounter + inc menu.ordinalcounter + else: discard + +proc reset*(form: HTMLFormElement) = + for input in form.inputs: + case input.inputType + of INPUT_SEARCH, INPUT_TEXT, INPUT_PASSWORD: + input.value = input.attr("value") + of INPUT_CHECKBOX, INPUT_RADIO: + input.checked = input.attrb("checked") + of INPUT_FILE: + input.file = none(Url) + else: discard + input.rendered = false diff --git a/src/html/parser.nim b/src/html/parser.nim index 305d1afa..b998525a 100644 --- a/src/html/parser.nim +++ b/src/html/parser.nim @@ -24,6 +24,7 @@ type textNode: Text commentNode: Comment document: Document + formowners: seq[HTMLFormElement] func inputSize*(str: string): int = if str.len == 0: @@ -249,9 +250,14 @@ proc processDocumentStartElement(state: var HTMLParseState, element: Element, ta HTMLSelectElement(element).name = element.attr("name") HTMLSelectElement(element).value = element.attr("value") of TAG_INPUT: - HTMLInputElement(element).value = element.attr("value") - HTMLInputElement(element).itype = element.attr("type").inputType() - HTMLInputElement(element).size = element.attr("size").inputSize() + let element = HTMLInputElement(element) + element.value = element.attr("value") + element.inputType = element.attr("type").inputType() + element.size = element.attr("size").inputSize() + element.checked = element.attrb("checked") + if state.formowners.len > 0: + element.form = state.formowners[^1] + element.form.inputs.add(element) of TAG_A: HTMLAnchorElement(element).href = element.attr("href") of TAG_OPTION: @@ -287,6 +293,14 @@ proc processDocumentStartElement(state: var HTMLParseState, element: Element, ta of TAG_LINK: HTMLLinkElement(element).href = element.attr("href") HTMLLinkElement(element).rel = element.attr("rel") + of TAG_FORM: + let element = HTMLFormElement(element) + element.name = element.attr("name") + element.smethod = element.attr("method") + element.enctype = element.attr("enctype") + element.target = element.attr("target") + element.novalidate = element.attrb("novalidate") + state.formowners.add(element) else: discard if not state.in_body and not (element.tagType in HeadTagTypes): @@ -325,6 +339,9 @@ proc processDocumentEndElement(state: var HTMLParseState, tag: DOMParsedTag) = return of TAG_BODY: return + of TAG_FORM: + if state.formowners.len > 0: + discard state.formowners.pop() of TAG_STYLE: let style = HTMLStyleElement(state.elementNode) var str = "" diff --git a/src/io/buffer.nim b/src/io/buffer.nim index 6663458f..8140ce7b 100644 --- a/src/io/buffer.nim +++ b/src/io/buffer.nim @@ -1,5 +1,8 @@ +import httpclient import options +import os import streams +import tables import terminal import unicode @@ -9,6 +12,7 @@ import html/dom import html/tags import html/parser import io/cell +import io/lineedit import io/loader import io/term import layout/box @@ -197,16 +201,32 @@ func currentDisplayCell(buffer: Buffer): FixedCell = let row = (buffer.cursory - buffer.fromy) * buffer.width return buffer.display[row + buffer.currentCellOrigin()] -func getLink(node: Node): Element = +func getLink(node: Node): HTMLAnchorElement = if node == nil: return nil if node.nodeType == ELEMENT_NODE and Element(node).tagType == TAG_A: - return Element(node) - return node.findAncestor({TAG_A}) + return HTMLAnchorElement(node) + return HTMLAnchorElement(node.findAncestor({TAG_A})) + +const ClickableElements = { + TAG_A, TAG_INPUT +} + +func getClickable(node: Node): Element = + if node == nil: + return nil + if node.nodeType == ELEMENT_NODE: + let element = Element(node) + if element.tagType in ClickableElements: + return element + return node.findAncestor(ClickableElements) func getCursorLink(buffer: Buffer): Element = return buffer.currentDisplayCell().node.getLink() +func getCursorClickable(buffer: Buffer): Element = + return buffer.currentDisplayCell().node.getClickable() + func currentLine(buffer: Buffer): string = return buffer.lines[buffer.cursory].str @@ -470,12 +490,12 @@ proc cursorNextLink*(buffer: Buffer) = var i = line.findFormatN(buffer.currentCursorBytes()) - 1 var link: Element = nil if i >= 0: - link = line.formats[i].node.getLink() + link = line.formats[i].node.getClickable() inc i while i < line.formats.len: let format = line.formats[i] - let fl = format.node.getLink() + let fl = format.node.getClickable() if fl != nil and fl != link: buffer.setCursorXB(format.pos) return @@ -486,7 +506,7 @@ proc cursorNextLink*(buffer: Buffer) = i = 0 while i < line.formats.len: let format = line.formats[i] - let fl = format.node.getLink() + let fl = format.node.getClickable() if fl != nil and fl != link: buffer.setCursorXBY(format.pos, y) return @@ -497,12 +517,12 @@ proc cursorPrevLink*(buffer: Buffer) = var i = line.findFormatN(buffer.currentCursorBytes()) - 1 var link: Element = nil if i >= 0: - link = line.formats[i].node.getLink() + link = line.formats[i].node.getClickable() dec i while i >= 0: let format = line.formats[i] - let fl = format.node.getLink() + let fl = format.node.getClickable() if fl != nil and fl != link: buffer.setCursorXB(format.pos) return @@ -513,7 +533,7 @@ proc cursorPrevLink*(buffer: Buffer) = i = line.formats.len - 1 while i >= 0: let format = line.formats[i] - let fl = format.node.getLink() + let fl = format.node.getClickable() if fl != nil and fl != link: #go to beginning of link var ly = y #last y @@ -523,7 +543,7 @@ proc cursorPrevLink*(buffer: Buffer) = i = line.formats.len - 1 while i >= 0: let format = line.formats[i] - let nl = format.node.getLink() + let nl = format.node.getClickable() if nl == fl: ly = iy lx = format.pos @@ -688,9 +708,7 @@ proc updateHover(buffer: Buffer) = let link = thisnode.getLink() if link != nil: - if link.tagType == TAG_A: - let anchor = HTMLAnchorElement(link) - buffer.hovertext = parseUrl(anchor.href, buffer.location.some).serialize() + buffer.hovertext = parseUrl(link.href, buffer.location.some).serialize() else: buffer.hovertext = "" @@ -803,12 +821,230 @@ proc displayStatusMessage*(buffer: Buffer) = print(buffer.generateStatusMessage()) print(SGR()) -proc click*(buffer: Buffer): string = - let link = buffer.getCursorLink() - if link != nil: - if link.tagType == TAG_A: - return HTMLAnchorElement(link).href - return "" +type + ClickAction* = object + url*: string + smethod*: string + mimetype*: string + body*: string + multipart*: MultipartData + +# https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#constructing-the-form-data-set +proc constructEntryList(form: HTMLFormElement, submitter: Element = nil, encoding: string = ""): Table[string, string] = + if form.constructingentrylist: + return + form.constructingentrylist = true + + var entrylist: Table[string, string] + for field in form.inputs: + if field.findAncestor({TAG_DATALIST}) != nil or + field.attrb("disabled") or + field.isButton() and Element(field) != submitter: + continue + + if field.inputType == INPUT_IMAGE: + let name = if field.attr("name") != "": + field.attr("name") & '.' + else: + "" + entrylist[name & 'x'] = $field.xcoord + entrylist[name & 'y'] = $field.ycoord + continue + + if field.attr("name") == "": + continue + + let name = field.attr("name") + #TODO select + if field.inputType in {INPUT_CHECKBOX, INPUT_RADIO}: + let value = if field.attr("value") != "": + field.attr("value") + else: + "on" + entrylist[name] = value + elif field.inputType == INPUT_FILE: + #TODO file + discard + elif field.inputType == INPUT_HIDDEN and name.equalsIgnoreCase("_charset_"): + let charset = if encoding != "": + encoding + else: + "UTF-8" + entrylist[name] = charset + else: + entrylist[name] = field.value + if field.tagType == TAG_TEXTAREA or + field.tagType == TAG_INPUT and field.inputType in {INPUT_TEXT, INPUT_SEARCH}: + if field.attr("dirname") != "": + let dirname = field.attr("dirname") + let dir = "ltr" #TODO bidi + entrylist[dirname] = dir + + form.constructingentrylist = false + return entrylist + +#https://url.spec.whatwg.org/#concept-urlencoded-serializer +proc serializeApplicationXWWFormUrlEncoded(kvs: Table[string, string]): string = + for name, value in kvs: + if result != "": + result &= '&' + result.percentEncode(name, ApplicationXWWWFormUrlEncodedSet, true) + result &= '=' + result.percentEncode(value, ApplicationXWWWFormUrlEncodedSet, true) + +#https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#multipart/form-data-encoding-algorithm +proc makeCRLF(s: string): string = + result = newStringOfCap(s.len) + var i = 0 + while i < s.len - 1: + if s[i] == '\r' and s[i + 1] != '\n': + result &= '\r' + result &= '\n' + elif s[i] != '\r' and s[i + 1] == '\n': + result &= s[i] + result &= '\r' + result &= '\n' + inc i + else: + result &= s[i] + inc i + +proc serializeMultipartFormData(kvs: Table[string, string]): MultipartData = + new(result) + for name, value in kvs: + let name = makeCRLF(name) + let value = makeCRLF(value) + result[name] = value + +proc serializePlainTextFormData(kvs: Table[string, string]): string = + for name, value in kvs: + result &= name + result &= '=' + result &= value + result &= '\r' + result &= '\n' + +proc submitForm(form: HTMLFormElement, submitter: Element): Option[ClickAction] = + let entrylist = form.constructEntryList(submitter) + + let action = if submitter.action() == "": + $form.ownerDocument.location + else: + submitter.action() + + let url = parseUrl(action, submitter.ownerDocument.baseUrl.some) + if url.isnone: + return none(ClickAction) + + var parsedaction = url.get + let scheme = parsedaction.scheme + let enctype = submitter.enctype() + let smethod = submitter.smethod().toupper() + if smethod notin ["GET", "POST"]: + return none(ClickAction) #TODO this shouldn't be possible + + let target = if submitter.isSubmitButton() and submitter.attrb("formtarget"): + submitter.attr("formtarget") + else: + submitter.target() + let noopener = true #TODO + + template mutateActionUrl() = + let query = serializeApplicationXWWFormUrlEncoded(entrylist) + parsedaction.query = query.some + return ClickAction(url: $parsedaction, smethod: smethod).some + + template submitAsEntityBody() = + var body: string + var mimetype: string + var multipart: MultipartData + case enctype + of "application/x-www-form-urlencoded": + body = serializeApplicationXWWFormUrlEncoded(entrylist) + mimeType = enctype + of "multipart/form-data": + multipart = serializeMultipartFormData(entrylist) + #mime type set by httpclient + of "text/plain": + body = serializePlainTextFormData(entrylist) + mimetype = enctype + else: + return none(ClickAction) #TODO this shouldn't be possible + return ClickAction(url: $parsedaction, smethod: smethod, body: body, mimetype: mimetype, multipart: multipart).some + + template getActionUrl() = + return ClickAction(url: $parsedaction).some + + case scheme + of "http", "https": + if smethod == "GET": + mutateActionUrl + elif smethod == "POST": + submitAsEntityBody + of "ftp": + getActionUrl + of "data": + if smethod == "GET": + mutateActionUrl + elif smethod == "POST": + getActionUrl + +proc click*(buffer: Buffer): Option[ClickAction] = + let clickable = buffer.getCursorClickable() + if clickable != nil: + case clickable.tagType + of TAG_A: + return ClickAction(url: HTMLAnchorElement(clickable).href).some + of TAG_INPUT: + let input = HTMLInputElement(clickable) + case input.inputType + of INPUT_SEARCH, INPUT_TEXT, INPUT_PASSWORD: + var value = input.value + print(HVP(buffer.height + 1, 1)) + print(EL()) + let status = readLine("TEXT: ", value, buffer.width, {'\r', '\n'}) + if status: + input.value = value + input.rendered = false + buffer.reshape = true + of INPUT_FILE: + var path = if input.file.issome: + input.file.get.path.serialize_unicode() + else: + "" + print(HVP(buffer.height + 1, 1)) + print(EL()) + let status = readLine("Filename: ", path, buffer.width, {'\r', '\n'}) + if status: + let cdir = parseUrl("file://" & getCurrentDir() & DirSep) + let path = parseUrl(path, cdir) + if path.issome: + input.file = path + input.rendered = false + buffer.reshape = true + of INPUT_CHECKBOX: + input.checked = not input.checked + input.rendered = false + buffer.reshape = true + of INPUT_RADIO: + for radio in input.radiogroup: + radio.checked = false + radio.rendered = false + input.checked = true + input.rendered = false + buffer.reshape = true + of INPUT_RESET: + if input.form != nil: + input.form.reset() + buffer.reshape = true + of INPUT_SUBMIT, INPUT_BUTTON: + if input.form != nil: + let submitaction = submitForm(input.form, input) + return submitaction + else: + discard + else: + discard proc drawBuffer*(buffer: Buffer) = var format = newFormat() diff --git a/src/io/lineedit.nim b/src/io/lineedit.nim index 67b6194b..ce719d47 100644 --- a/src/io/lineedit.nim +++ b/src/io/lineedit.nim @@ -104,9 +104,9 @@ proc fullRedraw(state: var LineState) = state.redraw() -proc insertCharseq(state: var LineState, cs: var seq[Rune]) = +proc insertCharseq(state: var LineState, cs: var seq[Rune], disallowed: set[char]) = let escNext = state.escNext - cs.keepIf((r) => escNext or not r.isControlChar()) + cs.keepIf((r) => (escNext or not r.isControlChar) and not (r.isAscii and char(r) in disallowed)) state.escNext = false if cs.len == 0: return @@ -120,7 +120,7 @@ proc insertCharseq(state: var LineState, cs: var seq[Rune]) = state.cursor += cs.len state.fullRedraw() -proc readLine*(current: var string, minlen, maxlen: int): bool = +proc readLine*(current: var string, minlen, maxlen: int, disallowed: set[char] = {}): bool = var state: LineState state.news = current.toRunes() state.cursor = state.news.len @@ -257,10 +257,10 @@ proc readLine*(current: var string, minlen, maxlen: int): bool = state.feedNext = true elif validateUtf8(state.s) == -1: var cs = state.s.toRunes() - state.insertCharseq(cs) + state.insertCharseq(cs, disallowed) else: state.feedNext = true -proc readLine*(prompt: string, current: var string, termwidth: int): bool = +proc readLine*(prompt: string, current: var string, termwidth: int, disallowed: set[char] = {}): bool = printesc(prompt) - readLine(current, prompt.lwidth(), termwidth - prompt.len) + readLine(current, prompt.lwidth(), termwidth - prompt.len, disallowed) diff --git a/src/io/loader.nim b/src/io/loader.nim index 5bd35a90..4bd365a8 100644 --- a/src/io/loader.nim +++ b/src/io/loader.nim @@ -18,7 +18,7 @@ proc newFileLoader*(): FileLoader = new(result) result.http = newHttpClient() -proc getPage*(loader: FileLoader, url: Url): LoadResult = +proc getPage*(loader: FileLoader, url: Url, smethod: string = "GET", mimetype = "", body: string = "", multipart: MultipartData = nil): LoadResult = if url.scheme == "file": when defined(windows) or defined(OS2) or defined(DOS): let path = url.path.serialize_unicode_windows() @@ -27,10 +27,16 @@ proc getPage*(loader: FileLoader, url: Url): LoadResult = result.contenttype = guessContentType(path) result.s = newFileStream(path, fmRead) elif url.scheme == "http" or url.scheme == "https": - let resp = loader.http.get(url.serialize(true)) + let requestheaders = newHttpHeaders({ "User-Agent": "chawan", "Content-Type": mimetype}, true) + let requestmethod = if smethod == "": + "GET" + else: + smethod + let resp = loader.http.request(url.serialize(true), requestmethod, body, requestheaders, multipart) let ct = resp.contentType() if ct != "": result.contenttype = ct.until(';') else: result.contenttype = guessContentType(url.path.serialize()) + resp.bodystream.setPosition(0) result.s = resp.bodyStream diff --git a/src/layout/box.nim b/src/layout/box.nim index 64fe0741..9947cd22 100644 --- a/src/layout/box.nim +++ b/src/layout/box.nim @@ -54,11 +54,12 @@ type rows*: seq[InlineRow] thisrow*: InlineRow - whitespace*: bool + whitespacenum*: int maxwidth*: int viewport*: Viewport node*: Node shrink*: bool + format*: ComputedFormat BlockContext* = ref object of InlineAtom inline*: InlineContext diff --git a/src/layout/engine.nim b/src/layout/engine.nim index 18939a57..3752ce05 100644 --- a/src/layout/engine.nim +++ b/src/layout/engine.nim @@ -49,13 +49,13 @@ func cellheight(ictx: InlineContext): int {.inline.} = # Whitespace between words func computeShift(ictx: InlineContext, specified: CSSSpecifiedValues): int = - if ictx.whitespace: + if ictx.whitespacenum > 0: if ictx.thisrow.atoms.len > 0 or specified.whitespacepre: let spacing = specified{"word-spacing"} if spacing.auto: - return ictx.cellwidth + return ictx.cellwidth * ictx.whitespacenum #return spacing.cells_w(ictx.viewport, 0) - return spacing.px(ictx.viewport) + return spacing.px(ictx.viewport) * ictx.whitespacenum return 0 func computeLineHeight(viewport: Viewport, specified: CSSSpecifiedValues): int = @@ -65,13 +65,15 @@ func computeLineHeight(viewport: Viewport, specified: CSSSpecifiedValues): int = proc newWord(state: var InlineState) = let word = InlineWord() - word.format = ComputedFormat() + let format = ComputedFormat() let specified = state.specified - word.format.color = specified{"color"} - word.format.fontstyle = specified{"font-style"} - word.format.fontweight = specified{"font-weight"} - word.format.textdecoration = specified{"text-decoration"} - word.format.node = state.node + format.color = specified{"color"} + format.fontstyle = specified{"font-style"} + format.fontweight = specified{"font-weight"} + format.textdecoration = specified{"text-decoration"} + format.node = state.node + word.format = format + state.ictx.format = format state.word = word proc horizontalAlignRow(ictx: InlineContext, row: InlineRow, specified: CSSSpecifiedValues, maxwidth: int, last = false) = @@ -155,8 +157,21 @@ proc verticalAlignRow(ictx: InlineContext) = baseline - atom.height atom.rely += diff +proc addSpacing(row: InlineRow, width, height: int, format: ComputedFormat) {.inline.} = + let spacing = InlineSpacing(width: width, height: height, format: format) + spacing.relx = row.width + row.width += spacing.width + row.atoms.add(spacing) + +proc flushWhitespace(ictx: InlineContext, specified: CSSSpecifiedValues) = + let shift = ictx.computeShift(specified) + ictx.whitespacenum = 0 + if shift > 0: + ictx.thisrow.addSpacing(shift, ictx.cellheight, ictx.format) + proc finishRow(ictx: InlineContext, specified: CSSSpecifiedValues, maxwidth: int, force = false) = if ictx.thisrow.atoms.len != 0 or force: + ictx.flushWhitespace(specified) ictx.verticalAlignRow() let oldrow = ictx.thisrow @@ -170,37 +185,30 @@ proc finish(ictx: InlineContext, specified: CSSSpecifiedValues, maxwidth: int) = for row in ictx.rows: ictx.horizontalAlignRow(row, specified, maxwidth, row == ictx.rows[^1]) -proc addSpacing(row: InlineRow, width, height: int, format: ComputedFormat) {.inline.} = - let spacing = InlineSpacing(width: width, height: height, format: format) - spacing.relx = row.width - row.width += spacing.width - row.atoms.add(spacing) - proc addAtom(ictx: InlineContext, atom: InlineAtom, maxwidth: int, specified: CSSSpecifiedValues) = var shift = ictx.computeShift(specified) - ictx.whitespace = false + ictx.whitespacenum = 0 # Line wrapping - if specified{"white-space"} notin {WHITESPACE_NOWRAP, WHITESPACE_PRE}: + if not specified.whitespacepre: if ictx.thisrow.width + atom.width + shift > maxwidth: ictx.finishRow(specified, maxwidth, false) # Recompute on newline shift = ictx.computeShift(specified) - ictx.whitespace = false if atom.width > 0 and atom.height > 0: atom.vertalign = specified{"vertical-align"} if shift > 0: - let format = if atom of InlineWord: - InlineWord(atom).format - else: - nil - ictx.thisrow.addSpacing(shift, ictx.cellheight, format) + ictx.thisrow.addSpacing(shift, ictx.cellheight, ictx.format) atom.relx += ictx.thisrow.width ictx.thisrow.lineheight = max(ictx.thisrow.lineheight, computeLineHeight(ictx.viewport, specified)) ictx.thisrow.width += atom.width ictx.thisrow.height = max(ictx.thisrow.height, atom.height) + if atom of InlineWord: + ictx.format = InlineWord(atom).format + else: + ictx.format = nil ictx.thisrow.atoms.add(atom) proc addWord(state: var InlineState) = @@ -224,23 +232,23 @@ proc checkWrap(state: var InlineState, r: Rune) = if state.ictx.thisrow.width + state.word.width + shift + r.width() * state.ictx.cellwidth > state.maxwidth: state.addWord() state.ictx.finishRow(state.specified, state.maxwidth, false) - state.ictx.whitespace = false + state.ictx.whitespacenum = 0 of WORD_BREAK_KEEP_ALL: if state.ictx.thisrow.width + state.word.width + shift + r.width() * state.ictx.cellwidth > state.maxwidth: state.ictx.finishRow(state.specified, state.maxwidth, false) - state.ictx.whitespace = false + state.ictx.whitespacenum = 0 else: discard proc processWhitespace(state: var InlineState, c: char) = state.addWord() case state.specified{"white-space"} of WHITESPACE_NORMAL, WHITESPACE_NOWRAP: - state.ictx.whitespace = true + state.ictx.whitespacenum = max(state.ictx.whitespacenum, 1) of WHITESPACE_PRE_LINE, WHITESPACE_PRE, WHITESPACE_PRE_WRAP: if c == '\n': state.ictx.flushLine(state.specified, state.maxwidth) else: - state.ictx.whitespace = true + inc state.ictx.whitespacenum proc renderText*(ictx: InlineContext, str: string, maxwidth: int, specified: CSSSpecifiedValues, node: Node) = var state: InlineState @@ -248,6 +256,7 @@ proc renderText*(ictx: InlineContext, str: string, maxwidth: int, specified: CSS state.ictx = ictx state.maxwidth = maxwidth state.node = node + state.ictx.flushWhitespace(state.specified) state.newWord() #if str.strip().len > 0: @@ -420,7 +429,10 @@ proc arrangeInlines(bctx: BlockContext, selfcontained: bool) = bctx.width += bctx.padding_right - bctx.width = min(bctx.width, bctx.compwidth) + if bctx.specified{"width"}.auto: + bctx.width = min(bctx.width, bctx.compwidth) + else: + bctx.width = bctx.compwidth proc alignBlock(box: BlockBox, selfcontained = false) @@ -434,7 +446,7 @@ proc alignInlineBlock(bctx: BlockContext, box: InlineBlockBox) = box.bctx.width += box.bctx.margin_right box.ictx.addAtom(box.bctx, bctx.compwidth, box.specified) - box.ictx.whitespace = false + box.ictx.whitespacenum = 0 proc alignInline(bctx: BlockContext, box: InlineBox) = assert box.ictx != nil @@ -563,10 +575,7 @@ proc getTextBox(box: CSSBox): InlineBox = new(result) result.t = DISPLAY_INLINE result.inlinelayout = true - if box.specified{"display"} == DISPLAY_INLINE: - result.specified = box.specified - else: - result.specified = box.specified.inheritProperties() + result.specified = box.specified.inheritProperties() proc getPseudoBox(bctx: BlockContext, specified: CSSSpecifiedValues): CSSBox = let box = getBox(specified) @@ -591,6 +600,12 @@ proc getPseudoBox(bctx: BlockContext, specified: CSSSpecifiedValues): CSSBox = box.children.add(content) return box +func getInputBox(box: CSSBox, input: HTMLInputElement, viewport: Viewport): InlineBox = + let textbox = box.getTextBox() + textbox.node = input + textbox.text.add(input.inputString()) + return textbox + proc generateBox(elem: Element, viewport: Viewport, bctx: BlockContext = nil): CSSBox = elem.rendered = true if viewport.map[elem.uid] != nil: @@ -679,6 +694,10 @@ proc generateBox(elem: Element, viewport: Viewport, bctx: BlockContext = nil): C bbox.node = elem add_box(bbox) + if elem.tagType == TAG_INPUT: + let input = HTMLInputElement(elem) + add_box(box.getInputBox(input, viewport)) + for child in elem.childNodes: case child.nodeType of ELEMENT_NODE: diff --git a/src/render/renderdocument.nim b/src/render/renderdocument.nim index 732bcf47..8c34e273 100644 --- a/src/render/renderdocument.nim +++ b/src/render/renderdocument.nim @@ -128,6 +128,11 @@ proc renderInlineContext(grid: var FlexibleGrid, ctx: InlineContext, x, y: int, for row in ctx.rows: let x = x + row.relx let y = y + row.rely + + let r = y div term.ppl + while grid.len <= r: + grid.addLine() + for atom in row.atoms: if atom of BlockContext: let ctx = BlockContext(atom) diff --git a/src/types/url.nim b/src/types/url.nim index c8c7085e..d4c8b91c 100644 --- a/src/types/url.nim +++ b/src/types/url.nim @@ -42,7 +42,7 @@ type port: Option[uint16] host: Option[Host] path*: UrlPath - query: Option[string] + query*: Option[string] fragment: Option[string] blob: Option[BlobUrlEntry] diff --git a/src/utils/twtstr.nim b/src/utils/twtstr.nim index 428276f1..6f324f49 100644 --- a/src/utils/twtstr.nim +++ b/src/utils/twtstr.nim @@ -27,16 +27,6 @@ func ansiReset*(str: string): string = result &= str result &= ansiResetCode -func maxString*(str: string, max: int): string = - if max < str.runeLen(): - return str.runeSubstr(0, max - 2) & "$" - return str - -func fitValueToSize*(str: string, size: int): string = - if str.runeLen < size: - return str & ' '.repeat(size - str.runeLen) - return str.maxString(size) - func isWhitespace*(c: char): bool {.inline.} = return c in {' ', '\n', '\r', '\t', '\f'} @@ -419,14 +409,19 @@ const QueryPercentEncodeSet* = (ControlPercentEncodeSet + {' ', '"', '#', '<', ' const SpecialQueryPercentEncodeSet* = (QueryPercentEncodeSet + {'\''}) const PathPercentEncodeSet* = (QueryPercentEncodeSet + {'?', '`', '{', '}'}) const UserInfoPercentEncodeSet* = (PathPercentEncodeSet + {'/', ':', ';', '=', '@', '['..'^', '|'}) -proc percentEncode*(append: var string, c: char, set: set[char]) {.inline.} = - if c notin set: +const ComponentPercentEncodeSet* = (UserInfoPercentEncodeSet + {'$'..'&', '+', ','}) +const ApplicationXWWWFormUrlEncodedSet* = (ComponentPercentEncodeSet + {'!', '\''..')', '~'}) + +proc percentEncode*(append: var string, c: char, set: set[char], spaceAsPlus = false) {.inline.} = + if spaceAsPlus and c == ' ': + append &= c + elif c notin set: append &= c else: append &= '%' append &= c.toHex() -proc percentEncode*(append: var string, s: string, set: set[char]) {.inline.} = +proc percentEncode*(append: var string, s: string, set: set[char], spaceAsPlus = false) {.inline.} = for c in s: append.percentEncode(c, set) @@ -853,6 +848,23 @@ func width*(s: seq[Rune], min: int): int = func breaksWord*(r: Rune): bool = return not (r.isDigitAscii() or r.width() == 0 or r.isAlpha()) +func padToWidth*(str: string, size: int, schar = '$'): string = + if str.width() < size: + return str & ' '.repeat(size - str.width()) + else: + let size = size - 1 + result = newStringOfCap(str.len) + + var w = 0 + var i = 0 + while i < str.len: + var r: Rune + fastRuneAt(str, i, r) + if w + r.width <= size: + result &= r + w += r.width + result &= schar + const CanHaveDakuten = "かきくけこさしすせそたちつてとはひふへほカキクケコサシスセソタチツテトハヒフヘホ".toRunes() const CanHaveHandakuten = "はひふへほハヒフヘホ".toRunes() |