import terminal import options import uri import strutils import unicode import fusion/htmlparser/xmltree import buffer import termattrs import htmlelement import twtstr import twtio import config import enums proc clearStatusMsg*(at: int) = setCursorPos(0, at) eraseLine() proc statusMsg*(str: string, at: int) = clearStatusMsg(at) print(str.ansiStyle(styleReverse).ansiReset()) type RenderState = object x: int y: int lastwidth: int fmtline: string rawline: string centerqueue: seq[HtmlNode] centerlen: int blanklines: int blankspaces: int nextspaces: int docenter: bool indent: int listval: int func newRenderState(): RenderState = return RenderState() proc write(state: var RenderState, s: string) = state.fmtline &= s state.rawline &= s proc write(state: var RenderState, fs: string, rs: string) = state.fmtline &= fs state.rawline &= rs proc flushLine(buffer: Buffer, state: var RenderState) = if state.rawline.len == 0: inc state.blanklines assert(state.rawline.runeLen() < buffer.width, "line too long:\n" & state.rawline) buffer.writefmt(state.fmtline) buffer.writeraw(state.rawline) state.x = 0 inc state.y state.nextspaces = 0 state.fmtline = "" state.rawline = "" proc addSpaces(buffer: Buffer, state: var RenderState, n: int) = if state.x + n > buffer.width: buffer.flushLine(state) return state.blankspaces += n state.write(' '.repeat(n)) state.x += n proc writeWrappedText(buffer: Buffer, state: var RenderState, node: HtmlNode) = state.lastwidth = 0 var n = 0 var fmtword = "" var rawword = "" var prevl = false for w in node.fmttext: if w.len > 0 and w[0] == '\e': fmtword &= w continue for r in w.runes: if r == Rune(' '): if rawword[0] == ' ' and prevl: #first byte can't fool comparison to ascii fmtword = fmtword.substr(1) rawword = rawword.substr(1) state.x -= 1 prevl = false state.write(fmtword, rawword) fmtword = "" rawword = "" fmtword &= r rawword &= r state.x += mk_wcwidth_cjk(r) if state.x >= buffer.width: state.lastwidth = max(state.lastwidth, state.x) buffer.flushLine(state) state.x = mk_wcswidth_cjk(rawword) prevl = true else: state.lastwidth = max(state.lastwidth, state.x) inc n state.write(fmtword, rawword) if prevl: state.x += mk_wcswidth_cjk(rawword) prevl = false state.lastwidth = max(state.lastwidth, state.x) proc preAlignNode(buffer: Buffer, node: HtmlNode, state: var RenderState) = let elem = node.nodeAttr() if state.rawline.len > 0 and node.openblock and state.blanklines == 0: buffer.flushLine(state) if node.openblock: while state.blanklines < max(elem.margin, elem.margintop): buffer.flushLine(state) state.indent += elem.indent if state.rawline.len > 0 and state.blanklines == 0 and node.displayed(): buffer.addSpaces(state, state.nextspaces) state.nextspaces = 0 if state.blankspaces < max(elem.margin, elem.marginleft): buffer.addSpaces(state, max(elem.margin, elem.marginleft) - state.blankspaces) if elem.centered and state.rawline.len == 0 and node.displayed(): buffer.addSpaces(state, max(buffer.width div 2 - state.centerlen div 2, 0)) state.centerlen = 0 if node.isElemNode() and elem.display == DISPLAY_LIST_ITEM and state.indent > 0: buffer.flushLine(state) var listchar = "" case elem.parentElement.tagType of TAG_UL: listchar = "•" of TAG_OL: inc state.listval listchar = $state.listval & ")" else: return buffer.addSpaces(state, state.indent) state.write(listchar) state.x += listchar.runeLen() buffer.addSpaces(state, 1) proc postAlignNode(buffer: Buffer, node: HtmlNode, state: var RenderState) = let elem = node.nodeAttr() if node.getRawLen() > 0: state.blanklines = 0 state.blankspaces = 0 if state.rawline.len > 0 and state.blanklines == 0: state.nextspaces += max(elem.margin, elem.marginright) if node.closeblock and (node.isTextNode() or elem.numChildNodes == 0): state.write($node.nodeAttr().tagType) buffer.flushLine(state) if node.closeblock: while state.blanklines < max(elem.margin, elem.marginbottom): buffer.flushLine(state) if node.isElemNode(): state.indent -= elem.indent if elem.tagType == TAG_BR and not node.openblock: buffer.flushLine(state) proc renderNode(buffer: Buffer, node: HtmlNode, state: var RenderState) = if node.isDocument(): return let elem = node.nodeAttr() if elem.tagType == TAG_TITLE: if node.isTextNode(): buffer.title = node.rawtext return else: discard if elem.hidden: return if not state.docenter: if elem.centered: state.centerqueue.add(node) if node.closeblock or elem.tagType == TAG_BR: state.docenter = true state.centerlen = 0 for node in state.centerqueue: state.centerlen += node.getRawLen() for node in state.centerqueue: buffer.renderNode(node, state) state.centerqueue.setLen(0) state.docenter = false return else: return if state.centerqueue.len > 0: state.docenter = true state.centerlen = 0 for node in state.centerqueue: state.centerlen += node.getRawLen() for node in state.centerqueue: buffer.renderNode(node, state) state.centerqueue.setLen(0) state.docenter = false buffer.preAlignNode(node, state) node.x = state.x node.y = state.y buffer.writeWrappedText(state, node) node.ex = state.x node.ey = state.y node.width = state.lastwidth - node.x - 1 node.height = state.y - node.y + 1 buffer.postAlignNode(node, state) iterator revItems*(n: XmlNode): XmlNode {.inline.} = var i = n.len - 1 while i >= 0: if n[i].kind != xnComment: yield n[i] i -= 1 type XmlHtmlNode* = ref XmlHtmlNodeObj XmlHtmlNodeObj = object xml*: XmlNode html*: HtmlNode proc setLastHtmlLine(buffer: Buffer, state: var RenderState) = if state.rawline.len != 0: buffer.flushLine(state) proc renderHtml*(buffer: Buffer) = var stack: seq[XmlHtmlNode] let first = XmlHtmlNode(xml: buffer.htmlSource, html: getHtmlNode(buffer.htmlSource, buffer.document)) stack.add(first) var state = newRenderState() while stack.len > 0: let currElem = stack.pop() buffer.addNode(currElem.html) buffer.renderNode(currElem.html, state) if currElem.xml.len > 0: var last = false for item in currElem.xml.revItems: let child = XmlHtmlNode(xml: item, html: getHtmlNode(item, currElem.html)) stack.add(child) currElem.html.childNodes.add(child.html) if not last and not child.html.hidden: last = true if HtmlElement(currElem.html).display == DISPLAY_BLOCK: eprint "elem", HtmlElement(currElem.html).tagType, "close @", child.html.nodeAttr().tagType stack[^1].html.closeblock = true if last: eprint "elem", HtmlElement(currElem.html).tagType, "open @", stack[^1].html.nodeAttr().tagType if HtmlElement(currElem.html).display == DISPLAY_BLOCK: stack[^1].html.openblock = true buffer.setLastHtmlLine(state) proc nrenderHtml*(buffer: Buffer) = var stack: seq[HtmlNode] let first = buffer.document stack.add(first) var state = newRenderState() while stack.len > 0: let currElem = stack.pop() buffer.addNode(currElem) buffer.renderNode(currElem, state) var i = currElem.childNodes.len - 1 while i >= 0: stack.add(currElem.childNodes[i]) i -= 1 buffer.setLastHtmlLine(state) proc drawHtml(buffer: Buffer) = var state = newRenderState() for node in buffer.nodes: buffer.renderNode(node, state) buffer.setLastHtmlLine(state) proc statusMsgForBuffer(buffer: Buffer) = var msg = $(buffer.cursory + 1) & "/" & $(buffer.lastLine() + 1) & " (" & $buffer.atPercentOf() & "%) " & "<" & buffer.title & ">" if buffer.hovertext.len > 0: msg &= " " & buffer.hovertext statusMsg(msg.maxString(buffer.width), buffer.height) proc cursorBufferPos(buffer: Buffer) = var x = buffer.cursorx var y = buffer.cursory - 1 - buffer.fromY termGoto(x, y + 1) proc displayBuffer(buffer: Buffer) = eraseScreen() termGoto(0, 0) print(buffer.visibleText().ansiReset()) proc inputLoop(attrs: TermAttributes, buffer: Buffer): bool = var s = "" var feedNext = false while true: stdout.showCursor() buffer.cursorBufferPos() if not feedNext: s = "" else: feedNext = false let c = getch() s &= c let action = getNormalAction(s) var redraw = false var reshape = false var nostatus = false case action of ACTION_QUIT: eraseScreen() return false of ACTION_CURSOR_LEFT: redraw = buffer.cursorLeft() of ACTION_CURSOR_DOWN: redraw = buffer.cursorDown() of ACTION_CURSOR_UP: redraw = buffer.cursorUp() of ACTION_CURSOR_RIGHT: redraw = buffer.cursorRight() of ACTION_CURSOR_LINEBEGIN: buffer.cursorLineBegin() of ACTION_CURSOR_LINEEND: buffer.cursorLineEnd() of ACTION_CURSOR_NEXT_WORD: redraw = buffer.cursorNextWord() of ACTION_CURSOR_PREV_WORD: redraw = buffer.cursorPrevWord() of ACTION_CURSOR_NEXT_LINK: redraw = buffer.cursorNextLink() of ACTION_CURSOR_PREV_LINK: redraw = buffer.cursorPrevLink() of ACTION_PAGE_DOWN: redraw = buffer.pageDown() of ACTION_PAGE_UP: redraw = buffer.pageUp() of ACTION_HALF_PAGE_DOWN: redraw = buffer.halfPageDown() of ACTION_HALF_PAGE_UP: redraw = buffer.halfPageUp() of ACTION_CURSOR_FIRST_LINE: redraw = buffer.cursorFirstLine() of ACTION_CURSOR_LAST_LINE: redraw = buffer.cursorLastLine() of ACTION_CURSOR_TOP: redraw = buffer.cursorTop() of ACTION_CURSOR_MIDDLE: redraw = buffer.cursorMiddle() of ACTION_CURSOR_BOTTOM: redraw = buffer.cursorBottom() of ACTION_CENTER_LINE: redraw = buffer.centerLine() of ACTION_SCROLL_DOWN: redraw = buffer.scrollDown() of ACTION_SCROLL_UP: redraw = buffer.scrollUp() of ACTION_CLICK: let selectedElem = buffer.findSelectedElement() if selectedElem.isSome: case selectedElem.get().tagType of TAG_INPUT: clearStatusMsg(buffer.height) let status = readLine("TEXT:", HtmlInputElement(selectedElem.get()).value) if status: reshape = true redraw = true else: discard if selectedElem.get().islink: let anchor = HtmlAnchorElement(buffer.selectedlink.ancestor(TAG_A)).href buffer.gotoLocation(parseUri(anchor)) return true of ACTION_CHANGE_LOCATION: var url = $buffer.document.location clearStatusMsg(buffer.height) let status = readLine("URL:", url) if status: buffer.setLocation(parseUri(url)) return true of ACTION_LINE_INFO: statusMsg("line " & $buffer.cursory & "/" & $buffer.lastLine() & " col " & $buffer.cursorx & "/" & $buffer.realCurrentLineLength(), buffer.width) nostatus = true of ACTION_FEED_NEXT: feedNext = true of ACTION_RELOAD: return true of ACTION_RESHAPE: reshape = true redraw = true of ACTION_REDRAW: redraw = true else: discard stdout.hideCursor() let prevlink = buffer.selectedlink let sel = buffer.checkLinkSelection() if sel: buffer.clearText() buffer.drawHtml() termGoto(0, buffer.selectedlink.y - buffer.fromy) stdout.eraseLine() for i in buffer.selectedlink.y..buffer.selectedlink.ey: if i < buffer.fromy + buffer.height - 1: let line = buffer.fmttext[i] print(line) print('\n') print("".ansiReset()) if prevlink != nil: buffer.clearText() buffer.drawHtml() termGoto(0, prevlink.y - buffer.fromy) for i in prevlink.y..prevlink.ey: if i < buffer.fromy + buffer.height - 1: let line = buffer.fmttext[i] stdout.eraseLine() print(line) print('\n') print("".ansiReset()) if buffer.refreshTermAttrs(): redraw = true reshape = true if reshape: buffer.clearText() buffer.drawHtml() if redraw: buffer.displayBuffer() if not nostatus: buffer.statusMsgForBuffer() else: nostatus = false proc displayPage*(attrs: TermAttributes, buffer: Buffer): bool = #buffer.printwrite = true discard buffer.gotoAnchor() buffer.displayBuffer() buffer.statusMsgForBuffer() return inputLoop(attrs, buffer)