From d05a5f3c46d8baa0d6935e5387caf7ca5ba4235e Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Tue, 21 Apr 2020 21:14:54 -0400 Subject: Create dynamic script/CSS loader utility --- ext/mixed/js/dynamic-loader.js | 80 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 ext/mixed/js/dynamic-loader.js (limited to 'ext/mixed/js/dynamic-loader.js') diff --git a/ext/mixed/js/dynamic-loader.js b/ext/mixed/js/dynamic-loader.js new file mode 100644 index 00000000..20ed8454 --- /dev/null +++ b/ext/mixed/js/dynamic-loader.js @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2020 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +const dynamicLoader = (() => { + function loadStyles(urls) { + const parent = document.head; + for (const url of urls) { + const node = parent.querySelector(`link[href='${escapeCSSAttribute(url)}']`); + if (node !== null) { continue; } + + const style = document.createElement('link'); + style.rel = 'stylesheet'; + style.type = 'text/css'; + style.href = url; + parent.appendChild(style); + } + } + + function loadScripts(urls) { + return new Promise((resolve) => { + const parent = document.body; + for (const url of urls) { + const node = parent.querySelector(`script[src='${escapeCSSAttribute(url)}']`); + if (node !== null) { continue; } + + const script = document.createElement('script'); + script.async = false; + script.src = url; + parent.appendChild(script); + } + + loadScriptSentinel(resolve); + }); + } + + function loadScriptSentinel(resolve, reject) { + const script = document.createElement('script'); + + const sentinelEventName = 'dynamicLoaderSentinel'; + const sentinelEventCallback = (e) => { + if (e.script !== script) { return; } + yomichan.off(sentinelEventName, sentinelEventCallback); + resolve(); + }; + yomichan.on(sentinelEventName, sentinelEventCallback); + + try { + script.async = false; + script.src = '/mixed/js/dynamic-loader-sentinel.js'; + document.body.appendChild(script); + } catch (e) { + yomichan.off(sentinelEventName, sentinelEventCallback); + reject(e); + } + } + + function escapeCSSAttribute(value) { + return value.replace(/['\\]/g, (character) => `\\${character}`); + } + + + return { + loadStyles, + loadScripts + }; +})(); -- cgit v1.2.3 From 749b747728175d8a7d5638af2c6ec01102a282c1 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Wed, 22 Apr 2020 17:48:42 -0400 Subject: Remove sentinel script node after load --- ext/mixed/js/dynamic-loader.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'ext/mixed/js/dynamic-loader.js') diff --git a/ext/mixed/js/dynamic-loader.js b/ext/mixed/js/dynamic-loader.js index 20ed8454..29672d36 100644 --- a/ext/mixed/js/dynamic-loader.js +++ b/ext/mixed/js/dynamic-loader.js @@ -48,12 +48,14 @@ const dynamicLoader = (() => { } function loadScriptSentinel(resolve, reject) { + const parent = document.body; const script = document.createElement('script'); const sentinelEventName = 'dynamicLoaderSentinel'; const sentinelEventCallback = (e) => { if (e.script !== script) { return; } yomichan.off(sentinelEventName, sentinelEventCallback); + parent.removeChild(script); resolve(); }; yomichan.on(sentinelEventName, sentinelEventCallback); @@ -61,7 +63,7 @@ const dynamicLoader = (() => { try { script.async = false; script.src = '/mixed/js/dynamic-loader-sentinel.js'; - document.body.appendChild(script); + parent.appendChild(script); } catch (e) { yomichan.off(sentinelEventName, sentinelEventCallback); reject(e); -- cgit v1.2.3 From c4ea9321dcffbda9004461a7b0027cf5c893f3c0 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 2 May 2020 13:00:46 -0400 Subject: Validate document nodes before use (#493) * Validate document.body before use in loadScripts This also fixes an issue where reject wasn't being passed to loadScriptSentinel. * Validate document nodes before use in _getSiteColor * Validate document.body before use in _getViewport * Validate document.body before use in setContentScale * Validate document.body before use in docImposterCreate --- ext/fg/js/document.js | 5 ++++- ext/fg/js/float.js | 4 +++- ext/fg/js/popup.js | 12 +++++++++--- ext/mixed/js/dynamic-loader.js | 12 ++++++++---- 4 files changed, 24 insertions(+), 9 deletions(-) (limited to 'ext/mixed/js/dynamic-loader.js') diff --git a/ext/fg/js/document.js b/ext/fg/js/document.js index 3b4cc28f..6103c7c5 100644 --- a/ext/fg/js/document.js +++ b/ext/fg/js/document.js @@ -28,6 +28,9 @@ function docSetImposterStyle(style, propertyName, value) { } function docImposterCreate(element, isTextarea) { + const body = document.body; + if (body === null) { return [null, null]; } + const elementStyle = window.getComputedStyle(element); const elementRect = element.getBoundingClientRect(); const documentRect = document.documentElement.getBoundingClientRect(); @@ -78,7 +81,7 @@ function docImposterCreate(element, isTextarea) { } container.appendChild(imposter); - document.body.appendChild(container); + body.appendChild(container); // Adjust size const imposterRect = imposter.getBoundingClientRect(); diff --git a/ext/fg/js/float.js b/ext/fg/js/float.js index 294093cd..77e8edd8 100644 --- a/ext/fg/js/float.js +++ b/ext/fg/js/float.js @@ -162,7 +162,9 @@ class DisplayFloat extends Display { } setContentScale(scale) { - document.body.style.fontSize = `${scale}em`; + const body = document.body; + if (body === null) { return; } + body.style.fontSize = `${scale}em`; } async getDocumentTitle() { diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index 2b33b714..f5cb6f77 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -389,8 +389,13 @@ class Popup { _getSiteColor() { const color = [255, 255, 255]; - Popup._addColor(color, Popup._getColorInfo(window.getComputedStyle(document.documentElement).backgroundColor)); - Popup._addColor(color, Popup._getColorInfo(window.getComputedStyle(document.body).backgroundColor)); + const {documentElement, body} = document; + if (documentElement !== null) { + Popup._addColor(color, Popup._getColorInfo(window.getComputedStyle(documentElement).backgroundColor)); + } + if (body !== null) { + Popup._addColor(color, Popup._getColorInfo(window.getComputedStyle(body).backgroundColor)); + } const dark = (color[0] < 128 && color[1] < 128 && color[2] < 128); return dark ? 'dark' : 'light'; } @@ -575,10 +580,11 @@ class Popup { } } + const body = document.body; return { left: 0, top: 0, - right: document.body.clientWidth, + right: (body !== null ? body.clientWidth : 0), bottom: window.innerHeight }; } diff --git a/ext/mixed/js/dynamic-loader.js b/ext/mixed/js/dynamic-loader.js index 29672d36..51b6821b 100644 --- a/ext/mixed/js/dynamic-loader.js +++ b/ext/mixed/js/dynamic-loader.js @@ -31,8 +31,13 @@ const dynamicLoader = (() => { } function loadScripts(urls) { - return new Promise((resolve) => { + return new Promise((resolve, reject) => { const parent = document.body; + if (parent === null) { + reject(new Error('Missing body')); + return; + } + for (const url of urls) { const node = parent.querySelector(`script[src='${escapeCSSAttribute(url)}']`); if (node !== null) { continue; } @@ -43,12 +48,11 @@ const dynamicLoader = (() => { parent.appendChild(script); } - loadScriptSentinel(resolve); + loadScriptSentinel(parent, resolve, reject); }); } - function loadScriptSentinel(resolve, reject) { - const parent = document.body; + function loadScriptSentinel(parent, resolve, reject) { const script = document.createElement('script'); const sentinelEventName = 'dynamicLoaderSentinel'; -- cgit v1.2.3 From dd673f0b2626c2bffbcb301dc364009f077c3d08 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Tue, 19 May 2020 20:33:06 -0400 Subject: Dynamic loader load style (#521) * Remove unnecessary load of /fg/css/client.css * Replace dynamicLoader.loadStyles with dynamicLoader.loadStyle * Replace Popup._injectStylesheet with dynamicLoader.loadStyle * Remove unused global --- ext/bg/js/search-main.js | 3 -- ext/bg/settings-popup-preview.html | 1 + ext/fg/js/popup.js | 78 ++------------------------------------ ext/manifest.json | 1 + ext/mixed/js/dynamic-loader.js | 77 +++++++++++++++++++++++++++++++------ 5 files changed, 70 insertions(+), 90 deletions(-) (limited to 'ext/mixed/js/dynamic-loader.js') diff --git a/ext/bg/js/search-main.js b/ext/bg/js/search-main.js index 6e092fbc..54fa549d 100644 --- a/ext/bg/js/search-main.js +++ b/ext/bg/js/search-main.js @@ -23,9 +23,6 @@ */ async function injectSearchFrontend() { - dynamicLoader.loadStyles([ - '/fg/css/client.css' - ]); await dynamicLoader.loadScripts([ '/mixed/js/text-scanner.js', '/fg/js/frontend-api-receiver.js', diff --git a/ext/bg/settings-popup-preview.html b/ext/bg/settings-popup-preview.html index 3d7f6455..2f0b841b 100644 --- a/ext/bg/settings-popup-preview.html +++ b/ext/bg/settings-popup-preview.html @@ -121,6 +121,7 @@ + diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index 9e9debd8..b7d4b57e 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -17,8 +17,8 @@ /* global * DOM - * apiInjectStylesheet * apiOptionsGet + * dynamicLoader */ class Popup { @@ -180,12 +180,7 @@ class Popup { } async setCustomOuterCss(css, useWebExtensionApi) { - return await this._injectStylesheet( - 'yomichan-popup-outer-user-stylesheet', - 'code', - css, - useWebExtensionApi - ); + return await dynamicLoader.loadStyle('yomichan-popup-outer-user-stylesheet', 'code', css, useWebExtensionApi); } setChildrenSupported(value) { @@ -391,7 +386,7 @@ class Popup { async _injectStyles() { try { - await this._injectStylesheet('yomichan-popup-outer-stylesheet', 'file', '/fg/css/client.css', true); + await dynamicLoader.loadStyle('yomichan-popup-outer-stylesheet', 'file', '/fg/css/client.css', true); } catch (e) { // NOP } @@ -717,71 +712,6 @@ class Popup { }; } - async _injectStylesheet(id, type, value, useWebExtensionApi) { - const injectedStylesheets = Popup._injectedStylesheets; - - if (yomichan.isExtensionUrl(window.location.href)) { - // Permissions error will occur if trying to use the WebExtension API to inject - // into an extension page. - useWebExtensionApi = false; - } - - let styleNode = injectedStylesheets.get(id); - if (typeof styleNode !== 'undefined') { - if (styleNode === null) { - // Previously injected via WebExtension API - throw new Error(`Stylesheet with id ${id} has already been injected using the WebExtension API`); - } - } else { - styleNode = null; - } - - if (useWebExtensionApi) { - // Inject via WebExtension API - if (styleNode !== null && styleNode.parentNode !== null) { - styleNode.parentNode.removeChild(styleNode); - } - - await apiInjectStylesheet(type, value); - - injectedStylesheets.set(id, null); - return null; - } - - // Create node in document - const parentNode = document.head; - if (parentNode === null) { - throw new Error('No parent node'); - } - - // Create or reuse node - const isFile = (type === 'file'); - const tagName = isFile ? 'link' : 'style'; - if (styleNode === null || styleNode.nodeName.toLowerCase() !== tagName) { - if (styleNode !== null && styleNode.parentNode !== null) { - styleNode.parentNode.removeChild(styleNode); - } - styleNode = document.createElement(tagName); - styleNode.id = id; - } - - // Update node style - if (isFile) { - styleNode.rel = value; - } else { - styleNode.textContent = value; - } - - // Update parent - if (styleNode.parentNode !== parentNode) { - parentNode.appendChild(styleNode); - } - - // Add to map - injectedStylesheets.set(id, styleNode); - return styleNode; - } - static isFrameAboutBlank(frame) { try { const contentDocument = frame.contentDocument; @@ -793,5 +723,3 @@ class Popup { } } } - -Popup._injectedStylesheets = new Map(); diff --git a/ext/manifest.json b/ext/manifest.json index 865fe3f3..c87b9296 100644 --- a/ext/manifest.json +++ b/ext/manifest.json @@ -38,6 +38,7 @@ "mixed/js/core.js", "mixed/js/dom.js", "mixed/js/api.js", + "mixed/js/dynamic-loader.js", "mixed/js/text-scanner.js", "fg/js/document.js", "fg/js/frontend-api-sender.js", diff --git a/ext/mixed/js/dynamic-loader.js b/ext/mixed/js/dynamic-loader.js index 51b6821b..ce946109 100644 --- a/ext/mixed/js/dynamic-loader.js +++ b/ext/mixed/js/dynamic-loader.js @@ -15,19 +15,72 @@ * along with this program. If not, see . */ +/* global + * apiInjectStylesheet + */ + const dynamicLoader = (() => { - function loadStyles(urls) { - const parent = document.head; - for (const url of urls) { - const node = parent.querySelector(`link[href='${escapeCSSAttribute(url)}']`); - if (node !== null) { continue; } - - const style = document.createElement('link'); - style.rel = 'stylesheet'; - style.type = 'text/css'; - style.href = url; - parent.appendChild(style); + const injectedStylesheets = new Map(); + + async function loadStyle(id, type, value, useWebExtensionApi=false) { + if (useWebExtensionApi && yomichan.isExtensionUrl(window.location.href)) { + // Permissions error will occur if trying to use the WebExtension API to inject into an extension page + useWebExtensionApi = false; + } + + let styleNode = injectedStylesheets.get(id); + if (typeof styleNode !== 'undefined') { + if (styleNode === null) { + // Previously injected via WebExtension API + throw new Error(`Stylesheet with id ${id} has already been injected using the WebExtension API`); + } + } else { + styleNode = null; + } + + if (useWebExtensionApi) { + // Inject via WebExtension API + if (styleNode !== null && styleNode.parentNode !== null) { + styleNode.parentNode.removeChild(styleNode); + } + + injectedStylesheets.set(id, null); + await apiInjectStylesheet(type, value); + return null; } + + // Create node in document + const parentNode = document.head; + if (parentNode === null) { + throw new Error('No parent node'); + } + + // Create or reuse node + const isFile = (type === 'file'); + const tagName = isFile ? 'link' : 'style'; + if (styleNode === null || styleNode.nodeName.toLowerCase() !== tagName) { + if (styleNode !== null && styleNode.parentNode !== null) { + styleNode.parentNode.removeChild(styleNode); + } + styleNode = document.createElement(tagName); + } + + // Update node style + if (isFile) { + styleNode.rel = 'stylesheet'; + styleNode.href = value; + } else { + styleNode.textContent = value; + } + + // Update parent + if (styleNode.parentNode !== parentNode) { + parentNode.appendChild(styleNode); + } + + // Add to map + injectedStylesheets.set(id, styleNode); + return styleNode; } function loadScripts(urls) { @@ -80,7 +133,7 @@ const dynamicLoader = (() => { return { - loadStyles, + loadStyle, loadScripts }; })(); -- cgit v1.2.3