about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2024-03-17 21:14:51 +0100
committerbptato <nincsnevem662@gmail.com>2024-03-17 21:20:04 +0100
commitfc8937b53327f99b5809f78e3257e62a05bd1c79 (patch)
tree64655d81478c9fb8ac93d67259c60cb4fd246b5f
parentd385d07b197cef65c2d2a800378de9152551e3e6 (diff)
downloadchawan-fc8937b53327f99b5809f78e3257e62a05bd1c79.tar.gz
config: clean up/simplify
* Parse the default config at runtime. There's no significant
  performance difference, but this makes it much less painful to write
  config code.
* Add better error reporting
* Make fromJS2 easier to use
* Unquote ChaPaths while parsing config
-rw-r--r--src/config/chapath.nim4
-rw-r--r--src/config/config.nim531
-rw-r--r--src/config/toml.nim2
-rw-r--r--src/js/fromjs.nim10
-rw-r--r--src/loader/headers.nim13
-rw-r--r--src/loader/request.nim19
-rw-r--r--src/local/client.nim14
-rw-r--r--src/local/pager.nim26
-rw-r--r--src/main.nim26
-rw-r--r--src/types/color.nim34
10 files changed, 349 insertions, 330 deletions
diff --git a/src/config/chapath.nim b/src/config/chapath.nim
index adf852a0..c27df1e9 100644
--- a/src/config/chapath.nim
+++ b/src/config/chapath.nim
@@ -284,8 +284,8 @@ proc unquote(p: string): ChaPathResult[string] =
 proc toJS*(ctx: JSContext, p: ChaPath): JSValue =
   toJS(ctx, $p)
 
-proc fromJS2*(ctx: JSContext, val: JSValue, o: var JSResult[ChaPath]) =
-  o = cast[JSResult[ChaPath]](fromJS[string](ctx, val))
+proc fromJSChaPath*(ctx: JSContext; val: JSValue): JSResult[ChaPath] =
+  return cast[JSResult[ChaPath]](fromJS[string](ctx, val))
 
 proc unquote*(p: ChaPath): ChaPathResult[string] =
   let s = ?unquote(string(p))
diff --git a/src/config/config.nim b/src/config/config.nim
index e42fe9e4..b019ec70 100644
--- a/src/config/config.nim
+++ b/src/config/config.nim
@@ -8,6 +8,7 @@ import config/mailcap
 import config/mimetypes
 import config/toml
 import js/error
+import js/fromjs
 import js/javascript
 import js/propertyenumlist
 import js/regex
@@ -29,27 +30,11 @@ type
 
   FormatMode* = set[FormatFlags]
 
+  ChaPathResolved* = distinct string
+
   ActionMap = object
     t: Table[string, string]
 
-  StaticSiteConfig = object
-    url: Opt[string]
-    host: Opt[string]
-    rewrite_url: Opt[string]
-    cookie: Opt[bool]
-    third_party_cookie: seq[string]
-    share_cookie_jar: Opt[string]
-    referer_from*: Opt[bool]
-    scripting: Opt[bool]
-    document_charset: seq[Charset]
-    images: Opt[bool]
-    stylesheet: Opt[string]
-    proxy: Opt[string]
-
-  StaticOmniRule = object
-    match: string
-    substitute_url: string
-
   SiteConfig* = object
     url*: Opt[Regex]
     host*: Opt[Regex]
@@ -86,12 +71,12 @@ type
     document_charset* {.jsgetset.}: seq[Charset]
 
   ExternalConfig = object
-    tmpdir* {.jsgetset.}: ChaPath
+    tmpdir* {.jsgetset.}: ChaPathResolved
     editor* {.jsgetset.}: string
-    mailcap* {.jsgetset.}: seq[ChaPath]
-    mime_types* {.jsgetset.}: seq[ChaPath]
-    cgi_dir* {.jsgetset.}: seq[ChaPath]
-    urimethodmap* {.jsgetset.}: seq[ChaPath]
+    mailcap* {.jsgetset.}: seq[ChaPathResolved]
+    mime_types* {.jsgetset.}: seq[ChaPathResolved]
+    cgi_dir* {.jsgetset.}: seq[ChaPathResolved]
+    urimethodmap* {.jsgetset.}: seq[ChaPathResolved]
     download_dir* {.jsgetset.}: string
     w3m_cgi_compat* {.jsgetset.}: bool
 
@@ -130,10 +115,10 @@ type
     force_pixels_per_column* {.jsgetset.}: bool
     force_pixels_per_line* {.jsgetset.}: bool
 
-  Config* = ref ConfigObj
-  ConfigObj* = object
+  Config* = ref object
+    jsctx: JSContext
     configdir {.jsget.}: string
-    `include` {.jsget.}: seq[ChaPath]
+    `include` {.jsget.}: seq[ChaPathResolved]
     start* {.jsget.}: StartConfig
     search* {.jsget.}: SearchConfig
     css* {.jsget.}: CSSConfig
@@ -143,8 +128,8 @@ type
     input* {.jsget.}: InputConfig
     display* {.jsget.}: DisplayConfig
     #TODO getset
-    siteconf: seq[StaticSiteConfig]
-    omnirule: seq[StaticOmniRule]
+    siteconf*: seq[SiteConfig]
+    omnirule*: seq[OmniRule]
     page* {.jsget.}: ActionMap
     line* {.jsget.}: ActionMap
 
@@ -162,102 +147,27 @@ jsDestructor(NetworkConfig)
 jsDestructor(DisplayConfig)
 jsDestructor(Config)
 
-proc `[]=`(a: var ActionMap, b, c: string) = a.t[b] = c
-proc `[]`*(a: ActionMap, b: string): string = a.t[b]
-proc contains*(a: ActionMap, b: string): bool = b in a.t
-proc getOrDefault(a: ActionMap, b: string): string = a.t.getOrDefault(b)
-proc hasKeyOrPut(a: var ActionMap, b, c: string): bool = a.t.hasKeyOrPut(b, c)
+converter toStr*(p: ChaPathResolved): string {.inline.} =
+  return string(p)
 
-func getRealKey(key: string): string
+proc fromJSChaPathResolved(ctx: JSContext; val: JSValue):
+    JSResult[ChaPathResolved] =
+  return cast[JSResult[ChaPathResolved]](fromJS[string](ctx, val))
 
-proc getter(a: ptr ActionMap, s: string): Opt[string] {.jsgetprop.} =
-  a.t.withValue(s, p):
-    return opt(p[])
+proc `[]=`(a: var ActionMap; b, c: string) =
+  a.t[b] = c
 
-proc setter(a: ptr ActionMap, k, v: string) {.jssetprop.} =
-  let k = getRealKey(k)
-  if k == "":
-    return
-  a[][k] = v
-  var teststr = k
-  teststr.setLen(teststr.high)
-  for i in countdown(k.high, 0):
-    if teststr notin a[]:
-      a[][teststr] = "client.feedNext()"
-    teststr.setLen(i)
+proc `[]`*(a: ActionMap; b: string): string =
+  a.t[b]
 
-proc delete(a: ptr ActionMap, k: string): bool {.jsdelprop.} =
-  let k = getRealKey(k)
-  let ina = k in a[]
-  a[].t.del(k)
-  return ina
+proc contains*(a: ActionMap; b: string): bool =
+  return b in a.t
 
-func names(ctx: JSContext, a: ptr ActionMap): JSPropertyEnumList
-    {.jspropnames.} =
-  let L = uint32(a[].t.len)
-  var list = newJSPropertyEnumList(ctx, L)
-  for key in a[].t.keys:
-    list.add(key)
-  return list
+proc getOrDefault(a: ActionMap; b: string): string =
+  return a.t.getOrDefault(b)
 
-proc bindPagerKey(config: Config, key, action: string) {.jsfunc.} =
-  (addr config.page).setter(key, action)
-
-proc bindLineKey(config: Config, key, action: string) {.jsfunc.} =
-  (addr config.line).setter(key, action)
-
-proc hasprop(a: ptr ActionMap, s: string): bool {.jshasprop.} =
-  return s in a[]
-
-func getProxy*(config: Config): URL =
-  if config.network.proxy.isSome:
-    let s = config.network.proxy.get
-    let x = parseURL(s)
-    if x.isSome:
-      return x.get
-    else:
-      raise newException(Defect, "Invalid proxy URL: " & s)
-  return nil
-
-func getDefaultHeaders*(config: Config): Headers =
-  return newHeaders(config.network.default_headers)
-
-proc getSiteConfig*(config: Config, jsctx: JSContext): seq[SiteConfig] =
-  for sc in config.siteconf:
-    var conf = SiteConfig(
-      cookie: sc.cookie,
-      scripting: sc.scripting,
-      share_cookie_jar: sc.share_cookie_jar,
-      referer_from: sc.referer_from,
-      document_charset: sc.document_charset,
-      images: sc.images
-    )
-    if sc.url.isSome:
-      conf.url = opt(compileMatchRegex(sc.url.get))
-    elif sc.host.isSome:
-      conf.host = opt(compileMatchRegex(sc.host.get))
-    for rule in sc.third_party_cookie:
-      conf.third_party_cookie.add(compileMatchRegex(rule).get)
-    if sc.rewrite_url.isSome:
-      let fun = jsctx.eval(sc.rewrite_url.get, "<siteconf>",
-        JS_EVAL_TYPE_GLOBAL)
-      conf.rewrite_url = getJSFunction[URL, URL](jsctx, fun)
-    if sc.proxy.isSome:
-      let x = parseURL(sc.proxy.get)
-      if x.isNone:
-        raise newException(Defect, "invalid URL: " & sc.proxy.get)
-      conf.proxy = opt(x.get)
-    result.add(conf)
-
-proc getOmniRules*(config: Config, jsctx: JSContext): seq[OmniRule] =
-  for rule in config.omnirule:
-    let re = compileMatchRegex(rule.match)
-    var conf = OmniRule(
-      match: re.get
-    )
-    let fun = jsctx.eval(rule.substitute_url, "<siteconf>", JS_EVAL_TYPE_GLOBAL)
-    conf.substitute_url = getJSFunction[string, string](jsctx, fun)
-    result.add(conf)
+proc hasKeyOrPut(a: var ActionMap; b, c: string): bool =
+  return a.t.hasKeyOrPut(b, c)
 
 func getRealKey(key: string): string =
   var realk: string
@@ -301,11 +211,59 @@ func getRealKey(key: string): string =
     realk &= '\\'
   return realk
 
-proc openFileExpand(dir: string, file: ChaPath): FileStream =
-  let file0 = file.unquote()
-  if file0.isNone:
-    raise newException(ValueError, file0.error)
-  let file = file0.get
+proc getter(a: ptr ActionMap; s: string): Opt[string] {.jsgetprop.} =
+  a.t.withValue(s, p):
+    return opt(p[])
+
+proc setter(a: ptr ActionMap; k, v: string) {.jssetprop.} =
+  let k = getRealKey(k)
+  if k == "":
+    return
+  a[][k] = v
+  var teststr = k
+  teststr.setLen(teststr.high)
+  for i in countdown(k.high, 0):
+    if teststr notin a[]:
+      a[][teststr] = "client.feedNext()"
+    teststr.setLen(i)
+
+proc delete(a: ptr ActionMap; k: string): bool {.jsdelprop.} =
+  let k = getRealKey(k)
+  let ina = k in a[]
+  a[].t.del(k)
+  return ina
+
+func names(ctx: JSContext, a: ptr ActionMap): JSPropertyEnumList
+    {.jspropnames.} =
+  let L = uint32(a[].t.len)
+  var list = newJSPropertyEnumList(ctx, L)
+  for key in a[].t.keys:
+    list.add(key)
+  return list
+
+proc bindPagerKey(config: Config; key, action: string) {.jsfunc.} =
+  (addr config.page).setter(key, action)
+
+proc bindLineKey(config: Config; key, action: string) {.jsfunc.} =
+  (addr config.line).setter(key, action)
+
+proc hasprop(a: ptr ActionMap; s: string): bool {.jshasprop.} =
+  return s in a[]
+
+func getProxy*(config: Config): URL =
+  if config.network.proxy.isSome:
+    let s = config.network.proxy.get
+    let x = parseURL(s)
+    if x.isSome:
+      return x.get
+    else:
+      raise newException(ValueError, "Invalid proxy URL: " & s)
+  return nil
+
+func getDefaultHeaders*(config: Config): Headers =
+  return newHeaders(config.network.default_headers)
+
+proc openFileExpand(dir, file: string): FileStream =
   if file.len == 0:
     return nil
   if file[0] == '/':
@@ -314,7 +272,10 @@ proc openFileExpand(dir: string, file: ChaPath): FileStream =
     return newFileStream(dir / file)
 
 proc readUserStylesheet(dir, file: string): string =
-  let s = openFileExpand(dir, ChaPath(file))
+  let x = ChaPath(file).unquote()
+  if x.isNone:
+    raise newException(ValueError, x.error)
+  let s = openFileExpand(dir, x.get)
   if s != nil:
     result = s.readAll()
     s.close()
@@ -322,7 +283,7 @@ proc readUserStylesheet(dir, file: string): string =
 # The overall configuration will be obtained through the virtual concatenation
 # of several individual configuration files known as mailcap files.
 proc getMailcap*(config: Config): tuple[mailcap: Mailcap, errs: seq[string]] =
-  let configDir = getConfigDir() / "chawan" #TODO store this in config?
+  let configDir = config.configdir
   template uq(s: string): string =
     ChaPath(s).unquote.get
   let gopherPath = "${%CHA_LIBEXEC_DIR}/gopher2html -u \\$MAILCAP_URL".uq
@@ -375,7 +336,7 @@ proc getMimeTypes*(config: Config): MimeTypes =
   if config.external.mime_types.len == 0:
     return DefaultGuess
   var mimeTypes: MimeTypes
-  let configDir = getConfigDir() / "chawan" #TODO store this in config?
+  let configDir = config.configdir
   var found = false
   for p in config.external.mime_types:
     let f = openFileExpand(configDir, p)
@@ -389,7 +350,7 @@ proc getMimeTypes*(config: Config): MimeTypes =
 const DefaultURIMethodMap = parseURIMethodMap(staticRead"res/urimethodmap")
 
 proc getURIMethodMap*(config: Config): URIMethodMap =
-  let configDir = getConfigDir() / "chawan" #TODO store this in config?
+  let configDir = config.configdir
   var urimethodmap: URIMethodMap
   for p in config.external.urimethodmap:
     let f = openFileExpand(configDir, p)
@@ -399,45 +360,62 @@ proc getURIMethodMap*(config: Config): URIMethodMap =
   return urimethodmap
 
 proc getForkServerConfig*(config: Config): ForkServerConfig =
-  let tmpdir0 = config.external.tmpdir.unquote()
-  if tmpdir0.isNone:
-    raise newException(ValueError, tmpdir0.error)
   return ForkServerConfig(
-    tmpdir: tmpdir0.get,
+    tmpdir: config.external.tmpdir,
     ambiguous_double: config.display.double_width_ambiguous
   )
 
-proc parseConfig(config: Config, dir: string, stream: Stream, name = "<input>",
-  laxnames = false)
-proc parseConfig*(config: Config, dir: string, s: string, name = "<input>",
-  laxnames = false)
-
-proc loadConfig*(config: Config, s: string) {.jsfunc.} =
-  let s = if s.len > 0 and s[0] == '/':
-    s
-  else:
-    getCurrentDir() / s
-  if not fileExists(s): return
-  config.parseConfig(parentDir(s), newFileStream(s))
-
-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 ChaPath, 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 Opt[ColorMode], v: TomlValue, k: string)
-proc parseConfigValue(x: var Opt[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 RGBColor, v: TomlValue, k: string)
-proc parseConfigValue[T](x: var Opt[T], v: TomlValue, k: string)
-proc parseConfigValue(x: var ActionMap, v: TomlValue, k: string)
-proc parseConfigValue(x: var CSSConfig, v: TomlValue, k: string)
-proc parseConfigValue[U, V](x: var Table[U, V], v: TomlValue, k: string)
-proc parseConfigValue[T](x: var set[T], v: TomlValue, k: string)
+type ConfigParser = object
+  config: Config
+  dir: string
+  warnings: seq[string]
+
+proc parseConfigValue(ctx: var ConfigParser; x: var object; v: TomlValue;
+  k: string)
+proc parseConfigValue(ctx: var ConfigParser; x: var bool; v: TomlValue;
+  k: string)
+proc parseConfigValue(ctx: var ConfigParser; x: var string; v: TomlValue;
+  k: string)
+proc parseConfigValue(ctx: var ConfigParser; x: var ChaPath; v: TomlValue;
+  k: string)
+proc parseConfigValue[T](ctx: var ConfigParser; x: var seq[T]; v: TomlValue;
+  k: string)
+proc parseConfigValue(ctx: var ConfigParser; x: var Charset; v: TomlValue;
+  k: string)
+proc parseConfigValue(ctx: var ConfigParser; x: var int32; v: TomlValue;
+  k: string)
+proc parseConfigValue(ctx: var ConfigParser; x: var int64; v: TomlValue;
+  k: string)
+proc parseConfigValue(ctx: var ConfigParser; x: var Opt[ColorMode];
+  v: TomlValue; k: string)
+proc parseConfigValue(ctx: var ConfigParser; x: var Opt[FormatMode];
+  v: TomlValue; k: string)
+proc parseConfigValue(ctx: var ConfigParser; x: var FormatMode; v: TomlValue;
+  k: string)
+proc parseConfigValue(ctx: var ConfigParser; x: var RGBAColor; v: TomlValue;
+  k: string)
+proc parseConfigValue(ctx: var ConfigParser; x: var RGBColor; v: TomlValue;
+  k: string)
+proc parseConfigValue[T](ctx: var ConfigParser; x: var Opt[T]; v: TomlValue;
+  k: string)
+proc parseConfigValue(ctx: var ConfigParser; x: var ActionMap; v: TomlValue;
+  k: string)
+proc parseConfigValue(ctx: var ConfigParser; x: var CSSConfig; v: TomlValue;
+  k: string)
+proc parseConfigValue[U; V](ctx: var ConfigParser; x: var Table[U, V];
+  v: TomlValue; k: string)
+proc parseConfigValue[T](ctx: var ConfigParser; x: var set[T]; v: TomlValue;
+  k: string)
+proc parseConfigValue(ctx: var ConfigParser; x: var TomlTable; v: TomlValue;
+  k: string)
+proc parseConfigValue(ctx: var ConfigParser; x: var Regex; v: TomlValue;
+  k: string)
+proc parseConfigValue(ctx: var ConfigParser; x: var URL; v: TomlValue;
+  k: string)
+proc parseConfigValue[T](ctx: var ConfigParser; x: var proc(x: T): JSResult[T];
+  v: TomlValue; k: string)
+proc parseConfigValue(ctx: var ConfigParser; x: var ChaPathResolved;
+  v: TomlValue; k: string)
 
 proc typeCheck(v: TomlValue, vt: ValueType, k: string) =
   if v.vt != vt:
@@ -449,68 +427,84 @@ proc typeCheck(v: TomlValue, vt: set[ValueType], k: string) =
     raise newException(ValueError, "invalid type for key " & k &
       " (got " & $v.vt & ", expected " & $vt & ")")
 
-proc parseConfigValue(x: var object, v: TomlValue, k: string) =
+proc parseConfigValue(ctx: var ConfigParser; 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[U, V](x: var Table[U, V], v: TomlValue, k: string) =
+    when typeof(fv) isnot JSContext:
+      let kebabk = snakeToKebabCase(fk)
+      if kebabk in v:
+        let kkk = if k != "":
+          k & "." & fk
+        else:
+          fk
+        ctx.parseConfigValue(fv, v[kebabk], kkk)
+
+proc parseConfigValue[U, V](ctx: var ConfigParser; x: var Table[U, V];
+    v: TomlValue; k: string) =
   typeCheck(v, VALUE_TABLE, k)
   x.clear()
   for kk, vv in v:
     var y: V
     let kkk = k & "[" & kk & "]"
-    parseConfigValue(y, vv, kkk)
+    ctx.parseConfigValue(y, vv, kkk)
     x[kk] = y
 
-proc parseConfigValue(x: var bool, v: TomlValue, k: string) =
+proc parseConfigValue(ctx: var ConfigParser; x: var bool; v: TomlValue;
+    k: string) =
   typeCheck(v, VALUE_BOOLEAN, k)
   x = v.b
 
-proc parseConfigValue(x: var string, v: TomlValue, k: string) =
+proc parseConfigValue(ctx: var ConfigParser; x: var string; v: TomlValue;
+    k: string) =
   typeCheck(v, VALUE_STRING, k)
   x = v.s
 
-proc parseConfigValue(x: var ChaPath, v: TomlValue, k: string) =
+proc parseConfigValue(ctx: var ConfigParser; x: var ChaPath;
+    v: TomlValue; k: string) =
   typeCheck(v, VALUE_STRING, k)
   x = ChaPath(v.s)
 
-proc parseConfigValue[T](x: var seq[T], v: TomlValue, k: string) =
+proc parseConfigValue[T](ctx: var ConfigParser; 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)
+    ctx.parseConfigValue(y, v, k)
     x = @[y]
   else:
     if not v.ad:
       x.setLen(0)
     for i in 0 ..< v.a.len:
       var y: T
-      parseConfigValue(y, v.a[i], k & "[" & $i & "]")
+      ctx.parseConfigValue(y, v.a[i], k & "[" & $i & "]")
       x.add(y)
 
-proc parseConfigValue(x: var Charset, v: TomlValue, k: string) =
+proc parseConfigValue(ctx: var ConfigParser; x: var TomlTable; v: TomlValue;
+    k: string) =
+  typeCheck(v, {VALUE_TABLE}, k)
+  x = v.t
+
+proc parseConfigValue(ctx: var ConfigParser; 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) =
+proc parseConfigValue(ctx: var ConfigParser; 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) =
+proc parseConfigValue(ctx: var ConfigParser; x: var int64; v: TomlValue;
+    k: string) =
   typeCheck(v, VALUE_INTEGER, k)
   x = v.i
 
-proc parseConfigValue(x: var Opt[ColorMode], v: TomlValue, k: string) =
+proc parseConfigValue(ctx: var ConfigParser; x: var Opt[ColorMode];
+    v: TomlValue; k: string) =
   typeCheck(v, VALUE_STRING, k)
   case v.s
   of "auto": x.err()
@@ -522,16 +516,18 @@ proc parseConfigValue(x: var Opt[ColorMode], v: TomlValue, k: string) =
     raise newException(ValueError, "unknown color mode '" & v.s &
       "' for key " & k)
 
-proc parseConfigValue(x: var Opt[FormatMode], v: TomlValue, k: string) =
+proc parseConfigValue(ctx: var ConfigParser; x: var Opt[FormatMode];
+    v: TomlValue; k: string) =
   typeCheck(v, {VALUE_STRING, VALUE_ARRAY}, k)
   if v.vt == VALUE_STRING and v.s == "auto":
     x.err()
   else:
     var y: FormatMode
-    parseConfigValue(y, v, k)
+    ctx.parseConfigValue(y, v, k)
     x.ok(y)
 
-proc parseConfigValue(x: var FormatMode, v: TomlValue, k: string) =
+proc parseConfigValue(ctx: var ConfigParser; x: var FormatMode; v: TomlValue;
+    k: string) =
   typeCheck(v, VALUE_ARRAY, k)
   for i in 0 ..< v.a.len:
     let kk = k & "[" & $i & "]"
@@ -549,7 +545,8 @@ proc parseConfigValue(x: var FormatMode, v: TomlValue, k: string) =
       raise newException(ValueError, "unknown format mode '" & vv.s &
         "' for key " & kk)
 
-proc parseConfigValue(x: var RGBAColor, v: TomlValue, k: string) =
+proc parseConfigValue(ctx: var ConfigParser; x: var RGBAColor; v: TomlValue;
+    k: string) =
   typeCheck(v, VALUE_STRING, k)
   let c = parseRGBAColor(v.s)
   if c.isNone:
@@ -557,7 +554,8 @@ proc parseConfigValue(x: var RGBAColor, v: TomlValue, k: string) =
       "' for key " & k)
   x = c.get
 
-proc parseConfigValue(x: var RGBColor, v: TomlValue, k: string) =
+proc parseConfigValue(ctx: var ConfigParser; x: var RGBColor; v: TomlValue;
+    k: string) =
   typeCheck(v, VALUE_STRING, k)
   let c = parseLegacyColor(v.s)
   if c.isNone:
@@ -565,15 +563,17 @@ proc parseConfigValue(x: var RGBColor, v: TomlValue, k: string) =
       "' for key " & k)
   x = c.get
 
-proc parseConfigValue[T](x: var Opt[T], v: TomlValue, k: string) =
+proc parseConfigValue[T](ctx: var ConfigParser; x: var Opt[T]; v: TomlValue;
+    k: string) =
   if v.vt == VALUE_STRING and v.s == "auto":
     x.err()
   else:
     var y: T
-    parseConfigValue(y, v, k)
+    ctx.parseConfigValue(y, v, k)
     x.ok(y)
 
-proc parseConfigValue(x: var ActionMap, v: TomlValue, k: string) =
+proc parseConfigValue(ctx: var ConfigParser; x: var ActionMap; v: TomlValue;
+    k: string) =
   typeCheck(v, VALUE_TABLE, k)
   for kk, vv in v:
     typeCheck(vv, VALUE_STRING, k & "[" & kk & "]")
@@ -584,14 +584,16 @@ proc parseConfigValue(x: var ActionMap, v: TomlValue, k: string) =
       discard x.hasKeyOrPut(buf, "client.feedNext()")
     x[rk] = vv.s
 
-proc parseConfigValue[T: enum](x: var T, v: TomlValue, k: string) =
+proc parseConfigValue[T: enum](ctx: var ConfigParser; x: var T; v: TomlValue;
+    k: string) =
   typeCheck(v, VALUE_STRING, k)
   let e = strictParseEnum[T](v.s)
   if e.isNone:
     raise newException(ValueError, "invalid value '" & v.s & "' for key " & k)
   x = e.get
 
-proc parseConfigValue[T](x: var set[T], v: TomlValue, k: string) =
+proc parseConfigValue[T](ctx: var ConfigParser; x: var set[T]; v: TomlValue;
+    k: string) =
   typeCheck(v, {VALUE_STRING, VALUE_ARRAY}, k)
   if v.vt == VALUE_STRING:
     var xx: T
@@ -605,10 +607,9 @@ proc parseConfigValue[T](x: var set[T], v: TomlValue, k: string) =
       xx.parseConfigValue(v.a[i], kk)
       x.incl(xx)
 
-var gdir {.compileTime.}: string
-proc parseConfigValue(x: var CSSConfig, v: TomlValue, k: string) =
+proc parseConfigValue(ctx: var ConfigParser; 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
@@ -619,78 +620,128 @@ proc parseConfigValue(x: var CSSConfig, v: TomlValue, k: string) =
       typeCheck(vv, {VALUE_STRING, VALUE_ARRAY}, kkk)
       case vv.vt
       of VALUE_STRING:
-        x.stylesheet &= readUserStylesheet(dir, vv.s)
+        x.stylesheet &= readUserStylesheet(ctx.dir, vv.s)
       of VALUE_ARRAY:
         for child in vv.a:
-          x.stylesheet &= readUserStylesheet(dir, vv.s)
+          x.stylesheet &= readUserStylesheet(ctx.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.`include`.len > 0:
-    #TODO: warn about recursive includes
-    var includes = config.`include`
-    config.`include`.setLen(0)
-    for s in includes:
-      when nimvm:
-        config.parseConfig(dir, staticRead(dir / string(s)))
-      else:
-        config.parseConfig(dir, openFileExpand(dir, s))
+proc parseConfigValue(ctx: var ConfigParser; x: var Regex; v: TomlValue;
+    k: string) =
+  typeCheck(v, VALUE_STRING, k)
+  let y = compileMatchRegex(v.s)
+  if y.isNone:
+    raise newException(ValueError, "invalid regex " & k & " : " & y.error)
+  x = y.get
+
+proc parseConfigValue(ctx: var ConfigParser; x: var URL; v: TomlValue;
+    k: string) =
+  typeCheck(v, VALUE_STRING, k)
+  let y = parseURL(v.s)
+  if y.isNone:
+    raise newException(ValueError, "invalid URL " & k)
+  x = y.get
+
+proc parseConfigValue[T](ctx: var ConfigParser; x: var proc(x: T): JSResult[T];
+    v: TomlValue; k: string) =
+  typeCheck(v, VALUE_STRING, k)
+  let fun = ctx.config.jsctx.eval(v.s, "<config>", JS_EVAL_TYPE_GLOBAL)
+  x = getJSFunction[T, T](ctx.config.jsctx, fun)
+
+proc parseConfigValue(ctx: var ConfigParser; x: var ChaPathResolved;
+    v: TomlValue; k: string) =
+  typeCheck(v, VALUE_STRING, k)
+  let y = ChaPath(v.s).unquote()
+  if y.isErr:
+    raise newException(ValueError, y.error)
+  x = ChaPathResolved(y.get)
+
+type ParseConfigResult* = object
+  success*: bool
+  warnings*: seq[string] #TODO actually use warnings
+  errorMsg*: string
+
+proc parseConfig(config: Config; dir: string; stream: Stream; name = "<input>";
+  laxnames = false): ParseConfigResult
+
+proc parseConfig(config: Config; dir: string; t: TomlValue): ParseConfigResult =
+  var ctx = ConfigParser(config: config, dir: dir)
   config.configdir = dir
-  #TODO: for omnirule/siteconf, check if substitution rules are specified?
+  try:
+    var myRes = ParseConfigResult(success: true)
+    ctx.parseConfigValue(config[], t, "")
+    #TODO: for omnirule/siteconf, check if substitution rules are specified?
+    while config.`include`.len > 0:
+      #TODO: warn about recursive includes
+      var includes = config.`include`
+      config.`include`.setLen(0)
+      for s in includes:
+        let res = config.parseConfig(dir, openFileExpand(dir, s))
+        if not res.success:
+          return res
+        myRes.warnings.add(res.warnings)
+    myRes.warnings.add(ctx.warnings)
+    return myRes
+  except ValueError as e:
+    return ParseConfigResult(
+      success: false,
+      warnings: ctx.warnings,
+      errorMsg: e.msg
+    )
 
-proc parseConfig(config: Config, dir: string, stream: Stream, name = "<input>",
-    laxnames = false) =
+proc parseConfig(config: Config; dir: string; stream: Stream; name = "<input>";
+    laxnames = false): ParseConfigResult =
   let toml = parseToml(stream, dir / name, laxnames)
   if toml.isOk:
-    config.parseConfig(dir, toml.get)
+    return config.parseConfig(dir, toml.get)
   else:
-    when nimvm:
-      echo "Fatal error: Failed to parse config"
-      echo toml.error
-    else:
-      stderr.write("Fatal error: Failed to parse config\n")
-      stderr.write(toml.error & '\n')
-    quit(1)
-
-proc parseConfig*(config: Config, dir: string, s: string, name = "<input>",
-    laxnames = false) =
-  config.parseConfig(dir, newStringStream(s), name, laxnames)
+    return ParseConfigResult(
+      success: false,
+      errorMsg: "Fatal error: failed to parse config\n" & toml.error & '\n'
+    )
 
-proc staticReadConfig(): ConfigObj =
-  var config = Config()
-  config.parseConfig("res", staticRead"res/config.toml", "config.toml")
-  return config[]
+proc parseConfig*(config: Config; dir, s: string; name = "<input>";
+    laxnames = false): ParseConfigResult =
+  return config.parseConfig(dir, newStringStream(s), name, laxnames)
 
-const defaultConfig = staticReadConfig()
+const defaultConfig = staticRead"res/config.toml"
 
-proc readConfig(config: Config, dir, name: string) =
+proc readConfig(config: Config; dir, name: string): ParseConfigResult =
   let fs = if name.len > 0 and name[0] == '/':
     newFileStream(name)
   else:
     newFileStream(dir / name)
   if fs != nil:
-    config.parseConfig(dir, fs)
+    return config.parseConfig(dir, fs)
+  return ParseConfigResult(success: true)
+
+proc loadConfig*(config: Config; s: string) {.jsfunc.} =
+  let s = if s.len > 0 and s[0] == '/':
+    s
+  else:
+    getCurrentDir() / s
+  if not fileExists(s):
+    return
+  discard config.parseConfig(parentDir(s), newFileStream(s))
 
-proc getNormalAction*(config: Config, s: string): string =
+proc getNormalAction*(config: Config; s: string): string =
   return config.page.getOrDefault(s)
 
-proc getLinedAction*(config: Config, s: string): string =
+proc getLinedAction*(config: Config; s: string): string =
   return config.line.getOrDefault(s)
 
-proc readConfig*(pathOverride: Option[string]): Config =
-  result = Config()
-  result[] = defaultConfig
+proc readConfig*(pathOverride: Option[string]; jsctx: JSContext): Config =
+  result = Config(jsctx: jsctx)
+  discard result.parseConfig("res", newStringStream(defaultConfig)) #TODO TODO TODO
   if pathOverride.isNone:
     when defined(debug):
-      result.readConfig(getCurrentDir() / "res", "config.toml")
-    result.readConfig(getConfigDir() / "chawan", "config.toml")
+      discard result.readConfig(getCurrentDir() / "res", "config.toml")
+    discard result.readConfig(getConfigDir() / "chawan", "config.toml")
   else:
-    result.readConfig(getCurrentDir(), pathOverride.get)
+    discard result.readConfig(getCurrentDir(), pathOverride.get)
 
 proc addConfigModule*(ctx: JSContext) =
   ctx.registerType(ActionMap)
diff --git a/src/config/toml.nim b/src/config/toml.nim
index e46491c5..fc32f387 100644
--- a/src/config/toml.nim
+++ b/src/config/toml.nim
@@ -58,7 +58,7 @@ type
     key*: seq[string]
     value*: TomlValue
 
-  TomlTable = ref object of TomlNode
+  TomlTable* = ref object of TomlNode
     key: seq[string]
     nodes: seq[TomlNode]
     map: Table[string, TomlValue]
diff --git a/src/js/fromjs.nim b/src/js/fromjs.nim
index 112efd75..be90e192 100644
--- a/src/js/fromjs.nim
+++ b/src/js/fromjs.nim
@@ -471,6 +471,11 @@ proc fromJSEmptyPromise(ctx: JSContext, val: JSValue): JSResult[EmptyPromise] =
 type FromJSAllowedT = (object and not (Result|Option|Table|JSValue|JSDict|
   JSArrayBuffer|JSArrayBufferView|JSUint8Array))
 
+macro fromJS2(ctx: JSContext; val: JSValue; x: static string): untyped =
+  let id = ident("fromJS" & x)
+  return quote do:
+    `id`(`ctx`, `val`)
+
 proc fromJS*[T](ctx: JSContext, val: JSValue): JSResult[T] =
   when T is string:
     return fromJSString(ctx, val)
@@ -511,11 +516,8 @@ proc fromJS*[T](ctx: JSContext, val: JSValue): JSResult[T] =
     return fromJSArrayBuffer(ctx, val)
   elif T is JSArrayBufferView:
     return fromJSArrayBufferView(ctx, val)
-  elif compiles(fromJS2(ctx, val, result)):
-    fromJS2(ctx, val, result)
   else:
-    static:
-      error("Unrecognized type " & $T)
+    return fromJS2(ctx, val, $T)
 
 const JS_ATOM_TAG_INT = cuint(1u32 shl 31)
 
diff --git a/src/loader/headers.nim b/src/loader/headers.nim
index f097fa4f..20764760 100644
--- a/src/loader/headers.nim
+++ b/src/loader/headers.nim
@@ -22,18 +22,15 @@ type
 
 jsDestructor(Headers)
 
-proc fromJS2*(ctx: JSContext, val: JSValue, res: var JSResult[HeadersInit]) =
+proc fromJSHeadersInit(ctx: JSContext; val: JSValue): JSResult[HeadersInit] =
   if JS_IsUndefined(val) or JS_IsNull(val):
-    res.err(nil)
-    return
+    return err(nil)
   if isSequence(ctx, val):
     let x = fromJS[seq[(string, string)]](ctx, val)
     if x.isSome:
-      res.ok(HeadersInit(t: HEADERS_INIT_SEQUENCE, s: x.get))
-  else:
-    let x = fromJS[Table[string, string]](ctx, val)
-    if x.isSome:
-      res.ok(HeadersInit(t: HEADERS_INIT_TABLE, tab: x.get))
+      return ok(HeadersInit(t: HEADERS_INIT_SEQUENCE, s: x.get))
+  let x = ?fromJS[Table[string, string]](ctx, val)
+  return ok(HeadersInit(t: HEADERS_INIT_TABLE, tab: x))
 
 proc fill*(headers: Headers, s: seq[(string, string)]) =
   for (k, v) in s:
diff --git a/src/loader/request.nim b/src/loader/request.nim
index f56e1f76..05b02114 100644
--- a/src/loader/request.nim
+++ b/src/loader/request.nim
@@ -176,31 +176,26 @@ type
     proxyUrl: URL
     mode: Opt[RequestMode]
 
-proc fromJS2*(ctx: JSContext, val: JSValue, res: var JSResult[BodyInit]) =
+proc fromJSBodyInit(ctx: JSContext, val: JSValue): JSResult[BodyInit] =
   if JS_IsUndefined(val) or JS_IsNull(val):
-    res.err(nil)
-    return
+    return err(nil)
   block formData:
     let x = fromJS[FormData](ctx, val)
     if x.isSome:
-      res.ok(BodyInit(t: BODY_INIT_FORM_DATA, formData: x.get))
-      return
+      return ok(BodyInit(t: BODY_INIT_FORM_DATA, formData: x.get))
   block blob:
     let x = fromJS[Blob](ctx, val)
     if x.isSome:
-      res.ok(BodyInit(t: BODY_INIT_BLOB, blob: x.get))
-      return
+      return ok(BodyInit(t: BODY_INIT_BLOB, blob: x.get))
   block searchParams:
     let x = fromJS[URLSearchParams](ctx, val)
     if x.isSome:
-      res.ok(BodyInit(t: BODY_INIT_URL_SEARCH_PARAMS, searchParams: x.get))
-      return
+      return ok(BodyInit(t: BODY_INIT_URL_SEARCH_PARAMS, searchParams: x.get))
   block str:
     let x = fromJS[string](ctx, val)
     if x.isSome:
-      res.ok(BodyInit(t: BODY_INIT_STRING, str: x.get))
-      return
-  res.err(newTypeError("Invalid body init type"))
+      return ok(BodyInit(t: BODY_INIT_STRING, str: x.get))
+  return err(newTypeError("Invalid body init type"))
 
 func newRequest*[T: string|Request](ctx: JSContext, resource: T,
     init = none(RequestInit)): JSResult[Request] {.jsctor.} =
diff --git a/src/local/client.nim b/src/local/client.nim
index 7947ce15..60bdf411 100644
--- a/src/local/client.nim
+++ b/src/local/client.nim
@@ -774,8 +774,9 @@ proc dumpBuffers(client: Client) =
       quit(1)
   stdout.close()
 
-proc launchClient*(client: Client, pages: seq[string],
-    contentType: Option[string], cs: Charset, dump: bool) =
+proc launchClient*(client: Client; pages: seq[string];
+    contentType: Option[string]; cs: Charset; dump: bool;
+    warnings: seq[string]) =
   var infile: File
   var dump = dump
   if not dump:
@@ -794,6 +795,7 @@ proc launchClient*(client: Client, pages: seq[string],
   client.loader.unregisterFun = proc(fd: int) =
     selector.unregister(fd)
   client.pager.launchPager(infile, selector)
+  client.pager.alerts.add(warnings)
   let clearFun = proc() =
     client.clearConsole()
   let showFun = proc() =
@@ -881,17 +883,17 @@ proc addJSModules(client: Client, ctx: JSContext) =
 func getClient(client: Client): Client {.jsfget: "client".} =
   return client
 
-proc newClient*(config: Config, forkserver: ForkServer): Client =
+proc newClient*(config: Config; forkserver: ForkServer; jsctx: JSContext):
+    Client =
   setControlCHook(proc() {.noconv.} = quit(1))
-  let jsrt = newJSRuntime()
+  let jsrt = JS_GetRuntime(jsctx)
   JS_SetModuleLoaderFunc(jsrt, normalizeModuleName, clientLoadJSModule, nil)
-  let jsctx = jsrt.newJSContext()
   let pager = newPager(config, forkserver, jsctx)
   let loader = forkserver.newFileLoader(LoaderConfig(
     urimethodmap: config.getURIMethodMap(),
     w3mCGICompat: config.external.w3m_cgi_compat,
     cgiDir: pager.cgiDir,
-    tmpdir: pager.tmpdir
+    tmpdir: config.external.tmpdir
   ))
   pager.setLoader(loader)
   let client = Client(
diff --git a/src/local/pager.nim b/src/local/pager.nim
index 6448d1a8..e5d7d062 100644
--- a/src/local/pager.nim
+++ b/src/local/pager.nim
@@ -12,7 +12,6 @@ when defined(posix):
   import std/posix
 
 import bindings/libregexp
-import config/chapath
 import config/config
 import config/mailcap
 import config/mimetypes
@@ -100,7 +99,7 @@ type
 
   Pager* = ref object
     alertState: PagerAlertState
-    alerts: seq[string]
+    alerts*: seq[string]
     askcharpromise*: Promise[string]
     askcursor: int
     askpromise*: Promise[bool]
@@ -127,7 +126,6 @@ type
     mimeTypes: MimeTypes
     notnum*: bool # has a non-numeric character been input already?
     numload*: int # number of pages currently being loaded
-    omnirules: seq[OmniRule]
     precnum*: int32 # current number prefix (when vi-numeric-prefix is true)
     procmap*: seq[ProcMapItem]
     proxy: URL
@@ -136,10 +134,8 @@ type
     reverseSearch: bool
     scommand*: string
     selector*: Selector[int]
-    siteconf: seq[SiteConfig]
     statusgrid*: FixedGrid
     term*: Terminal
-    tmpdir*: string
     unreg*: seq[Container]
     urimethodmap: URIMethodMap
 
@@ -282,16 +278,9 @@ proc quit*(pager: Pager, code = 0) =
   pager.dumpAlerts()
 
 proc setPaths(pager: Pager): Err[string] =
-  let tmpdir0 = pager.config.external.tmpdir.unquote()
-  if tmpdir0.isErr:
-    return err("Error unquoting external.tmpdir: " & tmpdir0.error)
-  pager.tmpdir = tmpdir0.get
   var cgiDir: seq[string]
   for path in pager.config.external.cgi_dir:
-    let x = path.unquote()
-    if x.isErr:
-      return err("Error unquoting external.cgi-dir: " & x.error)
-    cgiDir.add(x.get)
+    cgiDir.add(path)
   pager.cgiDir = cgiDir
   return ok()
 
@@ -302,9 +291,7 @@ proc newPager*(config: Config; forkserver: ForkServer; ctx: JSContext): Pager =
     forkserver: forkserver,
     mailcap: mailcap,
     mimeTypes: config.getMimeTypes(),
-    omnirules: config.getOmniRules(ctx),
     proxy: config.getProxy(),
-    siteconf: config.getSiteConfig(ctx),
     term: newTerminal(stdout, config),
     urimethodmap: config.getURIMethodMap()
   )
@@ -889,7 +876,7 @@ func getEditorCommand(pager: Pager; file: string; line = 1): string {.jsfunc.} =
 
 proc openInEditor(pager: Pager; input: var string): bool =
   try:
-    let tmpf = getTempFile(pager.tmpdir)
+    let tmpf = getTempFile(pager.config.external.tmpdir)
     if input != "":
       writeFile(tmpf, input)
     let cmd = pager.getEditorCommand(tmpf)
@@ -931,7 +918,7 @@ proc applySiteconf(pager: Pager; url: var URL; charsetOverride: Charset;
   var charsets = pager.config.encoding.document_charset
   var userstyle = pager.config.css.stylesheet
   var proxy = pager.proxy
-  for sc in pager.siteconf:
+  for sc in pager.config.siteconf:
     if sc.url.isSome and not sc.url.get.match($url):
       continue
     elif sc.host.isSome and not sc.host.get.match(host):
@@ -1019,7 +1006,7 @@ proc gotoURL(pager: Pager, request: Request, prevurl = none(URL),
     pager.container.findAnchor(request.url.anchor)
 
 proc omniRewrite(pager: Pager, s: string): string =
-  for rule in pager.omnirules:
+  for rule in pager.config.omnirule:
     if rule.match.match(s):
       let sub = rule.substitute_url(s)
       if sub.isSome:
@@ -1514,9 +1501,8 @@ proc checkMailcap(pager: Pager; container: Container; stream: SocketStream;
   let entry = pager.mailcap.getMailcapEntry(contentType, "", url)
   if entry == nil:
     return CheckMailcapResult(connect: true, fdout: stream.fd, found: false)
-  let tmpdir = pager.tmpdir
   let ext = url.pathname.afterLast('.')
-  let tempfile = getTempFile(tmpdir, ext)
+  let tempfile = getTempFile(pager.config.external.tmpdir, ext)
   let outpath = if entry.nametemplate != "":
     unquoteCommand(entry.nametemplate, contentType, tempfile, url)
   else:
diff --git a/src/main.nim b/src/main.nim
index ea5a92dd..51b1219f 100644
--- a/src/main.nim
+++ b/src/main.nim
@@ -6,12 +6,11 @@ let forks = newForkServer()
 import std/options
 import std/os
 
-import config/chapath
 import config/config
 import io/serversocket
+import js/javascript
 import local/client
 import local/term
-import types/opt
 import utils/strwidth
 import utils/twtstr
 
@@ -178,9 +177,15 @@ Options:
       pages.add(param)
     inc i
 
-  let config = readConfig(configPath)
+  let jsrt = newJSRuntime()
+  let jsctx = jsrt.newJSContext()
+  let config = readConfig(configPath, jsctx)
+  var warnings = newSeq[string]()
   for opt in opts:
-    config.parseConfig(getCurrentDir(), opt, laxnames = true)
+    let res = config.parseConfig(getCurrentDir(), opt, laxnames = true)
+    if not res.success:
+      stderr.write(res.errorMsg)
+      quit(1)
   config.css.stylesheet &= stylesheet
 
   set_cjk_ambiguous(config.display.double_width_ambiguous)
@@ -200,16 +205,11 @@ Options:
       help(1)
 
   forks.loadForkServerConfig(config)
-  let tmpdir0 = config.external.tmpdir.unquote()
-  if tmpdir0.isErr:
-    stderr.write("Error unquoting external.tmpdir: " & tmpdir0.error)
-    stderr.write("Exiting...")
-    quit(1)
-  SocketDirectory = tmpdir0.get
-
-  let c = newClient(config, forks)
+  SocketDirectory = config.external.tmpdir
+
+  let c = newClient(config, forks, jsctx)
   try:
-    c.launchClient(pages, ctype, cs, dump)
+    c.launchClient(pages, ctype, cs, dump, warnings)
   except CatchableError:
     c.flushConsole()
     raise
diff --git a/src/types/color.nim b/src/types/color.nim
index 2f3f7310..6d46031d 100644
--- a/src/types/color.nim
+++ b/src/types/color.nim
@@ -517,14 +517,10 @@ proc toJS*(ctx: JSContext, rgb: RGBColor): JSValue =
   res.pushHex(rgb.b)
   return toJS(ctx, res)
 
-proc fromJS2*(ctx: JSContext, val: JSValue, o: var JSResult[RGBColor]) =
-  let s = fromJS[string](ctx, val)
-  if s.isSome:
-    o = parseLegacyColor(s.get)
-  else:
-    o.err(s.error)
+proc fromJSRGBColor*(ctx: JSContext, val: JSValue): JSResult[RGBColor] =
+  return parseLegacyColor(?fromJS[string](ctx, val))
 
-proc toJS*(ctx: JSContext, rgba: RGBAColor): JSValue =
+proc toJS*(ctx: JSContext; rgba: RGBAColor): JSValue =
   var res = "#"
   res.pushHex(rgba.r)
   res.pushHex(rgba.g)
@@ -532,22 +528,12 @@ proc toJS*(ctx: JSContext, rgba: RGBAColor): JSValue =
   res.pushHex(rgba.a)
   return toJS(ctx, res)
 
-proc fromJS2*(ctx: JSContext, val: JSValue, o: var JSResult[RGBAColor]) =
+proc fromJSRGBAColor*(ctx: JSContext; val: JSValue): JSResult[RGBAColor] =
   if JS_IsNumber(val):
     # as hex
-    let x = fromJS[uint32](ctx, val)
-    if x.isSome:
-      o.ok(RGBAColor(x.get))
-    else:
-      o.err(x.error)
-  else:
-    # parse
-    let s = fromJS[string](ctx, val)
-    if s.isSome:
-      let x = parseRGBAColor(s.get)
-      if x.isSome:
-        o.ok(x.get)
-      else:
-        o.err(newTypeError("Unrecognized color"))
-    else:
-      o.err(s.error)
+    return ok(RGBAColor(?fromJS[uint32](ctx, val)))
+  # parse
+  let x = parseRGBAColor(?fromJS[string](ctx, val))
+  if x.isSome:
+    return ok(x.get)
+  return errTypeError("Unrecognized color")