diff options
author | bptato <nincsnevem662@gmail.com> | 2022-10-19 11:24:45 +0200 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2022-10-19 11:25:56 +0200 |
commit | 08a758ed7a06e1bff8994c01d6c5d317d400ccf9 (patch) | |
tree | cb53cf39ebd323491715eb569cc509c00edcae91 /src | |
parent | c4e2de9cd8cad7e28b33e68b1b76f9044fe510be (diff) | |
download | chawan-08a758ed7a06e1bff8994c01d6c5d317d400ccf9.tar.gz |
Implement tree buffers, fix a js bug, refactor
Diffstat (limited to 'src')
-rw-r--r-- | src/display/client.nim | 593 | ||||
-rw-r--r-- | src/display/pager.nim | 380 | ||||
-rw-r--r-- | src/io/buffer.nim | 36 | ||||
-rw-r--r-- | src/io/loader.nim | 23 | ||||
-rw-r--r-- | src/js/javascript.nim | 1 | ||||
-rw-r--r-- | src/types/cookie.nim | 137 | ||||
-rw-r--r-- | src/utils/twtstr.nim | 1 |
7 files changed, 597 insertions, 574 deletions
diff --git a/src/display/client.nim b/src/display/client.nim index 82fc2299..1f15e907 100644 --- a/src/display/client.nim +++ b/src/display/client.nim @@ -1,11 +1,9 @@ import options import os import streams -import strutils import tables import terminal import times -import unicode import std/monotimes @@ -15,23 +13,21 @@ import display/pager import html/dom import html/htmlparser import io/buffer -import io/cell import io/lineedit import io/loader import io/request import io/term import js/javascript -import js/regex +import types/cookie import types/url import utils/twtstr type Client* = ref ClientObj ClientObj* = object - buffer*: Buffer + attrs: TermAttributes feednext: bool s: string - iserror: bool errormessage: string userstyle: CSSStylesheet loader: FileLoader @@ -40,11 +36,6 @@ type config: Config jsrt: JSRuntime jsctx: JSContext - regex: Option[Regex] - revsearch: bool - needsauth: bool - redirecturl: Option[Url] - cmdmode: bool timeoutid: int timeouts: Table[int, tuple[handler: proc(), time: int64]] added_timeouts: Table[int, tuple[handler: proc(), time: int64]] @@ -58,10 +49,6 @@ type lastbuf*: Buffer ibuf: string - ActionError* = object of IOError - LoadError* = object of ActionError - InterruptError* = object of LoadError - proc readChar(console: Console): char = if console.ibuf == "": return stdin.readChar() @@ -75,323 +62,26 @@ proc `=destroy`(client: var ClientObj) = free(client.jsrt) proc statusMode(client: Client) = - print(HVP(client.buffer.height + 1, 1)) + print(HVP(client.attrs.height + 1, 1)) print(EL()) -proc loadError(s: string) = - raise newException(LoadError, s) - -proc actionError(s: string) = - raise newException(ActionError, s) - -proc addBuffer(client: Client) = - if client.buffer == nil: - client.buffer = newBuffer() - else: - let oldnext = client.buffer.next - client.buffer.next = newBuffer() - if oldnext != nil: - oldnext.prev = client.buffer.next - client.buffer.next.prev = client.buffer - client.buffer.next.next = oldnext - client.buffer = client.buffer.next - client.buffer.loader = client.loader - client.buffer.userstyle = client.userstyle - -proc prevBuffer(client: Client) {.jsfunc.} = - if client.buffer.prev != nil: - client.buffer = client.buffer.prev - client.buffer.redraw = true - -proc nextBuffer(client: Client) {.jsfunc.} = - if client.buffer.next != nil: - client.buffer = client.buffer.next - client.buffer.redraw = true - -proc discardBuffer(buffer: Buffer) = - if buffer.next == nil and buffer.prev == nil: - actionError("Cannot discard last buffer!") - if buffer.sourcepair != nil: - buffer.sourcepair.sourcepair = nil - if buffer.next != nil: - buffer.next.prev = buffer.prev - if buffer.prev != nil: - buffer.prev.next = buffer.next - buffer.sourcepair = nil - buffer.next = nil - buffer.prev = nil - -proc discardBuffer(client: Client) {.jsfunc.} = - let old = client.buffer - if old.next != nil: - client.buffer = old.next - elif old.prev != nil: - client.buffer = old.prev - else: - actionError("Cannot discard last buffer!") - discardBuffer(old) - client.buffer.redraw = true - -proc setupBuffer(client: Client) = - let buffer = client.buffer - buffer.load() - buffer.render() - buffer.gotoAnchor() - buffer.redraw = true - -proc dupeBuffer(client: Client, location = none(URL)) {.jsfunc.} = - let prev = client.buffer - client.addBuffer() - client.buffer.contenttype = prev.contenttype - client.buffer.ispipe = prev.ispipe - client.buffer.istream = newStringStream(prev.source) - if location.issome: - client.buffer.location = location.get - else: - client.buffer.location = prev.location - client.buffer.document = prev.document - client.setupBuffer() - proc readPipe(client: Client, ctype: string) = - client.addBuffer() - client.buffer.contenttype = if ctype != "": ctype else: "text/plain" - client.buffer.ispipe = true - client.buffer.istream = newFileStream(stdin) - client.buffer.location = newURL("file://-") - client.buffer.load() + let buffer = newBuffer(client.config, client.loader) + buffer.contenttype = if ctype != "": ctype else: "text/plain" + buffer.ispipe = true + buffer.istream = newFileStream(stdin) + buffer.location = newURL("file://-") + client.pager.addBuffer(buffer) #TODO is this portable at all? if reopen(stdin, "/dev/tty", fmReadWrite): - client.setupBuffer() + buffer.setupBuffer() else: - client.buffer.drawBuffer() - -type Cookie = ref object of RootObj - name {.jsget.}: string - value {.jsget.}: string - expires {.jsget.}: int64 # unix time - maxAge {.jsget.}: int64 - secure {.jsget.}: bool - httponly {.jsget.}: bool - samesite {.jsget.}: bool - domain {.jsget.}: string - path {.jsget.}: string - -proc parseCookieDate(val: string): Option[DateTime] = - # cookie-date - const Delimiters = {'\t', ' '..'/', ';'..'@', '['..'`', '{'..'~'} - const NonDigit = Ascii + NonAscii - Digits - var foundTime = false - var foundDayOfMonth = false - var foundMonth = false - var foundYear = false - # date-token-list - var time: array[3, int] - var dayOfMonth: int - var month: int - var year: int - for dateToken in val.split(Delimiters): - if dateToken == "": continue # *delimiter - if not foundTime: - block timeBlock: # test for time - let hmsTime = dateToken.until(NonDigit - {':'}) - var i = 0 - for timeField in hmsTime.split(':'): - if i > 2: break timeBlock # too many time fields - # 1*2DIGIT - if timeField.len != 1 and timeField.len != 2: break timeBlock - var timeFields: array[3, int] - for c in timeField: - if c notin Digits: break timeBlock - timeFields[i] *= 10 - timeFields[i] += c.decValue - time = timeFields - inc i - if i != 3: break timeBlock - foundTime = true - continue - if not foundDayOfMonth: - block dayOfMonthBlock: # test for day-of-month - let digits = dateToken.until(NonDigit) - if digits.len != 1 and digits.len != 2: break dayOfMonthBlock - var n = 0 - for c in digits: - if c notin Digits: break dayOfMonthBlock - n *= 10 - n += c.decValue - dayOfMonth = n - foundDayOfMonth = true - continue - if not foundMonth: - block monthBlock: # test for month - if dateToken.len < 3: break monthBlock - case dateToken.substr(0, 2).toLower() - of "jan": month = 1 - of "feb": month = 2 - of "mar": month = 3 - of "apr": month = 4 - of "may": month = 5 - of "jun": month = 6 - of "jul": month = 7 - of "aug": month = 8 - of "sep": month = 9 - of "oct": month = 10 - of "nov": month = 11 - of "dec": month = 12 - else: break monthBlock - foundMonth = true - continue - if not foundYear: - block yearBlock: # test for year - let digits = dateToken.until(NonDigit) - if digits.len != 2 and digits.len != 4: break yearBlock - var n = 0 - for c in digits: - if c notin Digits: break yearBlock - n *= 10 - n += c.decValue - year = n - foundYear = true - continue - if not (foundDayOfMonth and foundMonth and foundYear and foundTime): return none(DateTime) - if dayOfMonth notin 0..31: return none(DateTime) - if year < 1601: return none(DateTime) - if time[0] > 23: return none(DateTime) - if time[1] > 59: return none(DateTime) - if time[2] > 59: return none(DateTime) - var dateTime = dateTime(year, Month(month), MonthdayRange(dayOfMonth), HourRange(time[0]), MinuteRange(time[1]), SecondRange(time[2])) - return some(dateTime) - -proc parseCookie(client: Client, str: string): Cookie {.jsfunc.} = - let cookie = new(Cookie) - var first = true - for part in str.split(';'): - if first: - cookie.name = part.until('=') - cookie.value = part.after('=') - first = false - continue - let part = percentDecode(part).strip(leading = true, trailing = false, AsciiWhitespace) - var n = 0 - for i in 0..part.high: - if part[i] == '=': - n = i - break - if n == 0: - continue - let key = part.substr(0, n - 1) - let val = part.substr(n + 1) - case key.toLower() - of "expires": - let date = parseCookieDate(val) - if date.issome: - cookie.expires = date.get.toTime().toUnix() - of "max-age": cookie.maxAge = parseInt64(val) - of "secure": cookie.secure = true - of "httponly": cookie.httponly = true - of "samesite": cookie.samesite = true - of "path": cookie.path = val - of "domain": cookie.domain = val - return cookie + buffer.load() + buffer.drawBuffer() proc doRequest(client: Client, req: Request): Response {.jsfunc.} = client.loader.doRequest(req) -# Load request in a new buffer. -var g_client: Client -proc gotoUrl(client: Client, request: Request, prevurl = none(URL), force = false, ctype = "") = - if force or prevurl.isnone or not prevurl.get.equals(request.url, true) or - prevurl.get.equals(request.url) or request.httpmethod != HTTP_GET: - let page = client.doRequest(request) - client.needsauth = page.status == 401 # Unauthorized - client.redirecturl = page.redirect - if page.body != nil: - client.addBuffer() - g_client = client - client.buffer.contenttype = if ctype != "": ctype else: page.contenttype - client.buffer.istream = page.body - client.buffer.location = request.url - client.setupBuffer() - else: - loadError("Couldn't load " & $request.url & " (" & $page.res & ")") - elif client.buffer != nil and prevurl.issome and prevurl.get.equals(request.url, true): - if client.buffer.hasAnchor(request.url.anchor): - client.dupeBuffer(request.url.some) - else: - loadError("Couldn't find anchor " & request.url.anchor) - -# Relative gotoUrl: either to prevurl, or if that's none, client.buffer.url. -proc gotoUrl(client: Client, url: string, prevurl = none(URL), force = false, ctype = "") = - var prevurl = prevurl - if prevurl.isnone and client.buffer != nil: - prevurl = client.buffer.location.some - let newurl = parseUrl(url, prevurl) - if newurl.isnone: - loadError("Invalid URL " & url) - client.gotoUrl(newRequest(newurl.get), prevurl, force, ctype) - -# When the user has passed a partial URL as an argument, they might've meant -# either: -# * file://$PWD/<file> -# * https://<url> -# So we attempt to load both, and see what works. -# (TODO: make this optional) -proc loadUrl(client: Client, url: string, ctype = "") = - let firstparse = parseUrl(url) - if firstparse.issome: - client.gotoUrl(newRequest(firstparse.get), none(Url), true, ctype) - else: - let cdir = parseUrl("file://" & getCurrentDir() & DirSep) - try: - # attempt to load local file - client.gotoUrl(url, cdir, true, ctype) - except LoadError: - try: - # attempt to load local file (this time percent encoded) - client.gotoUrl(percentEncode(url, LocalPathPercentEncodeSet), cdir, true, ctype) - except LoadError: - # attempt to load remote page - client.gotoUrl("https://" & url, none(Url), true, ctype) - -# Reload the page in a new buffer, then kill the previous buffer. -proc reloadPage(client: Client) {.jsfunc.} = - let buf = client.buffer - client.gotoUrl(newRequest(client.buffer.location), none(URL), true, client.buffer.contenttype) - discardBuffer(buf) - -# Open a URL prompt and visit the specified URL. -proc changeLocation(client: Client) {.jsfunc.} = - let buffer = client.buffer - var url = buffer.location.serialize(true) - client.statusMode() - let status = readLine("URL: ", url, buffer.width, config = client.config) - if status: - client.loadUrl(url) - -proc click(client: Client) {.jsfunc.} = - let req = client.buffer.click() - if req.issome: - client.gotoUrl(req.get, client.buffer.location.some) - -proc toggleSource*(client: Client) {.jsfunc.} = - let buffer = client.buffer - if buffer.sourcepair != nil: - client.buffer = buffer.sourcepair - client.buffer.redraw = true - else: - client.addBuffer() - client.buffer.sourcepair = client.buffer.prev - client.buffer.sourcepair.sourcepair = client.buffer - client.buffer.source = client.buffer.prev.source - client.buffer.streamclosed = true - client.buffer.location = client.buffer.sourcepair.location - client.buffer.ispipe = client.buffer.sourcepair.ispipe - let prevtype = client.buffer.prev.contenttype - if prevtype == "text/html": - client.buffer.contenttype = "text/plain" - else: - client.buffer.contenttype = "text/html" - client.setupBuffer() - proc interruptHandler(rt: JSRuntime, opaque: pointer): int {.cdecl.} = let client = cast[Client](opaque) try: @@ -433,184 +123,41 @@ proc command(client: Client, src: string) = restoreStdin() let previ = client.console.err.getPosition() client.command0(src) - g_client = client client.console.err.setPosition(previ) - if client.console.lastbuf == nil or client.console.lastbuf != client.buffer: - client.addBuffer() - client.buffer.istream = newStringStream(client.console.err.readAll()) #TODO - client.buffer.contenttype = "text/plain" - client.buffer.location = parseUrl("javascript:void(0);").get - client.console.lastbuf = client.buffer + if client.console.lastbuf == nil: + let buffer = newBuffer(client.config, client.loader) + buffer.istream = newStringStream(client.console.err.readAll()) #TODO + buffer.contenttype = "text/plain" + buffer.location = parseUrl("javascript:void(0);").get + client.console.lastbuf = buffer + client.pager.addBuffer(buffer) else: - client.buffer.istream = newStringStream(client.buffer.source & client.console.err.readAll()) - client.buffer.streamclosed = false - client.setupBuffer() - client.buffer.cursorLastLine() + client.console.lastbuf.istream = newStringStream(client.console.lastbuf.source & client.console.err.readAll()) + client.console.lastbuf.streamclosed = false + client.console.lastbuf.setupBuffer() + client.console.lastbuf.cursorLastLine() proc command(client: Client): bool {.jsfunc.} = var iput: string client.statusMode() - let status = readLine("COMMAND: ", iput, client.buffer.width, config = client.config) + let status = readLine("COMMAND: ", iput, client.attrs.width, config = client.config) if status: client.command(iput) return status proc commandMode(client: Client) {.jsfunc.} = - client.cmdmode = client.command() - -proc searchNext(client: Client) {.jsfunc.} = - if client.regex.issome: - if not client.revsearch: - discard client.buffer.cursorNextMatch(client.regex.get) - else: - discard client.buffer.cursorPrevMatch(client.regex.get) + client.pager.commandMode = client.command() -proc searchPrev(client: Client) {.jsfunc.} = - if client.regex.issome: - if not client.revsearch: - discard client.buffer.cursorPrevMatch(client.regex.get) - else: - discard client.buffer.cursorNextMatch(client.regex.get) - -proc search(client: Client) {.jsfunc.} = - client.statusMode() - var iput: string - let status = readLine("/", iput, client.buffer.width, config = client.config) - if status: - if iput.len != 0: - client.regex = compileSearchRegex(iput) - client.revsearch = false - client.searchNext() - -proc searchBack(client: Client) {.jsfunc.} = - client.statusMode() - var iput: string - let status = readLine("?", iput, client.buffer.width, config = client.config) - if status: - if iput.len != 0: - client.regex = compileSearchRegex(iput) - client.revsearch = true - client.searchNext() - -proc isearch(client: Client) {.jsfunc.} = - client.statusMode() - var iput: string - let cpos = client.buffer.cpos - var mark: Mark - template del_mark() = - if mark != nil: - client.buffer.removeMark(mark) - - let status = readLine("/", iput, client.buffer.width, {}, false, client.config, (proc(state: var LineState): bool = - del_mark - let regex = compileSearchRegex($state.news) - client.buffer.cpos = cpos - if regex.issome: - let match = client.buffer.cursorNextMatch(regex.get) - if match.success: - mark = client.buffer.addMark(match.x, match.y, match.str.width()) - client.buffer.redraw = true - client.buffer.refreshBuffer(true) - print(HVP(client.buffer.height + 1, 2)) - print(SGR()) - else: - del_mark - client.buffer.redraw = true - client.buffer.refreshBuffer(true) - print(HVP(client.buffer.height + 1, 2)) - print(SGR()) - return true - false - )) - - del_mark - client.buffer.redraw = true - client.buffer.refreshBuffer(true) - if status: - client.regex = compileSearchRegex(iput) - else: - client.buffer.cpos = cpos - -proc isearchBack(client: Client) {.jsfunc.} = - client.statusMode() - var iput: string - let cpos = client.buffer.cpos - var mark: Mark - template del_mark() = - if mark != nil: - client.buffer.removeMark(mark) - let status = readLine("?", iput, client.buffer.width, {}, false, client.config, (proc(state: var LineState): bool = - del_mark - let regex = compileSearchRegex($state.news) - client.buffer.cpos = cpos - if regex.issome: - let match = client.buffer.cursorPrevMatch(regex.get) - if match.success: - mark = client.buffer.addMark(match.x, match.y, match.str.width()) - client.buffer.redraw = true - client.buffer.refreshBuffer(true) - print(HVP(client.buffer.height + 1, 2)) - print(SGR()) - else: - del_mark - client.buffer.redraw = true - client.buffer.refreshBuffer(true) - print(HVP(client.buffer.height + 1, 2)) - print(SGR()) - return true - false - )) - del_mark - client.buffer.redraw = true - if status: - client.regex = compileSearchRegex(iput) - else: - client.buffer.cpos = cpos - -proc quit(client: Client) {.jsfunc.} = +proc quit(client: Client, code = 0) {.jsfunc.} = print(HVP(getTermAttributes().height, 0)) print(EL()) - quit(0) + quit(code) proc feedNext(client: Client) {.jsfunc.} = client.feednext = true -#TODO move this to a pager module or something -proc cursorLeft(client: Client) {.jsfunc.} = client.buffer.cursorLeft() -proc cursorDown(client: Client) {.jsfunc.} = client.buffer.cursorDown() -proc cursorUp(client: Client) {.jsfunc.} = client.buffer.cursorUp() -proc cursorRight(client: Client) {.jsfunc.} = client.buffer.cursorRight() -proc cursorLineBegin(client: Client) {.jsfunc.} = client.buffer.cursorLineBegin() -proc cursorLineEnd(client: Client) {.jsfunc.} = client.buffer.cursorLineEnd() -proc cursorNextWord(client: Client) {.jsfunc.} = client.buffer.cursorNextWord() -proc cursorPrevWord(client: Client) {.jsfunc.} = client.buffer.cursorPrevWord() -proc cursorNextLink(client: Client) {.jsfunc.} = client.buffer.cursorNextLink() -proc cursorPrevLink(client: Client) {.jsfunc.} = client.buffer.cursorPrevLink() -proc pageDown(client: Client) {.jsfunc.} = client.buffer.pageDown() -proc pageUp(client: Client) {.jsfunc.} = client.buffer.pageUp() -proc pageRight(client: Client) {.jsfunc.} = client.buffer.pageRight() -proc pageLeft(client: Client) {.jsfunc.} = client.buffer.pageLeft() -proc halfPageDown(client: Client) {.jsfunc.} = client.buffer.halfPageDown() -proc halfPageUp(client: Client) {.jsfunc.} = client.buffer.halfPageUp() -proc cursorFirstLine(client: Client) {.jsfunc.} = client.buffer.cursorFirstLine() -proc cursorLastLine(client: Client) {.jsfunc.} = client.buffer.cursorLastLine() -proc cursorTop(client: Client) {.jsfunc.} = client.buffer.cursorTop() -proc cursorMiddle(client: Client) {.jsfunc.} = client.buffer.cursorMiddle() -proc cursorBottom(client: Client) {.jsfunc.} = client.buffer.cursorBottom() -proc cursorLeftEdge(client: Client) {.jsfunc.} = client.buffer.cursorLeftEdge() -proc cursorVertMiddle(client: Client) {.jsfunc.} = client.buffer.cursorVertMiddle() -proc cursorRightEdge(client: Client) {.jsfunc.} = client.buffer.cursorRightEdge() -proc centerLine(client: Client) {.jsfunc.} = client.buffer.centerLine() -proc scrollDown(client: Client) {.jsfunc.} = client.buffer.scrollDown() -proc scrollUp(client: Client) {.jsfunc.} = client.buffer.scrollUp() -proc scrollLeft(client: Client) {.jsfunc.} = client.buffer.scrollLeft() -proc scrollRight(client: Client) {.jsfunc.} = client.buffer.scrollRight() -proc lineInfo(client: Client) {.jsfunc.} = client.buffer.lineInfo() -proc reshape(client: Client) {.jsfunc.} = client.buffer.reshape = true -proc redraw(client: Client) {.jsfunc.} = client.buffer.redraw = true - proc input(client: Client) = - if client.cmdmode: + if client.pager.commandMode: client.commandMode() return if not client.feednext: @@ -624,58 +171,16 @@ proc input(client: Client) = let action = getNormalAction(client.config, client.s) client.evalJSFree(action, "<command>") -proc followRedirect(client: Client) - -proc checkAuth(client: Client) = - if client.needsauth: - client.buffer.refreshBuffer() - client.statusMode() - var username = "" - let ustatus = readLine("Username: ", username, client.buffer.width, config = client.config) - if not ustatus: - client.needsauth = false - return - client.statusMode() - var password = "" - let pstatus = readLine("Password: ", password, client.buffer.width, hide = true, config = client.config) - if not pstatus: - client.needsauth = false - return - var url = client.buffer.location - url.username = username - url.password = password - var buf = client.buffer - client.gotoUrl(newRequest(url), prevurl = some(client.buffer.location)) - discardBuffer(buf) - client.followRedirect() - -proc followRedirect(client: Client) = - while client.redirecturl.issome: - client.statusMode() - print("Redirecting to ", $client.redirecturl.get) - stdout.flushFile() - client.buffer.refreshBuffer(true) - var buf = client.buffer - let redirecturl = client.redirecturl.get - client.redirecturl = none(Url) - client.gotoUrl(newRequest(redirecturl), prevurl = some(client.buffer.location)) - discardBuffer(buf) - if client.needsauth: - client.checkAuth() - proc inputLoop(client: Client) = while true: - g_client = client restoreStdin() - client.followRedirect() - client.checkAuth() - client.buffer.refreshBuffer() - if client.needsauth: # Unauthorized - client.checkAuth() - try: - client.input() - except ActionError as e: - client.buffer.setStatusMessage(e.msg) + client.pager.displayPage() + client.pager.followRedirect() + if client.pager.container != nil: + client.pager.container.buffer.refreshBuffer() + if client.pager.container.needsauth: # Unauthorized + client.pager.checkAuth() + client.input() #TODO this is dumb proc readFile(client: Client, path: string): string {.jsfunc.} = @@ -781,26 +286,22 @@ proc launchClient*(client: Client, pages: seq[string], ctype: string, dump: bool client.userstyle = client.config.stylesheet.parseStylesheet() if not stdin.isatty: client.readPipe(ctype) - try: - for page in pages: - client.loadUrl(page, ctype) - except LoadError as e: - eprint e.msg - quit(1) + for page in pages: + client.pager.loadURL(page, force = true, ctype = ctype) if stdout.isatty and not dump: when defined(posix): enableRawMode() client.inputLoop() else: - var buffer = client.buffer - while buffer.next != nil: - buffer = buffer.next - - buffer.drawBuffer() - while buffer.prev != nil: - buffer = buffer.prev - buffer.drawBuffer() + for msg in client.pager.status: + eprint msg + while client.pager.nextBuffer(): + discard + if client.pager.container != nil: + client.pager.container.buffer.drawBuffer() + while client.pager.prevBuffer(): + client.pager.container.buffer.drawBuffer() proc nimGCStats(client: Client): string {.jsfunc.} = return GC_getStatistics() @@ -827,13 +328,14 @@ proc newClient*(config: Config): Client = result.config = config result.loader = newFileLoader() result.console = newConsole() + result.attrs = getTermAttributes() + result.pager = newPager(config, result.attrs, result.loader) let rt = newJSRuntime() rt.setInterruptHandler(interruptHandler, cast[pointer](result)) let ctx = rt.newJSContext() result.jsrt = rt result.jsctx = ctx var global = ctx.getGlobalObject() - ctx.registerType(Cookie) ctx.registerType(Client, asglobal = true) global.setOpaque(result) ctx.setProperty(global.val, "client", global.val) @@ -841,6 +343,7 @@ proc newClient*(config: Config): Client = ctx.registerType(Console) + ctx.addCookieModule() ctx.addUrlModule() ctx.addDOMModule() ctx.addHTMLModule() diff --git a/src/display/pager.nim b/src/display/pager.nim index 9fc8957d..22574998 100644 --- a/src/display/pager.nim +++ b/src/display/pager.nim @@ -1,16 +1,39 @@ +import options +import os +import terminal +import unicode + import config/config import io/buffer +import io/cell +import io/lineedit +import io/loader +import io/request +import io/term import js/javascript +import js/regex +import types/url +import utils/twtstr type Container = ref object - buffer: Buffer + buffer*: Buffer children: seq[Container] + pos: CursorPosition + parent: Container + sourcepair: Container + needsauth*: bool #TODO move to buffer? + redirecturl: Option[URL] Pager* = ref object - rootContainer: Container - container: Container + attrs: TermAttributes + commandMode*: bool + container*: Container config: Config + loader: FileLoader + regex: Option[Regex] + reverseSearch: bool + status*: seq[string] proc cursorLeft(pager: Pager) {.jsfunc.} = pager.container.buffer.cursorLeft() proc cursorDown(pager: Pager) {.jsfunc.} = pager.container.buffer.cursorDown() @@ -45,18 +68,357 @@ proc lineInfo(pager: Pager) {.jsfunc.} = pager.container.buffer.lineInfo() proc reshape(pager: Pager) {.jsfunc.} = pager.container.buffer.reshape = true proc redraw(pager: Pager) {.jsfunc.} = pager.container.buffer.redraw = true -proc newContainer(): Container = +proc searchNext(pager: Pager) {.jsfunc.} = + if pager.regex.issome: + if not pager.reverseSearch: + discard pager.container.buffer.cursorNextMatch(pager.regex.get) + else: + discard pager.container.buffer.cursorPrevMatch(pager.regex.get) + +proc searchPrev(pager: Pager) {.jsfunc.} = + if pager.regex.issome: + if not pager.reverseSearch: + discard pager.container.buffer.cursorPrevMatch(pager.regex.get) + else: + discard pager.container.buffer.cursorNextMatch(pager.regex.get) + +proc statusMode(pager: Pager) = + print(HVP(pager.attrs.height + 1, 1)) + print(EL()) + +proc search(pager: Pager) {.jsfunc.} = + pager.statusMode() + var iput: string + let status = readLine("/", iput, pager.attrs.width, config = pager.config) + if status: + if iput.len != 0: + pager.regex = compileSearchRegex(iput) + pager.reverseSearch = false + pager.searchNext() + +proc searchBack(pager: Pager) {.jsfunc.} = + pager.statusMode() + var iput: string + let status = readLine("?", iput, pager.attrs.width, config = pager.config) + if status: + if iput.len != 0: + pager.regex = compileSearchRegex(iput) + pager.reverseSearch = true + pager.searchNext() + +proc displayPage*(pager: Pager) = + let buffer = pager.container.buffer + if buffer.refreshBuffer(): + stdout.hideCursor() + print(buffer.generateFullOutput()) + stdout.showCursor() + +proc isearch(pager: Pager) {.jsfunc.} = + pager.statusMode() + var iput: string + let cpos = pager.container.buffer.cpos + var mark: Mark + template del_mark() = + if mark != nil: + pager.container.buffer.removeMark(mark) + + let status = readLine("/", iput, pager.attrs.width, {}, false, pager.config, (proc(state: var LineState): bool = + del_mark + let regex = compileSearchRegex($state.news) + pager.container.buffer.cpos = cpos + if regex.issome: + let match = pager.container.buffer.cursorNextMatch(regex.get) + if match.success: + mark = pager.container.buffer.addMark(match.x, match.y, match.str.width()) + pager.container.buffer.redraw = true + pager.container.buffer.refreshBuffer(true) + pager.displayPage() + print(HVP(pager.attrs.height + 1, 2)) + print(SGR()) + else: + del_mark + pager.container.buffer.redraw = true + pager.container.buffer.refreshBuffer(true) + pager.displayPage() + print(HVP(pager.attrs.height + 1, 2)) + print(SGR()) + return true + false + )) + + del_mark + pager.container.buffer.redraw = true + pager.container.buffer.refreshBuffer(true) + if status: + pager.regex = compileSearchRegex(iput) + else: + pager.container.buffer.cpos = cpos + +proc isearchBack(pager: Pager) {.jsfunc.} = + pager.statusMode() + var iput: string + let cpos = pager.container.buffer.cpos + var mark: Mark + template del_mark() = + if mark != nil: + pager.container.buffer.removeMark(mark) + let status = readLine("?", iput, pager.container.buffer.width, {}, false, pager.config, (proc(state: var LineState): bool = + del_mark + let regex = compileSearchRegex($state.news) + pager.container.buffer.cpos = cpos + if regex.issome: + let match = pager.container.buffer.cursorPrevMatch(regex.get) + if match.success: + mark = pager.container.buffer.addMark(match.x, match.y, match.str.width()) + pager.container.buffer.redraw = true + pager.container.buffer.refreshBuffer(true) + pager.displayPage() + print(HVP(pager.attrs.height + 1, 2)) + print(SGR()) + else: + del_mark + pager.container.buffer.redraw = true + pager.container.buffer.refreshBuffer(true) + pager.displayPage() + print(HVP(pager.attrs.height + 1, 2)) + print(SGR()) + return true + false + )) + del_mark + pager.container.buffer.redraw = true + if status: + pager.regex = compileSearchRegex(iput) + else: + pager.container.buffer.cpos = cpos + +proc newContainer(buffer: Buffer, parent: Container): Container = new(result) + result.buffer = buffer + result.parent = parent -proc newPager*(config: Config, buffer: Buffer): Pager = +proc newPager*(config: Config, attrs: TermAttributes, loader: FileLoader): Pager = + new(result) result.config = config - result.rootContainer = newContainer() + result.attrs = attrs + result.loader = loader proc addBuffer*(pager: Pager, buffer: Buffer) = - var ncontainer = newContainer() - ncontainer.buffer = buffer - pager.container.children.add(ncontainer) + var ncontainer = newContainer(buffer, pager.container) + if pager.container != nil: + pager.container.children.add(ncontainer) pager.container = ncontainer +proc dupeBuffer*(pager: Pager, location = none(URL)) {.jsfunc.} = + var clone: Buffer + clone = pager.container.buffer.dupeBuffer(location) + pager.addBuffer(clone) + +# The prevBuffer and nextBuffer procedures emulate w3m's PREV and NEXT +# commands by traversing the container tree in a depth-first order. +proc prevBuffer*(pager: Pager): bool {.jsfunc.} = + if pager.container == nil: + return false + if pager.container.parent == nil: + return false + for i in 0..pager.container.parent.children.high: + let child = pager.container.parent.children[i] + if child == pager.container: + if i > 0: + pager.container = pager.container.parent.children[i - 1] + else: + pager.container = pager.container.parent + return true + assert false, "Container not a child of its parent" + +proc nextBuffer*(pager: Pager): bool {.jsfunc.} = + if pager.container == nil: + return false + if pager.container.children.len > 0: + pager.container = pager.container.children[0] + return true + if pager.container.parent == nil: + return false + for i in countdown(pager.container.parent.children.high, 0): + let child = pager.container.parent.children[i] + if child == pager.container: + if i < pager.container.parent.children.high: + pager.container = pager.container.parent.children[i + 1] + return true + return false + assert false, "Container not a child of its parent" + +#TODO we should have a separate status message stack for all buffers AND the +# pager. +proc setStatusMessage(pager: Pager, msg: string) = + if pager.container != nil: + pager.container.buffer.setStatusMessage(msg) + else: + pager.status.add(msg) + +proc discardBuffer*(pager: Pager) {.jsfunc.} = + if pager.container.parent == nil and pager.container.children.len == 0: + pager.setStatusMessage("Cannot discard last buffer!") + else: + if pager.container.parent != nil: + let parent = pager.container.parent + let n = parent.children.find(pager.container) + assert n != -1, "Container not a child of its parent" + for i in countdown(pager.container.children.high, 0): + let child = pager.container.children[i] + child.parent = pager.container.parent + parent.children.insert(child, n + 1) + parent.children.delete(n) + pager.container = parent + else: + pager.container = pager.container.children[0] + pager.container.parent = nil + +proc drawBuffer*(pager: Pager) {.jsfunc.} = + pager.container.buffer.drawBuffer() #TODO move this to pager + +proc toggleSource*(pager: Pager) {.jsfunc.} = + if pager.container.sourcepair != nil: + pager.container = pager.container.sourcepair + else: + let buffer = newBuffer(pager.config, pager.loader) + buffer.source = pager.container.buffer.source + buffer.streamclosed = true + buffer.location = pager.container.buffer.location + buffer.ispipe = pager.container.buffer.ispipe + if pager.container.buffer.contenttype == "text/plain": + buffer.contenttype = "text/html" + else: + buffer.contenttype = "text/plain" + buffer.setupBuffer() + let container = newContainer(buffer, pager.container) + container.sourcepair = pager.container + pager.container.sourcepair = container + pager.container.children.add(container) + +# Load request in a new buffer. +proc gotoURL*(pager: Pager, request: Request, prevurl = none(URL), force = false, ctype = "", replace = false): bool {.discardable.} = + if force or prevurl.isnone or not prevurl.get.equals(request.url, true) or + request.url.hash == "" or request.httpmethod != HTTP_GET: + # Basically, we want to reload the page *only* when + # a) force == true + # b) or the new URL isn't just the old URL + an anchor + # I think this makes navigation pretty natural, or at least very close to + # what other browsers do. Still, it would be nice if we got some visual + # feedback on what is actually going to happen when typing a URL; TODO. + let response = pager.loader.doRequest(request) + if response.body != nil: + let buffer = newBuffer(pager.config, pager.loader) + buffer.contenttype = if ctype != "": ctype else: response.contenttype + buffer.istream = response.body + buffer.location = request.url + buffer.setupBuffer() + if replace: + pager.discardBuffer() + pager.addBuffer(buffer) + pager.container.needsauth = response.status == 401 # Unauthorized + pager.container.redirecturl = response.redirect + else: + pager.setStatusMessage("Couldn't load " & $request.url & " (" & $response.res & ")") + return false + else: + if pager.container.buffer.hasAnchor(request.url.anchor): + pager.dupeBuffer(request.url.some) + else: + pager.setStatusMessage("Couldn't find anchor " & request.url.anchor) + return false + return true + +# When the user has passed a partial URL as an argument, they might've meant +# either: +# * file://$PWD/<file> +# * https://<url> +# So we attempt to load both, and see what works. +# (TODO: make this optional) +proc loadURL*(pager: Pager, url: string, force = false, ctype = "") = + let firstparse = parseURL(url) + if firstparse.issome: + let prev = if pager.container != nil: + some(pager.container.buffer.location) + else: + none(URL) + pager.gotoURL(newRequest(firstparse.get), prev, force, ctype) + return + let cdir = parseURL("file://" & getCurrentDir() & DirSep) + let newurl = parseURL(url, cdir) + if newurl.isSome: + # attempt to load local file + if pager.gotoURL(newRequest(newurl.get), force = force, ctype = ctype): + return + block: + let purl = percentEncode(url, LocalPathPercentEncodeSet) + if purl != url: + let newurl = parseURL(purl, cdir) + if newurl.isSome: + if pager.gotoURL(newRequest(newurl.get), force = force, ctype = ctype): + pager.status.setLen(0) + return + block: + let newurl = parseURL("https://" & url) + if newurl.isSome: + # attempt to load remote page + if pager.gotoURL(newRequest(newurl.get), force = force, ctype = ctype): + pager.status.setLen(0) + return + pager.setStatusMessage("Invalid URL " & url) + +# Open a URL prompt and visit the specified URL. +proc changeLocation(pager: Pager) {.jsfunc.} = + var url = pager.container.buffer.location.serialize() + pager.statusMode() + let status = readLine("URL: ", url, pager.attrs.width, config = pager.config) + if status: + pager.loadURL(url) + +# Reload the page in a new buffer, then kill the previous buffer. +proc reloadPage(pager: Pager) {.jsfunc.} = + pager.gotoURL(newRequest(pager.container.buffer.location), none(URL), true, pager.container.buffer.contenttype, true) + +proc click(pager: Pager) {.jsfunc.} = + #TODO this conflicts with the planned event loop + let req = pager.container.buffer.click() + if req.issome: + pager.gotoURL(req.get, pager.container.buffer.location.some) + +proc followRedirect*(pager: Pager) + +proc checkAuth*(pager: Pager) = + if pager.container != nil and pager.container.needsauth: + pager.container.buffer.refreshBuffer() + pager.statusMode() + var username = "" + let ustatus = readLine("Username: ", username, pager.attrs.width, config = pager.config) + if not ustatus: + pager.container.needsauth = false + return + pager.statusMode() + var password = "" + let pstatus = readLine("Password: ", password, pager.attrs.width, hide = true, config = pager.config) + if not pstatus: + pager.container.needsauth = false + return + var url = pager.container.buffer.location + url.username = username + url.password = password + pager.gotoURL(newRequest(url), prevurl = some(pager.container.buffer.location), replace = true) + pager.followRedirect() + +proc followRedirect*(pager: Pager) = + while pager.container != nil and pager.container.redirecturl.issome: + pager.statusMode() + print("Redirecting to ", $pager.container.redirecturl.get) + stdout.flushFile() + pager.container.buffer.refreshBuffer(true) + let redirecturl = pager.container.redirecturl.get + pager.container.redirecturl = none(URL) + pager.gotoURL(newRequest(redirecturl), prevurl = some(pager.container.buffer.location), replace = true) + if pager.container.needsauth: + pager.checkAuth() + proc addPagerModule*(ctx: JSContext) = ctx.registerType(Pager) diff --git a/src/io/buffer.nim b/src/io/buffer.nim index b5e46297..1e0a1c29 100644 --- a/src/io/buffer.nim +++ b/src/io/buffer.nim @@ -63,19 +63,18 @@ type streamclosed*: bool source*: string prevnode*: StyledNode - sourcepair*: Buffer - prev*: Buffer - next*: Buffer userstyle*: CSSStylesheet loader*: FileLoader marks*: seq[Mark] config*: Config -proc newBuffer*(): Buffer = +proc newBuffer*(config: Config, loader: FileLoader): Buffer = new(result) result.attrs = getTermAttributes() result.width = result.attrs.width result.height = result.attrs.height - 1 + result.config = config + result.loader = loader result.display = newFixedGrid(result.width, result.height) result.prevdisplay = newFixedGrid(result.width, result.height) @@ -87,7 +86,7 @@ func fromx*(buffer: Buffer): int {.inline.} = buffer.cpos.fromx func fromy*(buffer: Buffer): int {.inline.} = buffer.cpos.fromy func xend*(buffer: Buffer): int {.inline.} = buffer.cpos.xend -func generateFullOutput(buffer: Buffer): string = +func generateFullOutput*(buffer: Buffer): string = var x = 0 var w = 0 var format = newFormat() @@ -878,6 +877,8 @@ proc render*(buffer: Buffer) = of "text/html": if buffer.viewport == nil: buffer.viewport = Viewport(term: buffer.attrs) + if buffer.userstyle == nil: + buffer.userstyle = buffer.config.stylesheet.parseStylesheet() let ret = renderDocument(buffer.document, buffer.attrs, buffer.userstyle, buffer.viewport, buffer.prevstyled) buffer.lines = ret[0] buffer.prevstyled = ret[1] @@ -1196,6 +1197,24 @@ proc click*(buffer: Buffer): Option[Request] = else: restore_focus +proc setupBuffer*(buffer: Buffer) = + buffer.load() + buffer.render() + buffer.gotoAnchor() + buffer.redraw = true + +proc dupeBuffer*(buffer: Buffer, location = none(URL)): Buffer = + let clone = newBuffer(buffer.config, buffer.loader) + clone.contenttype = buffer.contenttype + clone.ispipe = buffer.ispipe + if location.isSome: + clone.location = location.get + else: + clone.location = buffer.location + clone.istream = newStringStream(buffer.source) + clone.setupBuffer() + return clone + proc drawBuffer*(buffer: Buffer) = var format = newFormat() for line in buffer.lines: @@ -1218,8 +1237,11 @@ proc drawBuffer*(buffer: Buffer) = print(format.processFormat(newFormat())) print("\n") -proc refreshBuffer*(buffer: Buffer, peek = false) = +proc refreshTitle*(buffer: Buffer) = buffer.title = buffer.getTitle() + +proc refreshBuffer*(buffer: Buffer, peek = false): bool {.discardable.} = + buffer.refreshTitle() stdout.hideCursor() if buffer.refreshTermAttrs(): @@ -1229,6 +1251,7 @@ proc refreshBuffer*(buffer: Buffer, peek = false) = if buffer.redraw: buffer.refreshDisplay() buffer.displayBuffer() + #result = true buffer.redraw = false if not peek: @@ -1239,6 +1262,7 @@ proc refreshBuffer*(buffer: Buffer, peek = false) = buffer.reshape = false buffer.refreshDisplay() buffer.displayBufferSwapOutput() + #result = true if not peek: if not buffer.nostatus: diff --git a/src/io/loader.nim b/src/io/loader.nim index 1530bf5b..7e6faab4 100644 --- a/src/io/loader.nim +++ b/src/io/loader.nim @@ -76,12 +76,18 @@ proc loadResource(loader: FileLoader, request: Request, ostream: Stream) = ostream.swrite(-1) # error ostream.flush() -proc runFileLoader(loader: FileLoader, loadcb: proc()) = +proc runFileLoader(loader: FileLoader, fd: cint) = if curl_global_init(CURL_GLOBAL_ALL) != CURLE_OK: raise newException(Defect, "Failed to initialize libcurl.") let ssock = initServerSocket(getpid()) # The server has been initialized, so the main process can resume execution. - loadcb() + var writef: File + if not open(writef, FileHandle(fd), fmWrite): + raise newException(Defect, "Failed to open input handle.") + writef.write(char(0u8)) + writef.flushFile() + close(writef) + discard close(fd) while true: let stream = ssock.acceptSocketStream() try: @@ -111,7 +117,7 @@ proc doRequest*(loader: FileLoader, request: Request): Response = if "Content-Type" in result.headers.table: result.contenttype = result.headers.table["Content-Type"][0].until(';') else: - result.contenttype = guessContentType($request.url) + result.contenttype = guessContentType($request.url.path) if "Location" in result.headers.table: let location = result.headers.table["Location"][0] result.redirect = parseUrl(location, some(request.url)) @@ -131,15 +137,7 @@ proc newFileLoader*(defaultHeaders: HeaderList): FileLoader = elif pid == 0: # child process discard close(pipefd[0]) # close read - var writef: File - if not open(writef, FileHandle(pipefd[1]), fmWrite): - raise newException(Defect, "Failed to open input handle.") - result.runFileLoader((proc() = - writef.write(char(0u8)) - writef.flushFile() - close(writef) - discard close(pipefd[1]) - )) + result.runFileLoader(pipefd[1]) else: result.process = pid let readfd = pipefd[0] # get read @@ -150,7 +148,6 @@ proc newFileLoader*(defaultHeaders: HeaderList): FileLoader = assert readf.readChar() == char(0u8) close(readf) discard close(pipefd[0]) - proc newFileLoader*(): FileLoader = newFileLoader(DefaultHeaders) diff --git a/src/js/javascript.nim b/src/js/javascript.nim index 3047a9de..b453cb13 100644 --- a/src/js/javascript.nim +++ b/src/js/javascript.nim @@ -1302,6 +1302,7 @@ macro registerType*(ctx: typed, t: typed, parent: JSClassID = 0, asglobal = fals elif f1.strVal.startsWith("js_set"): setters[f0] = f1 else: + f0 = fun.name tabList.add(quote do: JS_CFUNC_DEF(`f0`, 0, cast[JSCFunction](`f1`))) diff --git a/src/types/cookie.nim b/src/types/cookie.nim new file mode 100644 index 00000000..b0578e44 --- /dev/null +++ b/src/types/cookie.nim @@ -0,0 +1,137 @@ +import options +import strutils +import times + +import js/javascript +import utils/twtstr + +type Cookie = ref object of RootObj + name {.jsget.}: string + value {.jsget.}: string + expires {.jsget.}: int64 # unix time + maxAge {.jsget.}: int64 + secure {.jsget.}: bool + httponly {.jsget.}: bool + samesite {.jsget.}: bool + domain {.jsget.}: string + path {.jsget.}: string + +proc parseCookieDate(val: string): Option[DateTime] = + # cookie-date + const Delimiters = {'\t', ' '..'/', ';'..'@', '['..'`', '{'..'~'} + const NonDigit = AllChars - AsciiDigit + var foundTime = false + var foundDayOfMonth = false + var foundMonth = false + var foundYear = false + # date-token-list + var time: array[3, int] + var dayOfMonth: int + var month: int + var year: int + for dateToken in val.split(Delimiters): + if dateToken == "": continue # *delimiter + if not foundTime: + block timeBlock: # test for time + let hmsTime = dateToken.until(NonDigit - {':'}) + var i = 0 + for timeField in hmsTime.split(':'): + if i > 2: break timeBlock # too many time fields + # 1*2DIGIT + if timeField.len != 1 and timeField.len != 2: break timeBlock + var timeFields: array[3, int] + for c in timeField: + if c notin AsciiDigit: break timeBlock + timeFields[i] *= 10 + timeFields[i] += c.decValue + time = timeFields + inc i + if i != 3: break timeBlock + foundTime = true + continue + if not foundDayOfMonth: + block dayOfMonthBlock: # test for day-of-month + let digits = dateToken.until(NonDigit) + if digits.len != 1 and digits.len != 2: break dayOfMonthBlock + var n = 0 + for c in digits: + if c notin AsciiDigit: break dayOfMonthBlock + n *= 10 + n += c.decValue + dayOfMonth = n + foundDayOfMonth = true + continue + if not foundMonth: + block monthBlock: # test for month + if dateToken.len < 3: break monthBlock + case dateToken.substr(0, 2).toLower() + of "jan": month = 1 + of "feb": month = 2 + of "mar": month = 3 + of "apr": month = 4 + of "may": month = 5 + of "jun": month = 6 + of "jul": month = 7 + of "aug": month = 8 + of "sep": month = 9 + of "oct": month = 10 + of "nov": month = 11 + of "dec": month = 12 + else: break monthBlock + foundMonth = true + continue + if not foundYear: + block yearBlock: # test for year + let digits = dateToken.until(NonDigit) + if digits.len != 2 and digits.len != 4: break yearBlock + var n = 0 + for c in digits: + if c notin AsciiDigit: break yearBlock + n *= 10 + n += c.decValue + year = n + foundYear = true + continue + if not (foundDayOfMonth and foundMonth and foundYear and foundTime): return none(DateTime) + if dayOfMonth notin 0..31: return none(DateTime) + if year < 1601: return none(DateTime) + if time[0] > 23: return none(DateTime) + if time[1] > 59: return none(DateTime) + if time[2] > 59: return none(DateTime) + var dateTime = dateTime(year, Month(month), MonthdayRange(dayOfMonth), HourRange(time[0]), MinuteRange(time[1]), SecondRange(time[2])) + return some(dateTime) + +proc newCookie(str: string): Cookie {.jsctor.} = + let cookie = new(Cookie) + var first = true + for part in str.split(';'): + if first: + cookie.name = part.until('=') + cookie.value = part.after('=') + first = false + continue + let part = percentDecode(part).strip(leading = true, trailing = false, AsciiWhitespace) + var n = 0 + for i in 0..part.high: + if part[i] == '=': + n = i + break + if n == 0: + continue + let key = part.substr(0, n - 1) + let val = part.substr(n + 1) + case key.toLower() + of "expires": + let date = parseCookieDate(val) + if date.issome: + cookie.expires = date.get.toTime().toUnix() + of "max-age": cookie.maxAge = parseInt64(val) + of "secure": cookie.secure = true + of "httponly": cookie.httponly = true + of "samesite": cookie.samesite = true + of "path": cookie.path = val + of "domain": cookie.domain = val + return cookie + +proc addCookieModule*(ctx: JSContext) = + ctx.registerType(Cookie) diff --git a/src/utils/twtstr.nim b/src/utils/twtstr.nim index 41ffd09f..3132510e 100644 --- a/src/utils/twtstr.nim +++ b/src/utils/twtstr.nim @@ -32,7 +32,6 @@ const Ascii* = {chr(0x00)..chr(0x7F)} const AsciiUpperAlpha* = {'A'..'Z'} const AsciiLowerAlpha* = {'a'..'z'} const AsciiAlpha* = (AsciiUpperAlpha + AsciiLowerAlpha) -const AllChars = {chr(0x00)..chr(0xFF)} const NonAscii* = (AllChars - Ascii) const AsciiDigit* = {'0'..'9'} const AsciiHexDigit* = (AsciiDigit + {'a'..'f', 'A'..'F'}) |