import streams import unicode import strutils import tables import json import types/enums import types/tagtypes import utils/twtstr import utils/radixtree import html/dom import html/entity type HTMLParseState = object closed: bool parents: seq[Node] parsedNode: Node a: string b: string attrs: seq[string] in_comment: bool in_script: bool in_style: bool in_noscript: bool in_body: bool elementNode: Element textNode: Text commentNode: Comment func inputSize*(str: string): int = if str.len == 0: return 20 for c in str: if not c.isDigit: return 20 return str.parseInt() #w3m's getescapecmd and parse_tag, transpiled to nim and heavily modified. #(C) Copyright 1994-2002 by Akinori Ito #(C) Copyright 2002-2011 by Akinori Ito, Hironori Sakamoto, Fumitoshi Ukai # #Use, modification and redistribution of this software is hereby granted, #provided that this entire copyright notice is included on any copies of #this software and applications and derivations thereof. # #This software is provided on an "as is" basis, without warranty of any #kind, either expressed or implied, as to any matter including, but not #limited to warranty of fitness of purpose, or merchantability, or #results obtained from use of this software. proc getescapecmd(buf: string, at: var int): string = var i = at if buf[i] == '#': #num inc i var num: int if buf[i].tolower() == 'x': #hex inc i if not isdigit(buf[i]): at = i return "" num = hexValue(buf[i]) inc i while i < buf.len and hexValue(buf[i]) != -1: num *= 0x10 num += hexValue(buf[i]) inc i else: #dec if not isDigit(buf[i]): at = i return "" num = decValue(buf[i]) inc i while i < buf.len and isDigit(buf[i]): num *= 10 num += decValue(buf[i]) inc i if buf[i] == ';': inc i at = i return $(Rune(num)) elif not isAlphaAscii(buf[i]): return "" var n = entityMap var s = "" while true: s &= buf[i] if not entityMap.hasPrefix(s, n): break let pn = n n = n{s} if n != pn: s = "" inc i if n.leaf: at = i return n.value return "" type DOMParsedTag = object tagid: TagType attrs: Table[string, string] open: bool proc parse_tag(buf: string, at: var int): DOMParsedTag = var tag = DOMParsedTag() tag.open = true #Parse tag name var tagname = "" inc at if buf[at] == '/': inc at tag.open = false at = skipBlanks(buf, at) while at < buf.len and not buf[at].isWhitespace() and not (tag.open and buf[at] == '/') and buf[at] != '>': tagname &= buf[at].tolower() at += buf.runeLenAt(at) tag.tagid = tagType(tagname) at = skipBlanks(buf, at) while at < buf.len and buf[at] != '>': var value = "" var attrname = "" while at < buf.len and buf[at] != '=' and not buf[at].isWhitespace() and buf[at] != '>': attrname &= buf[at].tolower() at += buf.runeLenAt(at) at = skipBlanks(buf, at) if buf[at] == '=': inc at at = skipBlanks(buf, at) if at < buf.len and (buf[at] == '"' or buf[at] == '\''): let startc = buf[at] inc at while at < buf.len and buf[at] != startc: if buf[at + 1] == '&': inc at value &= getescapecmd(buf, at) else: var r: Rune fastRuneAt(buf, at, r) value &= r if at < buf.len: inc at elif at < buf.len: while at < buf.len and not buf[at].isWhitespace() and buf[at] != '>': value &= buf[at] at += buf.runeLenAt(at) if attrname.len > 0: tag.attrs[attrname] = value while at < buf.len and buf[at] != '>': at += buf.runeLenAt(at) if at < buf.len and buf[at] == '>': inc at return tag proc insertNode(parent: Node, node: Node) = parent.childNodes.add(node) if parent.childNodes.len > 1: let prevSibling = parent.childNodes[^1] prevSibling.nextSibling = node node.previousSibling = prevSibling node.parentNode = parent if parent.nodeType == ELEMENT_NODE: node.parentElement = (Element)parent if parent.ownerDocument != nil: node.ownerDocument = parent.ownerDocument elif parent.nodeType == DOCUMENT_NODE: node.ownerDocument = (Document)parent if node.nodeType == ELEMENT_NODE: parent.children.add((Element)node) let element = ((Element)node) if element.ownerDocument != nil: node.ownerDocument.all_elements.add((Element)node) element.ownerDocument.type_elements[element.tagType].add(element) if element.id != "": if not (element.id in element.ownerDocument.id_elements): element.ownerDocument.id_elements[element.id] = newSeq[Element]() element.ownerDocument.id_elements[element.id].add(element) for c in element.classList: if not (c in element.ownerDocument.class_elements): element.ownerDocument.class_elements[c] = newSeq[Element]() element.ownerDocument.class_elements[c].add(element) proc processDocumentBody(state: var HTMLParseState) = if not state.in_body: state.in_body = true if state.elementNode.ownerDocument != nil: state.elementNode = state.elementNode.ownerDocument.body proc processDocumentAddNode(state: var HTMLParseState, newNode: Node) = if state.elementNode.nodeType == ELEMENT_NODE and state.elementNode.tagType == TAG_HTML: if state.in_body: state.elementNode = state.elementNode.ownerDocument.body else: state.elementNode = state.elementNode.ownerDocument.head insertNode(state.elementNode, newNode) proc processDocumentEndNode(state: var HTMLParseState) = if state.elementNode == nil or state.elementNode.nodeType == DOCUMENT_NODE: return state.elementNode = state.elementNode.parentElement proc processDocumentText(state: var HTMLParseState) = if state.textNode != nil and state.textNode.data.len > 0: processDocumentBody(state) if state.textNode == nil: state.textNode = newText() processDocumentAddNode(state, state.textNode) proc processDocumentStartElement(state: var HTMLParseState, element: Element, tag: DOMParsedTag) = var add = true for k, v in tag.attrs: element.attributes[k] = element.newAttr(k, v) element.id = element.getAttrValue("id") if element.attributes.hasKey("class"): for w in unicode.split(element.attributes["class"].value, Rune(' ')): element.classList.add(w) case element.tagType of TAG_SCRIPT: state.in_script = true of TAG_NOSCRIPT: state.in_noscript = true of TAG_STYLE: state.in_style = true of TAG_SELECT: HTMLSelectElement(element).name = element.getAttrValue("name") HTMLSelectElement(element).value = element.getAttrValue("value") of TAG_INPUT: HTMLInputElement(element).value = element.getAttrValue("value") HTMLInputElement(element).itype = element.getAttrValue("type").inputType() HTMLInputElement(element).size = element.getAttrValue("size").inputSize() of TAG_A: HTMLAnchorElement(element).href = element.getAttrValue("href") of TAG_OPTION: HTMLOptionElement(element).value = element.getAttrValue("href") of TAG_HTML: add = false of TAG_HEAD: add = false of TAG_BODY: add = false processDocumentBody(state) else: discard if state.elementNode.nodeType == ELEMENT_NODE: case element.tagType of SelfClosingTagTypes: if state.elementNode.tagType == element.tagType: processDocumentEndNode(state) of TAG_H1: HTMLHeadingElement(element).rank = 1 of TAG_H2: HTMLHeadingElement(element).rank = 2 of TAG_H3: HTMLHeadingElement(element).rank = 3 of TAG_H4: HTMLHeadingElement(element).rank = 4 of TAG_H5: HTMLHeadingElement(element).rank = 5 of TAG_H6: HTMLHeadingElement(element).rank = 6 else: discard if state.elementNode.tagType == TAG_P and element.tagType in PClosingTagTypes: processDocumentEndNode(state) if add: processDocumentAddNode(state, element) state.elementNode = element if element.tagType in VoidTagTypes: processDocumentEndNode(state) proc processDocumentEndElement(state: var HTMLParseState, tag: DOMParsedTag) = if tag.tagid in VoidTagTypes: return if tag.tagid == TAG_HEAD: state.in_body = true return if tag.tagid == TAG_BODY: return if state.elementNode.nodeType == ELEMENT_NODE and tag.tagid != state.elementNode.tagType: if state.elementNode.tagType in SelfClosingTagTypes: processDocumentEndNode(state) processDocumentEndNode(state) proc processDocumentTag(state: var HTMLParseState, tag: DOMParsedTag) = if state.in_script: if tag.tagid == TAG_SCRIPT: state.in_script = false else: return if state.in_style: if tag.tagid == TAG_STYLE: state.in_style = false else: return if state.in_noscript: if tag.tagid == TAG_NOSCRIPT: state.in_noscript = false else: return if tag.open: processDocumentStartElement(state, newHtmlElement(tag.tagid), tag) else: processDocumentEndElement(state, tag) proc processDocumentPart(state: var HTMLParseState, buf: string) = var at = 0 var max = 0 var was_script = false max = buf.len while at < max: case buf[at] of '&': inc at let p = getescapecmd(buf, at) if state.in_comment: state.commentNode.data &= p else: processDocumentText(state) state.textNode.data &= p of '<': if state.in_comment: state.commentNode.data &= buf[at] inc at else: var p = at inc p if p < max and buf[p] == '!': inc p if p < max and buf[p] == '-': inc p if p < max and buf[p] == '-': inc p at = p state.in_comment = true let comment = newComment() state.commentNode = comment processDocumentAddNode(state, comment) if state.textNode != nil: state.textNode = nil else: #TODO for doctype while p < max and buf[p] != '>': inc p at = p + 1 continue if not state.in_comment: if state.textNode != nil: state.textNode = nil p = at var tag = parse_tag(buf, at) was_script = state.in_script processDocumentTag(state, tag) # if (was_script) { # if (state->in_script) { # ptr = p; # processDocumentText(&state->parentNode, &state->textNode); # Strcat_char(((CharacterData *)state->textNode)->data, *ptr++); # } else if (buffer->javascript_enabled) { # loadJSToBuffer(buffer, childTextContentNode(state->parentNode->lastChild)->ptr, "", state->document); # } # } elif buf[at] == '-' and state.in_comment: var p = at inc p if p < max and buf[p] == '-': inc p if p < max and buf[p] == '>': inc p at = p state.commentNode = nil state.in_comment = false if state.in_comment: state.commentNode.data &= buf[at] inc at else: var r: Rune fastRuneAt(buf, at, r) if state.in_comment: state.commentNode.data &= $r else: processDocumentText(state) state.textNode.data &= $r proc parseHtml*(inputStream: Stream): Document = let document = newDocument() insertNode(document, document.root) insertNode(document.root, document.head) insertNode(document.root, document.body) var state = HTMLParseState() state.elementNode = document.root var till_when = false var buf = "" var lineBuf: string while not inputStream.atEnd(): lineBuf = inputStream.readLine() buf &= lineBuf var at = 0 while at < lineBuf.len: case lineBuf[at] of '<': till_when = true of '>': till_when = false else: discard at += lineBuf.runeLenAt(at) if till_when: continue processDocumentPart(state, buf) buf = "" inputStream.close() return document