texinfo-commits
[Top][All Lists]
Advanced

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

[no subject]


From: Gavin D. Smith
Date: Wed, 30 Nov 2022 13:36:45 -0500 (EST)

branch: old/qt-info
commit e50e97df7052e7d6f22f48ff1da46cab6db6d58f
Author: Gavin Smith <gavinsmith0123@gmail.com>
AuthorDate: Sat Apr 20 21:33:58 2019 +0100

    exdent body of info.js two spaces
---
 js/info.js | 3485 ++++++++++++++++++++++++++++++------------------------------
 1 file changed, 1743 insertions(+), 1742 deletions(-)

diff --git a/js/info.js b/js/info.js
index ca0af0be9d..a703a2a362 100644
--- a/js/info.js
+++ b/js/info.js
@@ -31,410 +31,410 @@ var user_config = window["INFO_CONFIG"];
 // (function (features, user_config) {
 
 
-  /*-------------------.
-  | Define constants.  |
-  `-------------------*/
-
-  /** To override those default parameters, define 'INFO_CONFIG' in the global
-      environment before loading this script.  */
-  var config = {
-    EXT: ".html",
-    TOP_NAME: "index.html",
-    TOP_ID: "index",
-    MAIN_ANCHORS: ["Top", "SEC_Contents"],
-    WARNING_TIMEOUT: 3000,
-    SCREEN_MIN_WIDTH: 700,
-    hooks: {
-      /** Define a function called after 'DOMContentLoaded' event in
-          the TOP_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
-    },
-    show_welcome_message: true,
-    embedded_help: true
-  };
-
-  /*-------------------.
-  | 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;
-
-  /** Create a Store that calls its listeners at each state change.
-      @arg {function (Object, Action): Object} reducer
-      @arg {Object} state  */
-  function
-  Store (state)
-  {
-    this.listeners = [];
-    this.state = state;
-  }
-
-  /** @arg {Action} action */
-  Store.prototype.dispatch = function dispatch (action) {
-    var new_state = updater (this.state, action);
-    if (new_state !== this.state)
+/*-------------------.
+| Define constants.  |
+`-------------------*/
+
+/** To override those default parameters, define 'INFO_CONFIG' in the global
+    environment before loading this script.  */
+var config = {
+  EXT: ".html",
+  TOP_NAME: "index.html",
+  TOP_ID: "index",
+  MAIN_ANCHORS: ["Top", "SEC_Contents"],
+  WARNING_TIMEOUT: 3000,
+  SCREEN_MIN_WIDTH: 700,
+  hooks: {
+    /** Define a function called after 'DOMContentLoaded' event in
+        the TOP_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
+  },
+  show_welcome_message: true,
+  embedded_help: true
+};
+
+/*-------------------.
+| 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;
+
+/** Create a Store that calls its listeners at each state change.
+    @arg {function (Object, Action): Object} reducer
+    @arg {Object} state  */
+function
+Store (state)
+{
+  this.listeners = [];
+  this.state = state;
+}
+
+/** @arg {Action} action */
+Store.prototype.dispatch = function dispatch (action) {
+  var new_state = updater (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 = top;
+}
+
+/** 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) {
+    if (undef_or_null (history))
+      history = "pushState";
+    return { type: "current-url", url: linkid, history: history };
+  },
+
+  /** 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" };
+  },
+
+  external_manual: function (linkid, history) {
+    return { type: "external-manual", url: linkid };
+  },
+
+  /** @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 = {};
+    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];
+        dict[link.textContent] = link.getAttribute ("href");
       }
-  };
+    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 };
+  },
+
+  /** 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 = top;
+    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) {
-      if (undef_or_null (history))
-        history = "pushState";
-      return { type: "current-url", url: linkid, history: history };
-    },
-
-    /** 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" };
-    },
-
-    external_manual: function (linkid, history) {
-      return { type: "external-manual", url: linkid };
-    },
-
-    /** @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 = {};
-      for (var i = 0; i < links.length; i += 1)
-        {
-          var link = links[i];
-          dict[link.textContent] = link.getAttribute ("href");
-        }
-      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 };
-    },
-
-    /** 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
-      functional 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
+    functional 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":
-        {
-          res.index = Object.assign ({}, res.index, action.links);
-          return res;
-        }
-      case "current-url":
-        {
-          linkid = (action.pointer) ?
-              state.loaded_nodes[action.pointer] : action.url;
-
-          linkid = href_hash(with_sidebar_query(linkid));
-
-          res.current = linkid;
-          res.history = action.history;
-          res.text_input = null;
-          res.warning = 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 "external-manual":
-        {
-          // This is expected to replace the entire page, therefore the
-          // following return statement shouldn't run.
-          window.location.href = action.url;
-          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 "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.TOP_NAME' anchors specially.  */
-          if (link.pageid === config.TOP_ID)
-            current = config.TOP_ID;
+        return Object.assign (res, { loaded_nodes: nodes });
+      }
+    case "cache-index-links":
+      {
+        res.index = Object.assign ({}, res.index, action.links);
+        return res;
+      }
+    case "current-url":
+      {
+        linkid = (action.pointer) ?
+            state.loaded_nodes[action.pointer] : action.url;
+
+        linkid = href_hash(with_sidebar_query(linkid));
+
+        res.current = linkid;
+        res.history = action.history;
+        res.text_input = null;
+        res.warning = 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 "external-manual":
+      {
+        // This is expected to replace the entire page, therefore the
+        // following return statement shouldn't run.
+        window.location.href = action.url;
+        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 "navigate":
+      {
+        var current = state.current;
+        var link = linkid_split (state.current);
 
-          var ids = state.loaded_nodes[current];
-          linkid = ids[action.direction];
-          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.TOP_NAME' anchors specially.  */
+        if (link.pageid === config.TOP_ID)
+          current = config.TOP_ID;
 
-          if (!linkid)
-            return state;
-          else
-            {
-              if (linkid === "Top")
-                {
-                  /* This if the link is coming from a file that has not
-                     had fix_links called on it. */
-                  linkid = config.TOP_ID;
-                }
-              res.current = linkid;
-              res.history = action.history;
-              res.text_input = null;
-              res.warning = 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.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)
+          {
+            /* 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 "warning":
-        {
-          res.warning = action.msg;
-          if (action.msg !== null)
+        if (!linkid)
+          return state;
+        else
+          {
+            if (linkid === "Top")
+              {
+                /* This if the link is coming from a file that has not
+                   had fix_links called on it. */
+                linkid = config.TOP_ID;
+              }
+            res.current = linkid;
+            res.history = action.history;
             res.text_input = null;
-          return res;
-        }
-      default:
-        console.error ("unhandled action type:", action.type);
-        return state;
+            res.warning = 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.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 "warning":
+      {
+        res.warning = action.msg;
+        if (action.msg !== null)
+          res.text_input = null;
+        return res;
+      }
+    default:
+      console.error ("unhandled 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_top_page ()
+{
+  /*--------------------------.
+  | Component for help page.  |
+  `--------------------------*/
 
-  /** Initialize the index page of the manual which manages the state of the
-      application.  */
   function
-  init_top_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>\
@@ -469,1530 +469,1531 @@ var user_config = window["INFO_CONFIG"];
 </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 ();
-        }
-    };
-
-    /*---------------------------------.
-    | Components for menu navigation.  |
-    `---------------------------------*/
-
-    /** @arg {string} id */
-    function
-    Search_input (id)
-    {
-      this.id = id;
-      this.render = null;
-      this.prompt = document.createTextNode ("Text search" + ": ");
-
-      /* 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 ("keyup", (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));
-    }
-
-    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 ();
-    };
-
-    Search_input.prototype.hide = function hide () {
-      this.element.setAttribute ("hidden", "true");
-      this.input.value = "";
-    };
-
-    /** @arg {string} id */
-    function
-    Text_input (id)
-    {
-      this.id = id;
-      this.data = null;
-      this.datalist = null;
-      this.render = null;
-
-      /* 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 ("keyup", (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)
-              store.dispatch (actions.set_current_url (linkid));
-          }
-        event.stopPropagation ();
-      }).bind (this));
-    }
+    var span = div.querySelector (".close");
+    div.addEventListener ("click", function (event) {
+      if (event.target === span || event.target === div)
+        store.dispatch ({ type: "unfocus" });
+    }, false);
 
-    /* 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 ();
-    };
+    this.element = div;
+  }
 
-    Text_input.prototype.hide = function hide () {
-      this.element.setAttribute ("hidden", "true");
-      this.input.value = "";
-      if (this.datalist)
-        {
-          this.datalist.remove ();
-          this.datalist = null;
-        }
-    };
+  Help_page.prototype.render = function render (state) {
+    if (!state.help)
+      this.element.style.display = "none";
+    else
+      {
+        this.element.style.display = "block";
+        this.element.focus ();
+      }
+  };
 
-    function
-    Minibuffer ()
-    {
-      /* Create global container.*/
-      var elem = document.createElement ("div");
-      elem.classList.add ("text-input");
+  /*---------------------------------.
+  | Components for menu navigation.  |
+  `---------------------------------*/
 
-      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"));
-        }
-      };
+  /** @arg {string} id */
+  function
+  Search_input (id)
+  {
+    this.id = id;
+    this.render = null;
+    this.prompt = document.createTextNode ("Text search" + ": ");
 
-      var index = new Text_input ("index");
-      index.render = function (state) {
-        if (state.text_input === "index")
-          this.show (state.index);
-      };
+    /* 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 ("keyup", (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));
+  }
 
-      var search = new Search_input ("regexp-search");
-      search.render = function (state) {
-        if (state.text_input === "regexp-search")
-          this.show ();
-      };
+  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 ();
+  };
 
-      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;
-    }
+  Search_input.prototype.hide = function hide () {
+    this.element.setAttribute ("hidden", "true");
+    this.input.value = "";
+  };
 
-    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;
-        }
+  /** @arg {string} id */
+  function
+  Text_input (id)
+  {
+    this.id = id;
+    this.data = null;
+    this.datalist = null;
+    this.render = null;
 
-      if (!state.text_input || state.warning)
-        {
-          this.menu.hide ();
-          this.index.hide ();
-          this.search.hide ();
-        }
-      else
+    /* 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 ("keyup", (function (event) {
+      if (is_escape_key (event.key))
+        store.dispatch ({ type: "unfocus" });
+      else if (event.key === "Enter")
         {
-          this.index.render (state);
-          this.menu.render (state);
-          this.search.render (state);
+          var linkid = this.data[this.input.value];
+          if (linkid)
+            store.dispatch (actions.set_current_url (linkid));
         }
-    };
-
-    function
-    Echo_area ()
-    {
-      var elem = document.createElement ("div");
-      elem.classList.add ("echo-area");
-      elem.setAttribute ("hidden", "true");
-
-      this.element = elem;
-      this.toid = null;
-    }
+      event.stopPropagation ();
+    }).bind (this));
+  }
 
-    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;
-        }
-    };
+  /* 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 ();
+  };
 
-    /*----------------------------.
-    | Component for the sidebar.  |
-    `----------------------------*/
+  Text_input.prototype.hide = function hide () {
+    this.element.setAttribute ("hidden", "true");
+    this.input.value = "";
+    if (this.datalist)
+      {
+        this.datalist.remove ();
+        this.datalist = null;
+      }
+  };
 
-    function
-    Sidebar ()
-    {
-      this.element = document.createElement ("div");
-      this.element.setAttribute ("id", "slider");
-      var div = document.createElement ("div");
-      div.classList.add ("toc-sidebar");
-      var toc = document.querySelector (".contents");
-      toc.remove ();
-
-      /* Move contents of <body> into a a fresh <div> to let the components
-         treat the top 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);
-
-      /* Remove table of contents header.  */
-      document.querySelector (".contents-heading").remove ();
-
-      /* Also remove any short table of contents.  Links therein would
-         go to the table of contents, which we have just removed. */
-      function maybe_remove (query)
-        {
-          var element = document.querySelector (query);
-          if (element)
-            element.remove ();
-        }
-      maybe_remove (".shortcontents-heading");
-      maybe_remove (".shortcontents");
-    }
+  function
+  Minibuffer ()
+  {
+    /* Create global container.*/
+    var elem = document.createElement ("div");
+    elem.classList.add ("text-input");
 
-    /* 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'.*/
-      var msg = { message_kind: "update-sidebar", selected: state.current };
-      window.postMessage (msg, "*");
+    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"));
+      }
     };
 
-    /*--------------------------.
-    | Component for the pages.  |
-    `--------------------------*/
-
-    /** @arg {HTMLDivElement} top_div */
-    function
-    Pages (top_div)
-    {
-      top_div.setAttribute ("id", config.TOP_ID);
-      top_div.setAttribute ("node", config.TOP_ID);
-      top_div.setAttribute ("hidden", "true");
-      this.element = document.createElement ("div");
-      this.element.setAttribute ("id", "sub-pages");
-      this.element.appendChild (top_div);
-      /** @type {string[]} Currently created divs.  */
-      this.ids = [config.TOP_ID];
-      /** @type {string} */
-      this.prev_id = null;
-      /** @type {HTMLElement} */
-      this.prev_div = null;
-      this.prev_search = null;
-    }
-
-    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);
+    var index = new Text_input ("index");
+    index.render = function (state) {
+      if (state.text_input === "index")
+        this.show (state.index);
     };
 
-    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");
-
-      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);
-            }
-          var div = resolve_page (state.current, true);
-          /* Remove any anchor. */
-          var link = linkid_split (state.current);
-          state.current = link.pageid;
-          update_history (state.current, state.history);
-          this.prev_id = state.current;
-          this.prev_div = div;
-        }
-
-      if (state.search
-          && (this.prev_search !== state.search)
-          && state.search.status === "ready")
-        {
-          this.prev_search = state.search;
-          if (state.search.current_pageid === config.TOP_ID)
-            {
-              window.setTimeout (function () {
-                store.dispatch ({ type: "search-query" });
-                var res = search (document.getElementById (config.TOP_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.TOP_ID)
-            remove_highlight (document.getElementById (config.TOP_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;
-        }
+    var search = new Search_input ("regexp-search");
+    search.render = function (state) {
+      if (state.text_input === "regexp-search")
+        this.show ();
     };
 
-    /*--------------.
-    | 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 top
-         page.  */
-      if ((pageid === config.TOP_ID) && visible)
-        {
-          div.removeAttribute ("hidden");
-        }
-      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.TOP_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.TOP_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.TOP_NAME' DOM.  */
-    function
-    on_load ()
-    {
-      fix_links (document.links);
-      add_icons (document);
-      document.body.classList.add ("mainbar");
-
-      /* Move contents of <body> into a a fresh <div> to let the components
-         treat the top page like other iframe page.  */
-      var top_div = document.createElement ("div");
-      for (var ch = document.body.firstChild; ch; ch = 
document.body.firstChild)
-        top_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.TOP_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 ();
-                }
-            }
-        }
-      };
-
-      components.add (new Pages (top_div));
-      components.add (new Sidebar ());
-      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.TOP_ID] = navigation_links (document);
-      store.dispatch (actions.cache_links (links));
-      store.dispatch ({ type: "iframe-ready", id: config.TOP_ID });
-      if (config.show_welcome_message)
-        {
-          store.dispatch ({
-            type: "echo",
-            msg: "Welcome to Texinfo documentation viewer 6.1, type '?' for 
help."
-          });
-        }
-
-      /* Call user hook.  */
-      if (config.hooks.on_main_load)
-        config.hooks.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);
-        }
-      else if (data.message_kind === "scroll-to")
-        {
-          /* Used for text search.  Scroll to the anchor corresponding to 
-             HASH.  */
-          var anchor = data.hash.substr(1); /* Remove the #. */
-          document.getElementById(anchor).scrollIntoView ();
-
-          /* 'location.replace (data.hash);' triggers a DOMContentLoaded event
-             which has bad results for the top page. */
-
-        }
-    }
-
-    /* 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
-    };
+    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;
   }
 
-  /** 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
-    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");
-                }
-            }
-        }
-    }
-
-    /** Scan ToC entries to see which should be hidden.
-        @arg {HTMLElement} elem
-        @arg {string} linkid */
-    function
-    scan_toc (elem, url)
-    {
-      /** @type {Element} */
-      var res;
-
-      /** Set CURRENT to the node corresponding to URL linkid.
-          @arg {Element} elem */
-      function
-      find_current (elem)
+  Minibuffer.prototype.render = function render (state) {
+    if (!state.warning)
       {
-        /* XXX: No template literals for IE compatibility.  */
-        /* The ^= is a selector to account for the fragment identifier
-           which follows. */
-        if (elem.matches ("a[href^=\"" + url + "\\.html" + "\"]"))
-          {
-            elem.setAttribute ("toc-current", "yes");
-            var sib = elem.nextElementSibling;
-            if (sib && sib.matches ("ul"))
-              hide_grand_child_nodes (sib);
-            res = elem;
-          }
+        this.warn.setAttribute ("hidden", "true");
+        this.toid = null;
       }
-
-      var ul = elem.querySelector ("ul");
-      if (url === config.TOP_ID)
-        {
-          hide_grand_child_nodes (ul);
-          res = document.getElementById('sidebar_start');
-        }
-      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.TOP_ID;
-      var links = {};
-
-      function
-      add_link (elem)
+    else if (!this.toid)
       {
-        if (elem.matches ("a") && elem.hasAttribute ("href"))
-          {
-            var id = basename (remove_hash (elem.getAttribute ("href")),
-                               /[.]x?html$/);
-            links[prev_id] =
-              Object.assign ({}, links[prev_id], { forward: id });
-            links[id] = Object.assign ({}, links[id], { backward: prev_id });
-            prev_id = id;
-          }
+        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;
       }
 
-      depth_first_walk (nav, add_link, Node.ELEMENT_NODE);
-      /* Add a reference to the first and last node of the manual.  */
-      links["*TOP*"] = config.TOP_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)
-        {
-          var header = document.createElement ("header");
-          var a = document.createElement ("a");
-          a.setAttribute ("href", config.TOP_NAME);
-          a.setAttribute ("id", "sidebar_start");
-          header.appendChild (a);
-          var div = document.createElement ("div");
-          a.appendChild (div);
-          var span = document.createElement ("span");
-          span.textContent = h1.textContent;
-          div.appendChild (span);
-          elem.parentElement.insertBefore (header, elem);
-        }
-    }
-
-    /*---------------------------------.
-    | Event handlers for the sidebar   |
-    `---------------------------------*/
-
-    /* Initialize sidebar. */
-    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.TOP_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);
-
-          /* Highlight the current LINKID in the table of content.  */
-          var elem = scan_toc (toc_div, data.selected);
-          if (elem)
-            elem.scrollIntoView (true);
-        }
-    }
-
-    return {
-      on_load: on_load,
-      on_message: on_message
-    };
-  }
+    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 iframes which contain pages of the manual.
-      IFRAME is the iframe element in the containing page if we are injecting 
-      Javascript. */
   function
-  init_iframe ()
+  Echo_area ()
   {
-    /* Initialize the DOM for generic pages loaded in the context of an
-       iframe.  */
-    function
-    on_load ()
-    {
-      var links = {};
-      var linkid = basename (location.pathname, /[.]x?html$/);
-      links[linkid] = navigation_links (document);
-      store.dispatch (actions.cache_links (links));
+    var elem = document.createElement ("div");
+    elem.classList.add ("echo-area");
+    elem.setAttribute ("hidden", "true");
 
-      if (linkid_contains_index (linkid))
-        {
-          /* Scan links that should be added to the index.  */
-          var index_links = document.querySelectorAll ("td[valign=top] a");
-          store.dispatch (actions.cache_index_links (index_links));
-        }
-
-      add_icons (document);
-
-      /* Call user hook.  */
-      if (config.hooks.on_iframe_load)
-        config.hooks.on_iframe_load ();
-    }
-
-    /* Handle messages received via the Message API.  */
-    function
-    on_message (event)
-    {
-      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")
-        {
-          /* Scroll to the anchor corresponding to HASH.  */
-          if (data.hash)
-            location.replace (data.hash);
-          else
-            scroll (0, 0);
-        }
-    }
-
-    return {
-      on_load: on_load,
-      on_message: on_message
-    };
+    this.element = elem;
+    this.toid = null;
   }
 
-  /*-------------------------
-  | Common event handlers.  |
-  `------------------------*/
-
-  /** Handle click events.  */
-  function
-  on_click (event)
-  {
-    for (var target = event.target; target !== null; target = 
target.parentNode)
+  Echo_area.prototype.render = function render (state) {
+    if (!state.echo)
       {
-        if ((target instanceof Element) && target.matches ("a"))
-          {
-            var href = target.getAttribute ("href");
-            if (href)
-             {
-                if (external_manual_url_p (href))
-                  {
-                    store.dispatch (actions.external_manual (href));
-                    event.preventDefault ();
-                    event.stopPropagation ();
-                    return;
-                  }
-                else if (!absolute_url_p (href))
-                  {
-                    store.dispatch (actions.set_current_url (href));
-                    event.preventDefault ();
-                    event.stopPropagation ();
-                    return;
-                  }
-              }
-          }
+        this.element.setAttribute ("hidden", "true");
+        this.toid = null;
       }
-  }
-
-  /** 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);
-    }
-  }
+    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.  |
+  `----------------------------*/
 
-  /** Handle Keyboard 'keyup' events.
-      @arg {KeyboardEvent} event */
   function
-  on_keyup (event)
+  Sidebar ()
   {
-    if (is_escape_key (event.key))
-      store.dispatch ({ type: "unfocus" });
-    else
+    this.element = document.createElement ("div");
+    this.element.setAttribute ("id", "slider");
+    var div = document.createElement ("div");
+    div.classList.add ("toc-sidebar");
+    var toc = document.querySelector (".contents");
+    toc.remove ();
+
+    /* Move contents of <body> into a a fresh <div> to let the components
+       treat the top 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);
+
+    /* Remove table of contents header.  */
+    document.querySelector (".contents-heading").remove ();
+
+    /* Also remove any short table of contents.  Links therein would
+       go to the table of contents, which we have just removed. */
+    function maybe_remove (query)
       {
-        var val = on_keyup.dict[event.key];
-        if (val)
-          {
-            if (typeof val === "function")
-              val ();
-            else
-              store.dispatch (val);
-          }
+        var element = document.querySelector (query);
+        if (element)
+          element.remove ();
       }
+    maybe_remove (".shortcontents-heading");
+    maybe_remove (".shortcontents");
   }
 
-  /* Associate an Event 'key' property to an action or a thunk.  */
-  on_keyup.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 ()
+  /* 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'.*/
+    var msg = { message_kind: "update-sidebar", selected: state.current };
+    window.postMessage (msg, "*");
   };
 
-  /** 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.  */
+  /*--------------------------.
+  | Component for the pages.  |
+  `--------------------------*/
+
+  /** @arg {HTMLDivElement} top_div */
   function
-  register_polyfills ()
+  Pages (top_div)
   {
-    function
-    includes (search, start)
-    {
-      start = start || 0;
-      return ((start + search.length) <= this.length)
-        && (this.indexOf (search, start) !== -1);
-    }
+    top_div.setAttribute ("id", config.TOP_ID);
+    top_div.setAttribute ("node", config.TOP_ID);
+    top_div.setAttribute ("hidden", "true");
+    this.element = document.createElement ("div");
+    this.element.setAttribute ("id", "sub-pages");
+    this.element.appendChild (top_div);
+    /** @type {string[]} Currently created divs.  */
+    this.ids = [config.TOP_ID];
+    /** @type {string} */
+    this.prev_id = null;
+    /** @type {HTMLElement} */
+    this.prev_div = null;
+    this.prev_search = null;
+  }
+
+  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);
+  };
 
-    /* eslint-disable no-extend-native */
-    if (!Array.prototype.includes)
-      Array.prototype.includes = includes;
+  Pages.prototype.render = function render (state) {
+    var that = this;
 
-    if (!String.prototype.includes)
-      String.prototype.includes = includes;
+    /* 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); });
 
-    if (!String.prototype.endsWith)
-      {
-        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;
-        };
-      }
+    /* Blur pages if help screen is on.  */
+    this.element.classList[(state.help) ? "add" : "remove"] ("blurred");
 
-    if (!Element.prototype.matches)
+    if (state.current !== this.prev_id)
       {
-        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 (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);
+          }
+        var div = resolve_page (state.current, true);
+        /* Remove any anchor. */
+        var link = linkid_split (state.current);
+        state.current = link.pageid;
+        update_history (state.current, state.history);
+        this.prev_id = state.current;
+        this.prev_div = div;
       }
 
-    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.TOP_ID)
+          {
+            window.setTimeout (function () {
+              store.dispatch ({ type: "search-query" });
+              var res = search (document.getElementById (config.TOP_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.TOP_ID)
+          remove_highlight (document.getElementById (config.TOP_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.TOP_NAME'.  HREF must be a USVString representing an
-      absolute or relative URL.  For example "foo/bar.html" will return
-      "config.TOP_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.TOP_NAME)
-      return config.TOP_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.TOP_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.
-
-      @typedef {HTMLAnchorElement|HTMLAreaElement} Links
-      @arg {Links[]|HTMLCollectionOf<Links>} links
-      @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 top
+       page.  */
+    if ((pageid === config.TOP_ID) && visible)
       {
-        var link = links[i];
-        var href = link.getAttribute ("href");
-        if (!href)
-          continue;
-        else if (absolute_url_p (href))
-          link.setAttribute ("target", "_blank");
-        else if (external_manual_url_p (href))
-          link.setAttribute ("target", "_top");
+        div.removeAttribute ("hidden");
+      }
+    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;
   }
 
-  /** 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.TOP_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.TOP_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.TOP_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.TOP_ID)
-      return config.TOP_NAME;
+    if (pageid === config.TOP_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' is an absolute URL.  Return true if this is the case
-      otherwise return false.  'URL' must be a USVString representing a valid
-      URL.  */
-  function
-  absolute_url_p (url)
-  {
-    if (typeof url !== "string")
-      throw new TypeError ("'" + url + "' is not a string");
-
-    return url.includes (":");
-  }
+  /*--------------------------------------------
+  | 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.TOP_NAME' DOM.  */
   function
-  external_manual_url_p (url)
+  on_load ()
   {
-    if (typeof url !== "string")
-      throw new TypeError ("'" + url + "' is not a string");
-
-    return url.match(/^..\//);
-  }
+    fix_links (document.links);
+    add_icons (document);
+    document.body.classList.add ("mainbar");
+
+    /* Move contents of <body> into a a fresh <div> to let the components
+       treat the top page like other iframe page.  */
+    var top_div = document.createElement ("div");
+    for (var ch = document.body.firstChild; ch; ch = document.body.firstChild)
+      top_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.TOP_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 ();
+              }
+          }
+      }
+    };
 
-  /** 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), "");
-  }
+    components.add (new Pages (top_div));
+    components.add (new Sidebar ());
+    components.add (new Help_page ());
+    components.add (new Minibuffer ());
+    components.add (new Echo_area ());
+    store.listeners.push (components.render.bind (components));
 
-  /** 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");
-  }
+    if (window.location.hash)
+      {
+        var linkid = normalize_hash (window.location.hash);
+        store.dispatch (actions.set_current_url (linkid, "replaceState"));
+      }
 
-  /** 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);
+    /* Retrieve NEXT link and local menu.  */
+    var links = {};
+    links[config.TOP_ID] = navigation_links (document);
+    store.dispatch (actions.cache_links (links));
+    store.dispatch ({ type: "iframe-ready", id: config.TOP_ID });
+    if (config.show_welcome_message)
+      {
+        store.dispatch ({
+          type: "echo",
+          msg: "Welcome to Texinfo documentation viewer 6.1, type '?' for 
help."
+        });
+      }
 
-    for (var child = node.firstChild; child; child = child.nextSibling)
-      depth_first_walk (child, func, node_type);
+    /* Call user hook.  */
+    if (config.hooks.on_main_load)
+      config.hooks.on_main_load ();
   }
 
-  /** 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.  */
+  /* Handle messages received via the Message API.
+     @arg {MessageEvent} event */
   function
-  href_hash (href)
+  on_message (event)
   {
-    if (typeof href !== "string")
-      throw new TypeError (href + " is not a string");
+    var data = event.data;
+    if (data.message_kind === "action")
+      {
+        /* Follow up actions to the store.  */
+        store.dispatch (data.action);
+      }
+    else if (data.message_kind === "scroll-to")
+      {
+        /* Used for text search.  Scroll to the anchor corresponding to 
+           HASH.  */
+        var anchor = data.hash.substr(1); /* Remove the #. */
+        document.getElementById(anchor).scrollIntoView ();
 
-    if (href.includes ("#"))
-      return href.replace (/.*#/, "");
-    else
-      return "";
+        /* 'location.replace (data.hash);' triggers a DOMContentLoaded event
+           which has bad results for the top page. */
+
+      }
   }
 
-  /** Remove a fragment identifier from the end of a URL. */
+  /* Event handler for 'popstate' events.  */
   function
-  remove_hash (href)
+  on_popstate (event)
   {
-    return href.replace (/#.*$/, '');
+    /* 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));
   }
 
-  /** Check if LINKID corresponds to a page containing index links.  */
+  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
-  linkid_contains_index (linkid)
+  hide_grand_child_nodes (ul, excluded)
   {
-    return linkid.match (/^.*-index$/i) || linkid.match (/^Index$/);
+    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");
+          }
+      }
   }
 
-  /** 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...).  */
+  /** Make the parent of ELEMS visible.
+      @arg {HTMLElement} elem */
   function
-  navigation_links (content)
+  mark_parent_elements (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)
+    if (elem && elem.parentElement && elem.parentElement.parentElement)
       {
-        var link = links[i];
-        var nav_id = navigation_links.dict[link.getAttribute ("accesskey")];
-        if (nav_id)
+        var pparent = elem.parentElement.parentElement;
+        for (var sib = pparent.firstElementChild; sib;
+             sib = sib.nextElementSibling)
           {
-            var linkid = basename (remove_hash (link.getAttribute ("href")),
-                                   /[.]x?html$/);
-            if (linkid === config.TOP_NAME)
-              res[nav_id] = config.TOP_ID;
-            else
-              res[nav_id] = linkid;
-          }
-        else /* this link is part of local table of content. */
-          {
-            res.menu = res.menu || {};
-            res.menu[link.text] = href_hash (link.getAttribute ("href"));
+            if (sib !== elem.parentElement
+                && sib.firstElementChild
+                && sib.firstElementChild.nextElementSibling)
+              {
+                sib.firstElementChild
+                   .nextElementSibling
+                   .setAttribute ("toc-detail", "yes");
+              }
           }
       }
-
-      return res;
   }
 
-  navigation_links.dict = { n: "next", p: "prev", u: "up" };
-
+  /** Scan ToC entries to see which should be hidden.
+      @arg {HTMLElement} elem
+      @arg {string} linkid */
   function
-  add_icons (document)
+  scan_toc (elem, url)
   {
-    if (!config.embedded_help)
-      return;
+    /** @type {Element} */
+    var res;
 
-    var div = document.createElement ("div");
-    div.setAttribute ("id", "icon-bar");
-    div.setAttribute ("style", "float: right");
-    var span = document.createElement ("span");
-    span.innerHTML = "?";
-    span.classList.add ("icon");
-    span.addEventListener ("click", function () {
-      store.dispatch (actions.show_help ());
-    }, false);
-    div.appendChild (span);
-    document.body.insertBefore (div, document.body.firstChild);
+    /** Set CURRENT to the node corresponding to URL linkid.
+        @arg {Element} elem */
+    function
+    find_current (elem)
+    {
+      /* XXX: No template literals for IE compatibility.  */
+      /* The ^= is a selector to account for the fragment identifier
+         which follows. */
+      if (elem.matches ("a[href^=\"" + url + "\\.html" + "\"]"))
+        {
+          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 (url === config.TOP_ID)
+      {
+        hide_grand_child_nodes (ul);
+        res = document.getElementById('sidebar_start');
+      }
+    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;
   }
 
-  /** Check if ELEM matches SEARCH
-      @arg {Element} elem
-      @arg {RegExp} rgxp
-      @return {boolean} */
+  /* 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
-  search (elem, rgxp)
+  create_link_dict (nav)
   {
-    /** @type {Text} */
-    var text = null;
+    var prev_id = config.TOP_ID;
+    var links = {};
 
-    /** @arg {Text} node */
     function
-    find (node)
+    add_link (elem)
     {
-      if (rgxp.test (node.textContent))
+      if (elem.matches ("a") && elem.hasAttribute ("href"))
         {
-          /* Ignore previous match.  */
-          var prev = node.parentElement.matches ("span.highlight");
-          text = (prev) ? null : (text || node);
+          var id = basename (remove_hash (elem.getAttribute ("href")),
+                             /[.]x?html$/);
+          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 (elem, find, Node.TEXT_NODE);
-    remove_highlight (elem);
-    if (!text)
-      return false;
-    else
+    depth_first_walk (nav, add_link, Node.ELEMENT_NODE);
+    /* Add a reference to the first and last node of the manual.  */
+    links["*TOP*"] = config.TOP_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)
       {
-        highlight_text (rgxp, text);
-        return true;
+        var header = document.createElement ("header");
+        var a = document.createElement ("a");
+        a.setAttribute ("href", config.TOP_NAME);
+        a.setAttribute ("id", "sidebar_start");
+        header.appendChild (a);
+        var div = document.createElement ("div");
+        a.appendChild (div);
+        var span = document.createElement ("span");
+        span.textContent = h1.textContent;
+        div.appendChild (span);
+        elem.parentElement.insertBefore (header, elem);
       }
   }
 
-  /** Find the pageid corresponding to forward direction.
-      @arg {any} state
-      @arg {string} linkid
-      @return {string} the forward pageid */
+  /*---------------------------------.
+  | Event handlers for the sidebar   |
+  `---------------------------------*/
+
+  /* Initialize sidebar. */
   function
-  forward_pageid (state, linkid)
+  on_load ()
   {
-    var data = state.loaded_nodes[linkid];
-    if (!data)
-      throw new Error ("page not loaded: " + linkid);
-    else if (!data.forward)
-      return null;
-    else
+    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.TOP_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 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);
+
+        /* Highlight the current LINKID in the table of content.  */
+        var elem = scan_toc (toc_div, data.selected);
+        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.
+    IFRAME is the iframe element in the containing page if we are injecting 
+    Javascript. */
+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 links = {};
+    var linkid = basename (location.pathname, /[.]x?html$/);
+    links[linkid] = navigation_links (document);
+    store.dispatch (actions.cache_links (links));
 
-    var matches = rgxp.exec (node.textContent);
-    if (matches)
+    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_links = document.querySelectorAll ("td[valign=top] a");
+        store.dispatch (actions.cache_index_links (index_links));
       }
+
+    add_icons (document);
+
+    /* Call user hook.  */
+    if (config.hooks.on_iframe_load)
+      config.hooks.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)
+          location.replace (data.hash);
+        else
+          scroll (0, 0);
       }
   }
 
-  /*--------------.
-  | Entry point.  |
-  `--------------*/
+  return {
+    on_load: on_load,
+    on_message: on_message
+  };
+}
+
+/*-------------------------
+| Common event handlers.  |
+`------------------------*/
+
+/** Handle click events.  */
+function
+on_click (event)
+{
+  for (var target = event.target; target !== null; target = target.parentNode)
+    {
+      if ((target instanceof Element) && target.matches ("a"))
+        {
+          var href = target.getAttribute ("href");
+          if (href)
+            {
+              if (external_manual_url_p (href))
+                {
+                  store.dispatch (actions.external_manual (href));
+                  event.preventDefault ();
+                  event.stopPropagation ();
+                  return;
+                }
+              else if (!absolute_url_p (href))
+                {
+                  store.dispatch (actions.set_current_url (href));
+                  event.preventDefault ();
+                  event.stopPropagation ();
+                  return;
+                }
+            }
+        }
+    }
+}
+
+/** 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 'keyup' events.
+    @arg {KeyboardEvent} event */
+function
+on_keyup (event)
+{
+  if (is_escape_key (event.key))
+    store.dispatch ({ type: "unfocus" });
+  else
+    {
+      var val = on_keyup.dict[event.key];
+      if (val)
+        {
+          if (typeof val === "function")
+            val ();
+          else
+            store.dispatch (val);
+        }
+    }
+}
+
+/* Associate an Event 'key' property to an action or a thunk.  */
+on_keyup.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;
+
+  if (!String.prototype.endsWith)
+    {
+      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 (!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;
+        };
+    }
 
-  /** Depending on the role of the document launching this script, different
-      event handlers are registered.  This script can be used in the context 
of:
+  if (typeof Object.assign != "function")
+    {
+      Object.assign = function assign (target) {
+        if (undef_or_null (target))
+          throw new TypeError ("Cannot convert undefined or null to object");
 
-      - the top 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
+        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 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.  */
+  (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.TOP_NAME'.  HREF must be a USVString representing an
+    absolute or relative URL.  For example "foo/bar.html" will return
+    "config.TOP_NAME#bar".
+
+    @arg {string} href.*/
+function
+with_sidebar_query (href)
+{
+  if (basename (href) === config.TOP_NAME)
+    return config.TOP_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.TOP_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.
+
+    @typedef {HTMLAnchorElement|HTMLAreaElement} Links
+    @arg {Links[]|HTMLCollectionOf<Links>} links
+    @return void  */
+function
+fix_links (links, id)
+{
+  for (var i = 0; i < links.length; i += 1)
+    {
+      var link = links[i];
+      var href = link.getAttribute ("href");
+      if (!href)
+        continue;
+      else if (absolute_url_p (href))
+        link.setAttribute ("target", "_blank");
+      else if (external_manual_url_p (href))
+        link.setAttribute ("target", "_top");
+    }
+}
+
+/** 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.TOP_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.TOP_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.TOP_ID)
+    return config.TOP_NAME;
+  else
+    {
+      var link = linkid_split (linkid);
+      return link.pageid + config.EXT + link.hash;
+    }
+}
+
+/** Check if 'URL' is an absolute URL.  Return true if this is the case
+    otherwise return false.  'URL' must be a USVString representing a valid
+    URL.  */
+function
+absolute_url_p (url)
+{
+  if (typeof url !== "string")
+    throw new TypeError ("'" + url + "' is not a string");
+
+  return url.includes (":");
+}
+
+/** 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 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 "";
+}
+
+/** Remove a fragment identifier from the end of a URL. */
+function
+remove_hash (href)
+{
+  return href.replace (/#.*$/, '');
+}
+
+/** 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 linkid = basename (remove_hash (link.getAttribute ("href")),
+                                 /[.]x?html$/);
+          if (linkid === config.TOP_NAME)
+            res[nav_id] = config.TOP_ID;
+          else
+            res[nav_id] = linkid;
+        }
+      else /* this link is part of local table of content. */
+        {
+          res.menu = res.menu || {};
+          res.menu[link.text] = href_hash (link.getAttribute ("href"));
+        }
+    }
 
-  /* Display MSG bail out message portably.  */
+    return res;
+}
+
+navigation_links.dict = { n: "next", p: "prev", u: "up" };
+
+function
+add_icons (document)
+{
+  if (!config.embedded_help)
+    return;
+
+  var div = document.createElement ("div");
+  div.setAttribute ("id", "icon-bar");
+  div.setAttribute ("style", "float: right");
+  var span = document.createElement ("span");
+  span.innerHTML = "?";
+  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
-  error (msg)
+  find (node)
   {
-    /* 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);
-      };
+    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
+    {
+      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
+    {
+      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 top 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);
+    };
+}
+
 var inside_top_page;
 
 /* Wrap init code */
-function init() {
+function init()
+{
   /* Check if current browser supports the minimum requirements required for
      properly using this script, otherwise bails out.  */
   if (features && !(features.es5



reply via email to

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