about summary refs log blame commit diff stats
path: root/src/HTNestedList.h
blob: 5a5f1039d7913d73330a01d6402c566d8d1b01be (plain) (tree)











































                                          
#ifndef HTNESTEDLIST_H
#define HTNESTEDLIST_H

#define HTML_OL1        (HTML_ELEMENTS+1)
#define HTML_OL2        (HTML_ELEMENTS+2)
#define HTML_OL3        (HTML_ELEMENTS+3)
#define HTML_OL4        (HTML_ELEMENTS+4)
#define HTML_OL5        (HTML_ELEMENTS+5)
#define HTML_OL6        (HTML_ELEMENTS+6)

#define HTML_MENU1      (HTML_ELEMENTS+7)
#define HTML_MENU2      (HTML_ELEMENTS+8)
#define HTML_MENU3      (HTML_ELEMENTS+9)
#define HTML_MENU4      (HTML_ELEMENTS+10)
#define HTML_MENU5      (HTML_ELEMENTS+11)
#define HTML_MENU6      (HTML_ELEMENTS+12)

#define HTML_DL1        (HTML_ELEMENTS+13)
#define HTML_DL2        (HTML_ELEMENTS+14)
#define HTML_DL3        (HTML_ELEMENTS+15)
#define HTML_DL4        (HTML_ELEMENTS+16)
#define HTML_DL5        (HTML_ELEMENTS+17)
#define HTML_DL6        (HTML_ELEMENTS+18)

#define HTML_DLC1       (HTML_ELEMENTS+19)
#define HTML_DLC2       (HTML_ELEMENTS+20)
#define HTML_DLC3       (HTML_ELEMENTS+21)
#define HTML_DLC4       (HTML_ELEMENTS+22)
#define HTML_DLC5       (HTML_ELEMENTS+23)
#define HTML_DLC6       (HTML_ELEMENTS+24)

#define HTML_HCENTER  	(HTML_ELEMENTS+25)
#define HTML_HLEFT      (HTML_ELEMENTS+26)
#define HTML_HRIGHT     (HTML_ELEMENTS+27)

#define HTML_DCENTER    (HTML_ELEMENTS+28)
#define HTML_DLEFT      (HTML_ELEMENTS+29)
#define HTML_DRIGHT     (HTML_ELEMENTS+30)

#define HTML_OBJECT_M   (HTML_ELEMENTS+31)

#define LYNX_HTML_EXTRA_ELEMENTS 31

#endif /* HTNESTEDLIST_H */
10 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815
import std/options
import std/os
import std/streams
import std/strutils
import std/tables

import bindings/quickjs
import config/chapath
import config/mailcap
import config/mimetypes
import config/toml
import js/error
import js/fromjs
import js/javascript
import js/jstypes
import js/propertyenumlist
import js/regex
import js/tojs
import loader/headers
import types/cell
import types/color
import types/cookie
import types/opt
import types/urimethodmap
import types/url
import utils/twtstr

import chagashi/charset

type
  ColorMode* = enum
    cmMonochrome, cmANSI, cmEightBit, cmTrueColor

  FormatMode* = set[FormatFlags]

  ImageMode* = enum
    imNone = "none", imSixel = "sixel", imKitty = "kitty"

  ChaPathResolved* = distinct string

  ActionMap = object
    t: Table[string, string]

  SiteConfig* = object
    url*: Option[Regex]
    host*: Option[Regex]
    rewrite_url*: Option[JSValueFunction]
    cookie*: Option[bool]
    third_party_cookie*: seq[Regex]
    share_cookie_jar*: Option[string]
    referer_from*: Option[bool]
    scripting*: Option[bool]
    document_charset*: seq[Charset]
    images*: Option[bool]
    stylesheet*: Option[string]
    proxy*: Option[URL]
    default_headers*: TableRef[string, string]

  OmniRule* = object
    match*: Regex
    substitute_url*: Option[JSValueFunction]

  StartConfig = object
    visual_home* {.jsgetset.}: string
    startup_script* {.jsgetset.}: string
    headless* {.jsgetset.}: bool
    console_buffer* {.jsgetset.}: bool

  CSSConfig = object
    stylesheet* {.jsgetset.}: string

  SearchConfig = object
    wrap* {.jsgetset.}: bool
    ignore_case* {.jsgetset.}: bool

  EncodingConfig = object
    display_charset* {.jsgetset.}: Option[Charset]
    document_charset* {.jsgetset.}: seq[Charset]

  CommandConfig = object
    jsObj*: JSValue
    init*: seq[tuple[k, cmd: string]] # initial k/v map
    map*: Table[string, JSValue] # qualified name -> function

  ExternalConfig = object
    tmpdir* {.jsgetset.}: ChaPathResolved
    editor* {.jsgetset.}: string
    mailcap*: Mailcap
    mime_types*: MimeTypes
    cgi_dir* {.jsgetset.}: seq[ChaPathResolved]
    urimethodmap*: URIMethodMap
    download_dir* {.jsgetset.}: string
    w3m_cgi_compat* {.jsgetset.}: bool

  InputConfig = object
    vi_numeric_prefix* {.jsgetset.}: bool
    use_mouse* {.jsgetset.}: bool

  NetworkConfig = object
    max_redirect* {.jsgetset.}: int32
    prepend_https* {.jsgetset.}: bool
    prepend_scheme* {.jsgetset.}: string
    proxy* {.jsgetset.}: URL
    default_headers* {.jsgetset.}: Table[string, string]

  DisplayConfig = object
    color_mode* {.jsgetset.}: Option[ColorMode]
    format_mode* {.jsgetset.}: Option[FormatMode]
    no_format_mode* {.jsgetset.}: FormatMode
    image_mode* {.jsgetset.}: Option[ImageMode]
    emulate_overline* {.jsgetset.}: bool
    alt_screen* {.jsgetset.}: Option[bool]
    highlight_color* {.jsgetset.}: RGBAColor
    highlight_marks* {.jsgetset.}: bool
    double_width_ambiguous* {.jsgetset.}: bool
    minimum_contrast* {.jsgetset.}: int32
    force_clear* {.jsgetset.}: bool
    set_title* {.jsgetset.}: bool
    default_background_color* {.jsgetset.}: Option[RGBColor]
    default_foreground_color* {.jsgetset.}: Option[RGBColor]
    query_da1* {.jsgetset.}: bool
    columns* {.jsgetset.}: int32
    lines* {.jsgetset.}: int32
    pixels_per_column* {.jsgetset.}: int32
    pixels_per_line* {.jsgetset.}: int32
    force_columns* {.jsgetset.}: bool
    force_lines* {.jsgetset.}: bool
    force_pixels_per_column* {.jsgetset.}: bool
    force_pixels_per_line* {.jsgetset.}: bool

  Config* = ref object
    jsctx: JSContext
    configdir {.jsget.}: string
    `include` {.jsget.}: seq[ChaPathResolved]
    start* {.jsget.}: StartConfig
    search* {.jsget.}: SearchConfig
    css* {.jsget.}: CSSConfig
    encoding* {.jsget.}: EncodingConfig
    external* {.jsget.}: ExternalConfig
    network* {.jsget.}: NetworkConfig
    input* {.jsget.}: InputConfig
    display* {.jsget.}: DisplayConfig
    #TODO getset
    siteconf*: seq[SiteConfig]
    omnirule*: seq[OmniRule]
    cmd*: CommandConfig
    page* {.jsget.}: ActionMap
    line* {.jsget.}: ActionMap

  ForkServerConfig* = object
    tmpdir*: string
    ambiguous_double*: bool

jsDestructor(ActionMap)
jsDestructor(StartConfig)
jsDestructor(CSSConfig)
jsDestructor(SearchConfig)
jsDestructor(EncodingConfig)
jsDestructor(ExternalConfig)
jsDestructor(NetworkConfig)
jsDestructor(DisplayConfig)
jsDestructor(Config)

converter toStr*(p: ChaPathResolved): string {.inline.} =
  return string(p)

proc fromJSChaPathResolved(ctx: JSContext; val: JSValue):
    JSResult[ChaPathResolved] =
  return cast[JSResult[ChaPathResolved]](fromJS[string](ctx, val))

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 =
  return b in a.t

proc getOrDefault(a: ActionMap; b: string): string =
  return a.t.getOrDefault(b)

proc hasKeyOrPut(a: var ActionMap; b, c: string): bool =
  return a.t.hasKeyOrPut(b, c)

func getRealKey(key: string): string =
  var realk: string
  var control = 0
  var meta = 0
  var skip = false
  for c in key:
    if c == '\\':
      skip = true
    elif skip:
      realk &= c
      skip = false
    elif c == 'M' and meta == 0:
      inc meta
    elif c == 'C' and control == 0:
      inc control
    elif c == '-' and control == 1:
      inc control
    elif c == '-' and meta == 1:
      inc meta
    elif meta == 1:
      realk &= 'M' & c
      meta = 0
    elif control == 1:
      realk &= 'C' & c
      control = 0
    else:
      if meta == 2:
        realk &= '\e'
        meta = 0
      if control == 2:
        realk &= getControlChar(c)
        control = 0
      else:
        realk &= c
  if control == 1:
    realk &= 'C'
  if meta == 1:
    realk &= 'M'
  if skip:
    realk &= '\\'
  return realk

proc getter(a: ptr ActionMap; s: string): Option[string] {.jsgetprop.} =
  a.t.withValue(s, p):
    return some(p[])
  return none(string)

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[]

proc openFileExpand(dir, file: string): FileStream =
  if file.len == 0:
    return nil
  if file[0] == '/':
    return newFileStream(file)
  else:
    return newFileStream(dir / file)

proc readUserStylesheet(dir, file: string): string =
  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()

proc getForkServerConfig*(config: Config): ForkServerConfig =
  return ForkServerConfig(
    tmpdir: config.external.tmpdir,
    ambiguous_double: config.display.double_width_ambiguous
  )

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 Option[ColorMode];
  v: TomlValue; k: string)
proc parseConfigValue(ctx: var ConfigParser; x: var Option[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(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[U; V](ctx: var ConfigParser; x: var TableRef[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(ctx: var ConfigParser; x: var JSValueFunction;
  v: TomlValue; k: string)
proc parseConfigValue(ctx: var ConfigParser; x: var ChaPathResolved;
  v: TomlValue; k: string)
proc parseConfigValue(ctx: var ConfigParser; x: var MimeTypes; v: TomlValue;
  k: string)
proc parseConfigValue(ctx: var ConfigParser; x: var Mailcap; v: TomlValue;
  k: string)
proc parseConfigValue(ctx: var ConfigParser; x: var URIMethodMap; v: TomlValue;
  k: string)
proc parseConfigValue(ctx: var ConfigParser; x: var CommandConfig; v: TomlValue;
  k: string)

proc typeCheck(v: TomlValue; t: TomlValueType; k: string) =
  if v.t != t:
    raise newException(ValueError, "invalid type for key " & k &
      " (got " & $v.t & ", expected " & $t & ")")

proc typeCheck(v: TomlValue; t: set[TomlValueType]; k: string) =
  if v.t notin t:
    raise newException(ValueError, "invalid type for key " & k &
      " (got " & $v.t & ", expected " & $t & ")")

proc parseConfigValue(ctx: var ConfigParser; x: var object; v: TomlValue;
    k: string) =
  typeCheck(v, tvtTable, k)
  for fk, fv in x.fieldPairs:
    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, tvtTable, k)
  x.clear()
  for kk, vv in v:
    var y: V
    let kkk = k & "[" & kk & "]"
    ctx.parseConfigValue(y, vv, kkk)
    x[kk] = y

proc parseConfigValue[U, V](ctx: var ConfigParser; x: var TableRef[U, V];
    v: TomlValue; k: string) =
  typeCheck(v, tvtTable, k)
  x = TableRef[U, V]()
  for kk, vv in v:
    var y: V
    let kkk = k & "[" & kk & "]"
    ctx.parseConfigValue(y, vv, kkk)
    x[kk] = y

proc parseConfigValue(ctx: var ConfigParser; x: var bool; v: TomlValue;
    k: string) =
  typeCheck(v, tvtBoolean, k)
  x = v.b

proc parseConfigValue(ctx: var ConfigParser; x: var string; v: TomlValue;
    k: string) =
  typeCheck(v, tvtString, k)
  x = v.s

proc parseConfigValue(ctx: var ConfigParser; x: var ChaPath;
    v: TomlValue; k: string) =
  typeCheck(v, tvtString, k)
  x = ChaPath(v.s)

proc parseConfigValue[T](ctx: var ConfigParser; x: var seq[T]; v: TomlValue;
    k: string) =
  typeCheck(v, {tvtString, tvtArray}, k)
  if v.t != tvtArray:
    var y: T
    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
      ctx.parseConfigValue(y, v.a[i], k & "[" & $i & "]")
      x.add(y)

proc parseConfigValue(ctx: var ConfigParser; x: var TomlTable; v: TomlValue;
    k: string) =
  typeCheck(v, {tvtTable}, k)
  x = v.tab

proc parseConfigValue(ctx: var ConfigParser; x: var Charset; v: TomlValue;
    k: string) =
  typeCheck(v, tvtString, k)
  x = getCharset(v.s)
  if x == CHARSET_UNKNOWN:
    raise newException(ValueError, "unknown charset '" & v.s & "' for key " &
      k)

proc parseConfigValue(ctx: var ConfigParser; x: var int32; v: TomlValue;
    k: string) =
  typeCheck(v, tvtInteger, k)
  x = int32(v.i)

proc parseConfigValue(ctx: var ConfigParser; x: var int64; v: TomlValue;
    k: string) =
  typeCheck(v, tvtInteger, k)
  x = v.i

proc parseConfigValue(ctx: var ConfigParser; x: var Option[ColorMode];
    v: TomlValue; k: string) =
  typeCheck(v, tvtString, k)
  case v.s
  of "auto": x = none(ColorMode)
  of "monochrome": x = some(cmMonochrome)
  of "ansi": x = some(cmANSI)
  of "8bit", "eight-bit": x = some(cmEightBit)
  of "24bit", "true-color": x = some(cmTrueColor)
  else:
    raise newException(ValueError, "unknown color mode '" & v.s &
      "' for key " & k)

proc parseConfigValue(ctx: var ConfigParser; x: var Option[FormatMode];
    v: TomlValue; k: string) =
  typeCheck(v, {tvtString, tvtArray}, k)
  if v.t == tvtString and v.s == "auto":
    x = none(FormatMode)
  else:
    var y: FormatMode
    ctx.parseConfigValue(y, v, k)
    x = some(y)

proc parseConfigValue(ctx: var ConfigParser; x: var FormatMode; v: TomlValue;
    k: string) =
  typeCheck(v, tvtArray, k)
  for i in 0 ..< v.a.len:
    let kk = k & "[" & $i & "]"
    let vv = v.a[i]
    typeCheck(vv, tvtString, kk)
    case vv.s
    of "bold": x.incl(ffBold)
    of "italic": x.incl(ffItalic)
    of "underline": x.incl(ffUnderline)
    of "reverse": x.incl(ffReverse)
    of "strike": x.incl(ffStrike)
    of "overline": x.incl(ffOverline)
    of "blink": x.incl(ffBlink)
    else:
      raise newException(ValueError, "unknown format mode '" & vv.s &
        "' for key " & kk)

proc parseConfigValue(ctx: var ConfigParser; x: var RGBAColor; v: TomlValue;
    k: string) =
  typeCheck(v, tvtString, k)
  let c = parseRGBAColor(v.s)
  if c.isNone:
    raise newException(ValueError, "invalid color '" & v.s &
      "' for key " & k)
  x = c.get

proc parseConfigValue(ctx: var ConfigParser; x: var RGBColor; v: TomlValue;
    k: string) =
  typeCheck(v, tvtString, k)
  let c = parseLegacyColor(v.s)
  if c.isNone:
    raise newException(ValueError, "invalid color '" & v.s &
      "' for key " & k)
  x = c.get

proc parseConfigValue[T](ctx: var ConfigParser; x: var Option[T]; v: TomlValue;
    k: string) =
  if v.t == tvtString and v.s == "auto":
    x = none(T)
  else:
    var y: T
    ctx.parseConfigValue(y, v, k)
    x = some(y)

proc parseConfigValue(ctx: var ConfigParser; x: var ActionMap; v: TomlValue;
    k: string) =
  typeCheck(v, tvtTable, k)
  for kk, vv in v:
    typeCheck(vv, tvtString, k & "[" & kk & "]")
    let rk = getRealKey(kk)
    var buf: string
    for i in 0 ..< rk.high:
      buf &= rk[i]
      discard x.hasKeyOrPut(buf, "client.feedNext()")
    x[rk] = vv.s

proc parseConfigValue[T: enum](ctx: var ConfigParser; x: var T; v: TomlValue;
    k: string) =
  typeCheck(v, tvtString, 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](ctx: var ConfigParser; x: var set[T]; v: TomlValue;
    k: string) =
  typeCheck(v, {tvtString, tvtArray}, k)
  if v.t == tvtString:
    var xx: T
    xx.parseConfigValue(v, k)
    x = {xx}
  else:
    x = {}
    for i in 0 ..< v.a.len:
      let kk = k & "[" & $i & "]"
      var xx: T
      xx.parseConfigValue(v.a[i], kk)
      x.incl(xx)

proc parseConfigValue(ctx: var ConfigParser; x: var CSSConfig; v: TomlValue;
    k: string) =
  typeCheck(v, tvtTable, k)
  for kk, vv in v:
    let kkk = if k != "":
      k & "." & kk
    else:
      kk
    case kk
    of "include":
      typeCheck(vv, {tvtString, tvtArray}, kkk)
      case vv.t
      of tvtString:
        x.stylesheet &= readUserStylesheet(ctx.dir, vv.s)
      of tvtArray:
        for child in vv.a:
          x.stylesheet &= readUserStylesheet(ctx.dir, vv.s)
      else: discard
    of "inline":
      typeCheck(vv, tvtString, kkk)
      x.stylesheet &= vv.s

proc parseConfigValue(ctx: var ConfigParser; x: var Regex; v: TomlValue;
    k: string) =
  typeCheck(v, tvtString, 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, tvtString, k)
  let y = parseURL(v.s)
  if y.isNone:
    raise newException(ValueError, "invalid URL " & k)
  x = y.get

proc parseConfigValue(ctx: var ConfigParser; x: var JSValueFunction;
    v: TomlValue; k: string) =
  typeCheck(v, tvtString, k)
  let fun = ctx.config.jsctx.eval(v.s, "<config>", JS_EVAL_TYPE_GLOBAL)
  if JS_IsException(fun):
    raise newException(ValueError, "exception in " & k & ": " &
      ctx.config.jsctx.getExceptionMsg())
  if not JS_IsFunction(ctx.config.jsctx, fun):
    raise newException(ValueError, k & " is not a function")
  x = JSValueFunction(fun: fun)

proc parseConfigValue(ctx: var ConfigParser; x: var ChaPathResolved;
    v: TomlValue; k: string) =
  typeCheck(v, tvtString, k)
  let y = ChaPath(v.s).unquote()
  if y.isErr:
    raise newException(ValueError, y.error)
  x = ChaPathResolved(y.get)

proc parseConfigValue(ctx: var ConfigParser; x: var MimeTypes; v: TomlValue;
    k: string) =
  var paths: seq[ChaPathResolved]
  ctx.parseConfigValue(paths, v, k)
  x = default(MimeTypes)
  for p in paths:
    let f = openFileExpand(ctx.config.configdir, p)
    if f != nil:
      x.parseMimeTypes(f)

const DefaultMailcap = block:
  let ss = newStringStream(staticRead"res/mailcap")
  parseMailcap(ss).get

proc parseConfigValue(ctx: var ConfigParser; x: var Mailcap; v: TomlValue;
    k: string) =
  var paths: seq[ChaPathResolved]
  ctx.parseConfigValue(paths, v, k)
  x = default(Mailcap)
  for p in paths:
    let f = openFileExpand(ctx.config.configdir, p)
    if f != nil:
      let res = parseMailcap(f)
      if res.isSome:
        x.add(res.get)
      else:
        ctx.warnings.add("Error reading mailcap: " & res.error)
  x.add(DefaultMailcap)

const DefaultURIMethodMap = parseURIMethodMap(staticRead"res/urimethodmap")

proc parseConfigValue(ctx: var ConfigParser; x: var URIMethodMap; v: TomlValue;
    k: string) =
  var paths: seq[ChaPathResolved]
  ctx.parseConfigValue(paths, v, k)
  x = default(URIMethodMap)
  for p in paths:
    let f = openFileExpand(ctx.config.configdir, p)
    if f != nil:
      x.parseURIMethodMap(f.readAll())
  x.append(DefaultURIMethodMap)

func isCompatibleIdent(s: string): bool =
  if s.len == 0 or s[0] notin AsciiAlpha + {'_', '$'}:
    return false
  for i in 1 ..< s.len:
    if s[i] notin AsciiAlphaNumeric + {'_', '$'}:
      return false
  return true

proc parseConfigValue(ctx: var ConfigParser; x: var CommandConfig; v: TomlValue;
    k: string) =
  typeCheck(v, tvtTable, k)
  for kk, vv in v:
    let kkk = k & "." & kk
    typeCheck(vv, {tvtTable, tvtString}, kkk)
    if not kk.isCompatibleIdent():
      raise newException(ValueError, "invalid command name: " & kkk)
    if vv.t == tvtTable:
      ctx.parseConfigValue(x, vv, kkk)
    else: # tvtString
      # skip initial "cmd.", we don't need it
      x.init.add((kkk.substr("cmd.".len), vv.s))

type ParseConfigResult* = object
  success*: bool
  warnings*: seq[string]
  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
  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): ParseConfigResult =
  let toml = parseToml(stream, dir / name, laxnames)
  if toml.isOk:
    return config.parseConfig(dir, toml.get)
  else:
    return ParseConfigResult(
      success: false,
      errorMsg: "Fatal error: failed to parse config\n" & toml.error & '\n'
    )

proc parseConfig*(config: Config; dir, s: string; name = "<input>";
    laxnames = false): ParseConfigResult =
  return config.parseConfig(dir, newStringStream(s), name, laxnames)

const defaultConfig = staticRead"res/config.toml"

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:
    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 =
  return config.page.getOrDefault(s)

proc getLinedAction*(config: Config; s: string): string =
  return config.line.getOrDefault(s)

type ReadConfigResult = tuple
  config: Config
  res: ParseConfigResult

proc readConfig*(pathOverride: Option[string]; jsctx: JSContext):
    ReadConfigResult =
  let config = Config(jsctx: jsctx)
  var res = config.parseConfig("res", newStringStream(defaultConfig))
  if not res.success:
    return (nil, res)
  if pathOverride.isNone:
    when defined(debug):
      res = config.readConfig(getCurrentDir() / "res", "config.toml")
      if not res.success:
        return (nil, res)
    res = config.readConfig(getConfigDir() / "chawan", "config.toml")
  else:
    res = config.readConfig(getCurrentDir(), pathOverride.get)
  if not res.success:
    return (nil, res)
  return (config, res)

# called after parseConfig returns
proc initCommands*(config: Config): Err[string] =
  let ctx = config.jsctx
  let obj = JS_NewObject(ctx)
  defer: JS_FreeValue(ctx, obj)
  if JS_IsException(obj):
    return err(ctx.getExceptionMsg())
  for i in countdown(config.cmd.init.high, 0):
    let (k, cmd) = config.cmd.init[i]
    if k in config.cmd.map:
      # already in map; skip
      continue
    var objIt = JS_DupValue(ctx, obj)
    let name = k.afterLast('.')
    if name.len < k.len:
      for ss in k.substr(0, k.high - name.len - 1).split('.'):
        var prop = JS_GetPropertyStr(ctx, objIt, cstring(ss))
        if JS_IsUndefined(prop):
          prop = JS_NewObject(ctx)
          ctx.definePropertyE(objIt, ss, JS_DupValue(ctx, prop))
        if JS_IsException(prop):
          return err(ctx.getExceptionMsg())
        JS_FreeValue(ctx, objIt)
        objIt = prop
    if cmd == "":
      config.cmd.map[k] = JS_UNDEFINED
      continue
    let fun = ctx.eval(cmd, "<" & k & ">", JS_EVAL_TYPE_GLOBAL)
    if JS_IsException(fun):
      return err(ctx.getExceptionMsg())
    if not JS_IsFunction(ctx, fun):
      return err(k & " is not a function")
    ctx.definePropertyE(objIt, name, JS_DupValue(ctx, fun))
    config.cmd.map[k] = fun
  config.cmd.jsObj = JS_DupValue(ctx, obj)
  config.cmd.init = @[]
  ok()

proc addConfigModule*(ctx: JSContext) =
  ctx.registerType(ActionMap)
  ctx.registerType(StartConfig)
  ctx.registerType(CSSConfig)
  ctx.registerType(SearchConfig)
  ctx.registerType(EncodingConfig)
  ctx.registerType(ExternalConfig)
  ctx.registerType(NetworkConfig)
  ctx.registerType(DisplayConfig)
  ctx.registerType(Config)