about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2022-12-13 02:03:47 +0100
committerbptato <nincsnevem662@gmail.com>2022-12-13 02:03:47 +0100
commit89750ef4ad621fe7fdce533d1265f3d970098a19 (patch)
tree0aa80ceb07ce0ad49e5c3ba369f96bb67c1b6dd5
parentd9e430c8147c8c2d81b4ca5405786269b2cfc94d (diff)
downloadchawan-89750ef4ad621fe7fdce533d1265f3d970098a19.tar.gz
Update config and config docs
-rw-r--r--bonus/config.toml15
-rw-r--r--doc/config.md168
-rw-r--r--doc/layout_engine.txt67
-rw-r--r--res/config.toml10
-rw-r--r--src/buffer/container.nim7
-rw-r--r--src/config/config.nim19
-rw-r--r--src/config/toml.nim57
-rw-r--r--src/display/pager.nim15
8 files changed, 238 insertions, 120 deletions
diff --git a/bonus/config.toml b/bonus/config.toml
deleted file mode 100644
index 39e6dc52..00000000
--- a/bonus/config.toml
+++ /dev/null
@@ -1,15 +0,0 @@
-[[omnirule]]
-match = '^ddg:'
-substitute-url = '(x) => "https://lite.duckduckgo.com/lite/?kp=-1&kd=-1&q=" + x.substring(4)'
-
-[[omnirule]]
-match = '^wk:'
-substitute-url = '(x) => "https://en.wikipedia.org/wiki/Special:Search?search=" + x.substring(4)'
-
-[[siteconf]]
-url = '/^https?:\/\/(www\.)?npr\.org/'
-rewrite-url = '(x) => x.pathname = x.pathname.replace(/(.*)\/.*/, "$1").replace(/.*\//, "")'
-
-[[siteconf]]
-url = "^https://news.ycombinator.com.*"
-cookie = true
diff --git a/doc/config.md b/doc/config.md
index ebb10ec9..e7f10f82 100644
--- a/doc/config.md
+++ b/doc/config.md
@@ -1,17 +1,75 @@
 # Configuration
 
-Chawan supports custom keybindings and user stylesheets, defined in a toml
-configuration file.
+Chawan supports custom keybindings and user stylesheets. The configuration
+format is similar to the toml format, with the following exceptions:
+
+* Table arrays can be cleared like this:
+```
+omnirule = []
+
+[[omnirule]] # this is accepted
+```
+This allows users to disable default array rules.
+
+* Inline tables may span across multiple lines. Rationale: the toml specified
+  behavior is counter-intuitive.
 
 Chawan will look for a config file in the ~/.config/chawan/ directory called
 `config.toml`. See the default configuration in the res/ folder for the default
 configuration.
 
-A list of configurable options follows.
+**Table of contents**
+
+* [start](Start)
+* [external](External)
+* [display](Display)
+* [omnirule](Omnirule)
+* [siteconf](Siteconf)
+* [stylesheets](Stylesheets)
+* [keybindings](Keybindings)
+   * [pager-actions](Pager actions)
+   * [line-editing-actions](Line-editing actions)
+
+## Start
+
+Start-up options are to be placed in the section `[start]`.
+
+Following is a list of start-up options:
+
+<table>
+
+<tr>
+<th>**Name**</th>
+<th>**Value**</th>
+<th>**Function**</th>
+</tr>
+
+<tr>
+<td>visual-home</td>
+<td>url</td>
+<td>Page opened when cha is called with the -V option (and no other pages are
+passed as arguments.)</td>
+</tr>
+
+<tr>
+<td>run-script</td>
+<td>JavaScript code</td>
+<td>Script cha runs on start. Pages will not be loaded until this function
+exits. (setTimeout & friends do not block loading, though.)</td>
+</tr>
+
+<tr>
+<td>headless</td>
+<td>boolean</td>
+<td>Whether cha should always start in headless mode. Enabled when cha is
+called with -r.</td>
+</tr>
+
+</table>
 
 ## External
 
-External options must be placed in a section called `[external]`.
+External options are to be placed in the section `[external]`.
 
 Following is a list of external options:
 
@@ -40,7 +98,7 @@ the line number.</td>
 
 ## Display
 
-Display options must be placed in a section called `[display]`.
+Display options are to be placed in the section `[display]`.
 
 Following is a list of display options:
 
@@ -119,10 +177,75 @@ black letters on black background, etc).</td>
 
 </table>
 
+## Omnirule
+
+The omni-bar (by default opened with C-l) can be used to perform searches using
+omni-rules. These are to be placed in the table array `[[omnirule]]`.
+
+Examples:
+```
+# Search using DuckDuckGo Lite. (Bound to C-k by default.)
+[[omnirule]]
+match = '^ddg:'
+substitute-url = '(x) => "https://lite.duckduckgo.com/lite/?kp=-1&kd=-1&q=" + x.substring(4)'
+
+# Search using wikipedia, Firefox-style.
+[[omnirule]]
+match = '^@wikipedia'
+substitute-url = '(x) => "https://en.wikipedia.org/wiki/Special:Search?search=" + x.replace(/@wikipedia/, "")'
+```
+
+Omnirule options:
+
+<table>
+
+<tr>
+<th>**Name**</th>
+<th>**Value**</th>
+<th>**Function**</th>
+</tr>
+
+<tr>
+<td>match</td>
+<td>regex</td>
+<td>Regular expression used to match the input string. Note that websites
+passed as arguments are matched as well.</td>
+</tr>
+
+<tr>
+<td>substitute</td>
+<td>JavaScript function</td>
+<td>A JavaScript function cha will pass the input string to. If a new string is
+returned, it will be parsed instead of the old one.</td>
+</tr>
+
+</table>
+
 ## Siteconf
 
-Configuration options can be specified for individual sites. Each entry must
-be in the table array `[[siteconf]]`.
+Configuration options can be specified for individual sites. Entries are to be
+placed in the table array `[[siteconf]]`.
+
+Examples:
+```
+# Enable cookies on the orange website, to make log-in work.
+[[siteconf]]
+url = "^https://news.ycombinator.com/.*"
+cookie = true
+
+# Redirect npr.org to text.npr.org.
+[[siteconf]]
+host = '^(www\.)?npr\.org$'
+rewrite-url = '''
+(x) => {
+	x.host = "text.npr.org";
+	x.pathname = x.pathname.replace(/(.*)\/.*/, "$1").replace(/.*\//, "");
+	/* No need to return; URL objects are passed by reference. */
+}
+'''
+```
+
+Siteconf options:
 
 <table>
 
@@ -135,13 +258,30 @@ be in the table array `[[siteconf]]`.
 <tr>
 <td>url</td>
 <td>regex</td>
-<td>Regular expression used to match the URL.</td>
+<td>Regular expression used to match the URL. Either this or the `host` option
+must be specified.</td>
 </tr>
 
 <tr>
-<td>substitute_url</td>
-<td>JS function</td>
-<td>A JavaScript function returning the substituted url.</td>
+<td>host</td>
+<td>regex</td>
+<td>Regular expression used to match the host part of the URL (i.e. domain
+name/ip address.) Either this or the `url` option must be specified.</td>
+</tr>
+
+<tr>
+<td>rewrite-url</td>
+<td>JavaScript function</td>
+<td>A JavaScript function cha will pass the URL to. If a new URL is returned,
+it will replace the old one.</td>
+</tr>
+
+<tr>
+<td>cookie</td>
+<td>boolean</td>
+<td>Whether loading cookies should be allowed for this domain. Note: for now,
+third-party cookies are always blocked. A third-party cookie setting may be
+added in the future.</td>
 </tr>
 
 </table>
@@ -162,7 +302,7 @@ There are two ways to import user stylesheets:
 
 ## Keybindings
 
-Keybindings must be placed in these sections:
+Keybindings are to be placed in these sections:
 
 * for pager interaction: `[page]`
 * for line editing: `[line]`
@@ -256,8 +396,8 @@ is typed in. A list of built-in pager functions can be found below.
 </table>
 
 Some entries have an optional `bounds` parameter. If passed, this must be a
-JavaScript function with one parameter (the current unicode character), and
-must return true if the passed character should count as a word boundary.
+JavaScript function that expects one parameter (the current unicode character),
+and returns true if the passed character should count as a word boundary.
 
 ```Examples:
 # Control+A moves the cursor to the beginning of the line.
diff --git a/doc/layout_engine.txt b/doc/layout_engine.txt
deleted file mode 100644
index 0dfc13cf..00000000
--- a/doc/layout_engine.txt
+++ /dev/null
@@ -1,67 +0,0 @@
-CHAWAN LAYOUT ENGINE DESIGN DOCUMENT

-

-Laying out boxes turns out to be pretty damn expensive. This is the outline of

-a simple (YMMV) yet effective layout engine that can take advantage of previous

-layout passes. It doesn't include CSS cascading, which is, by the way, pretty

-damn expensive as well but is done by a different module.

-

-Most algorithms required by this engine are recursive, which is a bit of a

-problem because nim has no guarantees of your program not running out of stack

-randomly.

-

-In most cases this shouldn't be a problem but avoiding DOS attacks will be a

-bit difficult. Currently we don't support CSS that can be much deeper than the

-DOM anyway (does that even exist?) so it shouldn't be that much of a problem if

-we can limit the DOM depth. (I've read Safari had like a 250 depth limit at

-some point, which sounds reasonable.)

-

-BUILDER GENERATION:

--> *Box (BlockBox etc.) -> BlockBoxBuilder, InlineBoxBuilder, later also

-   Table/Ruby builders

---> this means that it's not a direct representation of the DOM. and it follows

-    that we can't preserve builders (?), so they have to be VERY cheap to

-    create. (maybe we can, but I kind of doubt it's worth the trouble. might

-    have to investigate the performance implications here.)

---> Builder attributes: children; css; direction; generated

---> generated should have 1:1 correspondence to builders. this helps figure

-    out what to keep at a tree rebuild: those with matching builders are kept,

-    those without are re-generated.

----> the re-generator algorithm sounds complicated... TODO figure something out

-     here

-

-BUILDING PROCESS:

--> *Context -> BlockContext, InlineContext

---> Context attributes: children; direction (for non-flow-roots, flow root

-    comes later); sizes: content box; padding box; border box; margin box

---> meaning exact positions are NOT calculated until rendering, but sizes are.

-    this allows us to avoid rebuilding sibling contexts when a block context

-    changes.

---> BlockContext children: children (BlockContext) OR flow root (InlineContext)

---> BlockContext can either contain block children only or be a flow root.

-    those trying to be both are broken up in builder generation.

---> flow root is whenever the outer display is block and the inner flow. in

-    practice it should be the same as flow-root. TODO: investigate non-flow

-    inline display.

---> InlineContext is built from InlineBoxBuilder (TODO decide if these are

-    contexts or boxes... context should be better I think...)

-    it contains a sequence of line boxes, laid out top to bottom (ideally they

-    could be left-to-right, etc...)

-    line boxes contain inline atoms, which need to have exact positions, I

-    think. an inline atom is either an inline block or a word. inline atoms

-    are separated by inline spaces which are basically spaces you can click.

---> apropos clicking, EVERY inline word has a reference to the html element it

-    belongs to.

---> if ANY inline context is rebuilt, EVERY inline context in the same flow

-    root must be rebuilt as well. keeping track of line boxes that haven't

-    changed sounds like a pain in the ass with minimal benefits so we don't.

-

-RENDERING PROCESS

--> calculating positions must be done here.

---> so arrangeInlines/arrangeBlocks must be done here

----> and we need to somehow manage margins as well.

---> since we should now have box sizes we can easily draw backgrounds as well.

--> the old layout engine renders the entire document, which kind of makes

-   rendering take way too long. so one thing we could try is to render the

-   pages the user is looking at, the one before that and the one after that.

-   not sure if this will actually help though, since it would mean we have to

-   re-render the entire page every time the user scrolls up/down...

diff --git a/res/config.toml b/res/config.toml
index af4a0dc9..af502275 100644
--- a/res/config.toml
+++ b/res/config.toml
@@ -18,13 +18,9 @@ double-width-ambiguous = false
 minimum-contrast = 100
 force-clear = false
 
-[[siteconf]]
-url = '^ddg:'
-substitute_url = '(x) => "https://lite.duckduckgo.com/lite/?kp=-1&kd=-1&q=" + x.substring(4)'
-
-[[siteconf]]
-url = '^wk:'
-substitute_url = '(x) => "https://en.wikipedia.org/wiki/Special:Search?search=" + x.substring(4)'
+[[omnirule]]
+match = '^ddg:'
+substitute-url = '(x) => "https://lite.duckduckgo.com/lite/?kp=-1&kd=-1&q=" + x.substring(4)'
 
 [page]
 q = 'quit()'
diff --git a/src/buffer/container.nim b/src/buffer/container.nim
index a148e0e1..a08f4cb0 100644
--- a/src/buffer/container.nim
+++ b/src/buffer/container.nim
@@ -97,8 +97,11 @@ type
     events*: Deque[ContainerEvent]
     startpos: Option[CursorPosition]
     hasstart: bool
+    redirectdepth*: int
 
-proc newBuffer*(dispatcher: Dispatcher, config: Config, source: BufferSource, cookiejar: CookieJar, title = ""): Container =
+proc newBuffer*(dispatcher: Dispatcher, config: Config, source: BufferSource,
+                cookiejar: CookieJar, title = "",
+                redirectdepth = 0): Container =
   let attrs = getWindowAttributes(stdout)
   let config = config.getBufferConfig(source.location, cookiejar)
   let ostream = dispatcher.forkserver.ostream
@@ -112,7 +115,7 @@ proc newBuffer*(dispatcher: Dispatcher, config: Config, source: BufferSource, co
   result = Container(
     source: source, attrs: attrs, width: attrs.width,
     height: attrs.height - 1, contenttype: source.contenttype,
-    title: title, config: config
+    title: title, config: config, redirectdepth: redirectdepth
   )
   istream.sread(result.process)
   result.pos.setx = -1
diff --git a/src/config/config.nim b/src/config/config.nim
index 73072763..16e1c652 100644
--- a/src/config/config.nim
+++ b/src/config/config.nim
@@ -315,14 +315,17 @@ proc parseConfig(config: Config, dir: string, t: TomlValue) =
         assert conf.url.isSome != conf.host.isSome
         config.siteconf.add(conf)
     of "omnirule":
-      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 != "":
-          config.omnirules.add(rule)
+      if v.vt == VALUE_ARRAY and v.a.len == 0:
+        config.omnirules.setLen(0)
+      else:
+        for v in v:
+          var rule = StaticOmniRule()
+          for k, v in v:
+            case k
+            of "match": rule.match = v.s
+            of "substitute": rule.subst = v.s
+          if rule.match != "":
+            config.omnirules.add(rule)
 
 proc parseConfig(config: Config, dir: string, stream: Stream) =
   config.parseConfig(dir, parseToml(stream))
diff --git a/src/config/toml.nim b/src/config/toml.nim
index 2c4550b5..89277d24 100644
--- a/src/config/toml.nim
+++ b/src/config/toml.nim
@@ -323,7 +323,10 @@ proc consumeNoState(state: var TomlParser): bool =
             node.map[table.key[i]] = TomlValue(vt: VALUE_TABLE, t: t2)
             node = t2
         if table.key[^1] in node.map:
-          let last = node.map[table.key[^1]]
+          var last = node.map[table.key[^1]]
+          if last.vt == VALUE_ARRAY and last.a.len == 0:
+            last = TomlValue(vt: VALUE_TABLE_ARRAY)
+            node.map[table.key[^1]] = last
           if last.vt != VALUE_TABLE_ARRAY:
             let key = table.key.join('.')
             state.valueError(fmt"re-definition of node {key} as table array (was {last.vt})")
@@ -396,11 +399,59 @@ proc consumeArray(state: var TomlParser): TomlValue =
     of ',':
       if val == nil:
         state.syntaxError("comma without element")
-      result.a.add(val)
+      if val.vt == VALUE_TABLE:
+        # inline table array
+        result = TomlValue(vt: VALUE_TABLE_ARRAY)
+        result.ta.add(val.t)
+      else:
+        result.a.add(val)
     else:
+      if val != nil:
+        state.syntaxError("missing comma")
       state.reconsume()
       val = state.consumeValue()
 
+proc consumeInlineTable(state: var TomlParser): TomlValue =
+  result = TomlValue(vt: VALUE_TABLE, t: TomlTable())
+  var key: seq[string]
+  var haskey: bool
+  var val: TomlValue
+  while state.has():
+    let c = state.consume()
+    case c
+    of ' ', '\t', '\n': discard
+    of '}':
+      if val != nil:
+        result.a.add(val)
+      break
+    of ',':
+      if key.len == 0:
+        state.syntaxError("missing key")
+      if val == nil:
+        state.syntaxError("comma without element")
+      var table = result.t
+      for i in 0 ..< key.high:
+        let k = key[i]
+        if k in table.map:
+          state.syntaxError(fmt"invalid re-definition of key {k}")
+        else:
+          let node = TomlTable()
+          table.map[k] = TomlValue(vt: VALUE_TABLE, t: node)
+          table = node
+      let k = key[^1]
+      if k in table.map:
+        state.syntaxError(fmt"invalid re-definition of key {k}")
+      table.map[k] = val
+    else:
+      if val != nil:
+        state.syntaxError("missing comma")
+      if not haskey:
+        key = state.consumeKey()
+        haskey = true
+      else:
+        state.reconsume()
+        val = state.consumeValue()
+
 proc consumeValue(state: var TomlParser): TomlValue =
   while state.has():
     let c = state.consume()
@@ -417,6 +468,8 @@ proc consumeValue(state: var TomlParser): TomlValue =
       #TODO date-time
     of '[':
       return state.consumeArray()
+    of '{':
+      return state.consumeInlineTable()
     elif c.isBare():
       let s = state.consumeBare(c)
       case s
diff --git a/src/display/pager.nim b/src/display/pager.nim
index 06b28346..66280edc 100644
--- a/src/display/pager.nim
+++ b/src/display/pager.nim
@@ -483,7 +483,9 @@ proc applySiteconf(pager: Pager, request: Request) =
       pager.cookiejars[request.url.host] = newCookieJar(request.url)
 
 # Load request in a new buffer.
-proc gotoURL*(pager: Pager, request: Request, prevurl = none(URL), ctype = none(string), replace: Container = nil) =
+proc gotoURL*(pager: Pager, request: Request, prevurl = none(URL),
+              ctype = none(string), replace: Container = nil,
+              redirectdepth = 0) =
   pager.applySiteconf(request)
   if prevurl.isnone or not prevurl.get.equals(request.url, true) or
       request.url.hash == "" or request.httpmethod != HTTP_GET:
@@ -500,7 +502,7 @@ proc gotoURL*(pager: Pager, request: Request, prevurl = none(URL), ctype = none(
       location: request.url
     )
     let cookiejar = pager.cookiejars.getOrDefault(request.url.host)
-    let container = pager.dispatcher.newBuffer(pager.config, source, cookiejar)
+    let container = pager.dispatcher.newBuffer(pager.config, source, cookiejar, redirectdepth = redirectdepth)
     if replace != nil:
       container.replace = replace
       container.copyCursorPos(container.replace)
@@ -711,9 +713,12 @@ proc handleEvent0(pager: Pager, container: Container, event: ContainerEvent): bo
     if pager.container == container:
       pager.authorize()
   of REDIRECT:
-    let redirect = event.location
-    pager.alert("Redirecting to " & $redirect)
-    pager.gotoURL(newRequest(redirect), some(pager.container.source.location), replace = pager.container)
+    if container.redirectdepth < 10:
+      let redirect = event.location
+      pager.alert("Redirecting to " & $redirect)
+      pager.gotoURL(newRequest(redirect), some(container.source.location), replace = container, redirectdepth = container.redirectdepth + 1)
+    else:
+      pager.alert("Error: maximum redirection depth reached")
   of ANCHOR:
     var url2 = newURL(container.source.location)
     url2.hash(event.anchor)