var splitUrl = function(url) { // Return a list of three elements: path, GET parameters, anchor var anchor = url.split('#')[1] || ""; var getParameters = url.split('#')[0].split('?')[1] || ""; var path = url.split('#')[0]; path = path.split('?')[0]; let result = [path, getParameters, anchor]; // console.log("Split URL", url, result); return result; } var virtualClick = function(evnt) { // Handle GET parameters and anchors // console.log("Virtual click", evnt); var el = evnt.currentTarget; var name = el.tagName.toLowerCase(); if (name == 'a') { var [path, getParameters, anchor] = splitUrl(el.getAttribute('href')); } else if (name == 'form') { var [path, getParameters, anchor] = splitUrl(el.getAttribute('action')); const formData = new FormData(el); getParameters = new URLSearchParams(formData).toString(); } else { console.error("Invalid element", el); } path = normalizePath(path); window.parent.postMessage({ action: "virtualClick", argument: { path: path, getParameters: getParameters, anchor: anchor, } }, '*'); evnt.preventDefault(); evnt.stopPropagation(); return false; }; var isVirtual = function(url) { // Return true if the url should be retrieved from the virtual file tree var _url = url.toString().toLowerCase(); return (! ( _url == "" || _url[0] == "#" || _url.startsWith('https:/') || _url.startsWith('http:/') || _url.startsWith('data:') || _url.startsWith('about:srcdoc') || _url.startsWith('blob:') )); }; var retrieveFile = function(path) { // console.log("Retrieving file: " + path); var fileTree = window.globalContext.fileTree; var file = fileTree[path]; if (!file) { console.warn("File not found: " + path); return ""; } else { return file; } }; var normalizePath = function(path) { // make relative paths absolute in context of our virtual file tree while (path && path[0] == '/') { path = path.substr(1); } var result = window.globalContext.current_path; result = result.split('/'); result.pop(); result = result.concat(path.split('/')); // resolve relative directories var array = []; Array.from(result).forEach( component => { if (component == '..') { if (array) { array.pop(); } } else if (component == '.') { } else { if (component) { array.push(component); } } }); result = array.join('/'); // console.log(`Normalized path: ${path} -> ${result} (@${window.globalContext.current_path})`); return result; }; var onSetData = function(argument) { // window.globalContext = argument; console.debug("Received data from parent", window.globalContext); try { // window.document.dispatchEvent(new Event("DOMContentLoaded", { bubbles: true, cancelable: true })); } finally { observer.observe(window.document.body, {subtree: true, childList: true}); // Set parent window title and trigger data transmission var favicon = window.document.querySelector("link[rel*='icon']"); if (favicon) { favicon = favicon.getAttribute('href'); } var title = window.document.title; window.parent.postMessage({ action: "set_title", argument: { title: title, favicon: favicon } }, '*'); window.parent.postMessage({ action: "show_iframe", argument: "", }, '*'); } } var fixLink = function(a) { if (isVirtual(a.getAttribute('href'))) { // a.addEventListener('click', virtualClick); a.setAttribute("onclick", "virtualClick(event)"); } else if (a.getAttribute('href').startsWith('#')) { a.setAttribute('href', "about:srcdoc" + a.getAttribute('href')) } else if (!a.getAttribute('href').startsWith('about:srcdoc')) { // External links should open in a new tab. Browsers block links to // sites of different origin within an iframe for security reasons. a.setAttribute('target', "_blank"); } }; var fixForm = function(form) { var href = form.getAttribute('action'); if (isVirtual(href) && form.getAttribute('method').toLowerCase() == 'get') { // form.addEventListener('submit', virtualClick); form.setAttribute("onsubmit", "virtualClick(event)"); } }; var embedImg = function(img) { if (img.hasAttribute('src')) { const src = img.getAttribute('src'); if (isVirtual(src)) { var path = normalizePath(src); const file = retrieveFile(path); const mime_type = window.globalContext.fileTree[path].mime_type; if (mime_type == 'image/svg+xml') { img.setAttribute('src', "data:image/svg+xml;charset=utf-8;base64, " + btoa(file)); } else { img.setAttribute('src', `data:${mime_type};base64, ${file}`); } }; }; }; var onScrollToAnchor = function(argument) { if (window.globalContext.anchor) { document.location.replace("about:srcdoc#" + window.globalContext.anchor); } } const observer = new MutationObserver((mutationList) => { // console.log("Fix mutated elements...", mutationList); mutationList.forEach((mutation) => { if (mutation.type == 'childList') { Array.from(mutation.target.querySelectorAll("a")).forEach( a => { fixLink(a); }); Array.from(mutation.target.querySelectorAll("img")).forEach( img => { embedImg(img); }); Array.from(mutation.target.querySelectorAll("form")).forEach( form => { fixForm(form); }); } }); }); var monkeyPatch = function() { if (typeof jQuery === 'undefined') {return;} // Only for jQuery at the moment /** * Monkey patch getQueryParameters * This function is defined in Sphinx' (v4) doctools.js and incompatible with our * approach. * This is a copy with effectively only the third line changed. * See: https://github.com/sphinx-doc/sphinx/blob/2329fdef8c20c6c75194f5d842b8f62ebad5c79d/sphinx/themes/basic/static/doctools.js#L54 */ jQuery._getQueryParameters = jQuery.getQueryParameters; jQuery.getQueryParameters = function(s) { if (typeof s === 'undefined') s = '?' + window.globalContext.getParameters; return jQuery._getQueryParameters(s); }; /** * Monkey patch jQuery.ajax * Only settings.url and settings.complete are supported for virtual * URLs. */ jQuery._ajax = jQuery.ajax; jQuery.ajax = function(settings) { url = normalizePath(settings.url); if (isVirtual(url)) { var result; var data; data = retrieveFile(url); result = settings.complete({responseText: data}, ""); return; // Return value not actually needed in searchtools.js } else { return jQuery.ajax(settings); }; }; } monkeyPatch(); // Set up message listener window.addEventListener("message", (evnt) => { console.log("Received message in iframe", evnt); if (evnt.data.action == 'set_data') { onSetData(evnt.data.argument); } else if (evnt.data.action == 'scroll_to_anchor') { onScrollToAnchor(evnt.data.argument); } }, false); window.parent.postMessage({ action: "ready", }, '*');