Zundler/zundler/assets/inject_post.js
Adrian Vollmer 2b870da3cb Process DOM of virtual HTML file before rendering
With this commit, the virtual HTML file is processed before it's fed to
the iframe and thus rendered. With the previous approach, script tags
with inline JavaScript were executed first and script tags with a
virtual `src` were executed last. This messed up the execution flow of
the document.
2024-02-18 21:05:51 +01:00

214 lines
6.4 KiB
JavaScript

var split_url = function(url) {
// Return a list of three elements: path, GET parameters, anchor
var anchor = url.split('#')[1] || "";
var get_parameters = url.split('#')[0].split('?')[1] || "";
var path = url.split('#')[0];
path = path.split('?')[0];
let result = [path, get_parameters, anchor];
// console.log("Split URL", url, result);
return result;
}
var virtual_click = 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, get_parameters, anchor] = split_url(el.getAttribute('href'));
} else if (name == 'form') {
var [path, get_parameters, anchor] = split_url(el.getAttribute('action'));
const formData = new FormData(el);
get_parameters = new URLSearchParams(formData).toString();
} else {
console.error("Invalid element", el);
}
path = normalize_path(path);
window.parent.postMessage({
action: "virtual_click",
argument: {
path: path,
get_parameters: get_parameters,
anchor: anchor,
}
}, '*');
evnt.preventDefault();
evnt.stopPropagation();
return false;
};
var is_virtual = 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 retrieve_file = function(path) {
// console.log("Retrieving file: " + path);
var file_tree = window.global_context.file_tree;
var file = file_tree[path];
if (!file) {
console.warn("File not found: " + path);
return "";
} else {
return file.data;
}
};
var normalize_path = function(path) {
// make relative paths absolute in context of our virtual file tree
while (path && path[0] == '/') {
path = path.substr(1);
}
var result = window.global_context.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.global_context.current_path})`);
return result;
};
var on_set_data = function(argument) {
window.global_context = argument;
console.log("Received data from parent", window.global_context);
try {
monkey_patch();
// Trigger DOMContentLoaded again, some scripts that have just
// been executed expect it.
window.document.dispatchEvent(new Event("DOMContentLoaded", {
bubbles: true,
cancelable: true
}));
} finally {
observer.observe(window.document.body, {subtree: true, childList: true});
window.parent.postMessage({
action: "show_iframe",
argument: "",
}, '*');
}
}
var on_scroll_to_anchor = function(argument) {
if (window.global_context.anchor) {
document.location.replace("about:srcdoc#" + window.global_context.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 => {
fix_link(a);
});
Array.from(mutation.target.querySelectorAll("img")).forEach( img => {
embed_img(img);
});
Array.from(mutation.target.querySelectorAll("form")).forEach( form => {
fix_form(form);
});
}
});
});
var monkey_patch = 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.global_context.get_parameters;
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 = normalize_path(settings.url);
if (is_virtual(url)) {
var result;
var data;
data = retrieve_file(url);
result = settings.complete({responseText: data}, "");
return; // Return value not actually needed in searchtools.js
} else {
return jQuery.ajax(settings);
};
};
}
var on_load = function() {
// Set up message listener
window.addEventListener("message", (evnt) => {
console.log("Received message in iframe", evnt);
if (evnt.data.action == 'set_data') {
on_set_data(evnt.data.argument);
} else if (evnt.data.action == 'scroll_to_anchor') {
on_scroll_to_anchor(evnt.data.argument);
}
}, false);
// 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.querySelector('head>title');
if (title) { title = title.innerText; }
window.parent.postMessage({
action: "set_title",
argument: {
title: title,
favicon: favicon
}
}, '*');
};
window.addEventListener('load', on_load);