diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/buffer/buffer.nim | 23 | ||||
-rw-r--r-- | src/config/config.nim | 453 | ||||
-rw-r--r-- | src/config/toml.nim | 46 | ||||
-rw-r--r-- | src/display/client.nim | 25 | ||||
-rw-r--r-- | src/display/pager.nim | 49 | ||||
-rw-r--r-- | src/display/term.nim | 24 | ||||
-rw-r--r-- | src/encoding/decoderstream.nim | 34 | ||||
-rw-r--r-- | src/html/htmlparser.nim | 100 | ||||
-rw-r--r-- | src/ips/editor.nim | 4 | ||||
-rw-r--r-- | src/ips/forkserver.nim | 8 | ||||
-rw-r--r-- | src/ips/serialize.nim | 2 | ||||
-rw-r--r-- | src/main.nim | 37 | ||||
-rw-r--r-- | src/render/rendertext.nim | 33 | ||||
-rw-r--r-- | src/types/buffersource.nim | 2 | ||||
-rw-r--r-- | src/utils/opt.nim | 34 | ||||
-rw-r--r-- | src/utils/twtstr.nim | 6 |
16 files changed, 549 insertions, 331 deletions
diff --git a/src/buffer/buffer.nim b/src/buffer/buffer.nim index 4a091091..b0dc0696 100644 --- a/src/buffer/buffer.nim +++ b/src/buffer/buffer.nim @@ -17,8 +17,8 @@ import css/cssparser import css/mediaquery import css/sheet import css/stylednode -import data/charset import config/config +import data/charset import html/dom import html/env import html/htmlparser @@ -72,7 +72,6 @@ type Buffer* = ref object fd: int alive: bool - cs: Charset readbufsize: int contenttype: string lines: FlexibleGrid @@ -239,6 +238,11 @@ macro task(fun: typed) = pfun.istask = true fun +func charsets(buffer: Buffer): seq[Charset] = + if buffer.source.charset.isSome: + return @[buffer.source.charset.get] + return buffer.config.charsets + func getTitleAttr(node: StyledNode): string = if node == nil: return "" @@ -646,12 +650,9 @@ proc finishLoad(buffer: Buffer): EmptyPromise = buffer.available = 0 if buffer.window == nil: buffer.window = newWindow(buffer.config.scripting) - let (doc, cs) = parseHTML(buffer.sstream, fallbackcs = buffer.cs, window = buffer.window, url = buffer.url) + let doc = parseHTML(buffer.sstream, charsets = buffer.charsets, + window = buffer.window, url = buffer.url) buffer.document = doc - if buffer.document == nil: # needsreinterpret - buffer.sstream.setPosition(0) - let (doc, _) = parseHTML(buffer.sstream, cs = some(cs), window = buffer.window, url = buffer.url) - buffer.document = doc buffer.state = LOADING_RESOURCES p = buffer.loadResources(buffer.document) else: @@ -741,8 +742,9 @@ proc cancel*(buffer: Buffer): int {.proxy.} = buffer.available = 0 if buffer.window == nil: buffer.window = newWindow(buffer.config.scripting) - let (doc, _) = parseHTML(buffer.sstream, cs = some(buffer.cs), window = buffer.window, url = buffer.url) # confidence: certain - buffer.document = doc + buffer.document = parseHTML(buffer.sstream, + charsets = buffer.charsets, window = buffer.window, + url = buffer.url, canReinterpret = false) buffer.do_reshape() return buffer.lines.len @@ -1220,7 +1222,6 @@ proc launchBuffer*(config: BufferConfig, source: BufferSource, mainproc: Pid) = let buffer = Buffer( alive: true, - cs: CHARSET_UTF_8, userstyle: parseStylesheet(config.userstyle), attrs: attrs, config: config, @@ -1235,7 +1236,7 @@ proc launchBuffer*(config: BufferConfig, source: BufferSource, buffer.selector = newSelector[int]() loader.registerFun = proc(fd: int) = buffer.selector.registerHandle(fd, {Read}, 0) loader.unregisterFun = proc(fd: int) = buffer.selector.unregister(fd) - buffer.srenderer = newStreamRenderer(buffer.sstream) + buffer.srenderer = newStreamRenderer(buffer.sstream, buffer.charsets) if buffer.config.scripting: buffer.window = newWindow(buffer.config.scripting, some(buffer.loader)) let socks = connectSocketStream(mainproc, false) diff --git a/src/config/config.nim b/src/config/config.nim index d3cad0b1..26c6d5ac 100644 --- a/src/config/config.nim +++ b/src/config/config.nim @@ -5,6 +5,7 @@ import streams import buffer/cell import config/toml +import data/charset import io/request import io/urlfilter import js/javascript @@ -13,6 +14,7 @@ import types/color import types/cookie import types/referer import types/url +import utils/opt import utils/twtstr type @@ -32,6 +34,7 @@ type sharecookiejar: Option[string] refererfrom*: Option[bool] scripting: Option[bool] + document_charset: seq[Charset] StaticOmniRule = object match: string @@ -46,36 +49,60 @@ type sharecookiejar*: Option[string] refererfrom*: Option[bool] scripting*: Option[bool] + document_charset*: seq[Charset] OmniRule* = object match*: Regex subst*: (proc(s: string): Option[string]) - Config* = ref ConfigObj - ConfigObj* = object - searchwrap* {.jsget, jsset.}: bool - maxredirect*: int - prependhttps* {.jsget, jsset.}: bool - termreload*: bool - nmap*: ActionMap - lemap*: ActionMap - stylesheet* {.jsget, jsset.}: string - startup*: string - ambiguous_double*: bool - hlcolor*: RGBAColor + StartConfig = object + visual_home*: string + startup_script*: string headless*: bool - colormode*: Option[ColorMode] - formatmode*: Option[FormatMode] - noformatmode*: FormatMode - altscreen*: Option[bool] - mincontrast*: int - editor*: string + + CSSConfig = object + stylesheet*: string + + SearchConfig = object + wrap*: bool + + EncodingConfig = object + document_charset*: seq[Charset] + + ExternalConfig = object tmpdir*: string + editor*: string + + NetworkConfig = object + max_redirect*: int32 + prepend_https*: bool + + DisplayConfig = object + color_mode*: Option[ColorMode] + format_mode*: Option[FormatMode] + no_format_mode*: FormatMode + emulate_overline*: bool + alt_screen*: Option[bool] + highlight_color*: RGBAColor + double_width_ambiguous*: bool + minimum_contrast*: int32 + force_clear*: bool + + #TODO: add JS wrappers for objects + Config* = ref ConfigObj + ConfigObj* = object + includes: seq[string] + start*: StartConfig + search*: SearchConfig + css*: CSSConfig + encoding*: EncodingConfig + external*: ExternalConfig + network*: NetworkConfig + display*: DisplayConfig siteconf: seq[StaticSiteConfig] omnirules: seq[StaticOmniRule] - forceclear*: bool - emulateoverline*: bool - visualhome*: string + page*: ActionMap + line*: ActionMap BufferConfig* = object userstyle*: string @@ -85,6 +112,7 @@ type refererfrom*: bool referrerpolicy*: ReferrerPolicy scripting*: bool + charsets*: seq[Charset] ForkServerConfig* = object tmpdir*: string @@ -100,19 +128,21 @@ const DefaultHeaders* = { func getForkServerConfig*(config: Config): ForkServerConfig = return ForkServerConfig( - tmpdir: config.tmpdir, - ambiguous_double: config.ambiguous_double + tmpdir: config.external.tmpdir, + ambiguous_double: config.display.double_width_ambiguous ) proc getBufferConfig*(config: Config, location: URL, cookiejar: CookieJar = nil, - headers: HeaderList = nil, refererfrom = false, scripting = false): BufferConfig = + headers: HeaderList = nil, refererfrom = false, scripting = false, + charsets = config.encoding.document_charset): BufferConfig = result = BufferConfig( - userstyle: config.stylesheet, + userstyle: config.css.stylesheet, filter: newURLFilter(scheme = some(location.scheme), default = true), cookiejar: cookiejar, headers: headers, refererfrom: refererfrom, - scripting: scripting + scripting: scripting, + charsets: charsets ) new(result.headers) result.headers[] = DefaultHeaders @@ -123,7 +153,8 @@ proc getSiteConfig*(config: Config, jsctx: JSContext): seq[SiteConfig] = cookie: sc.cookie, scripting: sc.scripting, sharecookiejar: sc.sharecookiejar, - refererfrom: sc.refererfrom + refererfrom: sc.refererfrom, + charsets: sc.document_charset ) if sc.url.isSome: conf.url = compileRegex(sc.url.get, 0) @@ -220,8 +251,8 @@ proc readUserStylesheet(dir, file: string): string = result = f.readAll() f.close() -proc parseConfig(config: Config, dir: string, stream: Stream) -proc parseConfig*(config: Config, dir: string, s: string) +proc parseConfig(config: Config, dir: string, stream: Stream, name = "<input>") +proc parseConfig*(config: Config, dir: string, s: string, name = "<input>") proc loadConfig*(config: Config, s: string) {.jsfunc.} = let s = if s.len > 0 and s[0] == '/': @@ -233,167 +264,231 @@ proc loadConfig*(config: Config, s: string) {.jsfunc.} = proc bindPagerKey*(config: Config, key, action: string) {.jsfunc.} = let k = getRealKey(key) - config.nmap[k] = action + config.page[k] = action var teststr = "" for c in k: teststr &= c - if teststr notin config.nmap: - config.nmap[teststr] = "client.feedNext()" + if teststr notin config.page: + config.page[teststr] = "client.feedNext()" proc bindLineKey*(config: Config, key, action: string) {.jsfunc.} = let k = getRealKey(key) - config.lemap[k] = action + config.line[k] = action var teststr = "" for c in k: teststr &= c - if teststr notin config.nmap: - config.lemap[teststr] = "client.feedNext()" + if teststr notin config.line: + config.line[teststr] = "client.feedNext()" + +proc parseConfigValue(x: var object, v: TomlValue, k: string) +proc parseConfigValue(x: var bool, v: TomlValue, k: string) +proc parseConfigValue(x: var string, v: TomlValue, k: string) +proc parseConfigValue(x: var seq[object], v: TomlValue, k: string) +proc parseConfigValue[T](x: var seq[T], v: TomlValue, k: string) +proc parseConfigValue(x: var Charset, v: TomlValue, k: string) +proc parseConfigValue(x: var int32, v: TomlValue, k: string) +proc parseConfigValue(x: var int64, v: TomlValue, k: string) +proc parseConfigValue(x: var Option[ColorMode], v: TomlValue, k: string) +proc parseConfigValue(x: var Option[FormatMode], v: TomlValue, k: string) +proc parseConfigValue(x: var FormatMode, v: TomlValue, k: string) +proc parseConfigValue(x: var RGBAColor, v: TomlValue, k: string) +proc parseConfigValue(x: var Option[bool], v: TomlValue, k: string) +proc parseConfigValue[T](x: var Option[T], v: TomlValue, k: string) +proc parseConfigValue(x: var ActionMap, v: TomlValue, k: string) +proc parseConfigValue(x: var CSSConfig, v: TomlValue, k: string) + +proc typeCheck(v: TomlValue, vt: ValueType, k: string) = + if v.vt != vt: + raise newException(ValueError, "invalid type for key " & k & + " (got " & $v.vt & ", expected " & $vt & ")") + +proc typeCheck(v: TomlValue, vt: set[ValueType], k: string) = + if v.vt notin vt: + raise newException(ValueError, "invalid type for key " & k & + " (got " & $v.vt & ", expected " & $vt & ")") + +proc parseConfigValue(x: var object, v: TomlValue, k: string) = + typeCheck(v, VALUE_TABLE, k) + for fk, fv in x.fieldPairs: + let kebabk = snakeToKebabCase(fk) + if kebabk in v: + let kkk = if k != "": + k & "." & fk + else: + fk + parseConfigValue(fv, v[kebabk], kkk) + +proc parseConfigValue(x: var bool, v: TomlValue, k: string) = + typeCheck(v, VALUE_BOOLEAN, k) + x = v.b + +proc parseConfigValue(x: var string, v: TomlValue, k: string) = + typeCheck(v, VALUE_STRING, k) + x = v.s + +proc parseConfigValue(x: var seq[object], v: TomlValue, k: string) = + typeCheck(v, {VALUE_TABLE_ARRAY, VALUE_ARRAY}, k) + if v.vt == VALUE_ARRAY: + #TODO if array and size != 0 + # actually, arrays and table arrays should be the same data type + assert v.a.len == 0 + x.setLen(0) + else: + for i in 0 ..< v.ta.len: + var y: typeof(x[0]) + let tab = TomlValue(vt: VALUE_TABLE, t: v.ta[i]) + parseConfigValue(y, tab, k & "[" & $i & "]") + x.add(y) + +proc parseConfigValue[T](x: var seq[T], v: TomlValue, k: string) = + typeCheck(v, {VALUE_STRING, VALUE_ARRAY}, k) + if v.vt != VALUE_ARRAY: + var y: T + parseConfigValue(y, v, k) + x.add(y) + else: + for i in 0 ..< v.a.len: + var y: T + parseConfigValue(y, v.a[i], k & "[" & $i & "]") + x.add(y) + +proc parseConfigValue(x: var Charset, v: TomlValue, k: string) = + typeCheck(v, VALUE_STRING, k) + x = getCharset(v.s) + if x == CHARSET_UNKNOWN: + raise newException(ValueError, "unknown charset '" & v.s & "' for key " & + k) + +proc parseConfigValue(x: var int32, v: TomlValue, k: string) = + typeCheck(v, VALUE_INTEGER, k) + x = int32(v.i) + +proc parseConfigValue(x: var int64, v: TomlValue, k: string) = + typeCheck(v, VALUE_INTEGER, k) + x = v.i + +proc parseConfigValue(x: var Option[ColorMode], v: TomlValue, k: string) = + typeCheck(v, VALUE_STRING, k) + case v.s + of "auto": x = none(ColorMode) + of "monochrome": x = some(MONOCHROME) + of "ansi": x = some(ANSI) + of "8bit": x = some(EIGHT_BIT) + of "24bit": x = some(TRUE_COLOR) + else: + raise newException(ValueError, "unknown color mode '" & v.s & + "' for key " & k) -proc parseConfig(config: Config, dir: string, t: TomlValue) = - for k, v in t: - case k +proc parseConfigValue(x: var Option[FormatMode], v: TomlValue, k: string) = + typeCheck(v, {VALUE_STRING, VALUE_ARRAY}, k) + if v.vt == VALUE_STRING and v.s == "auto": + x = none(FormatMode) + else: + var y: FormatMode + parseConfigValue(y, v, k) + x = some(y) + +proc parseConfigValue(x: var FormatMode, v: TomlValue, k: string) = + typeCheck(v, VALUE_ARRAY, k) + for i in 0 ..< v.a.len: + let s = v.a[i].s + let kk = k & "[" & $i & "]" + case s + of "bold": x.incl(FLAG_BOLD) + of "italic": x.incl(FLAG_ITALIC) + of "underline": x.incl(FLAG_UNDERLINE) + of "reverse": x.incl(FLAG_REVERSE) + of "strike": x.incl(FLAG_STRIKE) + of "overline": x.incl(FLAG_OVERLINE) + of "blink": x.incl(FLAG_BLINK) + else: + raise newException(ValueError, "unknown format mode '" & s & + "' for key " & kk) + +proc parseConfigValue(x: var RGBAColor, v: TomlValue, k: string) = + typeCheck(v, VALUE_STRING, k) + let c = parseRGBAColor(v.s) + if c.isNone: + raise newException(ValueError, "invalid color '" & v.s & + "' for key " & k) + x = c.get + +proc parseConfigValue(x: var Option[bool], v: TomlValue, k: string) = + typeCheck(v, {VALUE_STRING, VALUE_BOOLEAN}, k) + if v.vt == VALUE_STRING: + if v.s == "auto": + x = none(bool) + else: + raise newException(ValueError, "invalid value '" & v.s & + "' for key " & k) + else: + var y: bool + parseConfigValue(y, v, k) + x = some(y) + +proc parseConfigValue[T](x: var Option[T], v: TomlValue, k: string) = + var y: T + parseConfigValue(y, v, k) + x = some(y) + +proc parseConfigValue(x: var ActionMap, v: TomlValue, k: string) = + typeCheck(v, VALUE_TABLE, k) + for kk, vv in v: + typeCheck(vv, VALUE_STRING, k & "[" & kk & "]") + x[getRealKey(kk)] = vv.s + +var gdir {.compileTime.}: string +proc parseConfigValue(x: var CSSConfig, v: TomlValue, k: string) = + typeCheck(v, VALUE_TABLE, k) + let dir = gdir + for kk, vv in v: + let kkk = if k != "": + k & "." & kk + else: + kk + case kk of "include": - if v.vt == VALUE_STRING: - when nimvm: - config.parseConfig(dir, staticRead(dir / v.s)) - else: - config.parseConfig(dir, newFileStream(dir / v.s)) - elif t.vt == VALUE_ARRAY: - for v in t.a: - when nimvm: - config.parseConfig(dir, staticRead(dir / v.s)) - else: - config.parseConfig(dir, newFileStream(dir / v.s)) - of "search": - for k, v in v: - case k - of "wrap": - config.searchwrap = v.b - of "start": - for k, v in v: - case k - of "visual-home": - config.visualhome = v.s - of "run-script": - config.startup = v.s - of "headless": - config.headless = v.b - of "network": - for k, v in v: - case k - of "max-redirect": - config.maxredirect = int(v.i) - of "prepend-https": - config.prependhttps = v.b - of "page": - for k, v in v: - config.nmap[getRealKey(k)] = v.s - of "line": - for k, v in v: - config.lemap[getRealKey(k)] = v.s - of "css": - for k, v in v: - case k - of "include": - case v.vt - of VALUE_STRING: - config.stylesheet &= readUserStylesheet(dir, v.s) - of VALUE_ARRAY: - for child in v.a: - config.stylesheet &= readUserStylesheet(dir, v.s) - else: discard - of "inline": - config.stylesheet &= v.s - of "display": - template get_format_mode(v: TomlValue): FormatMode = - var mode: FormatMode - for vv in v.a: - case vv.s - of "bold": mode.incl(FLAG_BOLD) - of "italic": mode.incl(FLAG_ITALIC) - of "underline": mode.incl(FLAG_UNDERLINE) - of "reverse": mode.incl(FLAG_REVERSE) - of "strike": mode.incl(FLAG_STRIKE) - of "overline": mode.incl(FLAG_OVERLINE) - of "blink": mode.incl(FLAG_BLINK) - mode - for k, v in v: - case k - of "alt-screen": - if v.vt == VALUE_BOOLEAN: - config.altscreen = some(v.b) - elif v.vt == VALUE_STRING and v.s == "auto": - config.altscreen = none(bool) - of "color-mode": - case v.s - of "auto": config.colormode = none(ColorMode) - of "monochrome": config.colormode = some(MONOCHROME) - of "ansi": config.colormode = some(ANSI) - of "8bit": config.colormode = some(EIGHT_BIT) - of "24bit": config.colormode = some(TRUE_COLOR) - of "format-mode": - if v.vt == VALUE_STRING and v.s == "auto": - config.formatmode = none(FormatMode) - elif v.vt == VALUE_ARRAY: - config.formatmode = some(get_format_mode v) - of "no-format-mode": - config.noformatmode = get_format_mode v - of "highlight-color": - config.hlcolor = parseRGBAColor(v.s).get - of "double-width-ambiguous": - config.ambiguous_double = v.b - of "minimum-contrast": - config.mincontrast = int(v.i) - of "force-clear": config.forceclear = v.b - of "emulate-overline": config.emulateoverline = v.b - of "external": - for k, v in v: - case k - of "editor": config.editor = v.s - of "tmpdir": config.tmpdir = v.s - of "siteconf": - for v in v: - var conf = StaticSiteConfig() - for k, v in v: - case k - of "url": conf.url = some(v.s) - of "host": conf.host = some(v.s) - of "rewrite-url": conf.subst = some(v.s) - of "referer-from": conf.refererfrom = some(v.b) - of "cookie": conf.cookie = some(v.b) - of "third-party-cookie": - if v.vt == VALUE_STRING: - conf.thirdpartycookie = @[v.s] - else: - for v in v.a: - conf.thirdpartycookie.add(v.s) - of "share-cookie-jar": conf.sharecookiejar = some(v.s) - of "scripting": conf.scripting = some(v.b) - assert conf.url.isSome != conf.host.isSome - config.siteconf.add(conf) - of "omnirule": - if v.vt == VALUE_ARRAY and v.a.len == 0: - config.omnirules.setLen(0) + typeCheck(vv, {VALUE_STRING, VALUE_ARRAY}, kkk) + case vv.vt + of VALUE_STRING: + x.stylesheet &= readUserStylesheet(dir, vv.s) + of VALUE_ARRAY: + for child in vv.a: + x.stylesheet &= readUserStylesheet(dir, vv.s) + else: discard + of "inline": + typeCheck(vv, VALUE_STRING, kkk) + x.stylesheet &= vv.s + +proc parseConfig(config: Config, dir: string, t: TomlValue) = + gdir = dir + parseConfigValue(config[], t, "") + while config.includes.len > 0: + #TODO: warn about recursive includes + let includes = config.includes + config.includes.setLen(0) + for s in includes: + when nimvm: + config.parseConfig(dir, staticRead(dir / s)) else: - for v in v: - var rule = StaticOmniRule() - for k, v in v: - case k - of "match": rule.match = v.s - of "substitute-url": rule.subst = v.s - if rule.match != "": - assert rule.subst != "", "Unspecified substitution for rule " & rule.match - config.omnirules.add(rule) - -proc parseConfig(config: Config, dir: string, stream: Stream) = - config.parseConfig(dir, parseToml(stream)) - -proc parseConfig*(config: Config, dir: string, s: string) = - config.parseConfig(dir, newStringStream(s)) + config.parseConfig(dir, newFileStream(dir / s)) + #TODO: for omnirules/siteconf, check if substitution rules are specified? + +proc parseConfig(config: Config, dir: string, stream: Stream, name = "<input>") = + let toml = parseToml(stream, dir / name) + if toml.isOk: + config.parseConfig(dir, toml.get) + else: + eprint("Fatal error: Failed to parse config\n") + eprint(toml.error & "\n") + quit(1) + +proc parseConfig*(config: Config, dir: string, s: string, name = "<input>") = + config.parseConfig(dir, newStringStream(s), name) proc staticReadConfig(): ConfigObj = var config = new(Config) - config.parseConfig("res", staticRead"res/config.toml") + config.parseConfig("res", staticRead"res/config.toml", "config.toml") return config[] const defaultConfig = staticReadConfig() @@ -404,13 +499,13 @@ proc readConfig(config: Config, dir: string) = config.parseConfig(dir, fs) proc getNormalAction*(config: Config, s: string): string = - if config.nmap.hasKey(s): - return config.nmap[s] + if config.page.hasKey(s): + return config.page[s] return "" proc getLinedAction*(config: Config, s: string): string = - if config.lemap.hasKey(s): - return config.lemap[s] + if config.line.hasKey(s): + return config.line[s] return "" proc readConfig*(): Config = diff --git a/src/config/toml.nim b/src/config/toml.nim index 89277d24..c34e46a7 100644 --- a/src/config/toml.nim +++ b/src/config/toml.nim @@ -5,6 +5,7 @@ import strutils import strformat import unicode +import utils/opt import utils/twtstr type @@ -21,6 +22,7 @@ type SyntaxError = object of ValueError TomlParser = object + filename: string at: int line: int stream: Stream @@ -89,10 +91,10 @@ 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}") + raise newException(SyntaxError, fmt"{state.filename}({state.line}): {msg}") proc valueError(state: TomlParser, msg: string) = - raise newException(ValueError, fmt"on line {state.line}: {msg}") + raise newException(ValueError, fmt"{state.filename}({state.line}): {msg}") proc consume(state: var TomlParser): char = result = state.buf[state.at] @@ -405,6 +407,7 @@ proc consumeArray(state: var TomlParser): TomlValue = result.ta.add(val.t) else: result.a.add(val) + val = nil else: if val != nil: state.syntaxError("missing comma") @@ -479,27 +482,28 @@ proc consumeValue(state: var TomlParser): TomlValue = else: state.syntaxError(fmt"invalid character in value: {c}") -proc parseToml*(inputStream: Stream): TomlValue = +proc parseToml*(inputStream: Stream, filename = "<input>"): Result[TomlValue, string] = 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() - + state.filename = filename + try: 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}") - inputStream.close() - - return TomlValue(vt: VALUE_TABLE, t: state.root) + 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}") + inputStream.close() + return ok(TomlValue(vt: VALUE_TABLE, t: state.root)) + except SyntaxError, ValueError: + return err(getCurrentExceptionMsg()) diff --git a/src/display/client.nim b/src/display/client.nim index d0f618e1..6e9f6331 100644 --- a/src/display/client.nim +++ b/src/display/client.nim @@ -16,6 +16,7 @@ import bindings/quickjs import buffer/container import css/sheet import config/config +import data/charset import display/pager import display/term import html/dom @@ -450,7 +451,8 @@ proc newConsole(pager: Pager, tty: File): Console = if pipe(pipefd) == -1: raise newException(Defect, "Failed to open console pipe.") let url = newURL("javascript:console.show()") - result.container = pager.readPipe0(some("text/plain"), pipefd[0], option(url), "Browser console") + result.container = pager.readPipe0(some("text/plain"), none(Charset), + pipefd[0], option(url), "Browser console") var f: File if not open(f, pipefd[1], fmWrite): raise newException(Defect, "Failed to open file for console pipe.") @@ -477,7 +479,8 @@ proc dumpBuffers(client: Client) = quit(1) stdout.close() -proc launchClient*(client: Client, pages: seq[string], ctype: Option[string], dump: bool) = +proc launchClient*(client: Client, pages: seq[string], ctype: Option[string], + cs: Option[Charset], dump: bool) = var tty: File var dump = dump if not dump: @@ -499,26 +502,26 @@ proc launchClient*(client: Client, pages: seq[string], ctype: Option[string], du client.console = newConsole(client.pager, tty) client.alive = true addExitProc((proc() = client.quit())) - if client.config.startup != "": - let s = if fileExists(client.config.startup): - readFile(client.config.startup) + if client.config.start.startup_script != "": + let s = if fileExists(client.config.start.startup_script): + readFile(client.config.start.startup_script) else: - client.config.startup - client.command0(s, client.config.startup, silence = true) - client.userstyle = client.config.stylesheet.parseStylesheet() + client.config.start.startup_script + client.command0(s, client.config.start.startup_script, silence = true) + client.userstyle = client.config.css.stylesheet.parseStylesheet() if not stdin.isatty(): - client.pager.readPipe(ctype, stdin.getFileHandle()) + client.pager.readPipe(ctype, cs, stdin.getFileHandle()) for page in pages: - client.pager.loadURL(page, ctype = ctype) + client.pager.loadURL(page, ctype = ctype, cs = cs) client.acceptBuffers() client.pager.refreshStatusMsg() if not dump: client.inputLoop() else: client.dumpBuffers() - if client.config.headless: + if client.config.start.headless: client.headlessLoop() client.quit() diff --git a/src/display/pager.nim b/src/display/pager.nim index 805f0f51..bd07d52e 100644 --- a/src/display/pager.nim +++ b/src/display/pager.nim @@ -12,6 +12,7 @@ when defined(posix): import buffer/cell import buffer/container import config/config +import data/charset import display/term import io/lineedit import io/promise @@ -120,16 +121,16 @@ proc getter(pager: Pager, s: string): Option[JSValue] {.jsgetprop.} = proc searchNext(pager: Pager) {.jsfunc.} = if pager.regex.issome: if not pager.reverseSearch: - pager.container.cursorNextMatch(pager.regex.get, pager.config.searchwrap) + pager.container.cursorNextMatch(pager.regex.get, pager.config.search.wrap) else: - pager.container.cursorPrevMatch(pager.regex.get, pager.config.searchwrap) + pager.container.cursorPrevMatch(pager.regex.get, pager.config.search.wrap) proc searchPrev(pager: Pager) {.jsfunc.} = if pager.regex.issome: if not pager.reverseSearch: - pager.container.cursorPrevMatch(pager.regex.get, pager.config.searchwrap) + pager.container.cursorPrevMatch(pager.regex.get, pager.config.search.wrap) else: - pager.container.cursorNextMatch(pager.regex.get, pager.config.searchwrap) + pager.container.cursorNextMatch(pager.regex.get, pager.config.search.wrap) proc getLineHist(pager: Pager, mode: LineMode): LineHistory = if pager.linehist[mode] == nil: @@ -245,7 +246,7 @@ proc refreshDisplay(pager: Pager, container = pager.container) = let area = hl.colorArea(container.fromy + by, startw .. startw + aw) for i in area: var hlformat = pager.display[dls + i - startw].format - hlformat.bgcolor = pager.config.hlcolor.cellColor() + hlformat.bgcolor = pager.config.display.highlight_color.cellColor() pager.display[dls + i - startw].format = hlformat inc by @@ -544,6 +545,7 @@ proc applySiteconf(pager: Pager, request: Request): BufferConfig = var cookiejar: CookieJar var headers: HeaderList var scripting: bool + var charsets = pager.config.encoding.document_charset for sc in pager.siteconf: if sc.url.isSome and not sc.url.get.match(url): continue @@ -566,12 +568,15 @@ proc applySiteconf(pager: Pager, request: Request): BufferConfig = scripting = sc.scripting.get if sc.refererfrom.isSome: refererfrom = sc.refererfrom.get - return pager.config.getBufferConfig(request.url, cookiejar, headers, refererfrom, scripting) + if sc.document_charset.len > 0: + charsets = sc.document_charset + return pager.config.getBufferConfig(request.url, cookiejar, headers, + refererfrom, scripting, charsets) # Load request in a new buffer. proc gotoURL(pager: Pager, request: Request, prevurl = none(URL), - ctype = none(string), replace: Container = nil, - redirectdepth = 0, referrer: Container = nil) = + ctype = none(string), cs = none(Charset), replace: Container = nil, + redirectdepth = 0, referrer: Container = nil) = if referrer != nil and referrer.config.refererfrom: request.referer = referrer.source.location var bufferconfig = pager.applySiteconf(request) @@ -587,6 +592,7 @@ proc gotoURL(pager: Pager, request: Request, prevurl = none(URL), t: LOAD_REQUEST, request: request, contenttype: ctype, + charset: cs, location: request.url ) if referrer != nil: @@ -615,7 +621,8 @@ proc omniRewrite(pager: Pager, s: string): string = # * file://$PWD/<file> # * https://<url> # So we attempt to load both, and see what works. -proc loadURL*(pager: Pager, url: string, ctype = none(string)) = +proc loadURL*(pager: Pager, url: string, ctype = none(string), + cs = none(Charset)) = let url0 = pager.omniRewrite(url) let url = if url[0] == '~': expandPath(url0) else: url0 let firstparse = parseURL(url) @@ -624,10 +631,10 @@ proc loadURL*(pager: Pager, url: string, ctype = none(string)) = some(pager.container.source.location) else: none(URL) - pager.gotoURL(newRequest(firstparse.get), prev, ctype) + pager.gotoURL(newRequest(firstparse.get), prev, ctype, cs) return var urls: seq[URL] - if pager.config.prependhttps and url[0] != '/': + if pager.config.network.prepend_https and url[0] != '/': let pageurl = parseURL("https://" & url) if pageurl.isSome: # attempt to load remote page urls.add(pageurl.get) @@ -640,22 +647,25 @@ proc loadURL*(pager: Pager, url: string, ctype = none(string)) = pager.alert("Invalid URL " & url) else: let prevc = pager.container - pager.gotoURL(newRequest(urls.pop()), ctype = ctype) + pager.gotoURL(newRequest(urls.pop()), ctype = ctype, cs = cs) if pager.container != prevc: pager.container.retry = urls -proc readPipe0*(pager: Pager, ctype: Option[string], fd: FileHandle, location: Option[URL], title: string): Container = +proc readPipe0*(pager: Pager, ctype: Option[string], cs: Option[Charset], + fd: FileHandle, location: Option[URL], title: string): Container = let source = BufferSource( t: LOAD_PIPE, fd: fd, contenttype: some(ctype.get("text/plain")), + charset: cs, location: location.get(newURL("file://-")) ) let bufferconfig = pager.config.getBufferConfig(source.location) return pager.dispatcher.newBuffer(bufferconfig, source, title = title) -proc readPipe*(pager: Pager, ctype: Option[string], fd: FileHandle) = - let container = pager.readPipe0(ctype, fd, none(URL), "*pipe*") +proc readPipe*(pager: Pager, ctype: Option[string], cs: Option[Charset], + fd: FileHandle) = + let container = pager.readPipe0(ctype, cs, fd, none(URL), "*pipe*") pager.addContainer(container) proc command(pager: Pager) {.jsfunc.} = @@ -680,9 +690,9 @@ proc updateReadLineISearch(pager: Pager, linemode: LineMode) = if pager.iregex.isSome: pager.container.hlon = true if linemode == ISEARCH_F: - pager.container.cursorNextMatch(pager.iregex.get, pager.config.searchwrap) + pager.container.cursorNextMatch(pager.iregex.get, pager.config.search.wrap) else: - pager.container.cursorPrevMatch(pager.iregex.get, pager.config.searchwrap) + pager.container.cursorPrevMatch(pager.iregex.get, pager.config.search.wrap) pager.container.pushCursorPos() of FINISH: if pager.iregex.isSome: @@ -754,7 +764,8 @@ proc load(pager: Pager, s = "") {.jsfunc.} = # Reload the page in a new buffer, then kill the previous buffer. proc reload(pager: Pager) {.jsfunc.} = - pager.gotoURL(newRequest(pager.container.source.location), none(URL), pager.container.contenttype, pager.container) + pager.gotoURL(newRequest(pager.container.source.location), none(URL), + pager.container.contenttype, replace = pager.container) proc authorize(pager: Pager) = pager.setLineEdit("Username: ", USERNAME) @@ -799,7 +810,7 @@ proc handleEvent0(pager: Pager, container: Container, event: ContainerEvent): bo if pager.container == container: pager.authorize() of REDIRECT: - if container.redirectdepth < pager.config.maxredirect: + if container.redirectdepth < pager.config.network.max_redirect: pager.alert("Redirecting to " & $event.request.url) pager.gotoURL(event.request, some(container.source.location), replace = container, redirectdepth = container.redirectdepth + 1, diff --git a/src/display/term.nim b/src/display/term.nim index 239b1a53..ce518428 100644 --- a/src/display/term.nim +++ b/src/display/term.nim @@ -390,8 +390,8 @@ proc showCursor*(term: Terminal) = term.outfile.showCursor() func emulateOverline(term: Terminal): bool = - term.config.emulateoverline and FLAG_OVERLINE notin term.formatmode and - FLAG_UNDERLINE in term.formatmode + term.config.display.emulate_overline and + FLAG_OVERLINE notin term.formatmode and FLAG_UNDERLINE in term.formatmode proc writeGrid*(term: Terminal, grid: FixedGrid, x = 0, y = 0) = for ly in y ..< y + grid.height: @@ -412,28 +412,28 @@ proc writeGrid*(term: Terminal, grid: FixedGrid, x = 0, y = 0) = j += cell[].width() proc applyConfig(term: Terminal) = - if term.config.colormode.isSome: - term.colormode = term.config.colormode.get + if term.config.display.color_mode.isSome: + term.colormode = term.config.display.color_mode.get elif term.isatty(): term.colormode = ANSI let colorterm = getEnv("COLORTERM") case colorterm of "24bit", "truecolor": term.colormode = TRUE_COLOR - if term.config.formatmode.isSome: - term.formatmode = term.config.formatmode.get + if term.config.display.format_mode.isSome: + term.formatmode = term.config.display.format_mode.get for fm in FormatFlags: - if fm in term.config.noformatmode: + if fm in term.config.display.no_format_mode: term.formatmode.excl(fm) - if term.isatty() and term.config.altscreen.isSome: - term.smcup = term.config.altscreen.get - term.mincontrast = term.config.mincontrast + if term.isatty() and term.config.display.alt_screen.isSome: + term.smcup = term.config.display.alt_screen.get + term.mincontrast = term.config.display.minimum_contrast proc outputGrid*(term: Terminal) = - if term.config.termreload: + if term.config.display.force_clear: term.applyConfig() term.outfile.write(term.resetFormat()) let samesize = term.canvas.width == term.pcanvas.width and term.canvas.height == term.pcanvas.height - if term.config.forceclear or not term.cleared or not samesize: + if term.config.display.force_clear or not term.cleared or not samesize: term.outfile.write(term.generateFullOutput(term.canvas)) term.cleared = true else: diff --git a/src/encoding/decoderstream.nim b/src/encoding/decoderstream.nim index 362d9607..425f264f 100644 --- a/src/encoding/decoderstream.nim +++ b/src/encoding/decoderstream.nim @@ -505,26 +505,26 @@ proc decodeShiftJIS(stream: DecoderStream, iq: var seq[uint8], while i < ilen: let b = iq[i] if lead != 0: - let l = lead - lead = 0 + var ptrisnull = true; + var p = 0u16 let offset = if b < 0x7Fu8: 0x40u16 else: 0x41u16 - let leadoffset = if l < 0xA0: 0x81u16 else: 0xC1u16 + let leadoffset = if lead < 0xA0: 0x81u16 else: 0xC1u16 if b in 0x40u8..0x7Eu8 or b in 0x80u8..0xFCu8: - let p = (uint16(l) - leadoffset) * 188 + uint16(b) - offset - if p in 8836u16..10715u16: - stream.append_codepoint 0xE000u16 - 8836 + p, oq, olen, n - inc i - continue - if p < Jis0208Decode.len and Jis0208Decode[p] != 0: - let c = Jis0208Decode[p] - stream.append_codepoint c, oq, olen, n - inc i - continue - if cast[char](b) in Ascii: - continue # prepend (no inc i) + p = (uint16(lead) - leadoffset) * 188 + uint16(b) - offset + ptrisnull = false + lead = 0 + if not ptrisnull and p in 8836u16..10715u16: + stream.append_codepoint 0xE000u16 - 8836 + p, oq, olen, n + inc i + continue + elif not ptrisnull and p < Jis0208Decode.len and Jis0208Decode[p] != 0: + let c = Jis0208Decode[p] + stream.append_codepoint c, oq, olen, n else: stream.handleError(oq, olen, n) if stream.isend: break + if cast[char](b) in Ascii: + continue # prepend (no inc i) elif cast[char](b) in Ascii or b == 0x80: stream.append_codepoint b, oq, olen, n elif b in 0xA1u8..0xDFu8: @@ -640,11 +640,11 @@ proc decodeSingleByte(stream: DecoderStream, iq: var seq[uint8], if c in Ascii: stream.append_codepoint c, oq, olen, n else: - let p = map[c] + let p = map[cast[char](iq[i] - 0x80)] if p == 0u16: stream.handleError(oq, olen, n) else: - stream.append_codepoint cast[uint32](oq), oq, olen, n + stream.append_codepoint cast[uint32](p), oq, olen, n proc decodeReplacement(stream: DecoderStream, oq: ptr UncheckedArray[uint32], olen: int, n: var int) = if not stream.replreported: diff --git a/src/html/htmlparser.nim b/src/html/htmlparser.nim index 8cd34cd9..d03e0d24 100644 --- a/src/html/htmlparser.nim +++ b/src/html/htmlparser.nim @@ -2162,8 +2162,7 @@ proc constructTree(parser: var HTML5Parser): Document = else: parser.processInForeignContent(token) if parser.needsreinterpret: - return nil - + break return parser.document proc finishParsing(parser: var HTML5Parser) = @@ -2175,47 +2174,68 @@ proc finishParsing(parser: var HTML5Parser) = script.execute() #TODO events -proc parseHTML*(inputStream: Stream, cs = none(Charset), fallbackcs = CHARSET_UTF_8, window: Window = nil, url: URL = nil): (Document, Charset) = - var parser: HTML5Parser - var bom: string - if cs.isSome: - parser.charset = cs.get - parser.confidence = CONFIDENCE_CERTAIN - else: - # bom sniff - const u8bom = char(0xEF) & char(0xBB) & char(0xBF) - const bebom = char(0xFE) & char(0xFF) - const lebom = char(0xFF) & char(0xFE) - bom = inputStream.readStr(2) - if bom == bebom: - parser.charset = CHARSET_UTF_16_BE - parser.confidence = CONFIDENCE_CERTAIN - bom = "" - elif bom == lebom: - parser.charset = CHARSET_UTF_16_LE - parser.confidence = CONFIDENCE_CERTAIN - bom = "" +proc parseHTML*(inputStream: Stream, charsets: seq[Charset] = @[], + fallbackcs = CHARSET_UTF_8, window: Window = nil, + url: URL = nil, canReinterpret = true): Document = + var charsetStack: seq[Charset] + for i in countdown(charsets.high, 0): + charsetStack.add(charsets[i]) + var canReinterpret = canReinterpret + while true: + var parser: HTML5Parser + var bom: string + let islastcs = charsetStack.len == 0 + if not islastcs: + parser.charset = charsetStack.pop() + if not canReinterpret: + parser.confidence = CONFIDENCE_CERTAIN else: - bom &= inputStream.readChar() - if bom == u8bom: - parser.charset = CHARSET_UTF_8 + # bom sniff + const u8bom = char(0xEF) & char(0xBB) & char(0xBF) + const bebom = char(0xFE) & char(0xFF) + const lebom = char(0xFF) & char(0xFE) + bom = inputStream.readStr(2) + if bom == bebom: + parser.charset = CHARSET_UTF_16_BE + parser.confidence = CONFIDENCE_CERTAIN + bom = "" + elif bom == lebom: + parser.charset = CHARSET_UTF_16_LE parser.confidence = CONFIDENCE_CERTAIN bom = "" else: - parser.charset = fallbackcs - let decoder = newDecoderStream(inputStream, parser.charset) - for c in bom: - decoder.prepend(cast[uint32](c)) - parser.document = newDocument() - parser.document.contentType = "text/html" - if window != nil: - parser.document.window = window - window.document = parser.document - parser.document.url = url - parser.tokenizer = newTokenizer(decoder) - let document = parser.constructTree() - parser.finishParsing() - return (document, parser.charset) + bom &= inputStream.readChar() + if bom == u8bom: + parser.charset = CHARSET_UTF_8 + parser.confidence = CONFIDENCE_CERTAIN + bom = "" + else: + parser.charset = fallbackcs + let em = if islastcs or not canReinterpret: + DECODER_ERROR_MODE_REPLACEMENT + else: + DECODER_ERROR_MODE_FATAL + let decoder = newDecoderStream(inputStream, parser.charset, errormode = em) + for c in bom: + decoder.prepend(cast[uint32](c)) + parser.document = newDocument() + parser.document.contentType = "text/html" + if window != nil: + parser.document.window = window + window.document = parser.document + parser.document.url = url + parser.tokenizer = newTokenizer(decoder) + let document = parser.constructTree() + if parser.needsreinterpret and canReinterpret: + inputStream.setPosition(0) + charsetStack.add(parser.charset) + canReinterpret = false + continue + if decoder.failed and canReinterpret: + inputStream.setPosition(0) + continue + parser.finishParsing() + return document proc newDOMParser*(): DOMParser {.jsctor.} = new(result) @@ -2223,7 +2243,7 @@ proc newDOMParser*(): DOMParser {.jsctor.} = proc parseFromString(parser: DOMParser, str: string, t: string): Document {.jserr, jsfunc.} = case t of "text/html": - let (res, _) = parseHTML(newStringStream(str)) + let res = parseHTML(newStringStream(str)) return res of "text/xml", "application/xml", "application/xhtml+xml", "image/svg+xml": JS_ERR JS_InternalError, "XML parsing is not supported yet" diff --git a/src/ips/editor.nim b/src/ips/editor.nim index 4361c39f..0e2c91c8 100644 --- a/src/ips/editor.nim +++ b/src/ips/editor.nim @@ -30,7 +30,7 @@ func formatEditorName(editor, file: string, line: int): string = result &= file proc openEditor*(term: Terminal, config: Config, file: string, line = 1): bool = - var editor = config.editor + var editor = config.external.editor if editor == "": editor = getEnv("EDITOR") if editor == "": @@ -43,7 +43,7 @@ proc openEditor*(term: Terminal, config: Config, file: string, line = 1): bool = var tmpf_seq: int proc openInEditor*(term: Terminal, config: Config, input: var string): bool = try: - let tmpdir = config.tmpdir + let tmpdir = config.external.tmpdir if not dirExists(tmpdir): createDir(tmpdir) var tmpf = tmpdir / "chatmp" & $tmpf_seq diff --git a/src/ips/forkserver.nim b/src/ips/forkserver.nim index 009c5491..e7ddf1c0 100644 --- a/src/ips/forkserver.nim +++ b/src/ips/forkserver.nim @@ -75,8 +75,8 @@ proc forkLoader(ctx: var ForkServerContext, config: LoaderConfig): Pid = let e = getCurrentException() # taken from system/excpt.nim let msg = e.getStackTrace() & "Error: unhandled exception: " & e.msg & - " [" & $e.name & "]" - eprint(msg) + " [" & $e.name & "]\n" + stderr.write(msg) doAssert false let readfd = pipefd[0] # get read discard close(pipefd[1]) # close write @@ -121,8 +121,8 @@ proc forkBuffer(ctx: var ForkServerContext): Pid = let e = getCurrentException() # taken from system/excpt.nim let msg = e.getStackTrace() & "Error: unhandled exception: " & e.msg & - " [" & $e.name & "]" - eprint(msg) + " [" & $e.name & "]\n" + stderr.write(msg) doAssert false ctx.children.add((pid, loaderPid)) return pid diff --git a/src/ips/serialize.nim b/src/ips/serialize.nim index b8ffab0a..0636e2e9 100644 --- a/src/ips/serialize.nim +++ b/src/ips/serialize.nim @@ -325,6 +325,7 @@ proc swrite*(stream: Stream, source: BufferSource) = of LOAD_PIPE: stream.swrite(source.fd) stream.swrite(source.location) stream.swrite(source.contenttype) + stream.swrite(source.charset) proc sread*(stream: Stream, source: var BufferSource) = var t: BufferSourceType @@ -341,6 +342,7 @@ proc sread*(stream: Stream, source: var BufferSource) = stream.sread(source.fd) stream.sread(source.location) stream.sread(source.contenttype) + stream.sread(source.charset) func slen*(source: BufferSource): int = result += slen(source.t) diff --git a/src/main.nim b/src/main.nim index a0a6b064..0371c801 100644 --- a/src/main.nim +++ b/src/main.nim @@ -9,12 +9,13 @@ when defined(profile): import nimprof import config/config +import data/charset import display/client import ips/forkserver import utils/twtstr let conf = readConfig() -set_cjk_ambiguous(conf.ambiguous_double) +set_cjk_ambiguous(conf.display.double_width_ambiguous) let params = commandLineParams() proc version(long: static bool = false): string = @@ -34,7 +35,8 @@ Options: -c, --css <stylesheet> Pass stylesheet (e.g. -c 'a{color: blue}') -o, --opt <config> Pass config options (e.g. -o 'page.q="QUIT"') -T, --type <type> Specify content mime type - -M, --monochrome Alias of -o color-mode='monochrome' + -I, --input-charset <name> Specify document charset + -M, --monochrome Set color-mode to 'monochrome' -V, --visual Visual startup mode -r, --run <script/file> Run passed script or file -h, --help Print this usage message @@ -47,6 +49,7 @@ Options: var i = 0 var ctype = none(string) +var cs = none(Charset) var pages: seq[string] var dump = false var visual = false @@ -62,15 +65,25 @@ while i < params.len: echo version(true) quit(0) of "-M", "--monochrome": - conf.colormode = some(MONOCHROME) + conf.display.colormode = some(MONOCHROME) of "-V", "--visual": visual = true - of "-T": + of "-T", "--type": inc i if i < params.len: ctype = some(params[i]) else: help(1) + of "-I", "--input-charset": + inc i + if i < params.len: + let c = getCharset(params[i]) + if c == CHARSET_UNKNOWN: + stderr.write("Unknown charset " & params[i] & "\n") + quit(1) + cs = some(c) + else: + help(1) of "-": discard # emulate programs that accept - as stdin of "-d", "-dump", "--dump": @@ -78,7 +91,7 @@ while i < params.len: of "-c", "--css": inc i if i < params.len: - conf.stylesheet &= params[i] + conf.css.stylesheet &= params[i] else: help(1) of "-o", "--opt": @@ -92,8 +105,8 @@ while i < params.len: of "-r", "--run": inc i if i < params.len: - conf.startup = params[i] - conf.headless = true + conf.start.startup_script = params[i] + conf.start.headless = true dump = true else: help(1) @@ -108,7 +121,7 @@ while i < params.len: if pages.len == 0 and stdin.isatty(): if visual: - pages.add(conf.visualhome) + pages.add(conf.start.visual_home) else: let http = getEnv("HTTP_HOME") if http != "": pages.add(http) @@ -116,17 +129,17 @@ if pages.len == 0 and stdin.isatty(): let www = getEnv("WWW_HOME") if www != "": pages.add(www) -if pages.len == 0 and not conf.headless: +if pages.len == 0 and not conf.start.headless: if stdin.isatty: help(1) -conf.nmap = constructActionTable(conf.nmap) -conf.lemap = constructActionTable(conf.lemap) +conf.page = constructActionTable(conf.page) +conf.line = constructActionTable(conf.line) disp.forkserver.loadForkServerConfig(conf) let c = newClient(conf, disp) try: - c.launchClient(pages, ctype, dump) + c.launchClient(pages, ctype, cs, dump) except CatchableError: c.flushConsole() raise diff --git a/src/render/rendertext.nim b/src/render/rendertext.nim index e91c1e10..d0576c75 100644 --- a/src/render/rendertext.nim +++ b/src/render/rendertext.nim @@ -10,14 +10,35 @@ type StreamRenderer* = object ansiparser: AnsiCodeParser format: Format af: bool + stream: Stream decoder: DecoderStream + charsets: seq[Charset] newline: bool w: int -proc newStreamRenderer*(stream: Stream): StreamRenderer = +proc newStreamRenderer*(stream: Stream, charsets: seq[Charset]): StreamRenderer = result.format = newFormat() result.ansiparser.state = PARSE_DONE - result.decoder = newDecoderStream(stream, CHARSET_UTF_8) + for i in countdown(charsets.high, 0): + result.charsets.add(charsets[i]) + let cs = result.charsets.pop() + let em = if charsets.len > 0: + DECODER_ERROR_MODE_FATAL + else: + DECODER_ERROR_MODE_REPLACEMENT + result.stream = stream + result.decoder = newDecoderStream(stream, cs, errormode = em) + +proc rewind(renderer: var StreamRenderer) = + renderer.stream.setPosition(0) + let cs = renderer.charsets.pop() + let em = if renderer.charsets.len > 0: + DECODER_ERROR_MODE_FATAL + else: + DECODER_ERROR_MODE_REPLACEMENT + renderer.decoder = newDecoderStream(renderer.stream, cs, errormode = em) + renderer.format = newFormat() + renderer.ansiparser.state = PARSE_DONE proc renderStream*(grid: var FlexibleGrid, renderer: var StreamRenderer, len: int) = if len == 0: return @@ -28,7 +49,13 @@ proc renderStream*(grid: var FlexibleGrid, renderer: var StreamRenderer, len: in if grid.len == 0: grid.addLine() var buf = newSeq[Rune](len * 4) - let n = renderer.decoder.readData(addr buf[0], buf.len * sizeof(buf[0])) + var n: int + while true: + n = renderer.decoder.readData(addr buf[0], buf.len * sizeof(buf[0])) + if renderer.decoder.failed: + renderer.rewind() + continue + break for i in 0 ..< n div sizeof(buf[0]): if renderer.newline: # avoid newline at end of stream diff --git a/src/types/buffersource.nim b/src/types/buffersource.nim index dbf8fbbe..109e8361 100644 --- a/src/types/buffersource.nim +++ b/src/types/buffersource.nim @@ -3,6 +3,7 @@ import options when defined(posix): import posix +import data/charset import io/request import types/url @@ -13,6 +14,7 @@ type BufferSource* = object location*: URL contenttype*: Option[string] # override + charset*: Option[Charset] # override case t*: BufferSourceType of CLONE: clonepid*: Pid diff --git a/src/utils/opt.nim b/src/utils/opt.nim new file mode 100644 index 00000000..21d79af2 --- /dev/null +++ b/src/utils/opt.nim @@ -0,0 +1,34 @@ +# Inspired by nim-results. + +type + Result*[T, E] = object + val: T + has: bool + when not (E is void): + ex: E + + Opt*[T] = Result[T, void] + +template ok*[T, E](t: type Result[T, E], x: T): Result[T, E] = + Result[T, E](val: x, has: true) + +template ok*[T](x: T): auto = + ok(typeof(result), x) + +template ok*[T, E](res: var Result[T, E], x: T): Result[T, E] = + res.val = x + res.has = true + +template err*[T, E](t: type Result[T, E], e: E): Result[T, E] = + Result[T, E](ex: e) + +template err*[E](e: E): auto = + err(typeof(result), e) + +template err*[T, E](res: var Result[T, E], e: E) = + res.ex = e + +template isOk*(res: Result): bool = res.has +template isErr*(res: Result): bool = not res.has +template get*[T, E](res: Result[T, E]): T = res.val +template error*[T, E](res: Result[T, E]): E = res.ex diff --git a/src/utils/twtstr.nim b/src/utils/twtstr.nim index 027b26d1..d67e1ab7 100644 --- a/src/utils/twtstr.nim +++ b/src/utils/twtstr.nim @@ -116,6 +116,12 @@ func toScreamingSnakeCase*(str: string): string = # input is camel case else: result &= c.toUpperAscii() +func snakeToKebabCase*(str: string): string = + result = str + for c in result.mitems: + if c == '_': + c = '-' + func isAscii*(r: Rune): bool = return cast[uint32](r) < 128 |