From caad7b577162a73524277a943050493c489bfb59 Mon Sep 17 00:00:00 2001 From: bptato Date: Wed, 4 Aug 2021 17:54:27 +0200 Subject: More css stuff --- src/buffer.nim | 459 ----------------------------------------------- src/css/cssparser.nim | 16 +- src/css/selector.nim | 61 ++++++- src/css/style.nim | 193 ++++++++++++++------ src/html/dom.nim | 128 +++++++------ src/html/htmlparser.nim | 68 +------ src/io/buffer.nim | 463 ++++++++++++++++++++++++++++++++++++++++++++++++ src/io/display.nim | 4 +- src/io/twtio.nim | 11 +- src/main.nim | 13 +- src/types/enums.nim | 41 ++--- 11 files changed, 777 insertions(+), 680 deletions(-) delete mode 100644 src/buffer.nim create mode 100644 src/io/buffer.nim (limited to 'src') diff --git a/src/buffer.nim b/src/buffer.nim deleted file mode 100644 index dc171137..00000000 --- a/src/buffer.nim +++ /dev/null @@ -1,459 +0,0 @@ -import options -import uri -import tables -import strutils -import unicode - -import types/enums - -import utils/termattrs -import utils/twtstr - -import html/dom - -import io/twtio - -type - Buffer* = ref BufferObj - BufferObj = object - fmttext*: seq[string] - rawtext*: seq[string] - title*: string - hovertext*: string - width*: int - height*: int - cursorx*: int - cursory*: int - xend*: int - fromx*: int - fromy*: int - nodes*: seq[Node] - links*: seq[Node] - clickables*: seq[Node] - elements*: seq[Element] - idelements*: Table[string, Element] - selectedlink*: Node - printwrite*: bool - attrs*: TermAttributes - document*: Document - -proc newBuffer*(attrs: TermAttributes): Buffer = - return Buffer(width: attrs.termWidth, - height: attrs.termHeight, - attrs: attrs) - -func lastLine*(buffer: Buffer): int = - assert(buffer.fmttext.len == buffer.rawtext.len) - return buffer.fmttext.len - 1 - -func lastVisibleLine*(buffer: Buffer): int = - return min(buffer.fromy + buffer.height - 1, buffer.lastLine()) - -func currentLineLength*(buffer: Buffer): int = - return buffer.rawtext[buffer.cursory].width() - -func atPercentOf*(buffer: Buffer): int = - if buffer.fmttext.len == 0: return 100 - return (100 * (buffer.cursory + 1)) div (buffer.lastLine() + 1) - -func fmtBetween*(buffer: Buffer, sx: int, sy: int, ex: int, ey: int): string = - if sy < ey: - result &= buffer.rawtext[sy].runeSubstr(sx) - var i = sy + 1 - while i < ey - 1: - result &= buffer.rawtext[i] - inc i - result &= buffer.rawtext[i].runeSubstr(0, ex - sx) - else: - result &= buffer.rawtext[sy].runeSubstr(sx, ex - sx) - -func visibleText*(buffer: Buffer): string = - return buffer.fmttext[buffer.fromy..buffer.lastVisibleLine()].join("\n") - -func lastNode*(buffer: Buffer): Node = - return buffer.nodes[^1] - -func cursorOnNode*(buffer: Buffer, node: Node): bool = - if node.y == node.ey and node.y == buffer.cursory: - return buffer.cursorx >= node.x and buffer.cursorx < node.ex - else: - return (buffer.cursory == node.y and buffer.cursorx >= node.x) or - (buffer.cursory > node.y and buffer.cursory < node.ey) or - (buffer.cursory == node.ey and buffer.cursorx < node.ex) - -func findSelectedElement*(buffer: Buffer): Option[HtmlElement] = - if buffer.selectedlink != nil and buffer.selectedLink.parentNode of HtmlElement: - return some(HtmlElement(buffer.selectedlink.parentNode)) - for node in buffer.nodes: - if node.isElemNode(): - if node.getFmtLen() > 0: - if buffer.cursorOnNode(node): return some(HtmlElement(node)) - return none(HtmlElement) - -func canScroll*(buffer: Buffer): bool = - return buffer.lastLine() > buffer.height - -func getElementById*(buffer: Buffer, id: string): Element = - if buffer.idelements.hasKey(id): - return buffer.idelements[id] - return nil - -proc findSelectedNode*(buffer: Buffer): Option[Node] = - for node in buffer.nodes: - if node.getFmtLen() > 0 and node.displayed(): - if buffer.cursory >= node.y and buffer.cursory <= node.y + node.height and buffer.cursorx >= node.x and buffer.cursorx <= node.x + node.width: - return some(node) - return none(Node) - -proc addNode*(buffer: Buffer, node: Node) = - buffer.nodes.add(node) - - if node.isTextNode() and node.parentElement != nil and node.parentElement.getStyle().islink: - buffer.links.add(node) - - if node.isElemNode(): - case Element(node).tagType - of TAG_INPUT, TAG_OPTION: - if not Element(node).hidden: - buffer.clickables.add(node) - else: discard - elif node.isTextNode(): - if node.parentElement != nil and node.getStyle().islink: - let anchor = node.ancestor(TAG_A) - assert(anchor != nil) - buffer.clickables.add(anchor) - - if node.isElemNode(): - let elem = Element(node) - buffer.elements.add(elem) - if elem.id != "" and not buffer.idelements.hasKey(elem.id): - buffer.idelements[elem.id] = elem - -proc writefmt*(buffer: Buffer, str: string) = - buffer.fmttext &= str - if buffer.printwrite: - stdout.write(str) - -proc writefmt*(buffer: Buffer, c: char) = - buffer.rawtext &= $c - if buffer.printwrite: - stdout.write(c) - -proc writeraw*(buffer: Buffer, str: string) = - buffer.rawtext &= str - -proc writeraw*(buffer: Buffer, c: char) = - buffer.rawtext &= $c - -proc write*(buffer: Buffer, str: string) = - buffer.writefmt(str) - buffer.writeraw(str) - -proc write*(buffer: Buffer, c: char) = - buffer.writefmt(c) - buffer.writeraw(c) - -proc clearText*(buffer: Buffer) = - buffer.fmttext.setLen(0) - buffer.rawtext.setLen(0) - -proc clearNodes*(buffer: Buffer) = - buffer.nodes.setLen(0) - buffer.links.setLen(0) - buffer.clickables.setLen(0) - buffer.elements.setLen(0) - buffer.idelements.clear() - -proc clearBuffer*(buffer: Buffer) = - buffer.clearText() - buffer.clearNodes() - buffer.cursorx = 0 - buffer.cursory = 0 - buffer.fromx = 0 - buffer.fromy = 0 - buffer.hovertext = "" - buffer.selectedlink = nil - -proc scrollTo*(buffer: Buffer, y: int): bool = - if y == buffer.fromy: - return false - buffer.fromy = min(max(buffer.lastLine() - buffer.height + 1, 0), y) - buffer.cursory = min(max(buffer.fromy, buffer.cursory), buffer.fromy + buffer.height) - return true - -proc cursorTo*(buffer: Buffer, x: int, y: int): bool = - result = false - buffer.cursory = min(max(y, 0), buffer.lastLine()) - if buffer.fromy > buffer.cursory: - buffer.fromy = max(buffer.cursory, 0) - result = true - elif buffer.fromy + buffer.height - 1 <= buffer.cursory: - buffer.fromy = max(buffer.cursory - buffer.height + 2, 0) - result = true - buffer.cursorx = min(max(x, 0), buffer.currentLineLength()) - #buffer.fromX = min(max(buffer.currentLineLength() - buffer.width + 1, 0), 0) #TODO - -proc cursorDown*(buffer: Buffer): bool = - if buffer.cursory < buffer.lastLine(): - inc buffer.cursory - if buffer.cursorx >= buffer.currentLineLength(): - buffer.cursorx = max(buffer.currentLineLength() - 1, 0) - elif buffer.xend > 0: - buffer.cursorx = min(buffer.currentLineLength() - 1, buffer.xend) - if buffer.cursory >= buffer.lastVisibleLine() and buffer.lastVisibleLine() != buffer.lastLine(): - inc buffer.fromy - return true - return false - -proc cursorUp*(buffer: Buffer): bool = - if buffer.cursory > 0: - dec buffer.cursory - if buffer.cursorx > buffer.currentLineLength(): - if buffer.cursorx == 0: - buffer.xend = buffer.cursorx - buffer.cursorx = max(buffer.currentLineLength() - 1, 0) - elif buffer.xend > 0: - buffer.cursorx = min(buffer.currentLineLength() - 1, buffer.xend) - if buffer.cursory < buffer.fromy: - dec buffer.fromy - return true - return false - -proc cursorRight*(buffer: Buffer): bool = - if buffer.cursorx < buffer.currentLineLength() - 1: - inc buffer.cursorx - buffer.xend = 0 - else: - buffer.xend = buffer.cursorx - return false - -proc cursorLeft*(buffer: Buffer): bool = - if buffer.cursorx > 0: - dec buffer.cursorx - buffer.xend = 0 - return false - -proc cursorLineBegin*(buffer: Buffer) = - buffer.cursorx = 0 - buffer.xend = 0 - -proc cursorLineEnd*(buffer: Buffer) = - buffer.cursorx = buffer.currentLineLength() - 1 - buffer.xend = buffer.cursorx - -iterator revnodes*(buffer: Buffer): Node {.inline.} = - var i = buffer.nodes.len - 1 - while i >= 0: - yield buffer.nodes[i] - dec i - -proc cursorNextWord*(buffer: Buffer): bool = - let llen = buffer.currentLineLength() - 1 - var r: Rune - var x = buffer.cursorx - var y = buffer.cursory - if llen >= 0: - fastRuneAt(buffer.rawtext[y], x, r, false) - - while r != Rune(' '): - if x >= llen: - break - inc x - fastRuneAt(buffer.rawtext[y], x, r, false) - - while r == Rune(' '): - if x >= llen: - break - inc x - fastRuneAt(buffer.rawtext[y], x, r, false) - - if x >= llen: - if y < buffer.lastLine(): - inc y - x = 0 - return buffer.cursorTo(x, y) - -proc cursorPrevWord*(buffer: Buffer): bool = - var r: Rune - var x = buffer.cursorx - var y = buffer.cursory - if buffer.currentLineLength() > 0: - fastRuneAt(buffer.rawtext[y], x, r, false) - - while r != Rune(' '): - if x == 0: - break - dec x - fastRuneAt(buffer.rawtext[y], x, r, false) - - while r == Rune(' '): - if x == 0: - break - dec x - fastRuneAt(buffer.rawtext[y], x, r, false) - - if x == 0: - if y > 0: - dec y - x = buffer.rawtext[y].runeLen() - 1 - return buffer.cursorTo(x, y) - -iterator revclickables*(buffer: Buffer): Node {.inline.} = - var i = buffer.clickables.len - 1 - while i >= 0: - yield buffer.clickables[i] - dec i - -proc cursorNextLink*(buffer: Buffer): bool = - for node in buffer.clickables: - if node.y > buffer.cursory or (node.y == buffer.cursorY and node.x > buffer.cursorx): - result = buffer.cursorTo(node.x, node.y) - if buffer.cursorx < buffer.currentLineLength(): - var r: Rune - fastRuneAt(buffer.rawtext[buffer.cursory], buffer.cursorx, r, false) - if r == Rune(' '): - return result or buffer.cursorNextWord() - return result - return false - -proc cursorPrevLink*(buffer: Buffer): bool = - for node in buffer.revclickables: - if node.y < buffer.cursorY or (node.y == buffer.cursorY and node.x < buffer.cursorx): - return buffer.cursorTo(node.x, node.y) - return false - -proc cursorFirstLine*(buffer: Buffer): bool = - if buffer.fromy > 0: - buffer.fromy = 0 - result = true - else: - result = false - - buffer.cursorY = 0 - buffer.cursorLineBegin() - -proc cursorLastLine*(buffer: Buffer): bool = - if buffer.fromy < buffer.lastLine() - buffer.height: - buffer.fromy = buffer.lastLine() - (buffer.height - 2) - result = true - else: - result = false - buffer.cursory = buffer.lastLine() - buffer.cursorLineBegin() - -proc cursorTop*(buffer: Buffer): bool = - buffer.cursorY = buffer.fromy - return false - -proc cursorMiddle*(buffer: Buffer): bool = - buffer.cursorY = min(buffer.fromy + (buffer.height - 2) div 2, buffer.lastLine()) - return false - -proc cursorBottom*(buffer: Buffer): bool = - buffer.cursorY = min(buffer.fromy + buffer.height - 2, buffer.lastLine()) - return false - -proc centerLine*(buffer: Buffer): bool = - let ny = max(min(buffer.cursory - buffer.height div 2, buffer.lastLine() - buffer.height + 2), 0) - if ny != buffer.fromy: - buffer.fromy = ny - return true - return false - -proc halfPageUp*(buffer: Buffer): bool = - buffer.cursory = max(buffer.cursorY - buffer.height div 2 + 1, 0) - let nfy = max(0, buffer.fromy - buffer.height div 2 + 1) - if nfy != buffer.fromy: - buffer.fromy = nfy - return true - return false - -proc halfPageDown*(buffer: Buffer): bool = - buffer.cursory = min(buffer.cursorY + buffer.height div 2 - 1, buffer.lastLine()) - let nfy = min(max(buffer.lastLine() - buffer.height + 2, 0), buffer.fromy + buffer.height div 2 - 1) - if nfy != buffer.fromy: - buffer.fromy = nfy - return true - return false - -proc pageUp*(buffer: Buffer): bool = - buffer.cursorY = max(buffer.cursorY - buffer.height + 1, 1) - buffer.fromy = max(0, buffer.fromy - buffer.height) - return true - -proc pageDown*(buffer: Buffer): bool = - buffer.cursorY = min(buffer.cursorY + buffer.height div 2 - 1, buffer.lastLine()) - buffer.fromy = min(max(buffer.lastLine() - buffer.height + 1, 0), buffer.fromy + buffer.height div 2) - return true - -proc scrollDown*(buffer: Buffer): bool = - if buffer.fromy + buffer.height - 1 <= buffer.lastLine(): - inc buffer.fromy - if buffer.fromy >= buffer.cursory: - discard buffer.cursorDown() - return true - discard buffer.cursorDown() - return false - -proc scrollUp*(buffer: Buffer): bool = - if buffer.fromy > 0: - dec buffer.fromy - if buffer.fromy + buffer.height - 1 <= buffer.cursorY: - discard buffer.cursorUp() - return true - discard buffer.cursorUp() - return false - -proc checkLinkSelection*(buffer: Buffer): bool = - if buffer.selectedlink != nil: - if buffer.cursorOnNode(buffer.selectedlink): - return false - else: - let anchor = buffer.selectedlink.ancestor(TAG_A) - buffer.selectedlink.fmttext = buffer.selectedlink.getFmtText() - buffer.selectedlink = nil - buffer.hovertext = "" - var stack: seq[Node] - stack.add(anchor) - while stack.len > 0: - let elem = stack.pop() - elem.fmttext = elem.getFmtText() - for child in elem.childNodes: - stack.add(child) - for node in buffer.links: - if buffer.cursorOnNode(node): - buffer.selectedlink = node - let anchor = node.ancestor(TAG_A) - assert(anchor != nil) - buffer.hovertext = HtmlAnchorElement(anchor).href - var stack: seq[Node] - stack.add(anchor) - while stack.len > 0: - let elem = stack.pop() - elem.fmttext = elem.getFmtText() - for child in elem.childNodes: - stack.add(child) - return true - return false - -proc gotoAnchor*(buffer: Buffer): bool = - if buffer.document.location.anchor != "": - let node = buffer.getElementById(buffer.document.location.anchor) - if node != nil: - return buffer.scrollTo(max(node.y - buffer.height div 2, 0)) - return false - -proc setLocation*(buffer: Buffer, uri: Uri) = - buffer.document.location = uri - -proc gotoLocation*(buffer: Buffer, uri: Uri) = - buffer.document.location = buffer.document.location.combine(uri) - -proc refreshTermAttrs*(buffer: Buffer): bool = - let newAttrs = getTermAttributes() - if newAttrs != buffer.attrs: - buffer.attrs = newAttrs - buffer.width = newAttrs.termWidth - buffer.height = newAttrs.termHeight - return true - return false diff --git a/src/css/cssparser.nim b/src/css/cssparser.nim index 5ecb470a..5e76bcc0 100644 --- a/src/css/cssparser.nim +++ b/src/css/cssparser.nim @@ -6,6 +6,8 @@ import unicode import streams import math import options +import sequtils +import sugar import ../io/twtio @@ -464,6 +466,8 @@ func curr(state: CSSParseState): CSSParsedItem = func has(state: CSSParseState): bool = return state.at < state.tokens.len +proc consumeComponentValue(state: var CSSParseState): CSSComponentValue + proc consumeSimpleBlock(state: var CSSParseState): CSSSimpleBlock = state.reconsume() let t = CSSToken(state.consume()) @@ -483,11 +487,10 @@ proc consumeSimpleBlock(state: var CSSParseState): CSSSimpleBlock = if t == CSS_LBRACE_TOKEN or t == CSS_LBRACKET_TOKEN or t == CSS_LPAREN_TOKEN: result.value.add(state.consumeSimpleBlock()) else: - result.value.add(CSSComponentValue(t)) + state.reconsume() + result.value.add(state.consumeComponentValue()) return result -proc consumeComponentValue*(state: var CSSParseState): CSSComponentValue - proc consumeFunction(state: var CSSParseState): CSSFunction = let t = (CSSToken)state.consume() result = CSSFunction(name: t.value) @@ -691,6 +694,13 @@ proc parseCSSDeclaration*(inputStream: Stream): CSSDeclaration = proc parseListOfDeclarations(state: var CSSParseState): seq[CSSParsedItem] = return state.consumeListOfDeclarations() +proc parseCSSListOfDeclarations*(cvals: seq[CSSComponentValue]): seq[CSSParsedItem] = + var state = CSSParseState() + state.tokens = collect(newSeq): + for cval in cvals: + CSSParsedItem(cval) + return state.consumeListOfDeclarations() + proc parseCSSListOfDeclarations*(inputStream: Stream): seq[CSSParsedItem] = var state = CSSParseState() state.tokens = tokenizeCSS(inputStream) diff --git a/src/css/selector.nim b/src/css/selector.nim index 1ca417dd..a474bd8f 100644 --- a/src/css/selector.nim +++ b/src/css/selector.nim @@ -3,7 +3,7 @@ import unicode import ../types/enums import ../types/tagtypes -import cssparser +import ./cssparser type SelectorType* = enum @@ -52,10 +52,65 @@ proc setLen*(sellist: SelectorList, i: int) = sellist.sels.setLen(i) proc `[]`*(sellist: SelectorList, i: int): Selector = sellist.sels[i] proc len*(sellist: SelectorList): int = sellist.sels.len +func getSpecificity(sel: Selector): int = + case sel.t + of ID_SELECTOR: + result += 1000000 + of CLASS_SELECTOR, ATTR_SELECTOR, PSEUDO_SELECTOR: + result += 1000 + of TYPE_SELECTOR, PSELEM_SELECTOR: + result += 1 + of FUNC_SELECTOR: + case sel.name + of "is": + var best = 0 + for child in sel.selectors.sels: + let s = getSpecificity(child) + if s > best: + best = s + result += best + of "not": + for child in sel.selectors.sels: + result += getSpecificity(child) + else: discard + of UNIVERSAL_SELECTOR: + discard + +func getSpecificity*(sels: SelectorList): int = + for sel in sels.sels: + result += getSpecificity(sel) + +func optimizeSelectorList*(selectors: SelectorList): SelectorList = + new(result) + #pass 1: check for invalid sequences + var i = 1 + while i < selectors.len: + let sel = selectors[i] + if sel.t == TYPE_SELECTOR or sel.t == UNIVERSAL_SELECTOR: + return SelectorList() + inc i + + #pass 2: move selectors in combination + if selectors.len > 1: + var i = 0 + var slow = SelectorList() + if selectors[0].t == UNIVERSAL_SELECTOR: + inc i + + while i < selectors.len: + if selectors[i].t in {ATTR_SELECTOR, PSEUDO_SELECTOR, PSELEM_SELECTOR}: + slow.add(selectors[i]) + else: + result.add(selectors[i]) + inc i + + result.add(slow) + else: + result.add(selectors[0]) + proc parseSelectorToken(state: var SelectorParser, csstoken: CSSToken) = case csstoken.tokenType of CSS_IDENT_TOKEN: - var sel: Selector case state.query of QUERY_CLASS: state.selectors[^1].add(Selector(t: CLASS_SELECTOR, class: $csstoken.value)) @@ -120,7 +175,7 @@ proc parseSelectorSimpleBlock(state: var SelectorParser, cssblock: CSSSimpleBloc proc parseSelectorFunction(state: var SelectorParser, cssfunction: CSSFunction) = case $cssfunction.name - of "not": + of "not", "is": if state.query != QUERY_PSEUDO: return state.query = QUERY_TYPE diff --git a/src/css/style.nim b/src/css/style.nim index 56e6b00b..d98c1f6c 100644 --- a/src/css/style.nim +++ b/src/css/style.nim @@ -1,4 +1,3 @@ -import streams import unicode import terminal import tables @@ -9,7 +8,7 @@ import ../utils/twtstr import ../types/enums -import cssparser +import ./cssparser type CSSLength* = object @@ -36,13 +35,14 @@ type centered*: bool display*: DisplayType bold*: bool - italic*: bool + fontStyle*: CSSFontStyle underscore*: bool islink*: bool selected*: bool indent*: int color*: CSSColor position*: CSSPosition + content*: seq[Rune] CSSCanvas* = object rootBox*: CSSBox @@ -63,13 +63,32 @@ type paddingEdge*: CSSRect borderEdge*: CSSRect marginEdge*: CSSRect - color*: CSSColor props*: CSS2Properties content*: seq[Rune] dispcontent*: string children*: seq[CSSBox] CSSColor* = tuple[r: uint8, g: uint8, b: uint8, a: uint8] + + CSSComputedValue* = object of RootObj + case t: CSSRuleType + of RULE_ALL: discard + of RULE_COLOR: + color: CSSColor + of RULE_MARGIN, RULE_MARGIN_TOP, RULE_MARGIN_LEFT, RULE_MARGIN_RIGHT, + RULE_MARGIN_BOTTOM: + length: CSSLength + of RULE_FONT_STYLE: + fontStyle: CSSFontStyle + of RULE_DISPLAY: + display: DisplayType + of RULE_CONTENT: + content: seq[Rune] + + CSSSpecifiedValue* = object of CSSComputedValue + hasGlobalValue: bool + globalValue: CSSGlobalValueType + func `+`(a: CSSRect, b: CSSRect): CSSRect = result.x1 = a.x1 + b.x1 @@ -82,7 +101,7 @@ proc `+=`(a: var CSSRect, b: CSSRect) = func cells(l: CSSLength): int = case l.unit - of EM_UNIT: + of UNIT_EM: return int(l.num) else: #TODO @@ -112,22 +131,22 @@ const defaultColor = (0xffu8, 0xffu8, 0xffu8, 0x00u8) func cssLength(val: float64, unit: string): CSSLength = case unit - of "%": return CSSLength(num: val, unit: PERC_UNIT) - of "cm": return CSSLength(num: val, unit: CM_UNIT) - of "mm": return CSSLength(num: val, unit: MM_UNIT) - of "in": return CSSLength(num: val, unit: IN_UNIT) - of "px": return CSSLength(num: val, unit: PX_UNIT) - of "pt": return CSSLength(num: val, unit: PT_UNIT) - of "pc": return CSSLength(num: val, unit: PC_UNIT) - of "em": return CSSLength(num: val, unit: EM_UNIT) - of "ex": return CSSLength(num: val, unit: EX_UNIT) - of "ch": return CSSLength(num: val, unit: CH_UNIT) - of "rem": return CSSLength(num: val, unit: REM_UNIT) - of "vw": return CSSLength(num: val, unit: VW_UNIT) - of "vh": return CSSLength(num: val, unit: VH_UNIT) - of "vmin": return CSSLength(num: val, unit: VMIN_UNIT) - of "vmax": return CSSLength(num: val, unit: VMAX_UNIT) - else: return CSSLength(num: 0, unit: EM_UNIT) + of "%": return CSSLength(num: val, unit: UNIT_PERC) + of "cm": return CSSLength(num: val, unit: UNIT_CM) + of "mm": return CSSLength(num: val, unit: UNIT_MM) + of "in": return CSSLength(num: val, unit: UNIT_IN) + of "px": return CSSLength(num: val, unit: UNIT_PX) + of "pt": return CSSLength(num: val, unit: UNIT_PT) + of "pc": return CSSLength(num: val, unit: UNIT_PC) + of "em": return CSSLength(num: val, unit: UNIT_EM) + of "ex": return CSSLength(num: val, unit: UNIT_EX) + of "ch": return CSSLength(num: val, unit: UNIT_CH) + of "rem": return CSSLength(num: val, unit: UNIT_REM) + of "vw": return CSSLength(num: val, unit: UNIT_VW) + of "vh": return CSSLength(num: val, unit: UNIT_VH) + of "vmin": return CSSLength(num: val, unit: UNIT_VMIN) + of "vmax": return CSSLength(num: val, unit: UNIT_VMAX) + else: return CSSLength(num: 0, unit: UNIT_EM) func cssColor*(d: CSSDeclaration): CSSColor = if d.value.len > 0: @@ -165,7 +184,7 @@ func cssColor*(d: CSSDeclaration): CSSColor = else: eprint "else", tok.tokenType return defaultColor - elif d of CSSFunction: + elif d.value[0] of CSSFunction: let f = CSSFunction(d.value[0]) eprint "func", f.name #todo calc etc (cssnumber function or something) @@ -209,11 +228,11 @@ func cssLength(d: CSSDeclaration): CSSLength = return cssLength(tok.nvalue, $tok.unit) of CSS_IDENT_TOKEN: if $tok.value == "auto": - return CSSLength(num: 0, unit: EM_UNIT, auto: true) + return CSSLength(auto: true) else: - return CSSLength(num: 0, unit: EM_UNIT) + return CSSLength(num: 0, unit: UNIT_EM) - return CSSLength(num: 0, unit: EM_UNIT) + return CSSLength(num: 0, unit: UNIT_EM) func hasColor*(style: CSS2Properties): bool = return style.color.r != 0 or style.color.b != 0 or style.color.g != 0 or style.color.a != 0 @@ -228,35 +247,101 @@ func termColor*(style: CSS2Properties): ForegroundColor = else: return fgWhite -proc applyProperties*(box: CSSBox, s: string) = - let decls = parseCSSListOfDeclarations(newStringStream(s)) - if box.props == nil: - box.props = CSS2Properties() - let props = box.props - - for item in decls: - if item of CSSDeclaration: - let d = CSSDeclaration(item) - case $d.name - of "color": - props.color = cssColor(d) - eprint props.color #TODO - of "margin": - let l = cssLength(d) - props.margintop = l - props.marginbottom = l - props.marginleft = l - props.marginright = l - of "margin-top": - props.margintop = cssLength(d) - of "margin-left": - props.marginleft = cssLength(d) - of "margin-right": - props.marginright = cssLength(d) - of "margin-bottom": - props.marginbottom = cssLength(d) - else: - printc(d) #TODO +func isToken(d: CSSDeclaration): bool = d.value.len > 0 and d.value[0] of CSSToken +func getToken(d: CSSDeclaration): CSSToken = (CSSToken)d.value[0] + +func cssString(d: CSSDeclaration): seq[Rune] = + if isToken(d): + let tok = getToken(d) + case tok.tokenType + of CSS_IDENT_TOKEN, CSS_STRING_TOKEN: + return tok.value + else: return + +func cssDisplay(d: CSSDeclaration): DisplayType = + if isToken(d): + let tok = getToken(d) + if tok.tokenType == CSS_IDENT_TOKEN: + case $tok.value + of "block": return DISPLAY_BLOCK + of "inline": return DISPLAY_INLINE + of "inline-block": return DISPLAY_INLINE_BLOCK + of "list-item": return DISPLAY_LIST_ITEM + of "table-column": return DISPLAY_TABLE_COLUMN + of "none": return DISPLAY_NONE + else: return DISPLAY_INLINE + return DISPLAY_INLINE + +func cssFontStyle(d: CSSDeclaration): CSSFontStyle = + if isToken(d): + let tok = getToken(d) + if tok.tokenType == CSS_IDENT_TOKEN: + case $tok.value + of "normal": return FONTSTYLE_NORMAL + of "italic": return FONTSTYLE_ITALIC + of "oblique": return FONTSTYLE_OBLIQUE + else: return FONTSTYLE_NORMAL + return FONTSTYLE_NORMAL + +func getSpecifiedValue*(d: CSSDeclaration): CSSSpecifiedValue = + case $d.name + of "color": + return CSSSpecifiedValue(t: RULE_COLOR, color: cssColor(d)) + of "margin": + return CSSSpecifiedValue(t: RULE_MARGIN, length: cssLength(d)) + of "margin-top": + return CSSSpecifiedValue(t: RULE_MARGIN_TOP, length: cssLength(d)) + of "margin-left": + return CSSSpecifiedValue(t: RULE_MARGIN_LEFT, length: cssLength(d)) + of "margin-bottom": + return CSSSpecifiedValue(t: RULE_MARGIN_BOTTOM, length: cssLength(d)) + of "margin-right": + return CSSSpecifiedValue(t: RULE_MARGIN_RIGHT, length: cssLength(d)) + of "font-style": + return CSSSpecifiedValue(t: RULE_FONT_STYLE, fontStyle: cssFontStyle(d)) + of "display": + return CSSSpecifiedValue(t: RULE_DISPLAY, display: cssDisplay(d)) + of "content": + return CSSSpecifiedValue(t: RULE_CONTENT, content: cssString(d)) + +func getComputedValue*(rule: CSSSpecifiedValue): CSSComputedValue = + let inherit = rule.hasGlobalValue and (rule.globalValue == VALUE_INHERIT) + let initial = rule.hasGlobalValue and (rule.globalValue == VALUE_INHERIT) + let unset = rule.hasGlobalValue and (rule.globalValue == VALUE_INHERIT) + let revert = rule.hasGlobalValue and (rule.globalValue == VALUE_INHERIT) + #case rule.t + #of RULE_COLOR: + # return CSSComputedValue(t: rule.t, + +func getComputedValue*(d: CSSDeclaration): CSSComputedValue = + return getComputedValue(getSpecifiedValue(d)) + +proc applyProperty*(props: CSS2Properties, d: CSSDeclaration) = + case $d.name + of "color": + props.color = cssColor(d) + of "margin": + let l = cssLength(d) + props.margintop = l + props.marginbottom = l + props.marginleft = l + props.marginright = l + of "margin-top": + props.margintop = cssLength(d) + of "margin-left": + props.marginleft = cssLength(d) + of "margin-right": + props.marginright = cssLength(d) + of "margin-bottom": + props.marginbottom = cssLength(d) + of "font-style": + props.fontStyle = cssFontStyle(d) + of "display": + props.display = cssDisplay(d) + of "content": + props.content = cssString(d) + else: + printc(d) #TODO func getLength(s: seq[Rune], start: int, wlimit: int): tuple[wrap: bool, len: int, width: int] = var len = 0 diff --git a/src/html/dom.nim b/src/html/dom.nim index 74ebe5ea..1a393134 100644 --- a/src/html/dom.nim +++ b/src/html/dom.nim @@ -6,13 +6,13 @@ import tables import streams import sequtils import sugar +import algorithm import ../css/style import ../css/cssparser import ../css/selector import ../types/enums -import ../types/tagtypes import ../utils/twtstr @@ -46,6 +46,7 @@ type width*: int height*: int hidden*: bool + box*: CSSBox Attr* = ref AttrObj AttrObj = object of NodeObj @@ -89,7 +90,8 @@ type id*: string classList*: seq[string] attributes*: Table[string, Attr] - box*: CSSBox + style*: CSS2Properties + cssvalues*: seq[CSSComputedValue] HTMLElement* = ref HTMLElementObj HTMLElementObj = object of ElementObj @@ -159,8 +161,8 @@ func nodeAttr*(node: Node): HtmlElement = func getStyle*(node: Node): CSS2Properties = case node.nodeType - of TEXT_NODE: return node.parentElement.box.props - of ELEMENT_NODE: return Element(node).box.props + of TEXT_NODE: return node.parentElement.style + of ELEMENT_NODE: return Element(node).style else: assert(false) func displayed*(node: Node): bool = @@ -260,11 +262,11 @@ proc getRawText*(htmlNode: Node): string = #eprint "char data", chardata.data if htmlNode.parentElement != nil and htmlNode.parentElement.tagType != TAG_PRE: result = chardata.data.remove("\n") - if unicode.strip(result).runeLen() > 0: - if htmlNode.getStyle().display != DISPLAY_INLINE: - result = unicode.strip(result) - else: - result = "" + #if unicode.strip(result).runeLen() > 0: + # if htmlNode.getStyle().display != DISPLAY_INLINE: + # result = unicode.strip(result) + #else: + # result = "" else: result = unicode.strip(chardata.data) if htmlNode.parentElement != nil and htmlNode.parentElement.tagType == TAG_OPTION: @@ -290,7 +292,7 @@ func getFmtText*(node: Node): seq[string] = if style.bold: result = result.ansiStyle(styleBright).ansiReset() - if style.italic: + if style.fontStyle == FONTSTYLE_ITALIC or style.fontStyle == FONTSTYLE_OBLIQUE: result = result.ansiStyle(styleItalic).ansiReset() if style.underscore: result = result.ansiStyle(styleUnderscore).ansiReset() @@ -307,10 +309,6 @@ func newComment*(): Comment = new(result) result.nodeType = COMMENT_NODE -func newBox*(element: HTMLElement): CSSBox = - new(result) - result.props = CSS2Properties() - func newHtmlElement*(tagType: TagType): HTMLElement = case tagType of TAG_INPUT: @@ -332,7 +330,7 @@ func newHtmlElement*(tagType: TagType): HTMLElement = result.nodeType = ELEMENT_NODE result.tagType = tagType - result.box = result.newBox() + result.style = CSS2Properties() func newDocument*(): Document = new(result) @@ -414,7 +412,6 @@ func selectElems(document: Document, sel: Selector): seq[Element] = return document.class_elements[sel.class] of UNIVERSAL_SELECTOR: return document.all_elements - #TODO: following selectors are rather inefficient of ATTR_SELECTOR: return document.all_elements.filter((elem) => attrSelectorMatches(elem, sel)) of PSEUDO_SELECTOR: @@ -422,38 +419,13 @@ func selectElems(document: Document, sel: Selector): seq[Element] = of PSELEM_SELECTOR: return document.all_elements.filter((elem) => pseudoElemSelectorMatches(elem, sel)) of FUNC_SELECTOR: - if sel.name == "not": + case sel.name + of "not": return document.all_elements.filter((elem) => not selectorsMatch(elem, sel.selectors)) + of "is", "where": + return document.all_elements.filter((elem) => selectorsMatch(elem, sel.selectors)) return newSeq[Element]() -func optimizeSelectorList(selectors: SelectorList): SelectorList = - new(result) - #pass 1: check for invalid sequences - var i = 1 - while i < selectors.len: - let sel = selectors[i] - if sel.t == TYPE_SELECTOR or sel.t == UNIVERSAL_SELECTOR: - return SelectorList() - inc i - - #pass 2: move selectors in combination - if selectors.len > 1: - var i = 0 - var slow = SelectorList() - if selectors[0].t == UNIVERSAL_SELECTOR: - inc i - - while i < selectors.len: - if selectors[i].t in {ATTR_SELECTOR, PSEUDO_SELECTOR, PSELEM_SELECTOR}: - slow.add(selectors[i]) - else: - result.add(selectors[i]) - inc i - - result.add(slow) - else: - result.add(selectors[0]) - func selectElems(document: Document, selectors: SelectorList): seq[Element] = assert(selectors.len > 0) let sellist = optimizeSelectorList(selectors) @@ -462,8 +434,12 @@ func selectElems(document: Document, selectors: SelectorList): seq[Element] = while i < sellist.len: if sellist[i].t == FUNC_SELECTOR: - if sellist[i].name == "not": + case sellist[i].name + of "not": result = result.filter((elem) => not selectorsMatch(elem, sellist[i].selectors)) + of "is", "where": + result = result.filter((elem) => selectorsMatch(elem, sellist[i].selectors)) + else: discard else: result = result.filter((elem) => selectorMatches(elem, sellist[i])) inc i @@ -476,17 +452,65 @@ proc querySelector*(document: Document, q: string): seq[Element] = for sel in selectors: result.add(document.selectElems(sel)) -proc applyRule(elem: Element, rule: CSSRule) = - let selectors = parseSelectors(rule.prelude) - for sel in selectors: - if elem.selectorsMatch(sel): - eprint "match!" +func calcRules(elem: Element, rules: CSSStylesheet): seq[CSSSimpleBlock] = + var tosort: seq[tuple[s:int,b:CSSSimpleBlock]] + for rule in rules.value: + let selectors = parseSelectors(rule.prelude) #TODO perf: compute this once + for sel in selectors: + if elem.selectorsMatch(sel): + let spec = getSpecificity(sel) + tosort.add((spec,rule.oblock)) -proc applyRules(document: Document, rules: CSSStylesheet) = + tosort.sort((x, y) => cmp(x.s,y.s)) + return tosort.map((x) => x.b) + +proc applyRules*(document: Document, rules: CSSStylesheet): seq[tuple[e:Element,d:CSSDeclaration]] = + var stack: seq[Element] + + stack.add(document.firstElementChild) + while stack.len > 0: + let elem = stack.pop() + for oblock in calcRules(elem, rules): + let decls = parseCSSListOfDeclarations(oblock.value) + for item in decls: + if item of CSSDeclaration: + if ((CSSDeclaration)item).important: + result.add((elem, (CSSDeclaration)item)) + else: + elem.style.applyProperty((CSSDeclaration)item) + + for child in elem.children: + stack.add(child) + +proc addBoxes*(elem: Element) = + var b = false + for child in elem.childNodes: + if child.nodeType == ELEMENT_NODE: + if ((Element)child).style.display == DISPLAY_BLOCK: + b = true + break + for child in elem.childNodes: + if child.nodeType == ELEMENT_NODE: + if b: + child.box = CSSBox(display: DISPLAY_BLOCK) + else: + child.box = CSSBox() + +proc generateBoxModel(document: Document) = var stack: seq[Element] stack.add(document.firstElementChild) + document.firstElementChild.box = CSSBox() while stack.len > 0: let elem = stack.pop() + elem.addBoxes() for child in elem.children: stack.add(child) + +proc applyDefaultStylesheet*(document: Document) = + let important = document.applyRules(stylesheet) + for rule in important: + rule.e.style.applyProperty(rule.d) + rule.e.cssvalues.add(getComputedValue(rule.d)) + + document.generateBoxModel() diff --git a/src/html/htmlparser.nim b/src/html/htmlparser.nim index f43bcf40..3cfc1d8d 100644 --- a/src/html/htmlparser.nim +++ b/src/html/htmlparser.nim @@ -31,71 +31,6 @@ type parentNode: Node textNode: Text -#func newHtmlElement(tagType: TagType, parentNode: Node): HtmlElement = -# case tagType -# of TAG_INPUT: result = new(HtmlInputElement) -# of TAG_A: result = new(HtmlAnchorElement) -# of TAG_SELECT: result = new(HtmlSelectElement) -# of TAG_OPTION: result = new(HtmlOptionElement) -# else: result = new(HtmlElement) -# -# result.nodeType = ELEMENT_NODE -# result.tagType = tagType -# result.parentNode = parentNode -# if parentNode.isElemNode(): -# result.parentElement = HtmlElement(parentNode) -# -# if tagType in DisplayInlineTags: -# result.display = DISPLAY_INLINE -# elif tagType in DisplayBlockTags: -# result.display = DISPLAY_BLOCK -# elif tagType in DisplayInlineBlockTags: -# result.display = DISPLAY_INLINE_BLOCK -# elif tagType == TAG_LI: -# result.display = DISPLAY_LIST_ITEM -# else: -# result.display = DISPLAY_NONE -# -# case tagType -# of TAG_CENTER: -# result.centered = true -# of TAG_B: -# result.bold = true -# of TAG_I: -# result.italic = true -# of TAG_U: -# result.underscore = true -# of TAG_HEAD: -# result.hidden = true -# of TAG_STYLE: -# result.hidden = true -# of TAG_SCRIPT: -# result.hidden = true -# of TAG_OPTION: -# result.hidden = true #TODO -# of TAG_PRE, TAG_TD, TAG_TH: -# result.margin = 1 -# of TAG_UL, TAG_OL: -# result.indent = 2 -# result.margin = 1 -# of TAG_H1, TAG_H2, TAG_H3, TAG_H4, TAG_H5, TAG_H6: -# result.bold = true -# result.margin = 1 -# of TAG_A: -# result.islink = true -# of TAG_INPUT: -# HtmlInputElement(result).size = 20 -# else: discard -# -# if parentNode.isElemNode(): -# let parent = HtmlElement(parentNode) -# result.centered = result.centered or parent.centered -# result.bold = result.bold or parent.bold -# result.italic = result.italic or parent.italic -# result.underscore = result.underscore or parent.underscore -# result.hidden = result.hidden or parent.hidden -# result.islink = result.islink or parent.islink - func inputSize*(str: string): int = if str.len == 0: return 20 @@ -104,7 +39,7 @@ func inputSize*(str: string): int = return 20 return str.parseInt() -#w3m's getescapecmd and parse_tag, transpiled to nim. +#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 # @@ -153,7 +88,6 @@ proc getescapecmd(buf: string, at: var int): string = elif not isAlphaAscii(buf[i]): return "" - #TODO this could be way more efficient (and radixnode needs better interface) when defined(small): var n = entityMap var s = "" diff --git a/src/io/buffer.nim b/src/io/buffer.nim new file mode 100644 index 00000000..ce28e84e --- /dev/null +++ b/src/io/buffer.nim @@ -0,0 +1,463 @@ +import options +import uri +import tables +import strutils +import unicode + +import ../types/enums + +import ../utils/termattrs +import ../utils/twtstr + +import ../html/dom + +import ./twtio + +type + Buffer* = ref BufferObj + BufferObj = object + text*: seq[Rune] + title*: string + hovertext*: string + width*: int + height*: int + cursorx*: int + cursory*: int + xend*: int + fromx*: int + fromy*: int + nodes*: seq[Node] + links*: seq[Node] + clickables*: seq[Node] + elements*: seq[Element] + idelements*: Table[string, Element] + selectedlink*: Node + printwrite*: bool + attrs*: TermAttributes + document*: Document + + #TODO remove these + fmttext*: seq[string] + rawtext*: seq[string] + +proc newBuffer*(attrs: TermAttributes): Buffer = + return Buffer(width: attrs.termWidth, + height: attrs.termHeight, + attrs: attrs) + +#TODO go through these and remove ones that don't make sense in the new model +func lastLine*(buffer: Buffer): int = + assert(buffer.fmttext.len == buffer.rawtext.len) + return buffer.fmttext.len - 1 + +func lastVisibleLine*(buffer: Buffer): int = + return min(buffer.fromy + buffer.height - 1, buffer.lastLine()) + +func currentLineLength*(buffer: Buffer): int = + return buffer.rawtext[buffer.cursory].width() + +func atPercentOf*(buffer: Buffer): int = + if buffer.fmttext.len == 0: return 100 + return (100 * (buffer.cursory + 1)) div (buffer.lastLine() + 1) + +func fmtBetween*(buffer: Buffer, sx: int, sy: int, ex: int, ey: int): string = + if sy < ey: + result &= buffer.rawtext[sy].runeSubstr(sx) + var i = sy + 1 + while i < ey - 1: + result &= buffer.rawtext[i] + inc i + result &= buffer.rawtext[i].runeSubstr(0, ex - sx) + else: + result &= buffer.rawtext[sy].runeSubstr(sx, ex - sx) + +func visibleText*(buffer: Buffer): string = + return buffer.fmttext[buffer.fromy..buffer.lastVisibleLine()].join("\n") + +func lastNode*(buffer: Buffer): Node = + return buffer.nodes[^1] + +func cursorOnNode*(buffer: Buffer, node: Node): bool = + if node.y == node.ey and node.y == buffer.cursory: + return buffer.cursorx >= node.x and buffer.cursorx < node.ex + else: + return (buffer.cursory == node.y and buffer.cursorx >= node.x) or + (buffer.cursory > node.y and buffer.cursory < node.ey) or + (buffer.cursory == node.ey and buffer.cursorx < node.ex) + +func findSelectedElement*(buffer: Buffer): Option[HtmlElement] = + if buffer.selectedlink != nil and buffer.selectedLink.parentNode of HtmlElement: + return some(HtmlElement(buffer.selectedlink.parentNode)) + for node in buffer.nodes: + if node.isElemNode(): + if node.getFmtLen() > 0: + if buffer.cursorOnNode(node): return some(HtmlElement(node)) + return none(HtmlElement) + +func canScroll*(buffer: Buffer): bool = + return buffer.lastLine() > buffer.height + +func getElementById*(buffer: Buffer, id: string): Element = + if buffer.idelements.hasKey(id): + return buffer.idelements[id] + return nil + +proc findSelectedNode*(buffer: Buffer): Option[Node] = + for node in buffer.nodes: + if node.getFmtLen() > 0 and node.displayed(): + if buffer.cursory >= node.y and buffer.cursory <= node.y + node.height and buffer.cursorx >= node.x and buffer.cursorx <= node.x + node.width: + return some(node) + return none(Node) + +proc addNode*(buffer: Buffer, node: Node) = + buffer.nodes.add(node) + + if node.isTextNode() and node.parentElement != nil and node.parentElement.getStyle().islink: + buffer.links.add(node) + + if node.isElemNode(): + case Element(node).tagType + of TAG_INPUT, TAG_OPTION: + if not Element(node).hidden: + buffer.clickables.add(node) + else: discard + elif node.isTextNode(): + if node.parentElement != nil and node.getStyle().islink: + let anchor = node.ancestor(TAG_A) + assert(anchor != nil) + buffer.clickables.add(anchor) + + if node.isElemNode(): + let elem = Element(node) + buffer.elements.add(elem) + if elem.id != "" and not buffer.idelements.hasKey(elem.id): + buffer.idelements[elem.id] = elem + +proc writefmt*(buffer: Buffer, str: string) = + buffer.fmttext &= str + if buffer.printwrite: + stdout.write(str) + +proc writefmt*(buffer: Buffer, c: char) = + buffer.rawtext &= $c + if buffer.printwrite: + stdout.write(c) + +proc writeraw*(buffer: Buffer, str: string) = + buffer.rawtext &= str + +proc writeraw*(buffer: Buffer, c: char) = + buffer.rawtext &= $c + +proc write*(buffer: Buffer, str: string) = + buffer.writefmt(str) + buffer.writeraw(str) + +proc write*(buffer: Buffer, c: char) = + buffer.writefmt(c) + buffer.writeraw(c) + +proc clearText*(buffer: Buffer) = + buffer.fmttext.setLen(0) + buffer.rawtext.setLen(0) + +proc clearNodes*(buffer: Buffer) = + buffer.nodes.setLen(0) + buffer.links.setLen(0) + buffer.clickables.setLen(0) + buffer.elements.setLen(0) + buffer.idelements.clear() + +proc clearBuffer*(buffer: Buffer) = + buffer.clearText() + buffer.clearNodes() + buffer.cursorx = 0 + buffer.cursory = 0 + buffer.fromx = 0 + buffer.fromy = 0 + buffer.hovertext = "" + buffer.selectedlink = nil + +proc scrollTo*(buffer: Buffer, y: int): bool = + if y == buffer.fromy: + return false + buffer.fromy = min(max(buffer.lastLine() - buffer.height + 1, 0), y) + buffer.cursory = min(max(buffer.fromy, buffer.cursory), buffer.fromy + buffer.height) + return true + +proc cursorTo*(buffer: Buffer, x: int, y: int): bool = + result = false + buffer.cursory = min(max(y, 0), buffer.lastLine()) + if buffer.fromy > buffer.cursory: + buffer.fromy = max(buffer.cursory, 0) + result = true + elif buffer.fromy + buffer.height - 1 <= buffer.cursory: + buffer.fromy = max(buffer.cursory - buffer.height + 2, 0) + result = true + buffer.cursorx = min(max(x, 0), buffer.currentLineLength()) + #buffer.fromX = min(max(buffer.currentLineLength() - buffer.width + 1, 0), 0) #TODO + +proc cursorDown*(buffer: Buffer): bool = + if buffer.cursory < buffer.lastLine(): + inc buffer.cursory + if buffer.cursorx >= buffer.currentLineLength(): + buffer.cursorx = max(buffer.currentLineLength() - 1, 0) + elif buffer.xend > 0: + buffer.cursorx = min(buffer.currentLineLength() - 1, buffer.xend) + if buffer.cursory >= buffer.lastVisibleLine() and buffer.lastVisibleLine() != buffer.lastLine(): + inc buffer.fromy + return true + return false + +proc cursorUp*(buffer: Buffer): bool = + if buffer.cursory > 0: + dec buffer.cursory + if buffer.cursorx > buffer.currentLineLength(): + if buffer.cursorx == 0: + buffer.xend = buffer.cursorx + buffer.cursorx = max(buffer.currentLineLength() - 1, 0) + elif buffer.xend > 0: + buffer.cursorx = min(buffer.currentLineLength() - 1, buffer.xend) + if buffer.cursory < buffer.fromy: + dec buffer.fromy + return true + return false + +proc cursorRight*(buffer: Buffer): bool = + if buffer.cursorx < buffer.currentLineLength() - 1: + inc buffer.cursorx + buffer.xend = 0 + else: + buffer.xend = buffer.cursorx + return false + +proc cursorLeft*(buffer: Buffer): bool = + if buffer.cursorx > 0: + dec buffer.cursorx + buffer.xend = 0 + return false + +proc cursorLineBegin*(buffer: Buffer) = + buffer.cursorx = 0 + buffer.xend = 0 + +proc cursorLineEnd*(buffer: Buffer) = + buffer.cursorx = buffer.currentLineLength() - 1 + buffer.xend = buffer.cursorx + +iterator revnodes*(buffer: Buffer): Node {.inline.} = + var i = buffer.nodes.len - 1 + while i >= 0: + yield buffer.nodes[i] + dec i + +proc cursorNextWord*(buffer: Buffer): bool = + let llen = buffer.currentLineLength() - 1 + var r: Rune + var x = buffer.cursorx + var y = buffer.cursory + if llen >= 0: + fastRuneAt(buffer.rawtext[y], x, r, false) + + while r != Rune(' '): + if x >= llen: + break + inc x + fastRuneAt(buffer.rawtext[y], x, r, false) + + while r == Rune(' '): + if x >= llen: + break + inc x + fastRuneAt(buffer.rawtext[y], x, r, false) + + if x >= llen: + if y < buffer.lastLine(): + inc y + x = 0 + return buffer.cursorTo(x, y) + +proc cursorPrevWord*(buffer: Buffer): bool = + var r: Rune + var x = buffer.cursorx + var y = buffer.cursory + if buffer.currentLineLength() > 0: + fastRuneAt(buffer.rawtext[y], x, r, false) + + while r != Rune(' '): + if x == 0: + break + dec x + fastRuneAt(buffer.rawtext[y], x, r, false) + + while r == Rune(' '): + if x == 0: + break + dec x + fastRuneAt(buffer.rawtext[y], x, r, false) + + if x == 0: + if y > 0: + dec y + x = buffer.rawtext[y].runeLen() - 1 + return buffer.cursorTo(x, y) + +iterator revclickables*(buffer: Buffer): Node {.inline.} = + var i = buffer.clickables.len - 1 + while i >= 0: + yield buffer.clickables[i] + dec i + +proc cursorNextLink*(buffer: Buffer): bool = + for node in buffer.clickables: + if node.y > buffer.cursory or (node.y == buffer.cursorY and node.x > buffer.cursorx): + result = buffer.cursorTo(node.x, node.y) + if buffer.cursorx < buffer.currentLineLength(): + var r: Rune + fastRuneAt(buffer.rawtext[buffer.cursory], buffer.cursorx, r, false) + if r == Rune(' '): + return result or buffer.cursorNextWord() + return result + return false + +proc cursorPrevLink*(buffer: Buffer): bool = + for node in buffer.revclickables: + if node.y < buffer.cursorY or (node.y == buffer.cursorY and node.x < buffer.cursorx): + return buffer.cursorTo(node.x, node.y) + return false + +proc cursorFirstLine*(buffer: Buffer): bool = + if buffer.fromy > 0: + buffer.fromy = 0 + result = true + else: + result = false + + buffer.cursorY = 0 + buffer.cursorLineBegin() + +proc cursorLastLine*(buffer: Buffer): bool = + if buffer.fromy < buffer.lastLine() - buffer.height: + buffer.fromy = buffer.lastLine() - (buffer.height - 2) + result = true + else: + result = false + buffer.cursory = buffer.lastLine() + buffer.cursorLineBegin() + +proc cursorTop*(buffer: Buffer): bool = + buffer.cursorY = buffer.fromy + return false + +proc cursorMiddle*(buffer: Buffer): bool = + buffer.cursorY = min(buffer.fromy + (buffer.height - 2) div 2, buffer.lastLine()) + return false + +proc cursorBottom*(buffer: Buffer): bool = + buffer.cursorY = min(buffer.fromy + buffer.height - 2, buffer.lastLine()) + return false + +proc centerLine*(buffer: Buffer): bool = + let ny = max(min(buffer.cursory - buffer.height div 2, buffer.lastLine() - buffer.height + 2), 0) + if ny != buffer.fromy: + buffer.fromy = ny + return true + return false + +proc halfPageUp*(buffer: Buffer): bool = + buffer.cursory = max(buffer.cursorY - buffer.height div 2 + 1, 0) + let nfy = max(0, buffer.fromy - buffer.height div 2 + 1) + if nfy != buffer.fromy: + buffer.fromy = nfy + return true + return false + +proc halfPageDown*(buffer: Buffer): bool = + buffer.cursory = min(buffer.cursorY + buffer.height div 2 - 1, buffer.lastLine()) + let nfy = min(max(buffer.lastLine() - buffer.height + 2, 0), buffer.fromy + buffer.height div 2 - 1) + if nfy != buffer.fromy: + buffer.fromy = nfy + return true + return false + +proc pageUp*(buffer: Buffer): bool = + buffer.cursorY = max(buffer.cursorY - buffer.height + 1, 1) + buffer.fromy = max(0, buffer.fromy - buffer.height) + return true + +proc pageDown*(buffer: Buffer): bool = + buffer.cursorY = min(buffer.cursorY + buffer.height div 2 - 1, buffer.lastLine()) + buffer.fromy = min(max(buffer.lastLine() - buffer.height + 1, 0), buffer.fromy + buffer.height div 2) + return true + +proc scrollDown*(buffer: Buffer): bool = + if buffer.fromy + buffer.height - 1 <= buffer.lastLine(): + inc buffer.fromy + if buffer.fromy >= buffer.cursory: + discard buffer.cursorDown() + return true + discard buffer.cursorDown() + return false + +proc scrollUp*(buffer: Buffer): bool = + if buffer.fromy > 0: + dec buffer.fromy + if buffer.fromy + buffer.height - 1 <= buffer.cursorY: + discard buffer.cursorUp() + return true + discard buffer.cursorUp() + return false + +proc checkLinkSelection*(buffer: Buffer): bool = + if buffer.selectedlink != nil: + if buffer.cursorOnNode(buffer.selectedlink): + return false + else: + let anchor = buffer.selectedlink.ancestor(TAG_A) + buffer.selectedlink.fmttext = buffer.selectedlink.getFmtText() + buffer.selectedlink = nil + buffer.hovertext = "" + var stack: seq[Node] + stack.add(anchor) + while stack.len > 0: + let elem = stack.pop() + elem.fmttext = elem.getFmtText() + for child in elem.childNodes: + stack.add(child) + for node in buffer.links: + if buffer.cursorOnNode(node): + buffer.selectedlink = node + let anchor = node.ancestor(TAG_A) + assert(anchor != nil) + buffer.hovertext = HtmlAnchorElement(anchor).href + var stack: seq[Node] + stack.add(anchor) + while stack.len > 0: + let elem = stack.pop() + elem.fmttext = elem.getFmtText() + for child in elem.childNodes: + stack.add(child) + return true + return false + +proc gotoAnchor*(buffer: Buffer): bool = + if buffer.document.location.anchor != "": + let node = buffer.getElementById(buffer.document.location.anchor) + if node != nil: + return buffer.scrollTo(max(node.y - buffer.height div 2, 0)) + return false + +proc setLocation*(buffer: Buffer, uri: Uri) = + buffer.document.location = uri + +proc gotoLocation*(buffer: Buffer, uri: Uri) = + buffer.document.location = buffer.document.location.combine(uri) + +proc refreshTermAttrs*(buffer: Buffer): bool = + let newAttrs = getTermAttributes() + if newAttrs != buffer.attrs: + buffer.attrs = newAttrs + buffer.width = newAttrs.termWidth + buffer.height = newAttrs.termHeight + return true + return false diff --git a/src/io/display.nim b/src/io/display.nim index e3ce6bac..1c72d959 100644 --- a/src/io/display.nim +++ b/src/io/display.nim @@ -11,10 +11,10 @@ import ../utils/twtstr import ../html/dom -import ../buffer import ../config -import twtio +import ./buffer +import ./twtio proc clearStatusMsg*(at: int) = setCursorPos(0, at) diff --git a/src/io/twtio.nim b/src/io/twtio.nim index 34cf4ce6..4e2789b0 100644 --- a/src/io/twtio.nim +++ b/src/io/twtio.nim @@ -3,6 +3,7 @@ import tables import unicode import strutils import sequtils +import sugar import ../utils/twtstr import ../utils/radixtree @@ -21,9 +22,6 @@ template printesc*(s: string) = else: stdout.write($r) -template printspc(i: int) = - print(' '.repeat(i)) - template eprint*(s: varargs[string, `$`]) = {.cast(noSideEffect).}: var a = false for x in s: @@ -134,11 +132,12 @@ proc zeroShiftRedraw(state: var LineState) = proc insertCharseq(state: var LineState, cs: var seq[Rune]) = let escNext = state.escNext - cs.keepIf(func(r: Rune): bool = escNext or not r.isControlChar()) + cs.keepIf((r) => escNext or not r.isControlChar()) state.escNext = false if cs.len == 0: return - elif state.cursor >= state.news.len and state.news.width(state.shift, state.cursor) + cs.width() < state.displen: + + if state.cursor >= state.news.len and state.news.width(state.shift, state.cursor) + cs.width() < state.displen: state.news &= cs state.cursor += cs.len printesc($cs) @@ -185,7 +184,7 @@ proc readLine*(current: var string, minlen: int, maxlen: int): bool = state.minlen = minlen state.maxlen = maxlen state.displen = state.maxlen - 1 - #ugh + #cache strings for i in 0..(maxlen - minlen): state.spaces.add(' '.repeat(i)) printesc(current) diff --git a/src/main.nim b/src/main.nim index 539527f1..92d6b3b9 100644 --- a/src/main.nim +++ b/src/main.nim @@ -3,8 +3,6 @@ import uri import os import streams -import css/style - import utils/termattrs import html/dom @@ -12,8 +10,8 @@ import html/htmlparser import io/display import io/twtio +import io/buffer -import buffer import config let clientInstance = newHttpClient() @@ -39,6 +37,9 @@ proc getPageUri(uri: Uri): Stream = var buffers: seq[Buffer] + +const defaultcss = staticRead"../res/default.css" + proc main*() = if paramCount() != 1: eprint "Invalid parameters. Usage:\ntwt " @@ -50,10 +51,7 @@ proc main*() = let uri = parseUri(paramStr(1)) buffers.add(buffer) buffer.document = parseHtml(getPageUri(uri)) - let s = buffer.document.querySelector(":not(:first-child)") - eprint s.len - for q in s: - eprint q + buffer.document.applyDefaultStylesheet() buffer.setLocation(uri) buffer.renderHtml() var lastUri = uri @@ -71,4 +69,3 @@ proc main*() = buffer.renderHtml() lastUri = newUri main() -#parseCSS(newFileStream("default.css", fmRead)) diff --git a/src/types/enums.nim b/src/types/enums.nim index 21b27ab6..f88f665b 100644 --- a/src/types/enums.nim +++ b/src/types/enums.nim @@ -28,9 +28,8 @@ type INPUT_URL, INPUT_WEEK WhitespaceType* = enum - WHITESPACE_UNKNOWN, WHITESPACE_NORMAL, WHITESPACE_NOWRAP, WHITESPACE_PRE, - WHITESPACE_PRE_LINE, WHITESPACE_PRE_WRAP, WHITESPACE_INITIAL, - WHITESPACE_INHERIT + WHITESPACE_NORMAL, WHITESPACE_NOWRAP, WHITESPACE_PRE, WHITESPACE_PRE_LINE, + WHITESPACE_PRE_WRAP TagType* = enum TAG_UNKNOWN, TAG_HTML, TAG_BASE, TAG_HEAD, TAG_LINK, TAG_META, TAG_STYLE, @@ -62,34 +61,24 @@ type CSS_RBRACE_TOKEN CSSUnit* = enum - CM_UNIT, MM_UNIT, IN_UNIT, PX_UNIT, PT_UNIT, PC_UNIT, - EM_UNIT, EX_UNIT, CH_UNIT, REM_UNIT, VW_UNIT, VH_UNIT, VMIN_UNIT, VMAX_UNIT, - PERC_UNIT + UNIT_CM, UNIT_MM, UNIT_IN, UNIT_PX, UNIT_PT, UNIT_PC, + UNIT_EM, UNIT_EX, UNIT_CH, UNIT_REM, UNIT_VW, UNIT_VH, UNIT_VMIN, UNIT_VMAX, + UNIT_PERC CSSPosition* = enum - STATIC_POSITION, RELATIVE_POSITION, ABSOLUTE_POSITION, FIXED_POSITION, - INHERIT_POSITION + POSITION_STATIC, POSITION_RELATIVE, POSITION_ABSOLUTE, POSITION_FIXED, + POSITION_INHERIT -const DisplayInlineTags* = { - TAG_A, TAG_ABBR, TAG_B, TAG_BDO, TAG_BR, TAG_BUTTON, TAG_CITE, TAG_CODE, - TAG_DEL, TAG_DFN, TAG_EM, TAG_FONT, TAG_I, TAG_IMG, TAG_INS, TAG_INPUT, - TAG_IFRAME, TAG_KBD, TAG_LABEL, TAG_MAP, TAG_OBJECT, TAG_Q, TAG_SAMP, - TAG_SCRIPT, TAG_SELECT, TAG_SMALL, TAG_SPAN, TAG_STRONG, TAG_SUB, TAG_SUP, - TAG_TEXTAREA, TAG_TT, TAG_VAR, TAG_FONT, TAG_IFRAME, TAG_U, TAG_S, TAG_STRIKE, - TAG_FRAME, TAG_IMG, TAG_INPUT -} + CSSFontStyle* = enum + FONTSTYLE_NORMAL, FONTSTYLE_ITALIC, FONTSTYLE_OBLIQUE -const DisplayNoneTags* = { - TAG_AREA, TAG_BASE, TAG_SOURCE, TAG_TRACK, TAG_LINK, TAG_META, TAG_PARAM, TAG_WBR -} + CSSRuleType* = enum + RULE_ALL, RULE_COLOR, RULE_MARGIN, RULE_MARGIN_TOP, RULE_MARGIN_LEFT, + RULE_MARGIN_RIGHT, RULE_MARGIN_BOTTOM, RULE_FONT_STYLE, RULE_DISPLAY, + RULE_CONTENT -const DisplayInlineBlockTags* = { - TAG_IMG -} - -const DisplayTableColumnTags* = { - TAG_COL -} + CSSGlobalValueType* = enum + VALUE_INITIAL, VALUE_INHERIT, VALUE_REVERT, VALUE_UNSET const SelfClosingTagTypes* = { TAG_LI, TAG_P -- cgit 1.4.1-2-gfad0