diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/config/config.nim | 114 | ||||
-rw-r--r-- | src/config/toml.nim | 406 | ||||
-rw-r--r-- | src/css/parser.nim | 48 | ||||
-rw-r--r-- | src/io/buffer.nim | 2 | ||||
-rw-r--r-- | src/io/pipe.nim | 0 | ||||
-rw-r--r-- | src/utils/twtstr.nim | 88 |
6 files changed, 527 insertions, 131 deletions
diff --git a/src/config/config.nim b/src/config/config.nim index 91b17dda..98d08442 100644 --- a/src/config/config.nim +++ b/src/config/config.nim @@ -1,7 +1,9 @@ import tables import os import strutils +import streams +import config/toml import utils/twtstr import utils/radixtree @@ -33,19 +35,11 @@ type ACTION_LINED_ESC ActionMap = Table[string, TwtAction] - StaticConfig = object - nmap: ActionMap - lemap: ActionMap - stylesheet*: string - Config = object nmap*: ActionMap lemap*: ActionMap stylesheet*: string -func getConfig(s: StaticConfig): Config = - return Config(nmap: s.nmap, lemap: s.lemap) - func getRealKey(key: string): string = var realk: string var control = 0 @@ -106,6 +100,16 @@ func constructActionTable*(origTable: ActionMap): ActionMap = newTable[realk] = v return newTable +func getAction(s: string): TwtAction = + if s == "NULL": + return NO_ACTION + return parseEnum[TwtAction]("ACTION_" & s) + +func getLineAction(s: string): TwtAction = + if s == "NULL": + return NO_ACTION + return parseEnum[TwtAction]("ACTION_LINED_" & s) + proc readUserStylesheet(dir: string, file: string): string = if file.len == 0: return "" @@ -120,75 +124,33 @@ proc readUserStylesheet(dir: string, file: string): string = result = f.readAll() f.close() -proc parseConfigLine[T](line: string, config: var T) = - if line.len == 0 or line[0] == '#': - return - var cmd: seq[string] - var s = "" - var quote = false - var escape = false - for c in line: - if escape: - escape = false - s &= c - continue - - if not quote and c == ' ' and s.len > 0: - cmd.add(s) - s = "" - elif c == '"': - quote = not quote - elif c == '\\' and not quote: - escape = true - else: - s &= c - if s.len > 0: - cmd.add(s) - - if cmd.len == 3: - if cmd[0] == "nmap": - if cmd[2] == "NULL": - config.nmap[getRealKey(cmd[1])] = NO_ACTION - else: - config.nmap[getRealKey(cmd[1])] = parseEnum[TwtAction]("ACTION_" & cmd[2]) - elif cmd[0] == "lemap": - if cmd[2] == "NULL": - config.lemap[getRealKey(cmd[1])] = NO_ACTION - else: - config.lemap[getRealKey(cmd[1])] = parseEnum[TwtAction]("ACTION_LINED_" & cmd[2]) - elif cmd.len == 2: - if cmd[0] == "stylesheet": - config.stylesheet = cmd[1] - -proc staticReadConfig(): StaticConfig = - let default = staticRead"res/config" - for line in default.split('\n'): - parseConfigLine(line, result) - - result.nmap = constructActionTable(result.nmap) - result.lemap = constructActionTable(result.lemap) +func parseConfig(t: TomlValue): Config = + if "page" in t: + for k, v in t["page"].pairs: + result.nmap[getRealKey(k)] = getAction(v.s) + for k, v in t["line"].pairs: + result.nmap[getRealKey(k)] = getLineAction(v.s) + if "stylesheet" in t: + result.stylesheet = t["stylesheet"].s + +proc staticReadConfig(): Config = + result = parseConfig(parseToml(newStringStream(staticRead"res/config.toml"))) + result.stylesheet = readUserStylesheet("res", result.stylesheet) const defaultConfig = staticReadConfig() -var gconfig*: Config +var gconfig* = defaultConfig proc readConfig(dir: string) = - var f: File - let status = f.open(dir / "config", fmRead) - if status: - var line: TaintedString - while f.readLine(line): - parseConfigLine(line, gconfig) - - gconfig.nmap = constructActionTable(gconfig.nmap) - gconfig.lemap = constructActionTable(gconfig.lemap) - gconfig.stylesheet = readUserStylesheet(dir, gconfig.stylesheet) - f.close() - -proc readConfig*() = - gconfig = getConfig(defaultConfig) - when defined(debug): - readConfig(getCurrentDir() / "res") - readConfig(getConfigDir() / "twt") + let fs = newFileStream(dir / "config.toml") + if fs != nil: + let t = parseToml(fs) + if "page" in t: + for k, v in t["page"].pairs: + gconfig.nmap[getRealKey(k)] = getAction(v.s) + for k, v in t["line"].pairs: + gconfig.lemap[getRealKey(k)] = getLineAction(v.s) + if "stylesheet" in t: + gconfig.stylesheet = readUserStylesheet(dir, t["stylesheet"].s) proc getNormalAction*(s: string): TwtAction = if gconfig.nmap.hasKey(s): @@ -200,3 +162,9 @@ proc getLinedAction*(s: string): TwtAction = return gconfig.lemap[s] return NO_ACTION +proc readConfig*() = + when defined(debug): + readConfig(getCurrentDir() / "res") + readConfig(getConfigDir() / "twt") + gconfig.nmap = constructActionTable(gconfig.nmap) + gconfig.lemap = constructActionTable(gconfig.lemap) diff --git a/src/config/toml.nim b/src/config/toml.nim new file mode 100644 index 00000000..cfdb7763 --- /dev/null +++ b/src/config/toml.nim @@ -0,0 +1,406 @@ +import streams +import tables +import times +import strutils +import strformat +import unicode + +import utils/twtstr + +type + ValueType = enum + VALUE_STRING, VALUE_INTEGER, VALUE_FLOAT, VALUE_BOOLEAN, VALUE_DATE_TIME, + VALUE_TABLE, VALUE_ARRAY VALUE_TABLE_ARRAY + + SyntaxError = object of ValueError + + TomlParser = object + at: int + line: int + stream: Stream + buf: string + root: TomlTable + node: TomlNode + currkey: seq[string] + + TomlValue* = ref object + case vt*: ValueType + of VALUE_STRING: + s*: string + of VALUE_INTEGER: + i*: int64 + of VALUE_FLOAT: + f*: float64 + of VALUE_BOOLEAN: + b*: bool + of VALUE_TABLE: + t*: TomlTable + of VALUE_DATE_TIME: + dt*: DateTime + of VALUE_ARRAY: + a*: seq[TomlValue] + of VALUE_TABLE_ARRAY: + ta*: seq[TomlTable] + + TomlNode = ref object of RootObj + comment: string + + TomlKVPair = ref object of TomlNode + key*: seq[string] + value*: TomlValue + + TomlTable = ref object of TomlNode + key: seq[string] + nodes: seq[TomlNode] + map: Table[string, TomlValue] + +func `[]`*(val: TomlValue, key: string): TomlValue = + return val.t.map[key] + +iterator pairs*(val: TomlValue): (string, TomlValue) = + for k, v in val.t.map.pairs: + yield (k, v) + +func contains*(val: TomlValue, key: string): bool = + return key in val.t.map + +func isBare(c: char): bool = + return c == '-' or c == '_' or c.isAlphaNumeric() + +func peek(state: TomlParser, i: int): char = + return state.buf[state.at + i] + +func peek(state: TomlParser, i: int, len: int): string = + return state.buf.substr(state.at + i, state.at + i + len) + +proc syntaxError(state: TomlParser, msg: string) = + raise newException(SyntaxError, fmt"on line {state.line}: {msg}") + +proc valueError(state: TomlParser, msg: string) = + raise newException(ValueError, fmt"on line {state.line}: {msg}") + +proc consume(state: var TomlParser): char = + result = state.buf[state.at] + inc state.at + +proc reconsume(state: var TomlParser) = + dec state.at + +proc has(state: var TomlParser, i: int = 0): bool = + if state.at + i >= state.buf.len and not state.stream.atEnd(): + state.buf &= state.stream.readLine() & '\n' + return state.at + i < state.buf.len + +proc consumeEscape(state: var TomlParser, c: char): Rune = + var len = 4 + if c == 'U': + len = 8 + let c = state.consume() + var num = hexValue(c) + if num != -1: + var i = 0 + while state.has() and i < len: + let c = state.peek(0) + if hexValue(c) == -1: + break + discard state.consume() + num *= 0x10 + num += hexValue(c) + inc i + if i != len - 1: + state.syntaxError(fmt"invalid escaped length ({i}, needs {len})") + if num > 0x10FFFF or num in {0xD800..0xDFFF}: + state.syntaxError(fmt"invalid escaped codepoint {num}") + else: + return Rune(num) + else: + state.syntaxError(fmt"invalid escaped codepoint {c}") + +proc consumeString(state: var TomlParser, first: char): string = + var multiline = false + + if first == '"': + if state.has(1): + let s = state.peek(0, 2) + if s == "\"\"": + multiline = true + elif first == '\'': + if state.has(1): + let s = state.peek(0, 2) + if s == "''": + multiline = true + + if multiline: + let c = state.peek(0) + if c == '\n': + discard state.consume() + + var escape = false + var ml_trim = false + while state.has(): + let c = state.consume() + if c == '\n' and not multiline: + state.syntaxError(fmt"newline in string") + elif c == first: + if multiline and state.has(1): + let c2 = state.peek(0) + let c3 = state.peek(1) + if c2 == first and c3 == first: + break + else: + break + elif first == '"' and c == '\\': + escape = true + elif escape: + case c + of 'b': result &= '\b' + of 't': result &= '\t' + of 'n': result &= '\n' + of 'f': result &= '\f' + of 'r': result &= '\r' + of '"': result &= '"' + of '\\': result &= '\\' + of 'u', 'U': result &= state.consumeEscape(c) + of '\n': ml_trim = true + else: state.syntaxError(fmt"invalid escape sequence \{c}") + escape = false + elif ml_trim: + if not (c in {'\n', ' ', '\t'}): + result &= c + ml_trim = false + else: + result &= c + +proc consumeBare(state: var TomlParser, c: char): string = + result &= c + while state.has(): + let c = state.consume() + case c + of ' ', '\t': break + of '.', '=', ']', '\n': + state.reconsume() + break + elif c.isBare(): + result &= c + else: + state.syntaxError(fmt"invalid value in token: {c}") + +proc flushLine(state: var TomlParser) = + if state.node != nil: + if state.node of TomlKVPair: + var i = 0 + let keys = state.currkey & TomlKVPair(state.node).key + var table = state.root + while i < keys.len - 1: + if keys[i] in table.map: + let node = table.map[keys[i]] + if node.vt != VALUE_TABLE: + let s = keys.join(".") + state.valueError(fmt"re-definition of node {s}") + else: + table = node.t + else: + let node = TomlTable() + table.map[keys[i]] = TomlValue(vt: VALUE_TABLE, t: node) + table = node + inc i + + if keys[i] in table.map: + let s = keys.join(".") + state.valueError(fmt"re-definition of node {s}") + + table.map[keys[i]] = TomlKVPair(state.node).value + table.nodes.add(state.node) + state.node = nil + inc state.line + +proc consumeComment(state: var TomlParser) = + state.node = TomlNode() + while state.has(): + let c = state.consume() + if c == '\n': + state.reconsume() + break + else: + state.node.comment &= c + +proc consumeKey(state: var TomlParser): seq[string] = + var str = "" + while state.has(): + let c = state.consume() + case c + of '"', '\'': + if str.len > 0: + state.syntaxError("multiple strings without dot") + str = state.consumeString(c) + of '=', ']': + if str.len != 0: + result.add(str) + str = "" + return result + of '.': + if str.len == 0: #TODO empty strings are allowed, only empty keys aren't + state.syntaxError("redundant dot") + else: + result.add(str) + str = "" + of ' ', '\t': discard + of '\n': + if state.node != nil: + state.syntaxError("newline without value") + else: + state.flushLine() + elif c.isBare(): + if str.len > 0: + state.syntaxError(fmt"multiple strings without dot: {str}") + str = state.consumeBare(c) + else: state.syntaxError(fmt"invalid character in key: {c}") + + state.syntaxError("key without value") + +proc consumeTable(state: var TomlParser): TomlTable = + new(result) + while state.has(): + let c = state.peek(0) + case c + of ' ', '\t': discard + of '\n': + return result + of '[': + #TODO table array + state.syntaxError("arrays of tables are not supported yet") + of '"', '\'': + result.key = state.consumeKey() + elif c.isBare(): + result.key = state.consumeKey() + else: state.syntaxError(fmt"invalid character before key: {c}") + state.syntaxError("unexpected end of file") + +proc consumeNoState(state: var TomlParser): bool = + while state.has(): + let c = state.peek(0) + case c + of '#', '\n': + return false + of ' ', '\t': discard + of '[': + discard state.consume() + let table = state.consumeTable() + state.currkey = table.key + state.node = table + return false + elif c == '"' or c == '\'' or c.isBare(): + let kvpair = TomlKVPair() + kvpair.key = state.consumeKey() + state.node = kvpair + return true + else: state.syntaxError(fmt"invalid character before key: {c}") + state.syntaxError("unexpected end of file") + +proc consumeNumber(state: var TomlParser): TomlValue = + var repr: string + var isfloat = false + if state.has(): + if state.peek(0) == '+' or state.peek(0) == '-': + repr &= state.consume() + + while state.has() and isDigit(state.peek(0)): + repr &= state.consume() + + if state.has(1): + if state.peek(0) == '.' and isDigit(state.peek(1)): + repr &= state.consume() + repr &= state.consume() + isfloat = true + while state.has() and isDigit(state.peek(0)): + repr &= state.consume() + + if state.has(1): + if state.peek(0) == 'E' or state.peek(0) == 'e': + var j = 2 + if state.peek(1) == '-' or state.peek(1) == '+': + inc j + if state.has(j) and isDigit(state.peek(j)): + while j > 0: + repr &= state.consume() + dec j + + while state.has() and isDigit(state.peek(0)): + repr &= state.consume() + + if isfloat: + let val = parseFloat64(repr) + return TomlValue(vt: VALUE_FLOAT, f: val) + + let val = parseInt64(repr) + return TomlValue(vt: VALUE_INTEGER, i: val) + +proc consumeValue(state: var TomlParser): TomlValue + +proc consumeArray(state: var TomlParser): TomlValue = + result = TomlValue(vt: VALUE_ARRAY) + var val: TomlValue + while state.has(): + let c = state.consume() + case c + of ' ', '\t', '\n': discard + of ']': + if val != nil: + result.a.add(val) + break + of ',': + if val == nil: + state.syntaxError("comma without element") + result.a.add(val) + else: + state.reconsume() + val = state.consumeValue() + +proc consumeValue(state: var TomlParser): TomlValue = + while state.has(): + let c = state.consume() + case c + of '"', '\'': + return TomlValue(vt: VALUE_STRING, s: state.consumeString(c)) + of ' ', '\t': discard + of '\n': + state.syntaxError("newline without value") + of '#': + state.syntaxError("comment without value") + of '+', '-', '0'..'9': + return state.consumeNumber() + #TODO date-time + of '[': + return state.consumeArray() + elif c.isBare(): + let s = state.consumeBare(c) + case s + of "true": return TomlValue(vt: VALUE_BOOLEAN, b: true) + of "false": return TomlValue(vt: VALUE_BOOLEAN, b: false) + else: state.syntaxError(fmt"invalid token {s}") + else: + state.syntaxError(fmt"invalid character in value: {c}") + +proc parseToml*(inputStream: Stream): TomlValue = + var state: TomlParser + state.stream = inputStream + state.line = 1 + state.root = TomlTable() + + while state.has(): + if state.consumeNoState(): + let kvpair = TomlKVPair(state.node) + kvpair.value = state.consumeValue() + + while state.has(): + let c = state.consume() + case c + of '\n': + state.flushLine() + break + of '#': + state.consumeComment() + of '\t', ' ': discard + else: state.syntaxError(fmt"invalid character after value: {c}") + + return TomlValue(vt: VALUE_TABLE, t: state.root) diff --git a/src/css/parser.nim b/src/css/parser.nim index ec2b6591..0c1f91cc 100644 --- a/src/css/parser.nim +++ b/src/css/parser.nim @@ -1,6 +1,3 @@ -# CSS tokenizer and parser. It kinda works, though certain less specific -# details of the specification might have been implemented incorrectly. - import unicode import streams import math @@ -73,49 +70,6 @@ type func `==`*(a: CSSParsedItem, b: CSSTokenType): bool = return a of CSSToken and CSSToken(a).tokenType == b -func toNumber(s: seq[Rune]): float64 = - var sign = 1 - var t = 1 - var d = 0 - var integer: float64 = 0 - var f: float64 = 0 - var e: float64 = 0 - - var i = 0 - if i < s.len and s[i] == Rune('-'): - sign = -1 - inc i - elif i < s.len and s[i] == Rune('+'): - inc i - - while i < s.len and isDigitAscii(s[i]): - integer *= 10 - integer += float64(decValue(s[i])) - inc i - - if i < s.len and s[i] == Rune('.'): - inc i - while i < s.len and isDigitAscii(s[i]): - f *= 10 - f += float64(decValue(s[i])) - inc i - inc d - - if i < s.len and (s[i] == Rune('e') or s[i] == Rune('E')): - inc i - if i < s.len and s[i] == Rune('-'): - t = -1 - inc i - elif i < s.len and s[i] == Rune('+'): - inc i - - while i < s.len and isDigitAscii(s[i]): - e *= 10 - e += float64(decValue(s[i])) - inc i - - return float64(sign) * (integer + f * pow(10, float64(-d))) * pow(10, (float64(t) * e)) - func isNameStartCodePoint*(r: Rune): bool = return not isAscii(r) or r == Rune('_') or isAlphaAscii(r) @@ -273,7 +227,7 @@ proc consumeNumber(state: var CSSTokenizerState): tuple[t: tflagb, val: float64] while state.has() and isDigitAscii(state.curr()): repr &= state.consume() - let val = toNumber(repr) + let val = parseFloat64($repr) return (t, val) proc consumeNumericToken(state: var CSSTokenizerState): CSSToken = diff --git a/src/io/buffer.nim b/src/io/buffer.nim index 36700e12..18e4a3d6 100644 --- a/src/io/buffer.nim +++ b/src/io/buffer.nim @@ -719,7 +719,7 @@ proc renderPlainText*(buffer: Buffer, text: string) = buffer.updateCursor() -const css = staticRead"res/default.css" +const css = staticRead"res/ua.css" let ua_stylesheet = newStringStream(css).parseStylesheet() #TODO refactor diff --git a/src/io/pipe.nim b/src/io/pipe.nim new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/src/io/pipe.nim diff --git a/src/utils/twtstr.nim b/src/utils/twtstr.nim index 2dd8615b..d437be6a 100644 --- a/src/utils/twtstr.nim +++ b/src/utils/twtstr.nim @@ -5,6 +5,7 @@ import tables import json import bitops import os +import math when defined(posix): import posix @@ -40,15 +41,6 @@ func fitValueToSize*(str: string, size: int): string = return str & ' '.repeat(size - str.runeLen) return str.maxString(size) -func buttonFmt*(str: string): seq[string] = - return "[".ansiFgColor(fgRed) & str.ansiFgColor(fgRed).ansiReset() & "]".ansiFgColor(fgRed).ansiReset() - -func buttonFmt*(str: seq[string]): seq[string] = - return "[".ansiFgColor(fgRed) & str.ansiFgColor(fgRed).ansiReset() & "]".ansiFgColor(fgRed).ansiReset() - -func buttonRaw*(str: string): string = - return "[" & str & "]" - func remove*(str: string, c: string): string = let rem = c.toRunes()[0] for rune in str.runes: @@ -198,6 +190,83 @@ func skipBlanks*(buf: string, at: int): int = while result < buf.len and buf[result].isWhitespace(): inc result +func parseInt64*(s: string): int64 = + var sign = 1 + var t = 1 + var d = 0 + var integer: int64 = 0 + var e: int64 = 0 + + var i = 0 + if i < s.len and s[i] == '-': + sign = -1 + inc i + elif i < s.len and s[i] == '+': + inc i + + while i < s.len and isDigit(s[i]): + integer *= 10 + integer += decValue(s[i]) + inc i + + if i < s.len and (s[i] == 'e' or s[i] == 'E'): + inc i + if i < s.len and s[i] == '-': + t = -1 + inc i + elif i < s.len and s[i] == '+': + inc i + + while i < s.len and isDigit(s[i]): + e *= 10 + e += decValue(s[i]) + inc i + + return sign * integer * 10 ^ t * e + +func parseFloat64*(s: string): float64 = + var sign = 1 + var t = 1 + var d = 0 + var integer: float64 = 0 + var f: float64 = 0 + var e: float64 = 0 + + var i = 0 + if i < s.len and s[i] == '-': + sign = -1 + inc i + elif i < s.len and s[i] == '+': + inc i + + while i < s.len and isDigit(s[i]): + integer *= 10 + integer += float64(decValue(s[i])) + inc i + + if i < s.len and s[i] == '.': + inc i + while i < s.len and isDigit(s[i]): + f *= 10 + f += float64(decValue(s[i])) + inc i + inc d + + if i < s.len and (s[i] == 'e' or s[i] == 'E'): + inc i + if i < s.len and s[i] == '-': + t = -1 + inc i + elif i < s.len and s[i] == '+': + inc i + + while i < s.len and isDigit(s[i]): + e *= 10 + e += float64(decValue(s[i])) + inc i + + return float64(sign) * (integer + f * pow(10, float64(-d))) * pow(10, (float64(t) * e)) + proc expandPath*(path: string): string = if path.len == 0: return path @@ -599,4 +668,3 @@ proc fullwidth*(s: seq[Rune]): seq[Rune] = proc fullwidth*(s: string): string = return $fullwidth(s.toRunes()) - |