to let the components
treat the index page like other iframe page. */
var nav = document.createElement ("nav");
nav.classList.add ("contents");
for (var ch = toc.firstChild; ch; ch = toc.firstChild)
nav.appendChild (ch);
var div$ = document.createElement ("div");
div$.classList.add ("toc");
div$.appendChild (nav);
div.appendChild (div$);
this.element.appendChild (div);
let header = document.createElement ("header");
div$.parentElement.insertBefore (header, div$);
let hider = document.createElement ("button");
hider.classList.add ("sidebar-hider");
hider.innerHTML = config.HIDE_SIDEBAR_HTML;
this.show_sidebar_button = hider;
header.appendChild(hider);
}
/* Render 'sidebar' according to STATE which is a new state. */
Sidebar.prototype.render = function render (state) {
/* Update sidebar to highlight the title corresponding to
'state.current'.*/
let currently_showing = document.body.getAttribute("show-sidebar");
let show = state.show_sidebar;
if (show == "hide-if-narrow")
show = is_narrow_window() || currently_showing == "no" ? "no" : "yes";
if (show === undefined)
show = "yes";
if (show !== currently_showing)
{
document.body.setAttribute("show-sidebar", show);
this.show_sidebar_button.innerHTML = show == "yes" ? config.HIDE_SIDEBAR_HTML : config.SHOW_SIDEBAR_HTML;
let tooltip = show == "yes" ? config.HIDE_SIDEBAR_TOOLTIP : config.SHOW_SIDEBAR_TOOLTIP;
if (tooltip)
this.show_sidebar_button.setAttribute("title", tooltip);
else
this.show_sidebar_button.removeAttribute("title");
}
var msg = { message_kind: "update-sidebar", selected: state.current, section_hash: state.section_hash };
window.postMessage (msg, "*");
};
/*--------------------------.
| Component for the pages. |
`--------------------------*/
/** @arg {HTMLDivElement} index_div */
function
Pages (index_div)
{
index_div.setAttribute ("id", config.INDEX_ID);
index_div.setAttribute ("node", config.INDEX_ID);
index_div.setAttribute ("hidden", "true");
this.element = document.createElement ("div");
this.element.setAttribute ("id", "sub-pages");
this.element.appendChild (index_div);
/** @type {string[]} Currently created divs. */
this.ids = [config.INDEX_ID];
/** @type {string} */
this.prev_id = null;
/** @type {HTMLElement} */
this.prev_div = null;
this.prev_search = null;
}
Pages.prototype.add_div = function add_div (pageid) {
var div = document.createElement ("div");
div.setAttribute ("id", pageid);
div.setAttribute ("node", pageid);
div.setAttribute ("hidden", "true");
this.ids.push (pageid);
this.element.appendChild (div);
if (linkid_contains_index (pageid))
load_page (pageid);
return div;
};
Pages.prototype.render = function render (state) {
var that = this;
/* Create div elements for pages corresponding to newly added
linkids from 'state.loaded_nodes'.*/
Object.keys (state.loaded_nodes)
.map (function (id) { return id.replace (/\..*$/, ""); })
.filter (function (id) { return !that.ids.includes (id); })
.reduce (function (acc, id) {
return ((acc.includes (id)) ? acc : acc.concat ([id]));
}, [])
.forEach (function (id) { return that.add_div (id); });
/* Blur pages if help screen is on. */
this.element.classList[(state.help) ? "add" : "remove"] ("blurred");
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);
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.INDEX_ID)
{
window.setTimeout (function () {
store.dispatch ({ type: "search-query" });
var res = search (document.getElementById (config.INDEX_ID),
state.search.regexp);
store.dispatch ({ type: "search-result", found: res });
}, 0);
}
else
{
window.setTimeout (function () {
store.dispatch ({ type: "search-query" });
var msg = {
message_kind: "search",
regexp: state.search.regexp,
id: state.search.current_pageid
};
post_message (state.search.current_pageid, msg);
}, 0);
}
}
/* Update highlight of current page. */
if (!state.highlight)
{
if (state.current === config.INDEX_ID)
remove_highlight (document.getElementById (config.INDEX_ID));
else
{
var link = linkid_split (state.current);
var msg$ = { message_kind: "highlight", regexp: null };
post_message (link.pageid, msg$);
}
}
/* Scroll to highlighted search result. */
if (state.search
&& (this.prev_search !== state.search)
&& state.search.status === "done"
&& state.search.found === true)
{
var link$ = linkid_split (state.current);
var msg$$ = { message_kind: "scroll-to", hash: "#search-result" };
post_message (link$.pageid, msg$$);
this.prev_search = state.search;
}
};
/*--------------.
| Utilitaries. |
`--------------*/
/** Load PAGEID.
@arg {string} pageid */
function
load_page (pageid)
{
var div = resolve_page (pageid, false);
/* Making the iframe visible triggers the load of the iframe DOM. */
if (div.hasAttribute ("hidden"))
{
div.removeAttribute ("hidden");
div.setAttribute ("hidden", "true");
}
}
/** Return the div element that correspond to LINKID. If VISIBLE is true
then display the div and position the corresponding iframe to the
anchor specified by LINKID.
@arg {string} linkid - link identifier
@arg {boolean} [visible]
@return {HTMLElement} the div element. */
function
resolve_page (linkid, visible)
{
var msg;
var link = linkid_split (linkid);
var pageid = link.pageid;
var div = document.getElementById (link.pageid);
if (!div)
{
msg = "no iframe container correspond to identifier: " + pageid;
throw new ReferenceError (msg);
}
/* Create iframe if necessary unless the div is refering to the Index
or Contents page. */
if (pageid === config.INDEX_ID || pageid === config.CONTENTS_ID)
{
div.removeAttribute ("hidden");
/* Unlike iframes, Elements are unlikely to be scrollable (CSSOM
Scroll-behavior), so choose an arbitrary element inside "index"
div and at the top of it. */
document.getElementById ("icon-bar").scrollIntoView ();
}
else
{
var iframe = div.querySelector ("iframe");
if (!iframe)
{
iframe = document.createElement ("iframe");
iframe.classList.add ("node");
iframe.setAttribute ("src", linkid_to_url (pageid));
div.appendChild (iframe);
iframe.addEventListener ("load", function () {
store.dispatch ({ type: "iframe-ready", id: pageid });
}, false);
}
if (visible)
{
div.removeAttribute ("hidden");
msg = { message_kind: "scroll-to", hash: link.hash };
post_message (pageid, msg);
}
}
return div;
}
/** Create a datalist element containing option elements corresponding
to the keys in MENU. */
function
create_datalist (menu)
{
var datalist = document.createElement ("datalist");
Object.keys (menu)
.sort ()
.forEach (function (title) {
var opt = document.createElement ("option");
opt.setAttribute ("value", title);
datalist.appendChild (opt);
});
return datalist;
}
/** Mutate the history of page navigation. Store LINKID in history
state, The actual way to store LINKID depends on HISTORY_MODE.
@arg {string} linkid
@arg {string} history_mode */
function
update_history (linkid, history_mode)
{
var method = window.history[history_mode];
if (method)
{
/* Pretend that the current page is the one corresponding to the
LINKID iframe. Handle errors since changing the visible file
name can fail on some browsers with the "file:" protocol. */
var visible_url =
dirname (window.location.pathname) + linkid_to_url (linkid);
try
{
method.call (window.history, linkid, null, visible_url);
}
catch (err)
{
/* Fallback to changing only the hash part which is safer. */
visible_url = window.location.pathname;
if (linkid !== config.INDEX_ID)
visible_url += ("#" + linkid);
method.call (window.history, linkid, null, visible_url);
}
}
}
/** Send MSG to the browsing context corresponding to PAGEID. This is a
wrapper around 'Window.postMessage' which ensures that MSG is sent
after PAGEID is loaded.
@arg {string} pageid
@arg {any} msg */
function
post_message (pageid, msg)
{
if (pageid === config.INDEX_ID || pageid === config.CONTENTS_ID)
window.postMessage (msg, "*");
else
{
load_page (pageid);
var iframe = document.getElementById (pageid)
.querySelector ("iframe");
/* Semantically this would be better to use "promises" however
they are not available in IE. */
if (store.state.ready[pageid])
iframe.contentWindow.postMessage (msg, "*");
else
{
iframe.addEventListener ("load", function handler () {
this.contentWindow.postMessage (msg, "*");
this.removeEventListener ("load", handler, false);
}, false);
}
}
}
/*--------------------------------------------
| Event handlers for the top-level context. |
`-------------------------------------------*/
/* Initialize the top level 'config.INDEX_NAME' DOM. */
function
on_load ()
{
fix_links (document.links);
add_icons ();
document.body.classList.add ("mainbar");
/* Move contents of into a a fresh
to let the components
treat the index page like other iframe page. */
var index_div = document.createElement ("div");
for (var ch = document.body.firstChild; ch; ch = document.body.firstChild)
index_div.appendChild (ch);
/* Aggregation of all the sub-components. */
var components = {
element: document.body,
components: [],
add: function add (component) {
this.components.push (component);
this.element.appendChild (component.element);
},
render: function render (state) {
this.components
.forEach (function (cmpt) { cmpt.render (state); });
/* Ensure that focus is on the current node unless some component is
focused. */
if (!state.focus)
{
var link = linkid_split (state.current);
var elem = document.getElementById (link.pageid);
if (link.pageid !== config.INDEX_ID && link.pageid !== config.CONTENTS_ID)
elem.querySelector ("iframe").focus ();
else
{
/* Move the focus to top DOM. */
document.documentElement.focus ();
/* Allow the spacebar scroll in the main page to work. */
elem.focus ();
}
}
}
};
var pages = new Pages (index_div);
components.add (pages);
var contents_node = pages.add_div(config.CONTENTS_ID);
components.add (new Sidebar (contents_node));
components.add (new Help_page ());
components.add (new Minibuffer ());
components.add (new Echo_area ());
store.listeners.push (components.render.bind (components));
if (window.location.hash)
{
var linkid = normalize_hash (window.location.hash);
store.dispatch (actions.set_current_url (linkid, "replaceState"));
}
/* Retrieve NEXT link and local menu. */
var links = {};
links[config.INDEX_ID] = navigation_links (document);
store.dispatch (actions.cache_links (links));
store.dispatch ({ type: "iframe-ready", id: config.INDEX_ID });
store.dispatch ({
type: "echo",
msg: "Welcome to Texinfo documentation viewer 6.1, type '?' for help."
});
/* Call user hook. */
if (config.on_main_load)
config.on_main_load ();
}
/* Handle messages received via the Message API.
@arg {MessageEvent} event */
function
on_message (event)
{
var data = event.data;
if (data.message_kind === "action")
{
/* Follow up actions to the store. */
store.dispatch (data.action);
}
}
/* Event handler for 'popstate' events. */
function
on_popstate (event)
{
/* When EVENT.STATE is 'null' it means that the user has manually
changed the hash part of the URL bar. */
var linkid = (event.state === null) ?
normalize_hash (window.location.hash) : event.state;
store.dispatch (actions.set_current_url (linkid, false));
}
return {
on_load: on_load,
on_message: on_message,
on_popstate: on_popstate
};
}
/** 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, linkid, section_hash = null)
{
/** @type {Element} */
var res;
if (section_hash)
{
let dot = linkid.lastIndexOf('.');
if (dot >= 0)
linkid = linkid.substring(0, dot+1) + section_hash;
}
var url = with_sidebar_query (linkid_to_url (linkid));
/** Set CURRENT to the node corresponding to URL linkid.
@arg {Element} elem */
function
find_current (elem)
{
if (elem.localName === "a" && link_href(elem) == url)
{
elem.setAttribute ("toc-current", "yes");
var sib = elem.nextElementSibling;
if (sib && sib.matches ("ul"))
hide_grand_child_nodes (sib);
res = elem;
}
}
var ul = elem.querySelector ("ul");
if (linkid === config.INDEX_ID || linkid === config.CONTENTS_ID)
{
hide_grand_child_nodes (ul);
res = document.getElementById(linkid);
}
else
{
depth_first_walk (ul, find_current, Node.ELEMENT_NODE);
/* Mark every parent node. */
var current = res;
while (current && current !== ul)
{
mark_parent_elements (current);
/* XXX: Special case for manuals with '@part' commands. */
if (current.parentElement === ul)
hide_grand_child_nodes (ul, current);
current = current.parentElement;
}
}
return res;
}
/* Build the global dictionary containing navigation links from NAV. NAV
must be an 'ul' DOM element containing the table of content of the
manual. */
function
create_link_dict (nav)
{
var prev_id = config.INDEX_ID;
var links = {};
function
add_link (elem)
{
let href;
if (elem.matches ("a") && (href = link_href(elem)))
{
var id = href_hash (href);
links[prev_id] =
Object.assign ({}, links[prev_id], { forward: id });
links[id] = Object.assign ({}, links[id], { backward: prev_id });
prev_id = id;
}
}
depth_first_walk (nav, add_link, Node.ELEMENT_NODE);
/* Add a reference to the first and last node of the manual. */
links["*TOP*"] = config.INDEX_ID;
links["*END*"] = prev_id;
return links;
}
/* 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 a = document.createElement ("a");
a.setAttribute ("href", config.INDEX_NAME);
a.setAttribute ("id", config.INDEX_ID);
let header = elem.previousSibling;
header.appendChild (a);
if (window.sidebarLinkAppendContents)
window.sidebarLinkAppendContents(a, h1.textContent);
else
{
var div = document.createElement ("div");
a.appendChild (div);
var span = document.createElement ("span");
span.textContent = h1.textContent;
div.appendChild (span);
}
}
}
/*------------------------------------------
| Event handlers for the sidebar context. |
`-----------------------------------------*/
/* Initialize TOC_FILENAME which must be loaded in the context of an
iframe. */
function
on_load ()
{
var toc_div = document.getElementById ("slider");
add_header (toc_div.querySelector (".toc"));
/* Specify the base URL to use for all relative URLs. */
/* FIXME: Add base also for sub-pages. */
var base = document.createElement ("base");
base.setAttribute ("href",
window.location.href.replace (/[/][^/]*$/, "/"));
document.head.appendChild (base);
scan_toc (toc_div, config.INDEX_NAME);
/* Get 'backward' and 'forward' link attributes. */
var dict = create_link_dict (toc_div.querySelector ("nav.contents ul"));
store.dispatch (actions.cache_links (dict));
}
/* Handle messages received via the Message API. */
function
on_message (event)
{
var data = event.data;
if (data.message_kind === "update-sidebar")
{
var toc_div = document.getElementById ("slider");
/* Reset previous calls to 'scan_toc'. */
depth_first_walk (toc_div, function clear_toc_styles (elem) {
elem.removeAttribute ("toc-detail");
elem.removeAttribute ("toc-current");
}, Node.ELEMENT_NODE);
/* Remove the hash part for the main page. */
var pageid = linkid_split (data.selected).pageid;
var selected = (pageid === config.INDEX_ID) ? pageid : data.selected;
/* Highlight the current LINKID in the table of content. */
var elem = scan_toc (toc_div, selected, data.section_hash);
if (elem)
elem.scrollIntoView (true);
}
}
return {
on_load: on_load,
on_message: on_message
};
}
/** Initialize iframes which contain pages of the manual. */
function
init_iframe ()
{
/* Initialize the DOM for generic pages loaded in the context of an
iframe. */
function
on_load ()
{
document.body.classList.add ("in-iframe");
fix_links (document.links);
var links = {};
var linkid = basename (window.location.pathname, /[.]x?html$/);
links[linkid] = navigation_links (document);
store.dispatch (actions.cache_links (links));
if (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 ();
/* Call user hook. */
if (config.on_iframe_load)
config.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)
{
let elem = document.getElementById(data.hash.substring(1));
// Check if hash reference is to a sectioing element.
// If not we need to find the outer sectioning element,
// so we can update the sidebar's ToC correctly.
if (elem)
{
let p = elem;
let section = null;
let id = null;
for (let p = elem;
section === null && p instanceof Element;
p = p.parentNode)
{
let sid = p.getAttribute("id");
if (sid == null)
continue;
let cl = p.classList;
for (let i = cl.length; --i >= 0; )
{
if (section_names.indexOf(cl.item(i)) >= 0)
{
section = p;
id = sid;
break;
}
}
}
if (section && section !== elem)
{
// Send section id to sidebar so it can properly update.
store.dispatch({ type: "section", hash: data.hash, section_hash: id } );
}
}
window.location.replace (data.hash);
}
else
window.scroll (0, 0);
}
}
return {
on_load: on_load,
on_message: on_message
};
}
/*-------------------------
| Common event handlers. |
`------------------------*/
/** Handle click events. */
function
on_click (event)
{
let in_sidebar = event.target.matches ("#slider *");
for (var target = event.target; target !== null; target = target.parentNode)
{
if (! (target instanceof Element))
continue;
if (target.matches ("a"))
{
var href = link_href(target);
if (href && maybe_pageref_url_p (href)
&& !external_manual_url_p (href))
{
var linkid = href_hash (href) || config.INDEX_ID;
if (linkid === "index.SEC_Contents")
linkid = config.CONTENTS_ID;
store.dispatch (actions.set_current_url (linkid, null,
in_sidebar ? "in-sidebar" : "in-page"));
event.preventDefault ();
event.stopPropagation ();
break;
}
}
if (target.matches (".sidebar-hider"))
{
let body = document.body;
let show = body.getAttribute("show-sidebar");
show_sidebar (show==="no");
return;
}
}
if (! in_sidebar)
hide_sidebar_if_narrow ();
}
// Only valid when showing sidebar.
function is_narrow_window ()
{
return document.body.firstChild.offsetLeft == 0;
}
function show_sidebar (show)
{
store.dispatch({ type: "show-sidebar", show: show ? "yes" : "no" });
}
function hide_sidebar_if_narrow ()
{
store.dispatch({ type: "show-sidebar", show: "hide-if-narrow" });
}
/** Handle unload events. */
function
on_unload ()
{
/* Cross origin requests are not supported in file protocol. */
if (window.location.protocol !== "file:")
{
var request = new XMLHttpRequest ();
request.open ("GET", "(WINDOW-CLOSED)");
request.send (null);
}
}
/** Handle Keyboard 'keydown' events.
@arg {KeyboardEvent} event */
function
on_keydown (event)
{
if (is_escape_key (event.key))
store.dispatch ({ type: "unfocus" });
else
{
var val = on_keydown.dict[event.key];
if (val)
{
if (typeof val === "function")
val ();
else
store.dispatch (val);
}
}
}
/* Associate an Event 'key' property to an action or a thunk. */
on_keydown.dict = {
i: actions.show_text_input ("index"),
l: window.history.back.bind (window.history),
m: actions.show_text_input ("menu"),
n: actions.navigate ("next"),
p: actions.navigate ("prev"),
r: window.history.forward.bind (window.history),
s: actions.show_text_input ("regexp-search"),
t: actions.set_current_url_pointer ("*TOP*"),
u: actions.navigate ("up"),
"]": actions.navigate ("forward"),
"[": actions.navigate ("backward"),
"<": actions.set_current_url_pointer ("*TOP*"),
">": actions.set_current_url_pointer ("*END*"),
"?": actions.show_help ()
};
/** Some standard methods used in this script might not be implemented by
the current browser. If that is the case then augment the prototypes
appropriately. */
function
register_polyfills ()
{
function
includes (search, start)
{
start = start || 0;
return ((start + search.length) <= this.length)
&& (this.indexOf (search, start) !== -1);
}
/* eslint-disable no-extend-native */
if (!Array.prototype.includes)
Array.prototype.includes = includes;
if (!String.prototype.includes)
String.prototype.includes = includes;
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;
};
}
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");
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;
};
}
(function (protos) {
protos.forEach (function (proto) {
if (!proto.hasOwnProperty ("remove"))
{
Object.defineProperty (proto, "remove", {
configurable: true,
enumerable: true,
writable: true,
value: function value () {
this.parentNode.removeChild (this);
}
});
}
});
} ([Element.prototype, CharacterData.prototype, DocumentType.prototype]));
/* eslint-enable no-extend-native */
}
/*---------------------.
| Common utilitaries. |
`---------------------*/
/** Check portably if KEY correspond to "Escape" key value.
@arg {string} key */
function
is_escape_key (key)
{
/* In Internet Explorer 9 and Firefox 36 and earlier, the Esc key
returns "Esc" instead of "Escape". */
return key === "Escape" || key === "Esc";
}
/** Check if OBJ is equal to 'undefined' or 'null'. */
function
undef_or_null (obj)
{
return (obj === null || typeof obj === "undefined");
}
/** Return a relative URL corresponding to HREF, which refers to an anchor
of 'config.INDEX_NAME'. HREF must be a USVString representing an
absolute or relative URL. For example "foo/bar.html" will return
"config.INDEX_NAME#bar".
@arg {string} href.*/
function
with_sidebar_query (href)
{
if (basename (href) === config.INDEX_NAME)
return config.INDEX_NAME;
else
{
/* XXX: Do not use the URL API for IE portability. */
var url = with_sidebar_query.url;
url.setAttribute ("href", href);
var new_hash = "#" + basename (url.pathname, /[.]x?html/);
/* XXX: 'new_hash !== url.hash' is a workaround to work with links
produced by makeinfo which link to an anchor element in a page
instead of directly to the page. */
if (url.hash && new_hash !== url.hash)
new_hash += ("." + url.hash.slice (1));
return config.INDEX_NAME + new_hash;
}
}
/* Use the same DOM element for every function call. */
with_sidebar_query.url = document.createElement ("a");
/** Modify LINKS to handle the iframe based navigation properly. Relative
links will be opened inside the corresponding iframe and absolute links
will be opened in a new tab. If ID is true then define an "id"
attribute with a linkid value for relative links.
@typedef {HTMLAnchorElement|HTMLAreaElement} Links
@arg {Links[]|HTMLCollectionOf} links
@arg {boolean} [id]
@return void */
function
fix_links (links, id)
{
for (var i = 0; i < links.length; i += 1)
{
var link = links[i];
var href = link_href(link);
if (!href)
continue;
else if (external_manual_url_p (href))
link.setAttribute ("target", "_top");
else if (! maybe_pageref_url_p (href))
link.setAttribute ("target", "_blank");
else
{
var href$ = with_sidebar_query (href);
if (href !== href$)
{
let protocol = location.protocol;
if (protocol == "https:" || protocol == "http:")
link._href = href$;
else
link.setAttribute ("href", href$);
}
if (id)
{
var linkid = (href$ === config.INDEX_NAME) ?
config.INDEX_ID : href_hash (href$);
link.setAttribute ("id", linkid);
}
}
}
}
/** Convert HASH which is something that can be found 'Location.hash' to a
"linkid" which can be handled in our model.
@arg {string} hash
@return {string} linkid */
function
normalize_hash (hash)
{
var text = hash.slice (1);
/* Some anchor elements are present in 'config.INDEX_NAME' and we need to
handle link to them specially (i.e. not try to find their corresponding
iframe).*/
if (config.MAIN_ANCHORS.includes (text))
return config.INDEX_ID + "." + text;
else
return text;
}
/** Return an object composed of the filename and the anchor of LINKID.
LINKID can have the form "foobar.anchor" or just "foobar".
@arg {string} linkid
@return {{pageid: string, hash: string}} */
function
linkid_split (linkid)
{
if (!linkid.includes ("."))
return { pageid: linkid, hash: "" };
else
{
var ref = linkid.match (/^(.+)\.(.*)$/).slice (1);
return { pageid: ref[0], hash: "#" + ref[1] };
}
}
/** Convert LINKID which has the form "foobar.anchor" or just "foobar", to
an URL of the form `foobar${config.EXT}#anchor`.
@arg {string} linkid */
function
linkid_to_url (linkid)
{
if (linkid === config.INDEX_ID)
return config.INDEX_NAME;
else
{
var link = linkid_split (linkid);
return link.pageid + config.EXT + link.hash;
}
}
/** Check if 'URL' may be a cross-reference to another page in this manual. */
function
maybe_pageref_url_p (url)
{
return url.match(config.LOCAL_HTML_PAGE_PATTERN);
}
/** Check if 'URL' is a link to another manual. For locally installed
manuals only. */
function
external_manual_url_p (url)
{
if (typeof url !== "string")
throw new TypeError ("'" + url + "' is not a string");
return url.match(/^..\//);
}
/** Return PATHNAME with any leading directory components removed. If
specified, also remove a trailing SUFFIX. */
function
basename (pathname, suffix)
{
var res = pathname.replace (/.*[/]/, "");
if (!suffix)
return res;
else if (suffix instanceof RegExp)
return res.replace (suffix, "");
else /* typeof SUFFIX === "string" */
return res.replace (new RegExp ("[.]" + suffix), "");
}
/** Strip last component from PATHNAME and keep the trailing slash. For
example if PATHNAME is "/foo/bar/baz.html" then return "/foo/bar/".
@arg {string} pathname */
function
dirname (pathname)
{
var res = pathname.match (/\/?.*\//);
if (res)
return res[0];
else
throw new Error ("'location.pathname' is not recognized");
}
/** Apply FUNC to each nodes in the NODE subtree. The walk follows a depth
first algorithm. Only consider the nodes of type NODE_TYPE. */
function
depth_first_walk (node, func, node_type)
{
if (!node_type || (node.nodeType === node_type))
func (node);
for (var child = node.firstChild; child; child = child.nextSibling)
depth_first_walk (child, func, node_type);
}
/** Return the "effective" href attribute of a link (a) element. */
function
link_href (alink)
{
return alink._href || alink.getAttribute("href");
}
/** Return the hash part of HREF without the '#' prefix. HREF must be a
string. If there is no hash part in HREF then return the empty
string. */
function
href_hash (href)
{
if (typeof href !== "string")
throw new TypeError (href + " is not a string");
if (href.includes ("#"))
return href.replace (/.*#/, "");
else
return "";
}
/** Check if LINKID corresponds to a page containing index links. */
function
linkid_contains_index (linkid)
{
return linkid.match (/^.*-index$/i) || linkid.match (/^Index$/);
}
/** Retrieve PREV, NEXT, and UP links, and local menu from CONTENT and
return an object containing references to those links. CONTENT must be
an object implementing the ParentNode interface (Element,
Document...). */
function
navigation_links (content)
{
var links = content.querySelectorAll ("a[accesskey][href]");
var res = {};
/* links have the form MAIN_FILE.html#FRAME-ID. For convenience
only store FRAME-ID. */
for (var i = 0; i < links.length; i += 1)
{
var link = links[i];
var nav_id = navigation_links.dict[link.getAttribute ("accesskey")];
if (nav_id)
{
var href = basename (link_href (link));
if (href === config.INDEX_NAME)
res[nav_id] = config.INDEX_ID;
else
res[nav_id] = href_hash (href);
}
else /* this link is part of local table of content. */
{
res.menu = res.menu || {};
res.menu[link.text] = href_hash (link_href (link));
}
}
return res;
}
navigation_links.dict = { n: "next", p: "prev", u: "up" };
function
add_icons ()
{
var div = document.createElement ("div");
div.setAttribute ("id", "icon-bar");
var span = document.createElement ("span");
span.innerHTML = "?";
// Set tool-tip (on hover)
span.setAttribute ("title", "Help for keyboard shortcuts");
span.classList.add ("icon");
span.addEventListener ("click", function () {
store.dispatch (actions.show_help ());
}, false);
div.appendChild (span);
document.body.insertBefore (div, document.body.firstChild);
}
/** Check if ELEM matches SEARCH
@arg {Element} elem
@arg {RegExp} rgxp
@return {boolean} */
function
search (elem, rgxp)
{
/** @type {Text} */
var text = null;
/** @arg {Text} node */
function
find (node)
{
if (rgxp.test (node.textContent))
{
/* Ignore previous match. */
var prev = node.parentElement.matches ("span.highlight");
text = (prev) ? null : (text || node);
}
}
depth_first_walk (elem, find, Node.TEXT_NODE);
remove_highlight (elem);
if (!text)
return false;
else
{
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 index page of the manual which manages the state of the application
- the iframe which contains the lateral table of content
- other iframes which contain other pages of the manual
This is done to allow referencing the same script inside every HTML page.
This has the benefits of reducing the number of HTTP requests required to
fetch the Javascript code and simplifying the work of the Texinfo HTML
converter. */
/* Display MSG bail out message portably. */
function
error (msg)
{
/* XXX: This code needs to be highly portable.
Check for details. */
return function () {
var div = document.createElement ("div");
div.setAttribute ("class", "error");
div.innerHTML = msg;
var elem = document.body.firstChild;
document.body.insertBefore (div, elem);
window.setTimeout (function () {
document.body.removeChild (div);
}, config.WARNING_TIMEOUT);
};
}
/* Check if current browser supports the minimum requirements required for
properly using this script, otherwise bails out. */
if (features && !(features.es5
&& features.classlist
&& features.eventlistener
&& features.hidden
&& features.history
&& features.postmessage
&& features.queryselector))
{
window.onload = error ("'info.js' is not compatible with this browser");
return;
}
/* Until we have a responsive design implemented, fallback to basic
HTML navigation for small screen. */
/*
if (window.screen.availWidth < config.SCREEN_MIN_WIDTH)
{
window.onload =
error ("screen width is too small to display the table of content");
return;
}
*/
register_polyfills ();
/* Let the config provided by the user mask the default one. */
config = Object.assign (config, user_config);
var inside_iframe = top !== window;
var inside_index_page = window.location.pathname === config.INDEX_NAME
|| window.location.pathname.endsWith ("/" + config.INDEX_NAME)
|| window.location.pathname.endsWith ("/");
if (inside_index_page)
{
var initial_state = {
/* Dictionary associating page ids to next, prev, up, forward,
backward link ids. */
loaded_nodes: {},
/* Dictionary associating keyword to linkids. */
index: {},
/* page id of the current page. */
current: config.INDEX_ID,
/* dictionary associating a page id to a boolean. */
ready: {},
/* Current mode for handling history. */
history: "replaceState",
/* Define the name of current text input. */
text_input: null
};
store = new Store (updater, initial_state);
var index = init_index_page ();
var sidebar = init_sidebar ();
window.addEventListener ("DOMContentLoaded", function () {
index.on_load ();
sidebar.on_load ();
}, false);
window.addEventListener ("message", index.on_message, false);
window.addEventListener ("message", sidebar.on_message, false);
window.onpopstate = index.on_popstate;
}
else if (inside_iframe)
{
store = new Remote_store ();
var iframe = init_iframe ();
window.addEventListener ("DOMContentLoaded", iframe.on_load, false);
window.addEventListener ("message", iframe.on_message, false);
}
else
{
/* Jump to 'config.INDEX_NAME' and adapt the selected iframe. */
window.location.replace (with_sidebar_query (window.location.href));
}
/* Register common event handlers. */
window.addEventListener ("beforeunload", on_unload, false);
window.addEventListener ("click", on_click, false);
/* XXX: handle 'keydown' event instead of 'keypress' since Chromium
doesn't handle the 'Escape' key properly. See
https://bugs.chromium.org/p/chromium/issues/detail?id=9061. */
window.addEventListener ("keydown", on_keydown, false);
} (window["Modernizr"], window["INFO_CONFIG"]));