texinfo-commits
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

branch master updated: * js/info.js: Exdent entire body of file (except


From: Gavin D. Smith
Subject: branch master updated: * js/info.js: Exdent entire body of file (except inside a string constant) in attempt to make nested functions more apparent.
Date: Sat, 05 Oct 2024 16:12:29 -0400

This is an automated email from the git hooks/post-receive script.

gavin pushed a commit to branch master
in repository texinfo.

The following commit(s) were added to refs/heads/master by this push:
     new 80cfe6f2aa * js/info.js: Exdent entire body of file (except inside a 
string constant) in attempt to make nested functions more apparent.
80cfe6f2aa is described below

commit 80cfe6f2aa34efcac07b014c93fa9029e2f61430
Author: Gavin Smith <gavinsmith0123@gmail.com>
AuthorDate: Sat Oct 5 21:11:45 2024 +0100

    * js/info.js: Exdent entire body of file (except inside a string
    constant) in attempt to make nested functions more apparent.
---
 ChangeLog  |    5 +
 js/info.js | 4009 ++++++++++++++++++++++++++++++------------------------------
 2 files changed, 2011 insertions(+), 2003 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index cb8a765e2f..8b40d430ae 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,8 @@
+2024-10-05  Gavin Smith <gavinsmith0123@gmail.com>
+
+       * js/info.js: Exdent entire body of file (except inside a string
+       constant) in attempt to make nested functions more apparent.
+
 2024-10-05  Patrice Dumas  <pertusus@free.fr>
 
        * tp/Texinfo/XS/convert/call_html_cxx_function.cpp,
diff --git a/js/info.js b/js/info.js
index 1091f1c394..63ae668e1f 100644
--- a/js/info.js
+++ b/js/info.js
@@ -16,484 +16,487 @@
    You should have received a copy of the GNU General Public License
    along with GNU Texinfo.  If not, see <http://www.gnu.org/licenses/>.  */
 
-(function (features, user_config) {
-  "use strict";
-
-  /*-------------------.
-  | Define constants.  |
-  `-------------------*/
-
-  /** To override those default parameters, define 'INFO_CONFIG' in the global
-      environment before loading this script.  */
-  var config = {
-    EXT: ".html",
-    INDEX_NAME: "index.html",
-    INDEX_ID: "index",
-    CONTENTS_ID: "SEC_Contents",
-    MAIN_ANCHORS: ["Top"],
-    WARNING_TIMEOUT: 3000,
-    SCREEN_MIN_WIDTH: 700,
-    LOCAL_HTML_PAGE_PATTERN: "^([^:/]*[.](html|htm|xhtml))?([#].*)?$",
-    SHOW_SIDEBAR_HTML: '<span class="hide-icon">&#x21db;</span>',
-    HIDE_SIDEBAR_HTML: '<span class="hide-icon">&#x21da;</span><span 
class="hide-text"></span>',
-    SHOW_SIDEBAR_TOOLTIP: 'Show navigation sidebar',
-    HIDE_SIDEBAR_TOOLTIP: 'Hide navigation sidebar',
-
-    // hooks:
-    /** Define a function called after 'DOMContentLoaded' event in
-        the INDEX_NAME context.
-        @type {function (): void}*/
-    on_main_load: null,
-    /** Define a function called after 'DOMContentLoaded' event in
-        the iframe context.
-        @type {(function (): void)} */
-    on_iframe_load: null
-  };
-
-  /*-------------------.
-  | State Management.  |
-  `-------------------*/
-
-  /** A 'store' is an object managing the state of the application and having
-      a dispatch method which accepts actions as parameter.  This method is
-      the only way to update the state.
-      @typedef {function (Action): void} Action_consumer
-      @type {{dispatch: Action_consumer, state?: any, listeners?: any[]}}.  */
-  var store;
-
-  var section_names = [
-    'top', 'chapter', 'unnumbered', 'chapheading', 'appendix',
-    'section', 'unnumberedsec', 'heading', 'appendixsec',
-    'subsection', 'unnumberedsubsec', 'subheading', 'appendixsubsec',
-    'subsubsection', 'unnumberedsubsubsec', 'subsubheading',
-    'appendixsubsubsec' ];
-
-  /** Create a Store that calls its listeners at each state change.
-      @arg {function (Object, Action): Object} reducer
-      @arg {Object} state  */
-  function
-  Store (reducer, state)
-  {
-    this.listeners = [];
-    this.reducer = reducer;
-    this.state = state;
-  }
+# Note that the entire body of this file apart from the first and last
+# lines are within a function call.
 
-  /** @arg {Action} action */
-  Store.prototype.dispatch = function dispatch (action) {
-    var new_state = this.reducer (this.state, action);
-    if (new_state !== this.state)
+(function (features, user_config) {
+"use strict";
+
+/*-------------------.
+| Define constants.  |
+`-------------------*/
+
+/** To override those default parameters, define 'INFO_CONFIG' in the global
+    environment before loading this script.  */
+var config = {
+  EXT: ".html",
+  INDEX_NAME: "index.html",
+  INDEX_ID: "index",
+  CONTENTS_ID: "SEC_Contents",
+  MAIN_ANCHORS: ["Top"],
+  WARNING_TIMEOUT: 3000,
+  SCREEN_MIN_WIDTH: 700,
+  LOCAL_HTML_PAGE_PATTERN: "^([^:/]*[.](html|htm|xhtml))?([#].*)?$",
+  SHOW_SIDEBAR_HTML: '<span class="hide-icon">&#x21db;</span>',
+  HIDE_SIDEBAR_HTML: '<span class="hide-icon">&#x21da;</span><span 
class="hide-text"></span>',
+  SHOW_SIDEBAR_TOOLTIP: 'Show navigation sidebar',
+  HIDE_SIDEBAR_TOOLTIP: 'Hide navigation sidebar',
+
+  // hooks:
+  /** Define a function called after 'DOMContentLoaded' event in
+      the INDEX_NAME context.
+      @type {function (): void}*/
+  on_main_load: null,
+  /** Define a function called after 'DOMContentLoaded' event in
+      the iframe context.
+      @type {(function (): void)} */
+  on_iframe_load: null
+};
+
+/*-------------------.
+| State Management.  |
+`-------------------*/
+
+/** A 'store' is an object managing the state of the application and having
+    a dispatch method which accepts actions as parameter.  This method is
+    the only way to update the state.
+    @typedef {function (Action): void} Action_consumer
+    @type {{dispatch: Action_consumer, state?: any, listeners?: any[]}}.  */
+var store;
+
+var section_names = [
+  'top', 'chapter', 'unnumbered', 'chapheading', 'appendix',
+  'section', 'unnumberedsec', 'heading', 'appendixsec',
+  'subsection', 'unnumberedsubsec', 'subheading', 'appendixsubsec',
+  'subsubsection', 'unnumberedsubsubsec', 'subsubheading',
+  'appendixsubsubsec' ];
+
+/** Create a Store that calls its listeners at each state change.
+    @arg {function (Object, Action): Object} reducer
+    @arg {Object} state  */
+function
+Store (reducer, state)
+{
+  this.listeners = [];
+  this.reducer = reducer;
+  this.state = state;
+}
+
+/** @arg {Action} action */
+Store.prototype.dispatch = function dispatch (action) {
+  var new_state = this.reducer (this.state, action);
+  if (new_state !== this.state)
+    {
+      this.state = new_state;
+      if (window["INFO_DEBUG"])
+        console.log ("state: ", new_state);
+      this.listeners.forEach (function (listener) {
+        listener (new_state);
+      });
+    }
+};
+
+/** Build a store delegate that will forward its actions to a "real"
+    store that can be in a different browsing context.  */
+function
+Remote_store ()
+{
+  /* The browsing context containing the real store.  */
+  this.delegate = window.parent;
+}
+
+/** Dispatch ACTION to the delegate browing context.  This method must be
+    used in conjunction of an event listener on "message" events in the
+    delegate browsing context which must forwards ACTION to an actual store.
+    @arg {Action} action */
+Remote_store.prototype.dispatch = function dispatch (action) {
+  this.delegate.postMessage ({ message_kind: "action", action: action }, "*");
+};
+
+/** @typedef {{type: string, [x: string]: any}} Action - Payloads
+    of information meant to be treated by the store which can receive them
+    using the 'store.dispatch' method.  */
+
+/* Place holder for action creators.  */
+var actions = {
+  /** Return an action that makes LINKID the current page.
+      @arg {string} linkid - link identifier
+      @arg {string|false} [history] - method name that will be applied on
+      the 'window.history' object.  */
+  set_current_url: function (linkid, history, clicked) {
+    if (undef_or_null (history))
+      history = "pushState";
+    return { type: "current-url", url: linkid,
+             history: history, clicked: clicked };
+  },
+
+  /** Set current URL to the node corresponding to POINTER which is an
+      id refering to a linkid (such as "*TOP*" or "*END*"). */
+  set_current_url_pointer: function (pointer) {
+    return { type: "current-url", pointer: pointer, history: "pushState" };
+  },
+
+  /** @arg {string} dir */
+  navigate: function (dir) {
+    return { type: "navigate", direction: dir, history: "pushState" };
+  },
+
+  /** @arg {{[id: string]: string}} links */
+  cache_links: function (links) {
+    return { type: "cache-links", links: links };
+  },
+
+  /** @arg {NodeListOf<Element>} links */
+  cache_index_links: function (links) {
+    var dict = {};
+    var text0 = "", text1 = ""; // for subentries
+    for (var i = 0; i < links.length; i += 1)
       {
-        this.state = new_state;
-        if (window["INFO_DEBUG"])
-          console.log ("state: ", new_state);
-        this.listeners.forEach (function (listener) {
-          listener (new_state);
-        });
+        var link = links[i];
+        var link_cl = link.classList;
+        var text = link.textContent;
+        if (link_cl.contains("index-entry-level-2"))
+          {
+              text = text0 + "; " + text1 + "; " + text;
+          }
+        else if (link_cl.contains("index-entry-level-1"))
+          {
+            text1 = text;
+              text = text0 + "; " + text;
+          }
+        else
+          {
+            text0 = text;
+          }
+
+        if ((link = link.nextSibling)
+            && link.classList.contains("printindex-index-section")
+            && (link = link.firstChild))
+          {
+            dict[text] = href_hash (link_href (link));
+          }
       }
-  };
+    return { type: "cache-index-links", links: dict };
+  },
+
+  /** Show or hide the help screen.  */
+  show_help: function () {
+    return { type: "help", visible: true };
+  },
+
+  /** Make the text input INPUT visible.  If INPUT is a falsy value then
+      hide current text input.
+      @arg {string} input */
+  show_text_input: function (input) {
+    return { type: "input", input: input };
+  },
+
+  /** Hide the current current text input.  */
+  hide_text_input: function () {
+    return { type: "input", input: null };
+  },
+
+  /** @arg {string} msg */
+  warn: function (msg) {
+    return { type: "warning", msg: msg };
+  },
+
+  window_title: function (title) {
+    return { type: "window-title", title: title };
+  },
+
+  /** Search EXP in the whole manual.
+      @arg {RegExp|string} exp*/
+  search: function (exp) {
+    var rgxp;
+    if (typeof exp === "object")
+      rgxp = exp;
+    else if (exp === "")
+      rgxp = null;
+    else
+      rgxp = new RegExp (exp, "i");
 
-  /** Build a store delegate that will forward its actions to a "real"
-      store that can be in a different browsing context.  */
-  function
-  Remote_store ()
-  {
-    /* The browsing context containing the real store.  */
-    this.delegate = window.parent;
+    return { type: "search-init", regexp: rgxp, input: exp };
   }
-
-  /** Dispatch ACTION to the delegate browing context.  This method must be
-      used in conjunction of an event listener on "message" events in the
-      delegate browsing context which must forwards ACTION to an actual store.
-      @arg {Action} action */
-  Remote_store.prototype.dispatch = function dispatch (action) {
-    this.delegate.postMessage ({ message_kind: "action", action: action }, 
"*");
-  };
-
-  /** @typedef {{type: string, [x: string]: any}} Action - Payloads
-      of information meant to be treated by the store which can receive them
-      using the 'store.dispatch' method.  */
-
-  /* Place holder for action creators.  */
-  var actions = {
-    /** Return an action that makes LINKID the current page.
-        @arg {string} linkid - link identifier
-        @arg {string|false} [history] - method name that will be applied on
-        the 'window.history' object.  */
-    set_current_url: function (linkid, history, clicked) {
-      if (undef_or_null (history))
-        history = "pushState";
-      return { type: "current-url", url: linkid,
-               history: history, clicked: clicked };
-    },
-
-    /** Set current URL to the node corresponding to POINTER which is an
-        id refering to a linkid (such as "*TOP*" or "*END*"). */
-    set_current_url_pointer: function (pointer) {
-      return { type: "current-url", pointer: pointer, history: "pushState" };
-    },
-
-    /** @arg {string} dir */
-    navigate: function (dir) {
-      return { type: "navigate", direction: dir, history: "pushState" };
-    },
-
-    /** @arg {{[id: string]: string}} links */
-    cache_links: function (links) {
-      return { type: "cache-links", links: links };
-    },
-
-    /** @arg {NodeListOf<Element>} links */
-    cache_index_links: function (links) {
-      var dict = {};
-      var text0 = "", text1 = ""; // for subentries
-      for (var i = 0; i < links.length; i += 1)
-        {
-          var link = links[i];
-          var link_cl = link.classList;
-          var text = link.textContent;
-          if (link_cl.contains("index-entry-level-2"))
-            {
-                text = text0 + "; " + text1 + "; " + text;
-            }
-          else if (link_cl.contains("index-entry-level-1"))
-            {
-              text1 = text;
-                text = text0 + "; " + text;
-            }
-          else
-            {
-              text0 = text;
-            }
-
-          if ((link = link.nextSibling)
-              && link.classList.contains("printindex-index-section")
-              && (link = link.firstChild))
-            {
-              dict[text] = href_hash (link_href (link));
-            }
-        }
-      return { type: "cache-index-links", links: dict };
-    },
-
-    /** Show or hide the help screen.  */
-    show_help: function () {
-      return { type: "help", visible: true };
-    },
-
-    /** Make the text input INPUT visible.  If INPUT is a falsy value then
-        hide current text input.
-        @arg {string} input */
-    show_text_input: function (input) {
-      return { type: "input", input: input };
-    },
-
-    /** Hide the current current text input.  */
-    hide_text_input: function () {
-      return { type: "input", input: null };
-    },
-
-    /** @arg {string} msg */
-    warn: function (msg) {
-      return { type: "warning", msg: msg };
-    },
-
-    window_title: function (title) {
-      return { type: "window-title", title: title };
-    },
-
-    /** Search EXP in the whole manual.
-        @arg {RegExp|string} exp*/
-    search: function (exp) {
-      var rgxp;
-      if (typeof exp === "object")
-        rgxp = exp;
-      else if (exp === "")
-        rgxp = null;
-      else
-        rgxp = new RegExp (exp, "i");
-
-      return { type: "search-init", regexp: rgxp, input: exp };
-    }
-  };
-
-  /** Update STATE based on the type of ACTION.  This update is purely
-      fonctional since STATE is not modified in place and a new state object
-      is returned instead.
-      @arg {Object} state
-      @arg {Action} action
-      @return {Object} a new state */
-  /* eslint-disable complexity */
-  /* A "big" switch has been preferred over splitting each case in a separate
-     function combined with a dictionary lookup.  */
-  function
-  updater (state, action)
-  {
-    var res = Object.assign ({}, state, { action: action });
-    var linkid;
-    switch (action.type)
+};
+
+/** Update STATE based on the type of ACTION.  This update is purely
+    fonctional since STATE is not modified in place and a new state object
+    is returned instead.
+    @arg {Object} state
+    @arg {Action} action
+    @return {Object} a new state */
+/* eslint-disable complexity */
+/* A "big" switch has been preferred over splitting each case in a separate
+   function combined with a dictionary lookup.  */
+function
+updater (state, action)
+{
+  var res = Object.assign ({}, state, { action: action });
+  var linkid;
+  switch (action.type)
+    {
+    case "cache-links":
       {
-      case "cache-links":
-        {
-          var nodes = Object.assign ({}, state.loaded_nodes);
-          Object
-            .keys (action.links)
-            .forEach (function (key) {
-              if (typeof action.links[key] === "object")
-                nodes[key] = Object.assign ({}, nodes[key], action.links[key]);
-              else
-                nodes[key] = action.links[key];
-            });
-
-          return Object.assign (res, { loaded_nodes: nodes });
-        }
-      case "cache-index-links":
-        {
-          // Initially res.index is undefined, which is ignored.
-          res.index = Object.assign ({}, res.index, action.links);
-          return res;
-        }
-      case "section":
-        {
-          res.section_hash = action.section_hash;
-          return res;
-        }
-      case "current-url":
-        {
-          if (document.body.getAttribute("show-sidebar") == "yes"
-              && is_narrow_window ()
-              && action.clicked === "in-page")
-            return state;
-          linkid = (action.pointer) ?
-              state.loaded_nodes[action.pointer] : action.url;
-
-          res.current = linkid;
-          res.section_hash = null;
-          res.history = action.history;
-          res.text_input = null;
-          res.warning = null;
-          res.window_title = null;
-          res.help = false;
-          res.focus = false;
-          res.highlight = null;
-          res.loaded_nodes = Object.assign ({}, res.loaded_nodes);
-          res.loaded_nodes[linkid] = res.loaded_nodes[linkid] || {};
-          return res;
-        }
-      case "unfocus":
-        {
-          if (!res.focus)
-            return state;
-          else
-            {
-              res.help = false;
-              res.text_input = null;
-              res.focus = false;
-              return res;
-            }
-        }
-      case "help":
-        {
-          res.help = action.visible;
-          res.focus = true;
-          return res;
-        }
-      case "show-sidebar": // "yes" (show), "no" (hide) or "hide-if-narrow"
-        {
-          res.show_sidebar = action.show === "hide-if-narrow" && 
res.show_sidebar === "no" ? "no" : action.show;
-          return res;
-        }
-      case "navigate":
-        {
-          var current = state.current;
-          var link = linkid_split (state.current);
+        var nodes = Object.assign ({}, state.loaded_nodes);
+        Object
+          .keys (action.links)
+          .forEach (function (key) {
+            if (typeof action.links[key] === "object")
+              nodes[key] = Object.assign ({}, nodes[key], action.links[key]);
+            else
+              nodes[key] = action.links[key];
+          });
 
-          /* Handle inner 'config.INDEX_NAME' anchors specially.  */
-          if (link.pageid === config.INDEX_ID)
-            current = config.INDEX_ID;
+        return Object.assign (res, { loaded_nodes: nodes });
+      }
+    case "cache-index-links":
+      {
+        // Initially res.index is undefined, which is ignored.
+        res.index = Object.assign ({}, res.index, action.links);
+        return res;
+      }
+    case "section":
+      {
+        res.section_hash = action.section_hash;
+        return res;
+      }
+    case "current-url":
+      {
+        if (document.body.getAttribute("show-sidebar") == "yes"
+            && is_narrow_window ()
+            && action.clicked === "in-page")
+          return state;
+        linkid = (action.pointer) ?
+            state.loaded_nodes[action.pointer] : action.url;
+
+        res.current = linkid;
+        res.section_hash = null;
+        res.history = action.history;
+        res.text_input = null;
+        res.warning = null;
+        res.window_title = null;
+        res.help = false;
+        res.focus = false;
+        res.highlight = null;
+        res.loaded_nodes = Object.assign ({}, res.loaded_nodes);
+        res.loaded_nodes[linkid] = res.loaded_nodes[linkid] || {};
+        return res;
+      }
+    case "unfocus":
+      {
+        if (!res.focus)
+          return state;
+        else
+          {
+            res.help = false;
+            res.text_input = null;
+            res.focus = false;
+            return res;
+          }
+      }
+    case "help":
+      {
+        res.help = action.visible;
+        res.focus = true;
+        return res;
+      }
+    case "show-sidebar": // "yes" (show), "no" (hide) or "hide-if-narrow"
+      {
+        res.show_sidebar = action.show === "hide-if-narrow" && 
res.show_sidebar === "no" ? "no" : action.show;
+        return res;
+      }
+    case "navigate":
+      {
+        var current = state.current;
+        var link = linkid_split (state.current);
 
-          var ids = state.loaded_nodes[current];
-          linkid = ids[action.direction];
-          if (!linkid)
-            {
-              // Look for sibling link in ToC.
-              // Needed for (say) @subsection without corresponding @node.
-              let toc_current =
-                document.querySelector ('#slider a[toc-current="yes"]');
-              if (toc_current)
-                {
-                  let item_current = toc_current.parentNode; // 'li' element
-                  let nlink = (action.direction === "next"
-                               ? item_current.nextElementSibling
-                               : action.direction === "prev"
-                               ? item_current.previousElementSibling
-                               : action.direction === "up"
-                               ? item_current.parentNode.parentNode
-                               : null);
-                  if (nlink)
-                    nlink = nlink.firstElementChild;
-                  if (nlink && nlink.matches("a"))
-                    {
-                      let href = link_href (nlink)
-                      if (href)
-                        linkid = href_hash (href) ;
-                    }
-                }
-            }
-          if (!linkid)
-            {
-              /* When CURRENT is in index but doesn't have the requested
-                 direction, ask its corresponding 'pageid'.  */
-              var is_index_ref =
-                Object.keys (state.index)
-                      .reduce (function (acc, val) {
-                        return acc || state.index[val] === current;
-                      }, false);
-              if (is_index_ref)
-                {
-                  ids = state.loaded_nodes[link.pageid];
-                  linkid = ids[action.direction];
-                }
-            }
+        /* Handle inner 'config.INDEX_NAME' anchors specially.  */
+        if (link.pageid === config.INDEX_ID)
+          current = config.INDEX_ID;
 
-          if (!linkid)
-            return state;
-          else
-            {
-              res.current = linkid;
-              res.section_hash = null;
-              res.history = action.history;
-              res.text_input = null;
-              res.warning = null;
-              res.window_title = null;
-              res.help = false;
-              res.highlight = null;
-              res.focus = false;
-              res.loaded_nodes = Object.assign ({}, res.loaded_nodes);
-              res.loaded_nodes[action.url] = res.loaded_nodes[action.url] || 
{};
-              return res;
-            }
-        }
-      case "search-init":
-        {
-          res.search = {
-            regexp: action.regexp || state.search.regexp,
-            input: action.input || state.search.input,
-            status: "ready",
-            current_pageid: linkid_split (state.current).pageid,
-            found: false
-          };
-          res.focus = false;
-          res.help = false;
-          res.text_input = null;
-          res.warning = null;
-          return res;
-        }
-      case "search-query":
-        {
-          res.search = Object.assign ({}, state.search);
-          res.search.status = "searching";
-          return res;
-        }
-      case "search-result":
-        {
-          res.search = Object.assign ({}, state.search);
-          if (action.found)
-            {
-              res.search.status = "done";
-              res.search.found = true;
-              res.current = res.search.current_pageid;
-              res.section_hash = null;
-              res.history = "pushState";
-              res.highlight = res.search.regexp;
-            }
-          else
-            {
-              var fwd = forward_pageid (state, state.search.current_pageid);
-              if (fwd === null)
-                {
-                  res.search.status = "done";
-                  res.warning = "Search failed: \"" + res.search.input + "\"";
-                  res.highlight = null;
-                }
-              else
-                {
-                  res.search.current_pageid = fwd;
-                  res.search.status = "ready";
-                }
-            }
-          return res;
-        }
-      case "input":
-        {
-          var needs_update = (state.text_input && !action.input)
-              || (!state.text_input && action.input)
-              || (state.text_input && action.input
-                  && state.text_input !== action.input);
+        var ids = state.loaded_nodes[current];
+        linkid = ids[action.direction];
+        if (!linkid)
+          {
+            // Look for sibling link in ToC.
+            // Needed for (say) @subsection without corresponding @node.
+            let toc_current =
+              document.querySelector ('#slider a[toc-current="yes"]');
+            if (toc_current)
+              {
+                let item_current = toc_current.parentNode; // 'li' element
+                let nlink = (action.direction === "next"
+                             ? item_current.nextElementSibling
+                             : action.direction === "prev"
+                             ? item_current.previousElementSibling
+                             : action.direction === "up"
+                             ? item_current.parentNode.parentNode
+                             : null);
+                if (nlink)
+                  nlink = nlink.firstElementChild;
+                if (nlink && nlink.matches("a"))
+                  {
+                    let href = link_href (nlink)
+                    if (href)
+                      linkid = href_hash (href) ;
+                  }
+              }
+          }
+        if (!linkid)
+          {
+            /* When CURRENT is in index but doesn't have the requested
+               direction, ask its corresponding 'pageid'.  */
+            var is_index_ref =
+              Object.keys (state.index)
+                    .reduce (function (acc, val) {
+                      return acc || state.index[val] === current;
+                    }, false);
+            if (is_index_ref)
+              {
+                ids = state.loaded_nodes[link.pageid];
+                linkid = ids[action.direction];
+              }
+          }
 
-          if (!needs_update)
-            return state;
-          else
-            {
-              res.focus = Boolean (action.input);
-              res.help = false;
-              res.text_input = action.input;
-              res.warning = null;
-              return res;
-            }
-        }
-      case "iframe-ready":
-        {
-          res.ready = Object.assign ({}, res.ready);
-          res.ready[action.id] = true;
-          return res;
-        }
-      case "echo":
-        {
-          res.echo = action.msg;
-          return res;
-        }
-      case "window-title":
-        {
-          res.window_title = action.title;
-          return res;
-        }
-      case "warning":
-        {
-          res.warning = action.msg;
-          if (action.msg !== null)
+        if (!linkid)
+          return state;
+        else
+          {
+            res.current = linkid;
+            res.section_hash = null;
+            res.history = action.history;
             res.text_input = null;
-          return res;
-        }
-      default:
-        console.error ("no reducer for action type:", action.type);
-        return state;
+            res.warning = null;
+            res.window_title = null;
+            res.help = false;
+            res.highlight = null;
+            res.focus = false;
+            res.loaded_nodes = Object.assign ({}, res.loaded_nodes);
+            res.loaded_nodes[action.url] = res.loaded_nodes[action.url] || {};
+            return res;
+          }
       }
-  }
-  /* eslint-enable complexity */
+    case "search-init":
+      {
+        res.search = {
+          regexp: action.regexp || state.search.regexp,
+          input: action.input || state.search.input,
+          status: "ready",
+          current_pageid: linkid_split (state.current).pageid,
+          found: false
+        };
+        res.focus = false;
+        res.help = false;
+        res.text_input = null;
+        res.warning = null;
+        return res;
+      }
+    case "search-query":
+      {
+        res.search = Object.assign ({}, state.search);
+        res.search.status = "searching";
+        return res;
+      }
+    case "search-result":
+      {
+        res.search = Object.assign ({}, state.search);
+        if (action.found)
+          {
+            res.search.status = "done";
+            res.search.found = true;
+            res.current = res.search.current_pageid;
+            res.section_hash = null;
+            res.history = "pushState";
+            res.highlight = res.search.regexp;
+          }
+        else
+          {
+            var fwd = forward_pageid (state, state.search.current_pageid);
+            if (fwd === null)
+              {
+                res.search.status = "done";
+                res.warning = "Search failed: \"" + res.search.input + "\"";
+                res.highlight = null;
+              }
+            else
+              {
+                res.search.current_pageid = fwd;
+                res.search.status = "ready";
+              }
+          }
+        return res;
+      }
+    case "input":
+      {
+        var needs_update = (state.text_input && !action.input)
+            || (!state.text_input && action.input)
+            || (state.text_input && action.input
+                && state.text_input !== action.input);
 
-  /*-----------------------.
-  | Context initializers.  |
-  `-----------------------*/
+        if (!needs_update)
+          return state;
+        else
+          {
+            res.focus = Boolean (action.input);
+            res.help = false;
+            res.text_input = action.input;
+            res.warning = null;
+            return res;
+          }
+      }
+    case "iframe-ready":
+      {
+        res.ready = Object.assign ({}, res.ready);
+        res.ready[action.id] = true;
+        return res;
+      }
+    case "echo":
+      {
+        res.echo = action.msg;
+        return res;
+      }
+    case "window-title":
+      {
+        res.window_title = action.title;
+        return res;
+      }
+    case "warning":
+      {
+        res.warning = action.msg;
+        if (action.msg !== null)
+          res.text_input = null;
+        return res;
+      }
+    default:
+      console.error ("no reducer for action type:", action.type);
+      return state;
+    }
+}
+/* eslint-enable complexity */
+
+/*-----------------------.
+| Context initializers.  |
+`-----------------------*/
+
+/** Initialize the index page of the manual which manages the state of the
+    application.  */
+function
+init_index_page ()
+{
+  /*--------------------------.
+  | Component for help page.  |
+  `--------------------------*/
 
-  /** Initialize the index page of the manual which manages the state of the
-      application.  */
   function
-  init_index_page ()
+  Help_page ()
   {
-    /*--------------------------.
-    | Component for help page.  |
-    `--------------------------*/
-
-    function
-    Help_page ()
-    {
-      this.id = "help-screen";
-      /* Create help div element.*/
-      var div = document.createElement ("div");
-      div.setAttribute ("hidden", "true");
-      div.classList.add ("modal");
-      div.innerHTML = "\
+    this.id = "help-screen";
+    /* Create help div element.*/
+    var div = document.createElement ("div");
+    div.setAttribute ("hidden", "true");
+    div.classList.add ("modal");
+    div.innerHTML = "\
 <div class=\"modal-content\">\
 <span class=\"close\">&times;</span>\
 <h2>Keyboard Shortcuts</h2>\
@@ -528,1728 +531,1728 @@
 </table>\
 </div>\
 ";
-      var span = div.querySelector (".close");
-      div.addEventListener ("click", function (event) {
-        if (event.target === span || event.target === div)
-          store.dispatch ({ type: "unfocus" });
-      }, false);
-
-      this.element = div;
-    }
-
-    Help_page.prototype.render = function render (state) {
-      if (!state.help)
-        this.element.style.display = "none";
-      else
-        {
-          this.element.style.display = "block";
-          this.element.focus ();
-        }
-    };
+    var span = div.querySelector (".close");
+    div.addEventListener ("click", function (event) {
+      if (event.target === span || event.target === div)
+        store.dispatch ({ type: "unfocus" });
+    }, false);
 
-    /*---------------------------------.
-    | Components for menu navigation.  |
-    `---------------------------------*/
+    this.element = div;
+  }
 
-    /** @arg {string} id */
-    function
-    Search_input (id)
-    {
-      this.id = id;
-      this.render = null;
-      this.prompt = document.createTextNode ("Text search" + ": ");
+  Help_page.prototype.render = function render (state) {
+    if (!state.help)
+      this.element.style.display = "none";
+    else
+      {
+        this.element.style.display = "block";
+        this.element.focus ();
+      }
+  };
 
-      /* Create input div element.*/
-      var div = document.createElement ("div");
-      div.setAttribute ("hidden", "true");
-      div.appendChild (this.prompt);
-      this.element = div;
-
-      /* Create input element.*/
-      var input = document.createElement ("input");
-      input.setAttribute ("type", "search");
-      this.input = input;
-      this.element.appendChild (input);
-
-      /* Define a special key handler when 'this.input' is focused and
-         visible.*/
-      this.input.addEventListener ("keydown", (function (event) {
-        if (is_escape_key (event.key))
-          store.dispatch ({ type: "unfocus" });
-        else if (event.key === "Enter")
-          store.dispatch (actions.search (this.input.value));
-        event.stopPropagation ();
-      }).bind (this));
-    }
+  /*---------------------------------.
+  | Components for menu navigation.  |
+  `---------------------------------*/
 
-    Search_input.prototype.show = function show () {
-      /* Display previous search. */
-      var search = store.state.search;
-      var input = search && (search.status === "done") && search.input;
-      if (input)
-        this.prompt.textContent = "Text search (default " + input + "): ";
-      this.element.removeAttribute ("hidden");
-      this.input.focus ();
-    };
+  /** @arg {string} id */
+  function
+  Search_input (id)
+  {
+    this.id = id;
+    this.render = null;
+    this.prompt = document.createTextNode ("Text search" + ": ");
 
-    Search_input.prototype.hide = function hide () {
-      this.element.setAttribute ("hidden", "true");
-      this.input.value = "";
-    };
+    /* Create input div element.*/
+    var div = document.createElement ("div");
+    div.setAttribute ("hidden", "true");
+    div.appendChild (this.prompt);
+    this.element = div;
+
+    /* Create input element.*/
+    var input = document.createElement ("input");
+    input.setAttribute ("type", "search");
+    this.input = input;
+    this.element.appendChild (input);
+
+    /* Define a special key handler when 'this.input' is focused and
+       visible.*/
+    this.input.addEventListener ("keydown", (function (event) {
+      if (is_escape_key (event.key))
+        store.dispatch ({ type: "unfocus" });
+      else if (event.key === "Enter")
+        store.dispatch (actions.search (this.input.value));
+      event.stopPropagation ();
+    }).bind (this));
+  }
 
-    /** @arg {string} id */
-    function
-    Text_input (id)
-    {
-      this.id = id;
-      this.data = null;
-      this.datalist = null;
-      this.render = null;
+  Search_input.prototype.show = function show () {
+    /* Display previous search. */
+    var search = store.state.search;
+    var input = search && (search.status === "done") && search.input;
+    if (input)
+      this.prompt.textContent = "Text search (default " + input + "): ";
+    this.element.removeAttribute ("hidden");
+    this.input.focus ();
+  };
 
-      /* Create input div element.*/
-      var div = document.createElement ("div");
-      div.setAttribute ("hidden", "true");
-      div.appendChild (document.createTextNode (id + ": "));
-      this.element = div;
-
-      /* Create input element.*/
-      var input = document.createElement ("input");
-      input.setAttribute ("type", "search");
-      input.setAttribute ("list", id + "-data");
-      this.input = input;
-      this.element.appendChild (input);
-
-      /* Define a special key handler when 'this.input' is focused and
-         visible.*/
-      this.input.addEventListener ("keydown", (function (event) {
-        if (is_escape_key (event.key))
-          store.dispatch ({ type: "unfocus" });
-        else if (event.key === "Enter")
-          {
-            var linkid = this.data[this.input.value];
-            if (linkid)
-              {
-                hide_sidebar_if_narrow ();
-                store.dispatch (actions.set_current_url (linkid));
-              }
-          }
-        event.stopPropagation ();
-      }).bind (this));
-    }
+  Search_input.prototype.hide = function hide () {
+    this.element.setAttribute ("hidden", "true");
+    this.input.value = "";
+  };
 
-    /* Display a text input for searching through DATA.*/
-    Text_input.prototype.show = function show (data) {
-      if (!features || features.datalistelem)
-        {
-          var datalist = create_datalist (data);
-          datalist.setAttribute ("id", this.id + "-data");
-          this.data = data;
-          this.datalist = datalist;
-          this.element.appendChild (datalist);
-        }
-      this.element.removeAttribute ("hidden");
-      this.input.focus ();
-    };
+  /** @arg {string} id */
+  function
+  Text_input (id)
+  {
+    this.id = id;
+    this.data = null;
+    this.datalist = null;
+    this.render = null;
 
-    Text_input.prototype.hide = function hide () {
-      this.element.setAttribute ("hidden", "true");
-      this.input.value = "";
-      if (this.datalist)
+    /* Create input div element.*/
+    var div = document.createElement ("div");
+    div.setAttribute ("hidden", "true");
+    div.appendChild (document.createTextNode (id + ": "));
+    this.element = div;
+
+    /* Create input element.*/
+    var input = document.createElement ("input");
+    input.setAttribute ("type", "search");
+    input.setAttribute ("list", id + "-data");
+    this.input = input;
+    this.element.appendChild (input);
+
+    /* Define a special key handler when 'this.input' is focused and
+       visible.*/
+    this.input.addEventListener ("keydown", (function (event) {
+      if (is_escape_key (event.key))
+        store.dispatch ({ type: "unfocus" });
+      else if (event.key === "Enter")
         {
-          this.datalist.remove ();
-          this.datalist = null;
+          var linkid = this.data[this.input.value];
+          if (linkid)
+            {
+              hide_sidebar_if_narrow ();
+              store.dispatch (actions.set_current_url (linkid));
+            }
         }
-    };
-
-    function
-    Minibuffer ()
-    {
-      /* Create global container.*/
-      var elem = document.createElement ("div");
-      elem.classList.add ("text-input");
+      event.stopPropagation ();
+    }).bind (this));
+  }
 
-      var menu = new Text_input ("menu");
-      menu.render = function (state) {
-        if (state.text_input === "menu")
-        {
-          var current_menu = state.loaded_nodes[state.current].menu;
-          if (current_menu)
-            this.show (current_menu);
-          else
-            store.dispatch (actions.warn ("No menu in this node"));
-        }
-      };
+  /* Display a text input for searching through DATA.*/
+  Text_input.prototype.show = function show (data) {
+    if (!features || features.datalistelem)
+      {
+        var datalist = create_datalist (data);
+        datalist.setAttribute ("id", this.id + "-data");
+        this.data = data;
+        this.datalist = datalist;
+        this.element.appendChild (datalist);
+      }
+    this.element.removeAttribute ("hidden");
+    this.input.focus ();
+  };
 
-      var index = new Text_input ("index");
-      index.render = function (state) {
-        if (state.text_input === "index")
-        {
-          if (state.index)
-            this.show (state.index);
-          else
-            store.dispatch (actions.warn ("No index in this document"))
-        }
-      };
+  Text_input.prototype.hide = function hide () {
+    this.element.setAttribute ("hidden", "true");
+    this.input.value = "";
+    if (this.datalist)
+      {
+        this.datalist.remove ();
+        this.datalist = null;
+      }
+  };
 
-      var search = new Search_input ("regexp-search");
-      search.render = function (state) {
-        if (state.text_input === "regexp-search")
-          this.show ();
-      };
+  function
+  Minibuffer ()
+  {
+    /* Create global container.*/
+    var elem = document.createElement ("div");
+    elem.classList.add ("text-input");
 
-      elem.appendChild (menu.element);
-      elem.appendChild (index.element);
-      elem.appendChild (search.element);
-
-      /* Create a container for warning when no menu in current page.*/
-      var warn$ = document.createElement ("div");
-      warn$.setAttribute ("hidden", "true");
-      elem.appendChild (warn$);
-
-      this.element = elem;
-      this.menu = menu;
-      this.index = index;
-      this.search = search;
-      this.warn = warn$;
-      this.toid = null;
-    }
+    var menu = new Text_input ("menu");
+    menu.render = function (state) {
+      if (state.text_input === "menu")
+      {
+        var current_menu = state.loaded_nodes[state.current].menu;
+        if (current_menu)
+          this.show (current_menu);
+        else
+          store.dispatch (actions.warn ("No menu in this node"));
+      }
+    };
 
-    Minibuffer.prototype.render = function render (state) {
-      if (!state.warning)
-        {
-          this.warn.setAttribute ("hidden", "true");
-          this.toid = null;
-        }
-      else if (!this.toid)
-        {
-          console.warn (state.warning);
-          var toid = window.setTimeout (function () {
-            store.dispatch ({ type: "warning", msg: null });
-          }, config.WARNING_TIMEOUT);
-          this.warn.innerHTML = state.warning;
-          this.warn.removeAttribute ("hidden");
-          this.toid = toid;
-        }
+    var index = new Text_input ("index");
+    index.render = function (state) {
+      if (state.text_input === "index")
+      {
+        if (state.index)
+          this.show (state.index);
+        else
+          store.dispatch (actions.warn ("No index in this document"))
+      }
+    };
 
-      if (!state.text_input || state.warning)
-        {
-          this.menu.hide ();
-          this.index.hide ();
-          this.search.hide ();
-        }
-      else
-        {
-          this.index.render (state);
-          this.menu.render (state);
-          this.search.render (state);
-        }
+    var search = new Search_input ("regexp-search");
+    search.render = function (state) {
+      if (state.text_input === "regexp-search")
+        this.show ();
     };
 
-    function
-    Echo_area ()
-    {
-      var elem = document.createElement ("div");
-      elem.classList.add ("echo-area");
-      elem.setAttribute ("hidden", "true");
-
-      this.element = elem;
-      this.toid = null;
-    }
-
-    Echo_area.prototype.render = function render (state) {
-      if (!state.echo)
-        {
-          this.element.setAttribute ("hidden", "true");
-          this.toid = null;
-        }
-      else if (!this.toid)
-        {
-          console.info (state.echo);
-          var toid = window.setTimeout (function () {
-            store.dispatch ({ type: "echo", msg: null });
-          }, config.WARNING_TIMEOUT);
-          this.element.innerHTML = state.echo;
-          this.element.removeAttribute ("hidden");
-          this.toid = toid;
-        }
-    };
-
-    /*----------------------------.
-    | Component for the sidebar.  |
-    `----------------------------*/
-
-    function
-    Sidebar (contents_node)
-    {
-      this.element = document.createElement ("div"); // FIXME unneeded?
-      this.element.setAttribute ("id", "slider");
-      var div = document.createElement ("div");
-      div.classList.add ("toc-sidebar");
-      var toc = document.querySelector ("#SEC_Contents");
-      toc.remove ();
+    elem.appendChild (menu.element);
+    elem.appendChild (index.element);
+    elem.appendChild (search.element);
+
+    /* Create a container for warning when no menu in current page.*/
+    var warn$ = document.createElement ("div");
+    warn$.setAttribute ("hidden", "true");
+    elem.appendChild (warn$);
+
+    this.element = elem;
+    this.menu = menu;
+    this.index = index;
+    this.search = search;
+    this.warn = warn$;
+    this.toid = null;
+  }
 
-      // Like n.cloneNode, but also copy _href
-      function cloneNode(n)
+  Minibuffer.prototype.render = function render (state) {
+    if (!state.warning)
       {
-        let r = n.cloneNode(false);
-        for (let c = n.firstChild; c; c = c.nextSibling)
-          {
-            let d = cloneNode(c);
-            let h = c._href;
-            if (h)
-              d._href = h;
-            r.appendChild(d);
-          }
-        return r;
+        this.warn.setAttribute ("hidden", "true");
+        this.toid = null;
+      }
+    else if (!this.toid)
+      {
+        console.warn (state.warning);
+        var toid = window.setTimeout (function () {
+          store.dispatch ({ type: "warning", msg: null });
+        }, config.WARNING_TIMEOUT);
+        this.warn.innerHTML = state.warning;
+        this.warn.removeAttribute ("hidden");
+        this.toid = toid;
       }
-      contents_node.appendChild(cloneNode(toc));
-
-      /* Remove table of contents header.  */
-      toc = toc.querySelector(".contents"); // skip ToC header
-
-      /* Move contents of <body> into a a fresh <div> to let the components
-         treat the index page like other iframe page.  */
-      var nav = document.createElement ("nav");
-      nav.classList.add ("contents");
-      for (var ch = toc.firstChild; ch; ch = toc.firstChild)
-        nav.appendChild (ch);
-
-      var div$ = document.createElement ("div");
-      div$.classList.add ("toc");
-      div$.appendChild (nav);
-      div.appendChild (div$);
-      this.element.appendChild (div);
-
-      let header = document.createElement ("header");
-      div$.parentElement.insertBefore (header, div$);
-      let hider = document.createElement ("button");
-      hider.classList.add ("sidebar-hider");
-      hider.innerHTML = config.HIDE_SIDEBAR_HTML;
-      this.show_sidebar_button = hider;
-      header.appendChild(hider);
-    }
-
-    /* Render 'sidebar' according to STATE which is a new state. */
-    Sidebar.prototype.render = function render (state) {
-      /* Update sidebar to highlight the title corresponding to
-         'state.current'.*/
-      let currently_showing = document.body.getAttribute("show-sidebar");
-      let show = state.show_sidebar;
-      if (show == "hide-if-narrow")
-        show = is_narrow_window() || currently_showing == "no" ? "no" : "yes";
-      if (show === undefined)
-        show = "yes";
-      if (show !== currently_showing)
-        {
-          document.body.setAttribute("show-sidebar", show);
-          this.show_sidebar_button.innerHTML = show == "yes" ? 
config.HIDE_SIDEBAR_HTML : config.SHOW_SIDEBAR_HTML;
-          let tooltip = show == "yes" ? config.HIDE_SIDEBAR_TOOLTIP : 
config.SHOW_SIDEBAR_TOOLTIP;
-          if (tooltip)
-            this.show_sidebar_button.setAttribute("title", tooltip);
-          else
-            this.show_sidebar_button.removeAttribute("title");
-        }
-      var msg = { message_kind: "update-sidebar", selected: state.current, 
section_hash: state.section_hash };
-      window.postMessage (msg, "*");
-    };
-
-    /*--------------------------.
-    | Component for the pages.  |
-    `--------------------------*/
-
-    /** @arg {HTMLDivElement} index_div */
-    function
-    Pages (index_div)
-    {
-      index_div.setAttribute ("id", config.INDEX_ID);
-      index_div.setAttribute ("node", config.INDEX_ID);
-      index_div.setAttribute ("hidden", "true");
-      this.element = document.createElement ("div");
-      this.element.setAttribute ("id", "sub-pages");
-      this.element.appendChild (index_div);
-      /** @type {string[]} Currently created divs.  */
-      this.ids = [config.INDEX_ID];
-      /** @type {string} */
-      this.prev_id = null;
-      /** @type {HTMLElement} */
-      this.prev_div = null;
-      this.prev_search = null;
-      this.main_title = document.title;
-    }
-
-    Pages.prototype.add_div = function add_div (pageid) {
-      var div = document.createElement ("div");
-      div.setAttribute ("id", pageid);
-      div.setAttribute ("node", pageid);
-      div.setAttribute ("hidden", "true");
-      this.ids.push (pageid);
-      this.element.appendChild (div);
-      if (linkid_contains_index (pageid))
-        load_page (pageid);
-      return div;
-    };
-
-    Pages.prototype.render = function render (state) {
-      var that = this;
-
-      /* Create div elements for pages corresponding to newly added
-         linkids from 'state.loaded_nodes'.*/
-      Object.keys (state.loaded_nodes)
-            .map (function (id) { return id.replace (/\..*$/, ""); })
-            .filter (function (id) { return !that.ids.includes (id); })
-            .reduce (function (acc, id) {
-              return ((acc.includes (id)) ? acc : acc.concat ([id]));
-            }, [])
-            .forEach (function (id) { return that.add_div (id); });
-
-      /* Blur pages if help screen is on.  */
-      this.element.classList[(state.help) ? "add" : "remove"] ("blurred");
-      let div = this.prev_div;
-      if (state.current !== this.prev_id)
-        {
-          if (this.prev_id)
-            {
-              this.prev_div.setAttribute ("hidden", "true");
-              /* Remove previous highlights.  */
-              var old = linkid_split (this.prev_id);
-              var msg = { message_kind: "highlight", regexp: null };
-              post_message (old.pageid, msg);
-            }
-          div = resolve_page (state.current, true);
-          update_history (state.current, state.history);
-          this.prev_id = state.current;
-          this.prev_div = div;
-        }
-        if (state.action.type === "window-title") {
-            div.setAttribute("title", state.action.title);
-        }
-        let title = div.getAttribute("title") || this.main_title;
-        if (title && document.title !== title)
-            document.title = title;
-
-      if (state.search
-          && (this.prev_search !== state.search)
-          && state.search.status === "ready")
-        {
-          this.prev_search = state.search;
-          if (state.search.current_pageid === config.INDEX_ID)
-            {
-              window.setTimeout (function () {
-                store.dispatch ({ type: "search-query" });
-                var res = search (document.getElementById (config.INDEX_ID),
-                                  state.search.regexp);
-                store.dispatch ({ type: "search-result", found: res });
-              }, 0);
-            }
-          else
-            {
-              window.setTimeout (function () {
-                store.dispatch ({ type: "search-query" });
-                var msg = {
-                  message_kind: "search",
-                  regexp: state.search.regexp,
-                  id: state.search.current_pageid
-                };
-                post_message (state.search.current_pageid, msg);
-              }, 0);
-            }
-        }
-
-      /* Update highlight of current page.  */
-      if (!state.highlight)
-        {
-          if (state.current === config.INDEX_ID)
-            remove_highlight (document.getElementById (config.INDEX_ID));
-          else
-            {
-              var link = linkid_split (state.current);
-              var msg$ = { message_kind: "highlight", regexp: null };
-              post_message (link.pageid, msg$);
-            }
-        }
-
-      /* Scroll to highlighted search result. */
-      if (state.search
-          && (this.prev_search !== state.search)
-          && state.search.status === "done"
-          && state.search.found === true)
-        {
-          var link$ = linkid_split (state.current);
-          var msg$$ = { message_kind: "scroll-to", hash: "#search-result" };
-          post_message (link$.pageid, msg$$);
-          this.prev_search = state.search;
-        }
-    };
-
-    /*--------------.
-    | Utilitaries.  |
-    `--------------*/
-
-    /** Load PAGEID.
-        @arg {string} pageid */
-    function
-    load_page (pageid)
-    {
-      var div = resolve_page (pageid, false);
-      /* Making the iframe visible triggers the load of the iframe DOM.  */
-      if (div.hasAttribute ("hidden"))
-        {
-          div.removeAttribute ("hidden");
-          div.setAttribute ("hidden", "true");
-        }
-    }
-
-    /** Return the div element that correspond to LINKID.  If VISIBLE is true
-        then display the div and position the corresponding iframe to the
-        anchor specified by LINKID.
-        @arg {string} linkid - link identifier
-        @arg {boolean} [visible]
-        @return {HTMLElement} the div element.  */
-    function
-    resolve_page (linkid, visible)
-    {
-      var msg;
-      var link = linkid_split (linkid);
-      var pageid = link.pageid;
-      var div = document.getElementById (link.pageid);
-      if (!div)
-        {
-          msg = "no iframe container correspond to identifier: " + pageid;
-          throw new ReferenceError (msg);
-        }
-
-      /* Create iframe if necessary unless the div is refering to the Index
-         or Contents page.  */
-      if (pageid === config.INDEX_ID || pageid === config.CONTENTS_ID)
-        {
-          div.removeAttribute ("hidden");
-          /* Unlike iframes, Elements are unlikely to be scrollable (CSSOM
-             Scroll-behavior), so choose an arbitrary element inside "index"
-             div and at the top of it.  */
-          document.getElementById ("icon-bar").scrollIntoView ();
-        }
-      else
-        {
-          var iframe = div.querySelector ("iframe");
-          if (!iframe)
-            {
-              iframe = document.createElement ("iframe");
-              iframe.classList.add ("node");
-              iframe.setAttribute ("src", linkid_to_url (pageid));
-              div.appendChild (iframe);
-              iframe.addEventListener ("load", function () {
-                store.dispatch ({ type: "iframe-ready", id: pageid });
-              }, false);
-            }
-          if (visible)
-            {
-              div.removeAttribute ("hidden");
-              msg = { message_kind: "scroll-to", hash: link.hash };
-              post_message (pageid, msg);
-            }
-        }
-
-      return div;
-    }
-
-    /** Create a datalist element containing option elements corresponding
-        to the keys in MENU.  */
-    function
-    create_datalist (menu)
-    {
-      var datalist = document.createElement ("datalist");
-      Object.keys (menu)
-            .sort ()
-            .forEach (function (title) {
-              var opt = document.createElement ("option");
-              opt.setAttribute ("value", title);
-              datalist.appendChild (opt);
-            });
-      return datalist;
-    }
-
-    /** Mutate the history of page navigation.  Store LINKID in history
-        state, The actual way to store LINKID depends on HISTORY_MODE.
-        @arg {string} linkid
-        @arg {string} history_mode */
-    function
-    update_history (linkid, history_mode)
-    {
-      var method = window.history[history_mode];
-      if (method)
-        {
-          /* Pretend that the current page is the one corresponding to the
-             LINKID iframe.  Handle errors since changing the visible file
-             name can fail on some browsers with the "file:" protocol.  */
-          var visible_url =
-            dirname (window.location.pathname) + linkid_to_url (linkid);
-          try
-            {
-              method.call (window.history, linkid, null, visible_url);
-            }
-          catch (err)
-            {
-              /* Fallback to changing only the hash part which is safer.  */
-              visible_url = window.location.pathname;
-              if (linkid !== config.INDEX_ID)
-                visible_url += ("#" + linkid);
-              method.call (window.history, linkid, null, visible_url);
-            }
-        }
-    }
-
-    /** Send MSG to the browsing context corresponding to PAGEID.  This is a
-        wrapper around 'Window.postMessage' which ensures that MSG is sent
-        after PAGEID is loaded.
-        @arg {string} pageid
-        @arg {any} msg */
-    function
-    post_message (pageid, msg)
-    {
-      if (pageid === config.INDEX_ID || pageid === config.CONTENTS_ID)
-        window.postMessage (msg, "*");
-      else
-        {
-          load_page (pageid);
-          var iframe = document.getElementById (pageid)
-                               .querySelector ("iframe");
-          /* Semantically this would be better to use "promises" however
-             they are not available in IE.  */
-          if (store.state.ready[pageid])
-            iframe.contentWindow.postMessage (msg, "*");
-          else
-            {
-              iframe.addEventListener ("load", function handler () {
-                this.contentWindow.postMessage (msg, "*");
-                this.removeEventListener ("load", handler, false);
-              }, false);
-            }
-        }
-    }
-
-    /*--------------------------------------------
-    | Event handlers for the top-level context.  |
-    `-------------------------------------------*/
-
-    /* Initialize the top level 'config.INDEX_NAME' DOM.  */
-    function
-    on_load ()
-    {
-      fix_links (document.links);
-      add_icons ();
-      document.body.classList.add ("mainbar");
-
-      /* Move contents of <body> into a a fresh <div> to let the components
-         treat the index page like other iframe page.  */
-      var index_div = document.createElement ("div");
-      for (var ch = document.body.firstChild; ch; ch = 
document.body.firstChild)
-        index_div.appendChild (ch);
-
-      /* Aggregation of all the sub-components.  */
-      var components = {
-        element: document.body,
-        components: [],
-
-        add: function add (component) {
-          this.components.push (component);
-          this.element.appendChild (component.element);
-        },
-
-        render: function render (state) {
-          this.components
-              .forEach (function (cmpt) { cmpt.render (state); });
-
-          /* Ensure that focus is on the current node unless some component is
-             focused.  */
-          if (!state.focus)
-            {
-              var link = linkid_split (state.current);
-              var elem = document.getElementById (link.pageid);
-              if (link.pageid !== config.INDEX_ID && link.pageid !== 
config.CONTENTS_ID)
-                elem.querySelector ("iframe").focus ();
-              else
-                {
-                  /* Move the focus to top DOM.  */
-                  document.documentElement.focus ();
-                  /* Allow the spacebar scroll in the main page to work.  */
-                  elem.focus ();
-                }
-            }
-        }
-      };
-      var pages = new Pages (index_div);
-      components.add (pages);
-      var contents_node = pages.add_div(config.CONTENTS_ID);
-      components.add (new Sidebar (contents_node));
-      components.add (new Help_page ());
-      components.add (new Minibuffer ());
-      components.add (new Echo_area ());
-      store.listeners.push (components.render.bind (components));
-
-      if (window.location.hash)
-        {
-          var linkid = normalize_hash (window.location.hash);
-          store.dispatch (actions.set_current_url (linkid, "replaceState"));
-        }
-
-      /* Retrieve NEXT link and local menu.  */
-      var links = {};
-      links[config.INDEX_ID] = navigation_links (document);
-      store.dispatch (actions.cache_links (links));
-      store.dispatch ({ type: "iframe-ready", id: config.INDEX_ID });
-      store.dispatch ({
-        type: "echo",
-        msg: "Welcome to Texinfo documentation viewer 7.1.90, type '?' for 
help."
-      });
-
-      /* Call user hook.  */
-      if (config.on_main_load)
-        config.on_main_load ();
-    }
-
-    /* Handle messages received via the Message API.
-       @arg {MessageEvent} event */
-    function
-    on_message (event)
-    {
-      var data = event.data;
-      if (data.message_kind === "action")
-        {
-          /* Follow up actions to the store.  */
-          store.dispatch (data.action);
-        }
-    }
-
-    /* Event handler for 'popstate' events.  */
-    function
-    on_popstate (event)
-    {
-      /* When EVENT.STATE is 'null' it means that the user has manually
-         changed the hash part of the URL bar.  */
-      var linkid = (event.state === null) ?
-        normalize_hash (window.location.hash) : event.state;
-      store.dispatch (actions.set_current_url (linkid, false));
-    }
 
-    return {
-      on_load: on_load,
-      on_message: on_message,
-      on_popstate: on_popstate
-    };
-  }
+    if (!state.text_input || state.warning)
+      {
+        this.menu.hide ();
+        this.index.hide ();
+        this.search.hide ();
+      }
+    else
+      {
+        this.index.render (state);
+        this.menu.render (state);
+        this.search.render (state);
+      }
+  };
 
-  /** Initialize the iframe which contains the lateral table of content.  */
   function
-  init_sidebar ()
+  Echo_area ()
   {
-    /* Keep children but remove grandchildren (Exception: don't remove
-       anything on the current page; however, that's not a problem in the Kawa
-       manual).  */
-    function
-    hide_grand_child_nodes (ul, excluded)
-    {
-      var lis = ul.children;
-      for (var i = 0; i < lis.length; i += 1)
-        {
-          if (lis[i] === excluded)
-            continue;
-          var first = lis[i].firstElementChild;
-          if (first && first.matches ("ul"))
-            hide_grand_child_nodes (first);
-          else if (first && first.matches ("a"))
-            {
-              var ul$ = first && first.nextElementSibling;
-              if (ul$)
-                ul$.setAttribute ("toc-detail", "yes");
-            }
-        }
-    }
-
-    /** Make the parent of ELEMS visible.
-        @arg {HTMLElement} elem */
-    function
-    mark_parent_elements (elem)
-    {
-      if (elem && elem.parentElement && elem.parentElement.parentElement)
-        {
-          var pparent = elem.parentElement.parentElement;
-          for (var sib = pparent.firstElementChild; sib;
-               sib = sib.nextElementSibling)
-            {
-              if (sib !== elem.parentElement
-                  && sib.firstElementChild
-                  && sib.firstElementChild.nextElementSibling)
-                {
-                  sib.firstElementChild
-                     .nextElementSibling
-                     .setAttribute ("toc-detail", "yes");
-                }
-            }
-        }
-    }
+    var elem = document.createElement ("div");
+    elem.classList.add ("echo-area");
+    elem.setAttribute ("hidden", "true");
 
-    /** Scan ToC entries to see which should be hidden.
-        @arg {HTMLElement} elem
-        @arg {string} linkid */
-    function
-    scan_toc (elem, linkid, section_hash = null)
-    {
-      /** @type {Element} */
-      var res;
-      if (section_hash)
-        {
-          let dot = linkid.lastIndexOf('.');
-          if (dot >= 0)
-            linkid = linkid.substring(0, dot+1) + section_hash;
-        }
-      var url = with_sidebar_query (linkid_to_url (linkid));
+    this.element = elem;
+    this.toid = null;
+  }
 
-      /** Set CURRENT to the node corresponding to URL linkid.
-          @arg {Element} elem */
-      function
-      find_current (elem)
+  Echo_area.prototype.render = function render (state) {
+    if (!state.echo)
       {
-        if (elem.localName === "a" && link_href(elem) == url)
-          {
-            elem.setAttribute ("toc-current", "yes");
-            var sib = elem.nextElementSibling;
-            if (sib && sib.matches ("ul"))
-              hide_grand_child_nodes (sib);
-            res = elem;
-          }
+        this.element.setAttribute ("hidden", "true");
+        this.toid = null;
       }
-
-      var ul = elem.querySelector ("ul");
-      if (linkid === config.INDEX_ID || linkid === config.CONTENTS_ID)
-        {
-          hide_grand_child_nodes (ul);
-          res = document.getElementById(linkid);
-        }
-      else
-        {
-          depth_first_walk (ul, find_current, Node.ELEMENT_NODE);
-          /* Mark every parent node.  */
-          var current = res;
-          while (current && current !== ul)
-            {
-              mark_parent_elements (current);
-              /* XXX: Special case for manuals with '@part' commands.  */
-              if (current.parentElement === ul)
-                hide_grand_child_nodes (ul, current);
-              current = current.parentElement;
-            }
-        }
-      return res;
-    }
-
-    /* Build the global dictionary containing navigation links from NAV.  NAV
-       must be an 'ul' DOM element containing the table of content of the
-       manual.  */
-    function
-    create_link_dict (nav)
-    {
-      var prev_id = config.INDEX_ID;
-      var links = {};
-
-      function
-      add_link (elem)
+    else if (!this.toid)
       {
-        let href;
-          if (elem.matches ("a") && (href = link_href(elem)))
-          {
-            var id = href_hash (href);
-            links[prev_id] =
-              Object.assign ({}, links[prev_id], { forward: id });
-            links[id] = Object.assign ({}, links[id], { backward: prev_id });
-            prev_id = id;
-          }
+        console.info (state.echo);
+        var toid = window.setTimeout (function () {
+          store.dispatch ({ type: "echo", msg: null });
+        }, config.WARNING_TIMEOUT);
+        this.element.innerHTML = state.echo;
+        this.element.removeAttribute ("hidden");
+        this.toid = toid;
       }
+  };
 
-      depth_first_walk (nav, add_link, Node.ELEMENT_NODE);
-      /* Add a reference to the first and last node of the manual.  */
-      links["*TOP*"] = config.INDEX_ID;
-      links["*END*"] = prev_id;
-      return links;
-    }
-
-    /* Add a link from the sidebar to the main index file.  ELEM is the first
-       sibling of the newly created header.  */
-    function
-    add_header (elem)
-    {
-      var h1 = document.querySelector ("h1.settitle");
-      if (!h1)
-        h1 = document.querySelector ("h1.top");
-      if (h1)
-        {
-          var a = document.createElement ("a");
-          a.setAttribute ("href", config.INDEX_NAME);
-          a.setAttribute ("id", config.INDEX_ID);
-
-          let header = elem.previousSibling;
-          header.appendChild (a);
-          if (window.sidebarLinkAppendContents)
-            window.sidebarLinkAppendContents(a, h1.textContent);
-          else
-            {
-              var div = document.createElement ("div");
-              a.appendChild (div);
-              var span = document.createElement ("span");
-              span.textContent = h1.textContent;
-              div.appendChild (span);
-            }
-        }
-    }
-
-    /*------------------------------------------
-    | Event handlers for the sidebar context.  |
-    `-----------------------------------------*/
-
-    /* Initialize TOC_FILENAME which must be loaded in the context of an
-       iframe.  */
-    function
-    on_load ()
-    {
-      var toc_div = document.getElementById ("slider");
-      add_header (toc_div.querySelector (".toc"));
-
-      /* Specify the base URL to use for all relative URLs.  */
-      /* FIXME: Add base also for sub-pages.  */
-      var base = document.createElement ("base");
-      base.setAttribute ("href",
-                         window.location.href.replace (/[/][^/]*$/, "/"));
-      document.head.appendChild (base);
-
-      scan_toc (toc_div, config.INDEX_NAME);
-
-      /* Get 'backward' and 'forward' link attributes.  */
-      var dict = create_link_dict (toc_div.querySelector ("nav.contents ul"));
-      store.dispatch (actions.cache_links (dict));
-    }
-
-    /* Handle messages received via the Message API.  */
-    function
-    on_message (event)
-    {
-      var data = event.data;
-      if (data.message_kind === "update-sidebar")
-        {
-          var toc_div = document.getElementById ("slider");
-
-          /* Reset previous calls to 'scan_toc'.  */
-          depth_first_walk (toc_div, function clear_toc_styles (elem) {
-            elem.removeAttribute ("toc-detail");
-            elem.removeAttribute ("toc-current");
-          }, Node.ELEMENT_NODE);
-
-          /* Remove the hash part for the main page.  */
-          var pageid = linkid_split (data.selected).pageid;
-          var selected = (pageid === config.INDEX_ID) ? pageid : data.selected;
-          /* Highlight the current LINKID in the table of content.  */
-          var elem = scan_toc (toc_div, selected, data.section_hash);
-          if (elem)
-            elem.scrollIntoView (true);
-        }
-    }
-
-    return {
-      on_load: on_load,
-      on_message: on_message
-    };
-  }
+  /*----------------------------.
+  | Component for the sidebar.  |
+  `----------------------------*/
 
-  /** Initialize iframes which contain pages of the manual.  */
   function
-  init_iframe ()
+  Sidebar (contents_node)
   {
-    /* Initialize the DOM for generic pages loaded in the context of an
-       iframe.  */
-    function
-    on_load ()
-    {
-      document.body.classList.add ("in-iframe");
-      fix_links (document.links);
-      var links = {};
-      var linkid = basename (window.location.pathname, /[.]x?html$/);
-      links[linkid] = navigation_links (document);
-      store.dispatch (actions.cache_links (links));
-      if (document.title)
-         store.dispatch (actions.window_title (document.title));
-
-      if (linkid_contains_index (linkid))
-        {
-          /* Scan links that should be added to the index.  */
-          var index_entries = document.querySelectorAll
-            ("td.printindex-index-entry");
-          store.dispatch (actions.cache_index_links (index_entries));
-        }
-
-      add_icons ();
-
-      /* Call user hook.  */
-      if (config.on_iframe_load)
-        config.on_iframe_load ();
-    }
+    this.element = document.createElement ("div"); // FIXME unneeded?
+    this.element.setAttribute ("id", "slider");
+    var div = document.createElement ("div");
+    div.classList.add ("toc-sidebar");
+    var toc = document.querySelector ("#SEC_Contents");
+    toc.remove ();
 
-    /* Handle messages received via the Message API.  */
-    function
-    on_message (event)
+    // Like n.cloneNode, but also copy _href
+    function cloneNode(n)
     {
-      var data = event.data;
-      if (data.message_kind === "highlight")
-        remove_highlight (document.body);
-      else if (data.message_kind === "search")
-        {
-          var found = search (document.body, data.regexp);
-          store.dispatch ({ type: "search-result", found: found });
-        }
-      else if (data.message_kind === "scroll-to")
+      let r = n.cloneNode(false);
+      for (let c = n.firstChild; c; c = c.nextSibling)
         {
-          /* Scroll to the anchor corresponding to HASH.  */
-          if (data.hash)
-            {
-              let elem = document.getElementById(data.hash.substring(1));
-              // Check if hash reference is to a sectioing element.  
-              // If not we need to find the outer sectioning element,
-              // so we can update the sidebar's ToC correctly.
-              if (elem)
-                {
-                  let p = elem;
-                  let section = null;
-                  let id = null;
-                  for (let p = elem;
-                       section === null && p instanceof Element;
-                        p = p.parentNode)
-                    {
-                      let sid = p.getAttribute("id");
-                      if (sid == null)
-                        continue;
-                      let cl = p.classList;
-                      for (let i = cl.length; --i >= 0; )
-                        {
-                          if (section_names.indexOf(cl.item(i)) >= 0)
-                            {
-                              section = p;
-                              id = sid;
-                              break;
-                            }
-                        }
-                    }
-                    if (section && section !== elem)
-                      {
-                        // Send section id to sidebar so it can properly 
update.
-                        store.dispatch({ type: "section", hash: data.hash, 
section_hash: id } );
-                      }
-                }
-                window.location.replace (data.hash);
-            }
-          else
-            window.scroll (0, 0);
+          let d = cloneNode(c);
+          let h = c._href;
+          if (h)
+            d._href = h;
+          r.appendChild(d);
         }
+      return r;
     }
-
-    return {
-      on_load: on_load,
-      on_message: on_message
-    };
+    contents_node.appendChild(cloneNode(toc));
+
+    /* Remove table of contents header.  */
+    toc = toc.querySelector(".contents"); // skip ToC header
+
+    /* Move contents of <body> into a a fresh <div> to let the components
+       treat the index page like other iframe page.  */
+    var nav = document.createElement ("nav");
+    nav.classList.add ("contents");
+    for (var ch = toc.firstChild; ch; ch = toc.firstChild)
+      nav.appendChild (ch);
+
+    var div$ = document.createElement ("div");
+    div$.classList.add ("toc");
+    div$.appendChild (nav);
+    div.appendChild (div$);
+    this.element.appendChild (div);
+
+    let header = document.createElement ("header");
+    div$.parentElement.insertBefore (header, div$);
+    let hider = document.createElement ("button");
+    hider.classList.add ("sidebar-hider");
+    hider.innerHTML = config.HIDE_SIDEBAR_HTML;
+    this.show_sidebar_button = hider;
+    header.appendChild(hider);
   }
 
-  /*-------------------------
-  | Common event handlers.  |
-  `------------------------*/
-
-  /** Handle click events.  */
-  function
-  on_click (event)
-  {
-    let in_sidebar = event.target.matches ("#slider *");
-    for (var target = event.target; target !== null; target = 
target.parentNode)
+  /* Render 'sidebar' according to STATE which is a new state. */
+  Sidebar.prototype.render = function render (state) {
+    /* Update sidebar to highlight the title corresponding to
+       'state.current'.*/
+    let currently_showing = document.body.getAttribute("show-sidebar");
+    let show = state.show_sidebar;
+    if (show == "hide-if-narrow")
+      show = is_narrow_window() || currently_showing == "no" ? "no" : "yes";
+    if (show === undefined)
+      show = "yes";
+    if (show !== currently_showing)
       {
-        if (! (target instanceof Element))
-          continue;
-        if (target.matches ("a"))
-          {
-            var href = link_href(target);
-            if (href && maybe_pageref_url_p (href)
-                  && !external_manual_url_p (href))
-              {
-                var linkid = href_hash (href) || config.INDEX_ID;
-                if (linkid === "index.SEC_Contents")
-                    linkid = config.CONTENTS_ID;
-                store.dispatch (actions.set_current_url (linkid, null,
-                                                         in_sidebar ? 
"in-sidebar" : "in-page"));
-                event.preventDefault ();
-                event.stopPropagation ();
-                break;
-              }
-          }
-        if (target.matches (".sidebar-hider"))
-          {
-              let body = document.body;
-              let show = body.getAttribute("show-sidebar");
-              show_sidebar (show==="no");
-              return;
-          }
+        document.body.setAttribute("show-sidebar", show);
+        this.show_sidebar_button.innerHTML = show == "yes" ? 
config.HIDE_SIDEBAR_HTML : config.SHOW_SIDEBAR_HTML;
+        let tooltip = show == "yes" ? config.HIDE_SIDEBAR_TOOLTIP : 
config.SHOW_SIDEBAR_TOOLTIP;
+        if (tooltip)
+          this.show_sidebar_button.setAttribute("title", tooltip);
+        else
+          this.show_sidebar_button.removeAttribute("title");
       }
-    if (! in_sidebar)
-      hide_sidebar_if_narrow ();
-  }
-
-  // Only valid when showing sidebar.
-  function is_narrow_window ()
-  {
-    return document.body.firstChild.offsetLeft == 0;
-  }
-
-  function show_sidebar (show)
-  {
-    store.dispatch({ type: "show-sidebar", show: show ? "yes" : "no" });
-  }
+    var msg = { message_kind: "update-sidebar", selected: state.current, 
section_hash: state.section_hash };
+    window.postMessage (msg, "*");
+  };
 
-  function hide_sidebar_if_narrow ()
-  {
-    store.dispatch({ type: "show-sidebar", show: "hide-if-narrow" });
-  }
+  /*--------------------------.
+  | Component for the pages.  |
+  `--------------------------*/
 
-  /** Handle unload events.  */
+  /** @arg {HTMLDivElement} index_div */
   function
-  on_unload ()
+  Pages (index_div)
   {
-    /* Cross origin requests are not supported in file protocol.  */
-    if (window.location.protocol !== "file:")
-    {
-      var request = new XMLHttpRequest ();
-      request.open ("GET", "(WINDOW-CLOSED)");
-      request.send (null);
-    }
+    index_div.setAttribute ("id", config.INDEX_ID);
+    index_div.setAttribute ("node", config.INDEX_ID);
+    index_div.setAttribute ("hidden", "true");
+    this.element = document.createElement ("div");
+    this.element.setAttribute ("id", "sub-pages");
+    this.element.appendChild (index_div);
+    /** @type {string[]} Currently created divs.  */
+    this.ids = [config.INDEX_ID];
+    /** @type {string} */
+    this.prev_id = null;
+    /** @type {HTMLElement} */
+    this.prev_div = null;
+    this.prev_search = null;
+    this.main_title = document.title;
   }
 
-  /** Handle Keyboard 'keydown' events.
-      @arg {KeyboardEvent} event */
-  function
-  on_keydown (event)
-  {
-    if (is_escape_key (event.key))
-      store.dispatch ({ type: "unfocus" });
-    else
-      {
-        var val = on_keydown.dict[event.key];
-        if (val)
-          {
-            if (typeof val === "function")
-              val ();
-            else
-              store.dispatch (val);
-            event.preventDefault();
-          }
-      }
-  }
-
-  /* Associate an Event 'key' property to an action or a thunk.  */
-  on_keydown.dict = {
-    i: actions.show_text_input ("index"),
-    l: window.history.back.bind (window.history),
-    m: actions.show_text_input ("menu"),
-    n: actions.navigate ("next"),
-    p: actions.navigate ("prev"),
-    r: window.history.forward.bind (window.history),
-    s: actions.show_text_input ("regexp-search"),
-    t: actions.set_current_url_pointer ("*TOP*"),
-    u: actions.navigate ("up"),
-    "]": actions.navigate ("forward"),
-    "[": actions.navigate ("backward"),
-    "<": actions.set_current_url_pointer ("*TOP*"),
-    ">": actions.set_current_url_pointer ("*END*"),
-    "?": actions.show_help ()
-  };
-
-  /** Some standard methods used in this script might not be implemented by
-      the current browser.  If that is the case then augment the prototypes
-      appropriately.  */
-  function
-  register_polyfills ()
-  {
-    function
-    includes (search, start)
-    {
-      start = start || 0;
-      return ((start + search.length) <= this.length)
-        && (this.indexOf (search, start) !== -1);
-    }
-
-    /* eslint-disable no-extend-native */
-    if (!Array.prototype.includes)
-      Array.prototype.includes = includes;
-
-    if (!String.prototype.includes)
-      String.prototype.includes = includes;
+  Pages.prototype.add_div = function add_div (pageid) {
+    var div = document.createElement ("div");
+    div.setAttribute ("id", pageid);
+    div.setAttribute ("node", pageid);
+    div.setAttribute ("hidden", "true");
+    this.ids.push (pageid);
+    this.element.appendChild (div);
+    if (linkid_contains_index (pageid))
+      load_page (pageid);
+    return div;
+  };
 
-    if (!String.prototype.endsWith)
+  Pages.prototype.render = function render (state) {
+    var that = this;
+
+    /* Create div elements for pages corresponding to newly added
+       linkids from 'state.loaded_nodes'.*/
+    Object.keys (state.loaded_nodes)
+          .map (function (id) { return id.replace (/\..*$/, ""); })
+          .filter (function (id) { return !that.ids.includes (id); })
+          .reduce (function (acc, id) {
+            return ((acc.includes (id)) ? acc : acc.concat ([id]));
+          }, [])
+          .forEach (function (id) { return that.add_div (id); });
+
+    /* Blur pages if help screen is on.  */
+    this.element.classList[(state.help) ? "add" : "remove"] ("blurred");
+    let div = this.prev_div;
+    if (state.current !== this.prev_id)
       {
-        String.prototype.endsWith = function endsWith (search, position) {
-          var subject_string = this.toString ();
-          if (typeof position !== "number"
-              || !isFinite (position)
-              || Math.floor (position) !== position
-              || position > subject_string.length)
-            position = subject_string.length;
-
-          position -= search.length;
-          var last_index = subject_string.lastIndexOf (search, position);
-          return last_index !== -1 && last_index === position;
-        };
+        if (this.prev_id)
+          {
+            this.prev_div.setAttribute ("hidden", "true");
+            /* Remove previous highlights.  */
+            var old = linkid_split (this.prev_id);
+            var msg = { message_kind: "highlight", regexp: null };
+            post_message (old.pageid, msg);
+          }
+        div = resolve_page (state.current, true);
+        update_history (state.current, state.history);
+        this.prev_id = state.current;
+        this.prev_div = div;
       }
-
-    if (!Element.prototype.matches)
-      {
-        Element.prototype.matches = Element.prototype["matchesSelector"]
-          || Element.prototype["mozMatchesSelector"]
-          || Element.prototype["mozMatchesSelector"]
-          || Element.prototype["msMatchesSelector"]
-          || Element.prototype["webkitMatchesSelector"]
-          || function element_matches (str) {
-            var document = (this.document || this.ownerDocument);
-            var matches = document.querySelectorAll (str);
-            var i = matches.length;
-            while ((i -= 1) >= 0 && matches.item (i) !== this);
-            return i > -1;
-          };
+      if (state.action.type === "window-title") {
+          div.setAttribute("title", state.action.title);
       }
+      let title = div.getAttribute("title") || this.main_title;
+      if (title && document.title !== title)
+          document.title = title;
 
-    if (typeof Object.assign != "function")
+    if (state.search
+        && (this.prev_search !== state.search)
+        && state.search.status === "ready")
       {
-        Object.assign = function assign (target) {
-          if (undef_or_null (target))
-            throw new TypeError ("Cannot convert undefined or null to object");
-
-          var to = Object (target);
-          for (var index = 1; index < arguments.length; index += 1)
-            {
-              var next_source = arguments[index];
-              if (undef_or_null (next_source))
-                continue;
-              for (var key in next_source)
-                {
-                  /* Avoid bugs when hasOwnProperty is shadowed.  */
-                  if (Object.prototype.hasOwnProperty.call (next_source, key))
-                    to[key] = next_source[key];
-                }
-            }
-          return to;
-        };
+        this.prev_search = state.search;
+        if (state.search.current_pageid === config.INDEX_ID)
+          {
+            window.setTimeout (function () {
+              store.dispatch ({ type: "search-query" });
+              var res = search (document.getElementById (config.INDEX_ID),
+                                state.search.regexp);
+              store.dispatch ({ type: "search-result", found: res });
+            }, 0);
+          }
+        else
+          {
+            window.setTimeout (function () {
+              store.dispatch ({ type: "search-query" });
+              var msg = {
+                message_kind: "search",
+                regexp: state.search.regexp,
+                id: state.search.current_pageid
+              };
+              post_message (state.search.current_pageid, msg);
+            }, 0);
+          }
       }
 
-    (function (protos) {
-      protos.forEach (function (proto) {
-        if (!proto.hasOwnProperty ("remove"))
+    /* Update highlight of current page.  */
+    if (!state.highlight)
+      {
+        if (state.current === config.INDEX_ID)
+          remove_highlight (document.getElementById (config.INDEX_ID));
+        else
           {
-            Object.defineProperty (proto, "remove", {
-              configurable: true,
-              enumerable: true,
-              writable: true,
-              value: function value () {
-                this.parentNode.removeChild (this);
-              }
-            });
+            var link = linkid_split (state.current);
+            var msg$ = { message_kind: "highlight", regexp: null };
+            post_message (link.pageid, msg$);
           }
-      });
-    } ([Element.prototype, CharacterData.prototype, DocumentType.prototype]));
-    /* eslint-enable no-extend-native */
-  }
+      }
 
-  /*---------------------.
-  | Common utilitaries.  |
-  `---------------------*/
+    /* Scroll to highlighted search result. */
+    if (state.search
+        && (this.prev_search !== state.search)
+        && state.search.status === "done"
+        && state.search.found === true)
+      {
+        var link$ = linkid_split (state.current);
+        var msg$$ = { message_kind: "scroll-to", hash: "#search-result" };
+        post_message (link$.pageid, msg$$);
+        this.prev_search = state.search;
+      }
+  };
 
-  /** Check portably if KEY correspond to "Escape" key value.
-      @arg {string} key */
-  function
-  is_escape_key (key)
-  {
-    /* In Internet Explorer 9 and Firefox 36 and earlier, the Esc key
-       returns "Esc" instead of "Escape".  */
-    return key === "Escape" || key === "Esc";
-  }
+  /*--------------.
+  | Utilitaries.  |
+  `--------------*/
 
-  /** Check if OBJ is equal to 'undefined' or 'null'.  */
+  /** Load PAGEID.
+      @arg {string} pageid */
   function
-  undef_or_null (obj)
+  load_page (pageid)
   {
-    return (obj === null || typeof obj === "undefined");
+    var div = resolve_page (pageid, false);
+    /* Making the iframe visible triggers the load of the iframe DOM.  */
+    if (div.hasAttribute ("hidden"))
+      {
+        div.removeAttribute ("hidden");
+        div.setAttribute ("hidden", "true");
+      }
   }
 
-  /** Return a relative URL corresponding to HREF, which refers to an anchor
-      of 'config.INDEX_NAME'.  HREF must be a USVString representing an
-      absolute or relative URL.  For example "foo/bar.html" will return
-      "config.INDEX_NAME#bar".
-
-      @arg {string} href.*/
+  /** Return the div element that correspond to LINKID.  If VISIBLE is true
+      then display the div and position the corresponding iframe to the
+      anchor specified by LINKID.
+      @arg {string} linkid - link identifier
+      @arg {boolean} [visible]
+      @return {HTMLElement} the div element.  */
   function
-  with_sidebar_query (href)
+  resolve_page (linkid, visible)
   {
-    if (basename (href) === config.INDEX_NAME)
-      return config.INDEX_NAME;
-    else
+    var msg;
+    var link = linkid_split (linkid);
+    var pageid = link.pageid;
+    var div = document.getElementById (link.pageid);
+    if (!div)
       {
-        /* XXX: Do not use the URL API for IE portability.  */
-        var url = with_sidebar_query.url;
-        url.setAttribute ("href", href);
-        var new_hash = "#" + basename (url.pathname, /[.]x?html/);
-        /* XXX: 'new_hash !== url.hash' is a workaround to work with links
-           produced by makeinfo which link to an anchor element in a page
-           instead of directly to the page. */
-        if (url.hash && new_hash !== url.hash)
-          new_hash += ("." + url.hash.slice (1));
-        return config.INDEX_NAME + new_hash;
+        msg = "no iframe container correspond to identifier: " + pageid;
+        throw new ReferenceError (msg);
       }
-  }
-
-  /* Use the same DOM element for every function call.  */
-  with_sidebar_query.url = document.createElement ("a");
 
-  /** Modify LINKS to handle the iframe based navigation properly.  Relative
-      links will be opened inside the corresponding iframe and absolute links
-      will be opened in a new tab.  If ID is true then define an "id"
-      attribute with a linkid value for relative links.
-
-      @typedef {HTMLAnchorElement|HTMLAreaElement} Links
-      @arg {Links[]|HTMLCollectionOf<Links>} links
-      @arg {boolean} [id]
-      @return void  */
-  function
-  fix_links (links, id)
-  {
-    for (var i = 0; i < links.length; i += 1)
+    /* Create iframe if necessary unless the div is refering to the Index
+       or Contents page.  */
+    if (pageid === config.INDEX_ID || pageid === config.CONTENTS_ID)
       {
-        var link = links[i];
-        var href = link_href(link);
-        if (!href)
-          continue;
-        else if (external_manual_url_p (href))
-          link.setAttribute ("target", "_top");
-        else if (! maybe_pageref_url_p (href))
-          link.setAttribute ("target", "_blank");
-        else
+        div.removeAttribute ("hidden");
+        /* Unlike iframes, Elements are unlikely to be scrollable (CSSOM
+           Scroll-behavior), so choose an arbitrary element inside "index"
+           div and at the top of it.  */
+        document.getElementById ("icon-bar").scrollIntoView ();
+      }
+    else
+      {
+        var iframe = div.querySelector ("iframe");
+        if (!iframe)
           {
-            var href$ = with_sidebar_query (href);
-            if (href !== href$)
-              {
-                let protocol = location.protocol;
-                if (protocol == "https:" || protocol == "http:")
-                  link._href = href$;
-                else
-                  link.setAttribute ("href", href$);
-              }
-            if (id)
-              {
-                var linkid = (href$ === config.INDEX_NAME) ?
-                    config.INDEX_ID : href_hash (href$);
-                link.setAttribute ("id", linkid);
-              }
+            iframe = document.createElement ("iframe");
+            iframe.classList.add ("node");
+            iframe.setAttribute ("src", linkid_to_url (pageid));
+            div.appendChild (iframe);
+            iframe.addEventListener ("load", function () {
+              store.dispatch ({ type: "iframe-ready", id: pageid });
+            }, false);
+          }
+        if (visible)
+          {
+            div.removeAttribute ("hidden");
+            msg = { message_kind: "scroll-to", hash: link.hash };
+            post_message (pageid, msg);
           }
       }
+
+    return div;
   }
 
-  /** Convert HASH which is something that can be found 'Location.hash' to a
-      "linkid" which can be handled in our model.
-      @arg {string} hash
-      @return {string} linkid */
+  /** Create a datalist element containing option elements corresponding
+      to the keys in MENU.  */
   function
-  normalize_hash (hash)
+  create_datalist (menu)
   {
-    var text = hash.slice (1);
-    /* Some anchor elements are present in 'config.INDEX_NAME' and we need to
-       handle link to them specially (i.e. not try to find their corresponding
-       iframe).*/
-    if (config.MAIN_ANCHORS.includes (text))
-      return config.INDEX_ID + "." + text;
-    else
-      return text;
+    var datalist = document.createElement ("datalist");
+    Object.keys (menu)
+          .sort ()
+          .forEach (function (title) {
+            var opt = document.createElement ("option");
+            opt.setAttribute ("value", title);
+            datalist.appendChild (opt);
+          });
+    return datalist;
   }
 
-  /** Return an object composed of the filename and the anchor of LINKID.
-      LINKID can have the form "foobar.anchor" or just "foobar".
+  /** Mutate the history of page navigation.  Store LINKID in history
+      state, The actual way to store LINKID depends on HISTORY_MODE.
       @arg {string} linkid
-      @return {{pageid: string, hash: string}} */
+      @arg {string} history_mode */
   function
-  linkid_split (linkid)
+  update_history (linkid, history_mode)
   {
-    if (!linkid.includes ("."))
-      return { pageid: linkid, hash: "" };
-    else
+    var method = window.history[history_mode];
+    if (method)
       {
-        var ref = linkid.match (/^(.+)\.(.*)$/).slice (1);
-        return { pageid: ref[0], hash: "#" + ref[1] };
+        /* Pretend that the current page is the one corresponding to the
+           LINKID iframe.  Handle errors since changing the visible file
+           name can fail on some browsers with the "file:" protocol.  */
+        var visible_url =
+          dirname (window.location.pathname) + linkid_to_url (linkid);
+        try
+          {
+            method.call (window.history, linkid, null, visible_url);
+          }
+        catch (err)
+          {
+            /* Fallback to changing only the hash part which is safer.  */
+            visible_url = window.location.pathname;
+            if (linkid !== config.INDEX_ID)
+              visible_url += ("#" + linkid);
+            method.call (window.history, linkid, null, visible_url);
+          }
       }
   }
 
-  /** Convert LINKID which has the form "foobar.anchor" or just "foobar", to
-      an URL of the form `foobar${config.EXT}#anchor`.
-      @arg {string} linkid */
+  /** Send MSG to the browsing context corresponding to PAGEID.  This is a
+      wrapper around 'Window.postMessage' which ensures that MSG is sent
+      after PAGEID is loaded.
+      @arg {string} pageid
+      @arg {any} msg */
   function
-  linkid_to_url (linkid)
+  post_message (pageid, msg)
   {
-    if (linkid === config.INDEX_ID)
-      return config.INDEX_NAME;
+    if (pageid === config.INDEX_ID || pageid === config.CONTENTS_ID)
+      window.postMessage (msg, "*");
     else
       {
-        var link = linkid_split (linkid);
-        return link.pageid + config.EXT + link.hash;
+        load_page (pageid);
+        var iframe = document.getElementById (pageid)
+                             .querySelector ("iframe");
+        /* Semantically this would be better to use "promises" however
+           they are not available in IE.  */
+        if (store.state.ready[pageid])
+          iframe.contentWindow.postMessage (msg, "*");
+        else
+          {
+            iframe.addEventListener ("load", function handler () {
+              this.contentWindow.postMessage (msg, "*");
+              this.removeEventListener ("load", handler, false);
+            }, false);
+          }
       }
   }
 
-  /** Check if 'URL' may be a cross-reference to another page in this manual. 
*/
-  function
-  maybe_pageref_url_p (url)
-  {
-    return url.match(config.LOCAL_HTML_PAGE_PATTERN);
-  }
+  /*--------------------------------------------
+  | Event handlers for the top-level context.  |
+  `-------------------------------------------*/
 
-  /** Check if 'URL' is a link to another manual.  For locally installed 
-      manuals only. */
+  /* Initialize the top level 'config.INDEX_NAME' DOM.  */
   function
-  external_manual_url_p (url)
+  on_load ()
   {
-    if (typeof url !== "string")
-      throw new TypeError ("'" + url + "' is not a string");
+    fix_links (document.links);
+    add_icons ();
+    document.body.classList.add ("mainbar");
+
+    /* Move contents of <body> into a a fresh <div> to let the components
+       treat the index page like other iframe page.  */
+    var index_div = document.createElement ("div");
+    for (var ch = document.body.firstChild; ch; ch = document.body.firstChild)
+      index_div.appendChild (ch);
+
+    /* Aggregation of all the sub-components.  */
+    var components = {
+      element: document.body,
+      components: [],
+
+      add: function add (component) {
+        this.components.push (component);
+        this.element.appendChild (component.element);
+      },
+
+      render: function render (state) {
+        this.components
+            .forEach (function (cmpt) { cmpt.render (state); });
+
+        /* Ensure that focus is on the current node unless some component is
+           focused.  */
+        if (!state.focus)
+          {
+            var link = linkid_split (state.current);
+            var elem = document.getElementById (link.pageid);
+            if (link.pageid !== config.INDEX_ID && link.pageid !== 
config.CONTENTS_ID)
+              elem.querySelector ("iframe").focus ();
+            else
+              {
+                /* Move the focus to top DOM.  */
+                document.documentElement.focus ();
+                /* Allow the spacebar scroll in the main page to work.  */
+                elem.focus ();
+              }
+          }
+      }
+    };
+    var pages = new Pages (index_div);
+    components.add (pages);
+    var contents_node = pages.add_div(config.CONTENTS_ID);
+    components.add (new Sidebar (contents_node));
+    components.add (new Help_page ());
+    components.add (new Minibuffer ());
+    components.add (new Echo_area ());
+    store.listeners.push (components.render.bind (components));
+
+    if (window.location.hash)
+      {
+        var linkid = normalize_hash (window.location.hash);
+        store.dispatch (actions.set_current_url (linkid, "replaceState"));
+      }
 
-    return url.match(/^..\//);
+    /* Retrieve NEXT link and local menu.  */
+    var links = {};
+    links[config.INDEX_ID] = navigation_links (document);
+    store.dispatch (actions.cache_links (links));
+    store.dispatch ({ type: "iframe-ready", id: config.INDEX_ID });
+    store.dispatch ({
+      type: "echo",
+      msg: "Welcome to Texinfo documentation viewer 7.1.90, type '?' for help."
+    });
+
+    /* Call user hook.  */
+    if (config.on_main_load)
+      config.on_main_load ();
   }
 
-  /** Return PATHNAME with any leading directory components removed.  If
-      specified, also remove a trailing SUFFIX.  */
+  /* Handle messages received via the Message API.
+     @arg {MessageEvent} event */
   function
-  basename (pathname, suffix)
+  on_message (event)
   {
-    var res = pathname.replace (/.*[/]/, "");
-    if (!suffix)
-      return res;
-    else if (suffix instanceof RegExp)
-      return res.replace (suffix, "");
-    else /* typeof SUFFIX === "string" */
-      return res.replace (new RegExp ("[.]" + suffix), "");
+    var data = event.data;
+    if (data.message_kind === "action")
+      {
+        /* Follow up actions to the store.  */
+        store.dispatch (data.action);
+      }
   }
 
-  /** Strip last component from PATHNAME and keep the trailing slash. For
-      example if PATHNAME is "/foo/bar/baz.html" then return "/foo/bar/".
-      @arg {string} pathname */
+  /* Event handler for 'popstate' events.  */
   function
-  dirname (pathname)
+  on_popstate (event)
   {
-    var res = pathname.match (/\/?.*\//);
-    if (res)
-      return res[0];
-    else
-      throw new Error ("'location.pathname' is not recognized");
+    /* When EVENT.STATE is 'null' it means that the user has manually
+       changed the hash part of the URL bar.  */
+    var linkid = (event.state === null) ?
+      normalize_hash (window.location.hash) : event.state;
+    store.dispatch (actions.set_current_url (linkid, false));
   }
 
-  /** Apply FUNC to each nodes in the NODE subtree.  The walk follows a depth
-      first algorithm.  Only consider the nodes of type NODE_TYPE.  */
+  return {
+    on_load: on_load,
+    on_message: on_message,
+    on_popstate: on_popstate
+  };
+}
+
+/** Initialize the iframe which contains the lateral table of content.  */
+function
+init_sidebar ()
+{
+  /* Keep children but remove grandchildren (Exception: don't remove
+     anything on the current page; however, that's not a problem in the Kawa
+     manual).  */
   function
-  depth_first_walk (node, func, node_type)
+  hide_grand_child_nodes (ul, excluded)
   {
-    if (!node_type || (node.nodeType === node_type))
-      func (node);
-
-    for (var child = node.firstChild; child; child = child.nextSibling)
-      depth_first_walk (child, func, node_type);
+    var lis = ul.children;
+    for (var i = 0; i < lis.length; i += 1)
+      {
+        if (lis[i] === excluded)
+          continue;
+        var first = lis[i].firstElementChild;
+        if (first && first.matches ("ul"))
+          hide_grand_child_nodes (first);
+        else if (first && first.matches ("a"))
+          {
+            var ul$ = first && first.nextElementSibling;
+            if (ul$)
+              ul$.setAttribute ("toc-detail", "yes");
+          }
+      }
   }
 
-  /** Return the "effective" href attribute of a link (a) element. */
+  /** Make the parent of ELEMS visible.
+      @arg {HTMLElement} elem */
   function
-  link_href (alink)
+  mark_parent_elements (elem)
   {
-    return alink._href || alink.getAttribute("href");
+    if (elem && elem.parentElement && elem.parentElement.parentElement)
+      {
+        var pparent = elem.parentElement.parentElement;
+        for (var sib = pparent.firstElementChild; sib;
+             sib = sib.nextElementSibling)
+          {
+            if (sib !== elem.parentElement
+                && sib.firstElementChild
+                && sib.firstElementChild.nextElementSibling)
+              {
+                sib.firstElementChild
+                   .nextElementSibling
+                   .setAttribute ("toc-detail", "yes");
+              }
+          }
+      }
   }
 
-  /** Return the hash part of HREF without the '#' prefix.  HREF must be a
-      string.  If there is no hash part in HREF then return the empty
-      string.  */
+  /** Scan ToC entries to see which should be hidden.
+      @arg {HTMLElement} elem
+      @arg {string} linkid */
   function
-  href_hash (href)
+  scan_toc (elem, linkid, section_hash = null)
   {
-    if (typeof href !== "string")
-      throw new TypeError (href + " is not a string");
+    /** @type {Element} */
+    var res;
+    if (section_hash)
+      {
+        let dot = linkid.lastIndexOf('.');
+        if (dot >= 0)
+          linkid = linkid.substring(0, dot+1) + section_hash;
+      }
+    var url = with_sidebar_query (linkid_to_url (linkid));
 
-    if (href.includes ("#"))
-      return href.replace (/.*#/, "");
+    /** Set CURRENT to the node corresponding to URL linkid.
+        @arg {Element} elem */
+    function
+    find_current (elem)
+    {
+      if (elem.localName === "a" && link_href(elem) == url)
+        {
+          elem.setAttribute ("toc-current", "yes");
+          var sib = elem.nextElementSibling;
+          if (sib && sib.matches ("ul"))
+            hide_grand_child_nodes (sib);
+          res = elem;
+        }
+    }
+
+    var ul = elem.querySelector ("ul");
+    if (linkid === config.INDEX_ID || linkid === config.CONTENTS_ID)
+      {
+        hide_grand_child_nodes (ul);
+        res = document.getElementById(linkid);
+      }
     else
-      return "";
+      {
+        depth_first_walk (ul, find_current, Node.ELEMENT_NODE);
+        /* Mark every parent node.  */
+        var current = res;
+        while (current && current !== ul)
+          {
+            mark_parent_elements (current);
+            /* XXX: Special case for manuals with '@part' commands.  */
+            if (current.parentElement === ul)
+              hide_grand_child_nodes (ul, current);
+            current = current.parentElement;
+          }
+      }
+    return res;
   }
 
-  /** Check if LINKID corresponds to a page containing index links.  */
+  /* Build the global dictionary containing navigation links from NAV.  NAV
+     must be an 'ul' DOM element containing the table of content of the
+     manual.  */
   function
-  linkid_contains_index (linkid)
+  create_link_dict (nav)
   {
-    return linkid.match (/^.*-index$/i) || linkid.match (/^Index$/);
+    var prev_id = config.INDEX_ID;
+    var links = {};
+
+    function
+    add_link (elem)
+    {
+      let href;
+        if (elem.matches ("a") && (href = link_href(elem)))
+        {
+          var id = href_hash (href);
+          links[prev_id] =
+            Object.assign ({}, links[prev_id], { forward: id });
+          links[id] = Object.assign ({}, links[id], { backward: prev_id });
+          prev_id = id;
+        }
+    }
+
+    depth_first_walk (nav, add_link, Node.ELEMENT_NODE);
+    /* Add a reference to the first and last node of the manual.  */
+    links["*TOP*"] = config.INDEX_ID;
+    links["*END*"] = prev_id;
+    return links;
   }
 
-  /** Retrieve PREV, NEXT, and UP links, and local menu from CONTENT and
-      return an object containing references to those links.  CONTENT must be
-      an object implementing the ParentNode interface (Element,
-      Document...).  */
+  /* Add a link from the sidebar to the main index file.  ELEM is the first
+     sibling of the newly created header.  */
   function
-  navigation_links (content)
+  add_header (elem)
   {
-    var links = content.querySelectorAll ("a[accesskey][href]");
-    var res = {};
-    /* links have the form MAIN_FILE.html#FRAME-ID.  For convenience
-       only store FRAME-ID.  */
-    for (var i = 0; i < links.length; i += 1)
+    var h1 = document.querySelector ("h1.settitle");
+    if (!h1)
+      h1 = document.querySelector ("h1.top");
+    if (h1)
       {
-        var link = links[i];
-        var nav_id = navigation_links.dict[link.getAttribute ("accesskey")];
-        if (nav_id)
-          {
-            var href = basename (link_href (link));
-            if (href === config.INDEX_NAME)
-              res[nav_id] = config.INDEX_ID;
-            else
-              res[nav_id] = href_hash (href);
-          }
-        else /* this link is part of local table of content. */
+        var a = document.createElement ("a");
+        a.setAttribute ("href", config.INDEX_NAME);
+        a.setAttribute ("id", config.INDEX_ID);
+
+        let header = elem.previousSibling;
+        header.appendChild (a);
+        if (window.sidebarLinkAppendContents)
+          window.sidebarLinkAppendContents(a, h1.textContent);
+        else
           {
-            res.menu = res.menu || {};
-            res.menu[link.text] = href_hash (link_href (link));
+            var div = document.createElement ("div");
+            a.appendChild (div);
+            var span = document.createElement ("span");
+            span.textContent = h1.textContent;
+            div.appendChild (span);
           }
       }
-
-      return res;
   }
 
-  navigation_links.dict = { n: "next", p: "prev", u: "up" };
+  /*------------------------------------------
+  | Event handlers for the sidebar context.  |
+  `-----------------------------------------*/
 
+  /* Initialize TOC_FILENAME which must be loaded in the context of an
+     iframe.  */
   function
-  add_icons ()
+  on_load ()
   {
-    var div = document.createElement ("div");
-    div.setAttribute ("id", "icon-bar");
-    var span = document.createElement ("span");
-    span.innerHTML = "?";
-    // Set tool-tip (on hover)
-    span.setAttribute ("title", "Help for keyboard shortcuts");
-    span.classList.add ("icon");
-    span.addEventListener ("click", function () {
-      store.dispatch (actions.show_help ());
-    }, false);
-    div.appendChild (span);
-    document.body.insertBefore (div, document.body.firstChild);
-  }
+    var toc_div = document.getElementById ("slider");
+    add_header (toc_div.querySelector (".toc"));
 
-  /** Check if ELEM matches SEARCH
-      @arg {Element} elem
-      @arg {RegExp} rgxp
-      @return {boolean} */
-  function
-  search (elem, rgxp)
-  {
-    /** @type {Text} */
-    var text = null;
+    /* Specify the base URL to use for all relative URLs.  */
+    /* FIXME: Add base also for sub-pages.  */
+    var base = document.createElement ("base");
+    base.setAttribute ("href",
+                       window.location.href.replace (/[/][^/]*$/, "/"));
+    document.head.appendChild (base);
 
-    /** @arg {Text} node */
-    function
-    find (node)
-    {
-      if (rgxp.test (node.textContent))
-        {
-          /* Ignore previous match.  */
-          var prev = node.parentElement.matches ("span.highlight");
-          text = (prev) ? null : (text || node);
-        }
-    }
+    scan_toc (toc_div, config.INDEX_NAME);
 
-    depth_first_walk (elem, find, Node.TEXT_NODE);
-    remove_highlight (elem);
-    if (!text)
-      return false;
-    else
-      {
-        highlight_text (rgxp, text);
-        return true;
-      }
+    /* Get 'backward' and 'forward' link attributes.  */
+    var dict = create_link_dict (toc_div.querySelector ("nav.contents ul"));
+    store.dispatch (actions.cache_links (dict));
   }
 
-  /** Find the pageid corresponding to forward direction.
-      @arg {any} state
-      @arg {string} linkid
-      @return {string} the forward pageid */
+  /* Handle messages received via the Message API.  */
   function
-  forward_pageid (state, linkid)
+  on_message (event)
   {
-    var data = state.loaded_nodes[linkid];
-    if (!data)
-      throw new Error ("page not loaded: " + linkid);
-    else if (!data.forward)
-      return null;
-    else
+    var data = event.data;
+    if (data.message_kind === "update-sidebar")
       {
-        var cur = linkid_split (linkid);
-        var fwd = linkid_split (data.forward);
-        if (!fwd.hash && fwd.pageid !== cur.pageid)
-          return fwd.pageid;
-        else
-          return forward_pageid (state, data.forward);
+        var toc_div = document.getElementById ("slider");
+
+        /* Reset previous calls to 'scan_toc'.  */
+        depth_first_walk (toc_div, function clear_toc_styles (elem) {
+          elem.removeAttribute ("toc-detail");
+          elem.removeAttribute ("toc-current");
+        }, Node.ELEMENT_NODE);
+
+        /* Remove the hash part for the main page.  */
+        var pageid = linkid_split (data.selected).pageid;
+        var selected = (pageid === config.INDEX_ID) ? pageid : data.selected;
+        /* Highlight the current LINKID in the table of content.  */
+        var elem = scan_toc (toc_div, selected, data.section_hash);
+        if (elem)
+          elem.scrollIntoView (true);
       }
   }
 
-  /** Highlight text in NODE which match RGXP.
-      @arg {RegExp} rgxp
-      @arg {Text} node */
+  return {
+    on_load: on_load,
+    on_message: on_message
+  };
+}
+
+/** Initialize iframes which contain pages of the manual.  */
+function
+init_iframe ()
+{
+  /* Initialize the DOM for generic pages loaded in the context of an
+     iframe.  */
   function
-  highlight_text (rgxp, node)
+  on_load ()
   {
-    /* Skip elements corresponding to highlighted words to avoid infinite
-       recursion.  */
-    if (node.parentElement.matches ("span.highlight"))
-      return;
-
-    var matches = rgxp.exec (node.textContent);
-    if (matches)
+    document.body.classList.add ("in-iframe");
+    fix_links (document.links);
+    var links = {};
+    var linkid = basename (window.location.pathname, /[.]x?html$/);
+    links[linkid] = navigation_links (document);
+    store.dispatch (actions.cache_links (links));
+    if (document.title)
+       store.dispatch (actions.window_title (document.title));
+
+    if (linkid_contains_index (linkid))
       {
-        /* Create an highlighted element containing first match.  */
-        var span = document.createElement ("span");
-        span.appendChild (document.createTextNode (matches[0]));
-        span.setAttribute ("id", "search-result");
-        span.classList.add ("highlight");
-
-        var right_node = node.splitText (matches.index);
-        /* Remove first match from right node.  */
-        right_node.textContent = right_node.textContent
-                                           .slice (matches[0].length);
-        node.parentElement.insertBefore (span, right_node);
+        /* Scan links that should be added to the index.  */
+        var index_entries = document.querySelectorAll
+          ("td.printindex-index-entry");
+        store.dispatch (actions.cache_index_links (index_entries));
       }
+
+    add_icons ();
+
+    /* Call user hook.  */
+    if (config.on_iframe_load)
+      config.on_iframe_load ();
   }
 
-  /** Remove every highlighted elements and inline their text content.
-      @arg {Element} elem */
+  /* Handle messages received via the Message API.  */
   function
-  remove_highlight (elem)
+  on_message (event)
   {
-    var spans = elem.getElementsByClassName ("highlight");
-    /* Replace spans with their inner text node.  */
-    while (spans.length > 0)
+    var data = event.data;
+    if (data.message_kind === "highlight")
+      remove_highlight (document.body);
+    else if (data.message_kind === "search")
+      {
+        var found = search (document.body, data.regexp);
+        store.dispatch ({ type: "search-result", found: found });
+      }
+    else if (data.message_kind === "scroll-to")
       {
-        var span = spans[0];
-        var parent = span.parentElement;
-        parent.replaceChild (span.firstChild, span);
+        /* Scroll to the anchor corresponding to HASH.  */
+        if (data.hash)
+          {
+            let elem = document.getElementById(data.hash.substring(1));
+            // Check if hash reference is to a sectioing element.  
+            // If not we need to find the outer sectioning element,
+            // so we can update the sidebar's ToC correctly.
+            if (elem)
+              {
+                let p = elem;
+                let section = null;
+                let id = null;
+                for (let p = elem;
+                     section === null && p instanceof Element;
+                      p = p.parentNode)
+                  {
+                    let sid = p.getAttribute("id");
+                    if (sid == null)
+                      continue;
+                    let cl = p.classList;
+                    for (let i = cl.length; --i >= 0; )
+                      {
+                        if (section_names.indexOf(cl.item(i)) >= 0)
+                          {
+                            section = p;
+                            id = sid;
+                            break;
+                          }
+                      }
+                  }
+                  if (section && section !== elem)
+                    {
+                      // Send section id to sidebar so it can properly update.
+                      store.dispatch({ type: "section", hash: data.hash, 
section_hash: id } );
+                    }
+              }
+              window.location.replace (data.hash);
+          }
+        else
+          window.scroll (0, 0);
       }
   }
 
-  /*--------------.
-  | Entry point.  |
-  `--------------*/
-
-  /** Depending on the role of the document launching this script, different
-      event handlers are registered.  This script can be used in the context 
of:
-
-      - the index page of the manual which manages the state of the application
-      - the iframe which contains the lateral table of content
-      - other iframes which contain other pages of the manual
-
-      This is done to allow referencing the same script inside every HTML page.
-      This has the benefits of reducing the number of HTTP requests required to
-      fetch the Javascript code and simplifying the work of the Texinfo HTML
-      converter.  */
-
-  /* Display MSG bail out message portably.  */
+  return {
+    on_load: on_load,
+    on_message: on_message
+  };
+}
+
+/*-------------------------
+| Common event handlers.  |
+`------------------------*/
+
+/** Handle click events.  */
+function
+on_click (event)
+{
+  let in_sidebar = event.target.matches ("#slider *");
+  for (var target = event.target; target !== null; target = target.parentNode)
+    {
+      if (! (target instanceof Element))
+        continue;
+      if (target.matches ("a"))
+        {
+          var href = link_href(target);
+          if (href && maybe_pageref_url_p (href)
+                && !external_manual_url_p (href))
+            {
+              var linkid = href_hash (href) || config.INDEX_ID;
+              if (linkid === "index.SEC_Contents")
+                  linkid = config.CONTENTS_ID;
+              store.dispatch (actions.set_current_url (linkid, null,
+                                                       in_sidebar ? 
"in-sidebar" : "in-page"));
+              event.preventDefault ();
+              event.stopPropagation ();
+              break;
+            }
+        }
+      if (target.matches (".sidebar-hider"))
+        {
+            let body = document.body;
+            let show = body.getAttribute("show-sidebar");
+            show_sidebar (show==="no");
+            return;
+        }
+    }
+  if (! in_sidebar)
+    hide_sidebar_if_narrow ();
+}
+
+// Only valid when showing sidebar.
+function is_narrow_window ()
+{
+  return document.body.firstChild.offsetLeft == 0;
+}
+
+function show_sidebar (show)
+{
+  store.dispatch({ type: "show-sidebar", show: show ? "yes" : "no" });
+}
+
+function hide_sidebar_if_narrow ()
+{
+  store.dispatch({ type: "show-sidebar", show: "hide-if-narrow" });
+}
+
+/** Handle unload events.  */
+function
+on_unload ()
+{
+  /* Cross origin requests are not supported in file protocol.  */
+  if (window.location.protocol !== "file:")
+  {
+    var request = new XMLHttpRequest ();
+    request.open ("GET", "(WINDOW-CLOSED)");
+    request.send (null);
+  }
+}
+
+/** Handle Keyboard 'keydown' events.
+    @arg {KeyboardEvent} event */
+function
+on_keydown (event)
+{
+  if (is_escape_key (event.key))
+    store.dispatch ({ type: "unfocus" });
+  else
+    {
+      var val = on_keydown.dict[event.key];
+      if (val)
+        {
+          if (typeof val === "function")
+            val ();
+          else
+            store.dispatch (val);
+          event.preventDefault();
+        }
+    }
+}
+
+/* Associate an Event 'key' property to an action or a thunk.  */
+on_keydown.dict = {
+  i: actions.show_text_input ("index"),
+  l: window.history.back.bind (window.history),
+  m: actions.show_text_input ("menu"),
+  n: actions.navigate ("next"),
+  p: actions.navigate ("prev"),
+  r: window.history.forward.bind (window.history),
+  s: actions.show_text_input ("regexp-search"),
+  t: actions.set_current_url_pointer ("*TOP*"),
+  u: actions.navigate ("up"),
+  "]": actions.navigate ("forward"),
+  "[": actions.navigate ("backward"),
+  "<": actions.set_current_url_pointer ("*TOP*"),
+  ">": actions.set_current_url_pointer ("*END*"),
+  "?": actions.show_help ()
+};
+
+/** Some standard methods used in this script might not be implemented by
+    the current browser.  If that is the case then augment the prototypes
+    appropriately.  */
+function
+register_polyfills ()
+{
   function
-  error (msg)
+  includes (search, start)
   {
-    /* XXX: This code needs to be highly portable.
-       Check <https://quirksmode.org/dom/core/> for details.  */
-    return function () {
-        var div = document.createElement ("div");
-        div.setAttribute ("class", "error");
-        div.innerHTML = msg;
-        var elem = document.body.firstChild;
-        document.body.insertBefore (div, elem);
-        window.setTimeout (function () {
-          document.body.removeChild (div);
-        }, config.WARNING_TIMEOUT);
-      };
+    start = start || 0;
+    return ((start + search.length) <= this.length)
+      && (this.indexOf (search, start) !== -1);
   }
 
-  /* Check if current browser supports the minimum requirements required for
-     properly using this script, otherwise bails out.  */
-  if (features && !(features.es5
-                    && features.classlist
-                    && features.eventlistener
-                    && features.hidden
-                    && features.history
-                    && features.postmessage
-                    && features.queryselector))
+  /* eslint-disable no-extend-native */
+  if (!Array.prototype.includes)
+    Array.prototype.includes = includes;
+
+  if (!String.prototype.includes)
+    String.prototype.includes = includes;
+
+  if (!String.prototype.endsWith)
     {
-      window.onload = error ("'info.js' is not compatible with this browser");
-      return;
+      String.prototype.endsWith = function endsWith (search, position) {
+        var subject_string = this.toString ();
+        if (typeof position !== "number"
+            || !isFinite (position)
+            || Math.floor (position) !== position
+            || position > subject_string.length)
+          position = subject_string.length;
+
+        position -= search.length;
+        var last_index = subject_string.lastIndexOf (search, position);
+        return last_index !== -1 && last_index === position;
+      };
     }
 
-  /* Until we have a responsive design implemented, fallback to basic
-     HTML navigation for small screen.  */
-    /*
-  if (window.screen.availWidth < config.SCREEN_MIN_WIDTH)
+  if (!Element.prototype.matches)
     {
-      window.onload =
-        error ("screen width is too small to display the table of content");
-      return;
+      Element.prototype.matches = Element.prototype["matchesSelector"]
+        || Element.prototype["mozMatchesSelector"]
+        || Element.prototype["mozMatchesSelector"]
+        || Element.prototype["msMatchesSelector"]
+        || Element.prototype["webkitMatchesSelector"]
+        || function element_matches (str) {
+          var document = (this.document || this.ownerDocument);
+          var matches = document.querySelectorAll (str);
+          var i = matches.length;
+          while ((i -= 1) >= 0 && matches.item (i) !== this);
+          return i > -1;
+        };
     }
-*/
-
-  register_polyfills ();
-  /* Let the config provided by the user mask the default one.  */
-  config = Object.assign (config, user_config);
 
-  var inside_iframe = top !== window;
-  var inside_index_page = window.location.pathname === config.INDEX_NAME
-      || window.location.pathname.endsWith ("/" + config.INDEX_NAME)
-      || window.location.pathname.endsWith ("/");
-
-  if (inside_index_page)
+  if (typeof Object.assign != "function")
     {
-      var initial_state = {
-        /* Dictionary associating page ids to next, prev, up, forward,
-           backward link ids.  */
-        loaded_nodes: {},
-        /* Dictionary associating keyword to linkids.  */
-        index: undefined,
-        /* page id of the current page.  */
-        current: config.INDEX_ID,
-        /* dictionary associating a page id to a boolean.  */
-        ready: {},
-        /* Current mode for handling history.  */
-        history: "replaceState",
-        /* Define the name of current text input.  */
-        text_input: null
+      Object.assign = function assign (target) {
+        if (undef_or_null (target))
+          throw new TypeError ("Cannot convert undefined or null to object");
+
+        var to = Object (target);
+        for (var index = 1; index < arguments.length; index += 1)
+          {
+            var next_source = arguments[index];
+            if (undef_or_null (next_source))
+              continue;
+            for (var key in next_source)
+              {
+                /* Avoid bugs when hasOwnProperty is shadowed.  */
+                if (Object.prototype.hasOwnProperty.call (next_source, key))
+                  to[key] = next_source[key];
+              }
+          }
+        return to;
       };
+    }
 
-      store = new Store (updater, initial_state);
-      var index = init_index_page ();
-      var sidebar = init_sidebar ();
-      window.addEventListener ("DOMContentLoaded", function () {
-        index.on_load ();
-        sidebar.on_load ();
-      }, false);
-      window.addEventListener ("message", index.on_message, false);
-      window.addEventListener ("message", sidebar.on_message, false);
-      window.onpopstate = index.on_popstate;
+  (function (protos) {
+    protos.forEach (function (proto) {
+      if (!proto.hasOwnProperty ("remove"))
+        {
+          Object.defineProperty (proto, "remove", {
+            configurable: true,
+            enumerable: true,
+            writable: true,
+            value: function value () {
+              this.parentNode.removeChild (this);
+            }
+          });
+        }
+    });
+  } ([Element.prototype, CharacterData.prototype, DocumentType.prototype]));
+  /* eslint-enable no-extend-native */
+}
+
+/*---------------------.
+| Common utilitaries.  |
+`---------------------*/
+
+/** Check portably if KEY correspond to "Escape" key value.
+    @arg {string} key */
+function
+is_escape_key (key)
+{
+  /* In Internet Explorer 9 and Firefox 36 and earlier, the Esc key
+     returns "Esc" instead of "Escape".  */
+  return key === "Escape" || key === "Esc";
+}
+
+/** Check if OBJ is equal to 'undefined' or 'null'.  */
+function
+undef_or_null (obj)
+{
+  return (obj === null || typeof obj === "undefined");
+}
+
+/** Return a relative URL corresponding to HREF, which refers to an anchor
+    of 'config.INDEX_NAME'.  HREF must be a USVString representing an
+    absolute or relative URL.  For example "foo/bar.html" will return
+    "config.INDEX_NAME#bar".
+
+    @arg {string} href.*/
+function
+with_sidebar_query (href)
+{
+  if (basename (href) === config.INDEX_NAME)
+    return config.INDEX_NAME;
+  else
+    {
+      /* XXX: Do not use the URL API for IE portability.  */
+      var url = with_sidebar_query.url;
+      url.setAttribute ("href", href);
+      var new_hash = "#" + basename (url.pathname, /[.]x?html/);
+      /* XXX: 'new_hash !== url.hash' is a workaround to work with links
+         produced by makeinfo which link to an anchor element in a page
+         instead of directly to the page. */
+      if (url.hash && new_hash !== url.hash)
+        new_hash += ("." + url.hash.slice (1));
+      return config.INDEX_NAME + new_hash;
+    }
+}
+
+/* Use the same DOM element for every function call.  */
+with_sidebar_query.url = document.createElement ("a");
+
+/** Modify LINKS to handle the iframe based navigation properly.  Relative
+    links will be opened inside the corresponding iframe and absolute links
+    will be opened in a new tab.  If ID is true then define an "id"
+    attribute with a linkid value for relative links.
+
+    @typedef {HTMLAnchorElement|HTMLAreaElement} Links
+    @arg {Links[]|HTMLCollectionOf<Links>} links
+    @arg {boolean} [id]
+    @return void  */
+function
+fix_links (links, id)
+{
+  for (var i = 0; i < links.length; i += 1)
+    {
+      var link = links[i];
+      var href = link_href(link);
+      if (!href)
+        continue;
+      else if (external_manual_url_p (href))
+        link.setAttribute ("target", "_top");
+      else if (! maybe_pageref_url_p (href))
+        link.setAttribute ("target", "_blank");
+      else
+        {
+          var href$ = with_sidebar_query (href);
+          if (href !== href$)
+            {
+              let protocol = location.protocol;
+              if (protocol == "https:" || protocol == "http:")
+                link._href = href$;
+              else
+                link.setAttribute ("href", href$);
+            }
+          if (id)
+            {
+              var linkid = (href$ === config.INDEX_NAME) ?
+                  config.INDEX_ID : href_hash (href$);
+              link.setAttribute ("id", linkid);
+            }
+        }
+    }
+}
+
+/** Convert HASH which is something that can be found 'Location.hash' to a
+    "linkid" which can be handled in our model.
+    @arg {string} hash
+    @return {string} linkid */
+function
+normalize_hash (hash)
+{
+  var text = hash.slice (1);
+  /* Some anchor elements are present in 'config.INDEX_NAME' and we need to
+     handle link to them specially (i.e. not try to find their corresponding
+     iframe).*/
+  if (config.MAIN_ANCHORS.includes (text))
+    return config.INDEX_ID + "." + text;
+  else
+    return text;
+}
+
+/** Return an object composed of the filename and the anchor of LINKID.
+    LINKID can have the form "foobar.anchor" or just "foobar".
+    @arg {string} linkid
+    @return {{pageid: string, hash: string}} */
+function
+linkid_split (linkid)
+{
+  if (!linkid.includes ("."))
+    return { pageid: linkid, hash: "" };
+  else
+    {
+      var ref = linkid.match (/^(.+)\.(.*)$/).slice (1);
+      return { pageid: ref[0], hash: "#" + ref[1] };
+    }
+}
+
+/** Convert LINKID which has the form "foobar.anchor" or just "foobar", to
+    an URL of the form `foobar${config.EXT}#anchor`.
+    @arg {string} linkid */
+function
+linkid_to_url (linkid)
+{
+  if (linkid === config.INDEX_ID)
+    return config.INDEX_NAME;
+  else
+    {
+      var link = linkid_split (linkid);
+      return link.pageid + config.EXT + link.hash;
+    }
+}
+
+/** Check if 'URL' may be a cross-reference to another page in this manual. */
+function
+maybe_pageref_url_p (url)
+{
+  return url.match(config.LOCAL_HTML_PAGE_PATTERN);
+}
+
+/** Check if 'URL' is a link to another manual.  For locally installed 
+    manuals only. */
+function
+external_manual_url_p (url)
+{
+  if (typeof url !== "string")
+    throw new TypeError ("'" + url + "' is not a string");
+
+  return url.match(/^..\//);
+}
+
+/** Return PATHNAME with any leading directory components removed.  If
+    specified, also remove a trailing SUFFIX.  */
+function
+basename (pathname, suffix)
+{
+  var res = pathname.replace (/.*[/]/, "");
+  if (!suffix)
+    return res;
+  else if (suffix instanceof RegExp)
+    return res.replace (suffix, "");
+  else /* typeof SUFFIX === "string" */
+    return res.replace (new RegExp ("[.]" + suffix), "");
+}
+
+/** Strip last component from PATHNAME and keep the trailing slash. For
+    example if PATHNAME is "/foo/bar/baz.html" then return "/foo/bar/".
+    @arg {string} pathname */
+function
+dirname (pathname)
+{
+  var res = pathname.match (/\/?.*\//);
+  if (res)
+    return res[0];
+  else
+    throw new Error ("'location.pathname' is not recognized");
+}
+
+/** Apply FUNC to each nodes in the NODE subtree.  The walk follows a depth
+    first algorithm.  Only consider the nodes of type NODE_TYPE.  */
+function
+depth_first_walk (node, func, node_type)
+{
+  if (!node_type || (node.nodeType === node_type))
+    func (node);
+
+  for (var child = node.firstChild; child; child = child.nextSibling)
+    depth_first_walk (child, func, node_type);
+}
+
+/** Return the "effective" href attribute of a link (a) element. */
+function
+link_href (alink)
+{
+  return alink._href || alink.getAttribute("href");
+}
+
+/** Return the hash part of HREF without the '#' prefix.  HREF must be a
+    string.  If there is no hash part in HREF then return the empty
+    string.  */
+function
+href_hash (href)
+{
+  if (typeof href !== "string")
+    throw new TypeError (href + " is not a string");
+
+  if (href.includes ("#"))
+    return href.replace (/.*#/, "");
+  else
+    return "";
+}
+
+/** Check if LINKID corresponds to a page containing index links.  */
+function
+linkid_contains_index (linkid)
+{
+  return linkid.match (/^.*-index$/i) || linkid.match (/^Index$/);
+}
+
+/** Retrieve PREV, NEXT, and UP links, and local menu from CONTENT and
+    return an object containing references to those links.  CONTENT must be
+    an object implementing the ParentNode interface (Element,
+    Document...).  */
+function
+navigation_links (content)
+{
+  var links = content.querySelectorAll ("a[accesskey][href]");
+  var res = {};
+  /* links have the form MAIN_FILE.html#FRAME-ID.  For convenience
+     only store FRAME-ID.  */
+  for (var i = 0; i < links.length; i += 1)
+    {
+      var link = links[i];
+      var nav_id = navigation_links.dict[link.getAttribute ("accesskey")];
+      if (nav_id)
+        {
+          var href = basename (link_href (link));
+          if (href === config.INDEX_NAME)
+            res[nav_id] = config.INDEX_ID;
+          else
+            res[nav_id] = href_hash (href);
+        }
+      else /* this link is part of local table of content. */
+        {
+          res.menu = res.menu || {};
+          res.menu[link.text] = href_hash (link_href (link));
+        }
     }
-  else if (inside_iframe)
+
+    return res;
+}
+
+navigation_links.dict = { n: "next", p: "prev", u: "up" };
+
+function
+add_icons ()
+{
+  var div = document.createElement ("div");
+  div.setAttribute ("id", "icon-bar");
+  var span = document.createElement ("span");
+  span.innerHTML = "?";
+  // Set tool-tip (on hover)
+  span.setAttribute ("title", "Help for keyboard shortcuts");
+  span.classList.add ("icon");
+  span.addEventListener ("click", function () {
+    store.dispatch (actions.show_help ());
+  }, false);
+  div.appendChild (span);
+  document.body.insertBefore (div, document.body.firstChild);
+}
+
+/** Check if ELEM matches SEARCH
+    @arg {Element} elem
+    @arg {RegExp} rgxp
+    @return {boolean} */
+function
+search (elem, rgxp)
+{
+  /** @type {Text} */
+  var text = null;
+
+  /** @arg {Text} node */
+  function
+  find (node)
+  {
+    if (rgxp.test (node.textContent))
+      {
+        /* Ignore previous match.  */
+        var prev = node.parentElement.matches ("span.highlight");
+        text = (prev) ? null : (text || node);
+      }
+  }
+
+  depth_first_walk (elem, find, Node.TEXT_NODE);
+  remove_highlight (elem);
+  if (!text)
+    return false;
+  else
     {
-      store = new Remote_store ();
-      var iframe = init_iframe ();
-      window.addEventListener ("DOMContentLoaded", iframe.on_load, false);
-      window.addEventListener ("message", iframe.on_message, false);
+      highlight_text (rgxp, text);
+      return true;
     }
+}
+
+/** Find the pageid corresponding to forward direction.
+    @arg {any} state
+    @arg {string} linkid
+    @return {string} the forward pageid */
+function
+forward_pageid (state, linkid)
+{
+  var data = state.loaded_nodes[linkid];
+  if (!data)
+    throw new Error ("page not loaded: " + linkid);
+  else if (!data.forward)
+    return null;
   else
     {
-      /* Jump to 'config.INDEX_NAME' and adapt the selected iframe.  */
-      window.location.replace (with_sidebar_query (window.location.href));
+      var cur = linkid_split (linkid);
+      var fwd = linkid_split (data.forward);
+      if (!fwd.hash && fwd.pageid !== cur.pageid)
+        return fwd.pageid;
+      else
+        return forward_pageid (state, data.forward);
+    }
+}
+
+/** Highlight text in NODE which match RGXP.
+    @arg {RegExp} rgxp
+    @arg {Text} node */
+function
+highlight_text (rgxp, node)
+{
+  /* Skip elements corresponding to highlighted words to avoid infinite
+     recursion.  */
+  if (node.parentElement.matches ("span.highlight"))
+    return;
+
+  var matches = rgxp.exec (node.textContent);
+  if (matches)
+    {
+      /* Create an highlighted element containing first match.  */
+      var span = document.createElement ("span");
+      span.appendChild (document.createTextNode (matches[0]));
+      span.setAttribute ("id", "search-result");
+      span.classList.add ("highlight");
+
+      var right_node = node.splitText (matches.index);
+      /* Remove first match from right node.  */
+      right_node.textContent = right_node.textContent
+                                         .slice (matches[0].length);
+      node.parentElement.insertBefore (span, right_node);
+    }
+}
+
+/** Remove every highlighted elements and inline their text content.
+    @arg {Element} elem */
+function
+remove_highlight (elem)
+{
+  var spans = elem.getElementsByClassName ("highlight");
+  /* Replace spans with their inner text node.  */
+  while (spans.length > 0)
+    {
+      var span = spans[0];
+      var parent = span.parentElement;
+      parent.replaceChild (span.firstChild, span);
     }
+}
+
+/*--------------.
+| Entry point.  |
+`--------------*/
+
+/** Depending on the role of the document launching this script, different
+    event handlers are registered.  This script can be used in the context of:
+
+    - the index page of the manual which manages the state of the application
+    - the iframe which contains the lateral table of content
+    - other iframes which contain other pages of the manual
+
+    This is done to allow referencing the same script inside every HTML page.
+    This has the benefits of reducing the number of HTTP requests required to
+    fetch the Javascript code and simplifying the work of the Texinfo HTML
+    converter.  */
+
+/* Display MSG bail out message portably.  */
+function
+error (msg)
+{
+  /* XXX: This code needs to be highly portable.
+     Check <https://quirksmode.org/dom/core/> for details.  */
+  return function () {
+      var div = document.createElement ("div");
+      div.setAttribute ("class", "error");
+      div.innerHTML = msg;
+      var elem = document.body.firstChild;
+      document.body.insertBefore (div, elem);
+      window.setTimeout (function () {
+        document.body.removeChild (div);
+      }, config.WARNING_TIMEOUT);
+    };
+}
+
+/* Check if current browser supports the minimum requirements required for
+   properly using this script, otherwise bails out.  */
+if (features && !(features.es5
+                  && features.classlist
+                  && features.eventlistener
+                  && features.hidden
+                  && features.history
+                  && features.postmessage
+                  && features.queryselector))
+  {
+    window.onload = error ("'info.js' is not compatible with this browser");
+    return;
+  }
+
+/* Until we have a responsive design implemented, fallback to basic
+   HTML navigation for small screen.  */
+  /*
+if (window.screen.availWidth < config.SCREEN_MIN_WIDTH)
+  {
+    window.onload =
+      error ("screen width is too small to display the table of content");
+    return;
+  }
+*/
+
+register_polyfills ();
+/* Let the config provided by the user mask the default one.  */
+config = Object.assign (config, user_config);
+
+var inside_iframe = top !== window;
+var inside_index_page = window.location.pathname === config.INDEX_NAME
+    || window.location.pathname.endsWith ("/" + config.INDEX_NAME)
+    || window.location.pathname.endsWith ("/");
+
+if (inside_index_page)
+  {
+    var initial_state = {
+      /* Dictionary associating page ids to next, prev, up, forward,
+         backward link ids.  */
+      loaded_nodes: {},
+      /* Dictionary associating keyword to linkids.  */
+      index: undefined,
+      /* page id of the current page.  */
+      current: config.INDEX_ID,
+      /* dictionary associating a page id to a boolean.  */
+      ready: {},
+      /* Current mode for handling history.  */
+      history: "replaceState",
+      /* Define the name of current text input.  */
+      text_input: null
+    };
+
+    store = new Store (updater, initial_state);
+    var index = init_index_page ();
+    var sidebar = init_sidebar ();
+    window.addEventListener ("DOMContentLoaded", function () {
+      index.on_load ();
+      sidebar.on_load ();
+    }, false);
+    window.addEventListener ("message", index.on_message, false);
+    window.addEventListener ("message", sidebar.on_message, false);
+    window.onpopstate = index.on_popstate;
+  }
+else if (inside_iframe)
+  {
+    store = new Remote_store ();
+    var iframe = init_iframe ();
+    window.addEventListener ("DOMContentLoaded", iframe.on_load, false);
+    window.addEventListener ("message", iframe.on_message, false);
+  }
+else
+  {
+    /* Jump to 'config.INDEX_NAME' and adapt the selected iframe.  */
+    window.location.replace (with_sidebar_query (window.location.href));
+  }
 
-  /* Register common event handlers.  */
-  window.addEventListener ("beforeunload", on_unload, false);
-  window.addEventListener ("click", on_click, false);
-  /* XXX: handle 'keydown' event instead of 'keypress' since Chromium
-     doesn't handle the 'Escape' key properly.  See
-     https://bugs.chromium.org/p/chromium/issues/detail?id=9061.  */
-  window.addEventListener ("keydown", on_keydown, false);
+/* Register common event handlers.  */
+window.addEventListener ("beforeunload", on_unload, false);
+window.addEventListener ("click", on_click, false);
+/* XXX: handle 'keydown' event instead of 'keypress' since Chromium
+   doesn't handle the 'Escape' key properly.  See
+   https://bugs.chromium.org/p/chromium/issues/detail?id=9061.  */
+window.addEventListener ("keydown", on_keydown, false);
 } (window["Modernizr"], window["INFO_CONFIG"]));



reply via email to

[Prev in Thread] Current Thread [Next in Thread]