From 2489e8ab3185b579a93040f27b303dd250b9af78 Mon Sep 17 00:00:00 2001 From: Adrian Vollmer Date: Sun, 25 Sep 2022 16:53:14 +0200 Subject: [PATCH] Add monkeypatching --- src/embed.py | 36 +++++++++++++++++++++++-------- src/init.js | 34 ++++++++++++++++++++++++++--- src/inject.js | 32 +++++++++++++++++---------- src/monkeypatch.js | 54 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 133 insertions(+), 23 deletions(-) create mode 100644 src/monkeypatch.js diff --git a/src/embed.py b/src/embed.py index b0a3e5b..f53f2e3 100644 --- a/src/embed.py +++ b/src/embed.py @@ -38,6 +38,7 @@ def embed_assets(index_file): 'inject.js', 'init.css', 'init.html', + 'monkeypatch.js', 'pako.min.js', ]: path = os.path.join(SCRIPT_PATH, filename) @@ -48,7 +49,12 @@ def embed_assets(index_file): new_base_name = 'SELF_CONTAINED_' + base_name result_file = os.path.join(base_dir, new_base_name) - file_tree = load_filetree(base_dir, init_files['inject.js'], exclude_pattern=new_base_name) + file_tree = load_filetree( + base_dir, + before=init_files['monkeypatch.js'], + after=init_files['inject.js'], + exclude_pattern=new_base_name, + ) file_tree = json.dumps(file_tree) logger.debug('total asset size: %d' % len(file_tree)) file_tree = deflate(file_tree) @@ -86,7 +92,7 @@ def embed_assets(index_file): return result_file -def pack_file(filename, js): +def pack_file(filename, before, after): _, ext = os.path.splitext(filename) ext = ext.lower()[1:] data = open(filename, 'rb').read() @@ -101,7 +107,12 @@ def pack_file(filename, js): data = base64.b64encode(data) elif ext in ['html', 'htm']: - data = embed_html_resources(data, os.path.dirname(filename), js).encode() + data = embed_html_resources( + data, + os.path.dirname(filename), + before, + after, + ).encode() if not isinstance(data, str): try: @@ -120,16 +131,22 @@ def deflate(data): return data -def embed_html_resources(html, base_dir, js): +def embed_html_resources(html, base_dir, before, after): """Embed fonts in preload links to avoid jumps when loading""" # This cannot be done in JavaScript, it would be too late import bs4 soup = bs4.BeautifulSoup(html, 'lxml') - script = soup.new_tag("script") - script.string = js - soup.find('body').append(script) + if before: + script = soup.new_tag("script") + script.string = before + soup.find('body').insert(0, script) + + if after: + script = soup.new_tag("script") + script.string = after + soup.find('body').append(script) return str(soup) @@ -202,7 +219,7 @@ def embed_css_resources(css, filename): return css -def load_filetree(base_dir, js, exclude_pattern=None): +def load_filetree(base_dir, before=None, after=None, exclude_pattern=None): """Load entire directory in a dict""" result = {} @@ -214,7 +231,8 @@ def load_filetree(base_dir, js, exclude_pattern=None): key = path.relative_to(base_dir).as_posix() result[key] = pack_file( path.as_posix(), - js, + before, + after, ) logger.debug('Packed file %s [%d]' % (key, len(result[key]))) diff --git a/src/init.js b/src/init.js index 96edcae..1b73b5e 100644 --- a/src/init.js +++ b/src/init.js @@ -32,13 +32,41 @@ var createIframe = function() { return iframe; } +// var load_blob = (function () { +// var a = document.createElement("a"); +// document.body.appendChild(a); +// a.style = "display: none"; +// return function (data, get_params, anchor) { +// var blob = new Blob([data], {type: "text/html"}), +// url = window.URL.createObjectURL(blob); +// if (get_params) { +// url += "?" + get_params; // This is considered a security issue +// } +// if (anchor) { +// url += "#" + anchor; +// } +// a.href = url; +// a.target = 'main'; +// a.click(); +// window.URL.revokeObjectURL(url); +// }; +// }()); +// +// var load_virtual_page = (function (path, get_params, anchor) { +// const data = window.data.file_tree[path]; +// var iframe = createIframe(); +// load_blob(data, get_params, anchor); +// window.data.current_path = path; +// }); + var load_virtual_page = (function (path, get_params, anchor) { const data = window.data.file_tree[path]; var iframe = createIframe(); - if (get_params) { iframe.src = path + '?' + get_params; } - else { iframe.src = path; } + window.data.get_parameters = get_params; iframe.contentDocument.write(data); - if (anchor) { iframe.contentDocument.location.hash = anchor; } + if (anchor) { + iframe.contentDocument.location.hash = anchor; + } window.data.current_path = path; }); diff --git a/src/inject.js b/src/inject.js index a5d717d..b81172a 100644 --- a/src/inject.js +++ b/src/inject.js @@ -51,8 +51,17 @@ var split_url = function(url) { var virtual_click = function(evnt) { // Handle GET parameters and anchors console.log("Virtual click", evnt); - var a = evnt.currentTarget; - let [path, get_parameters, anchor] = split_url(a.getAttribute('href')); + 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({ @@ -79,14 +88,8 @@ var fix_links = function(origin) { var fix_forms = function(origin) { Array.from(document.querySelectorAll("form")).forEach( form => { var href = form.getAttribute('action'); - if (is_virtual(href)) { - // TODO test this - // let [path, get_parameters, anchor] = split_url(href); - // path = normalize_path(path); - // var new_href = to_blob(retrieve_file(path), 'text/html'); - // if (get_parameters) { new_href += '?' + get_parameters; } - // if (anchor) { new_href += '?' + anchor; } - // form.action = new_href; + if (is_virtual(href) && form.getAttribute('method').toLowerCase() == 'get') { + form.addEventListener('submit', virtual_click); } }); }; @@ -164,11 +167,17 @@ window.addEventListener("message", (evnt) => { console.log("Received data from parent", window.data); // dynamically fix elements on this page try { + embed_js(); // This might change the DOM, so do this first embed_css(); fix_links(); fix_forms(); embed_img(); - embed_js(); + // Trigger DOMContentLoaded again, some scripts that have just + // been executed expect it. + window.document.dispatchEvent(new Event("DOMContentLoaded", { + bubbles: true, + cancelable: true + })); } finally { window.parent.postMessage({ action: "show_iframe", @@ -178,6 +187,7 @@ window.addEventListener("message", (evnt) => { } }, false); + // Set parent window title window.parent.postMessage({ action: "set_title", diff --git a/src/monkeypatch.js b/src/monkeypatch.js new file mode 100644 index 0000000..f579e82 --- /dev/null +++ b/src/monkeypatch.js @@ -0,0 +1,54 @@ +/* + * Monkeypatch URLSearchParams + * + * Sphinx documents that use `searchtool.js` rely on passing information via + * GET parameters (aka search parameters). Unfortunately, this doesn't work + * in our approach due to the same origin policy, so we have to get ... + * creative. + * + * Here, we patch the `URLSearchParams` class so it returns the information + * stored in `window.data.get_parameters`. + * + */ + +const originalGet = URLSearchParams.prototype.get; + +var myGet = function (arg) { + const originalResult = originalGet.apply(this, [arg]); + // If searchtools.js of sphinx is used + if ( + (arg == "q" || arg == "highlight") && + window.DOCUMENTATION_OPTIONS && + window.Scorer && + window.data.get_parameters && + (! originalResult) + ) { + const params = new URLSearchParams('?' + window.data.get_parameters); + const result = params.get("q"); + return result; + } else { + return originalResult; + } +}; + +URLSearchParams.prototype.get = myGet; + +/* + * Monkeypatch fetch + */ + +const { fetch: originalFetch } = window; + +window.fetch = async (...args) => { + let [resource, config ] = args; + var path = normalize_path(resource); + var response; + if (is_virtual(path)) { + var data = retrieve_file(path); + response = new Response(data); + + } else { + response = await originalFetch(resource, config); + } + return response; +};