From 51d48f6b00c5528b62a2c24ec7973b380c628965 Mon Sep 17 00:00:00 2001 From: Ulrik de Muelenaere Date: Sat, 28 Oct 2017 22:16:29 +0200 Subject: [PATCH 1/8] Rewrite user stylesheet injection for WebEngine This now works correctly in XML documents. The stylesheet is applied at document creation to reduce flickering, and is updated if the user_stylesheets setting is changed after page load. --- .../browser/webengine/webenginesettings.py | 33 ++-- qutebrowser/browser/webengine/webenginetab.py | 2 +- qutebrowser/javascript/stylesheet.js | 142 ++++++++++++++++++ .../mhtml/simple/simple-webengine.mht | 6 +- 4 files changed, 167 insertions(+), 16 deletions(-) create mode 100644 qutebrowser/javascript/stylesheet.js diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py index 12503a7c0..d48c58cf4 100644 --- a/qutebrowser/browser/webengine/webenginesettings.py +++ b/qutebrowser/browser/webengine/webenginesettings.py @@ -37,7 +37,7 @@ from qutebrowser.browser import shared from qutebrowser.browser.webengine import spell from qutebrowser.config import config, websettings from qutebrowser.utils import (utils, standarddir, javascript, qtutils, - message, log) + message, log, objreg) # The default QWebEngineProfile default_profile = None @@ -153,33 +153,41 @@ class DictionaryLanguageSetter(DefaultProfileSetter): def _init_stylesheet(profile): """Initialize custom stylesheets. - Mostly inspired by QupZilla: + Partially inspired by QupZilla: https://github.com/QupZilla/qupzilla/blob/v2.0/src/lib/app/mainapplication.cpp#L1063-L1101 - https://github.com/QupZilla/qupzilla/blob/v2.0/src/lib/tools/scripts.cpp#L119-L132 """ old_script = profile.scripts().findScript('_qute_stylesheet') if not old_script.isNull(): profile.scripts().remove(old_script) css = shared.get_user_stylesheet() - source = """ - (function() {{ - var css = document.createElement('style'); - css.setAttribute('type', 'text/css'); - css.appendChild(document.createTextNode('{}')); - document.getElementsByTagName('head')[0].appendChild(css); - }})() - """.format(javascript.string_escape(css)) + source = '\n'.join([ + '"use strict";', + 'window._qutebrowser = window._qutebrowser || {};', + utils.read_file('javascript/stylesheet.js'), + javascript.assemble('stylesheet', 'set_css', css), + ]) script = QWebEngineScript() script.setName('_qute_stylesheet') - script.setInjectionPoint(QWebEngineScript.DocumentReady) + script.setInjectionPoint(QWebEngineScript.DocumentCreation) script.setWorldId(QWebEngineScript.ApplicationWorld) script.setRunsOnSubFrames(True) script.setSourceCode(source) profile.scripts().insert(script) +def _update_stylesheet(): + """Update the custom stylesheet in existing tabs.""" + css = shared.get_user_stylesheet() + code = javascript.assemble('stylesheet', 'set_css', css) + for win_id in objreg.window_registry: + tab_registry = objreg.get('tab-registry', scope='window', + window=win_id) + for tab in tab_registry.values(): + tab.run_js_async(code) + + def _set_http_headers(profile): """Set the user agent and accept-language for the given profile. @@ -199,6 +207,7 @@ def _update_settings(option): if option in ['scrolling.bar', 'content.user_stylesheets']: _init_stylesheet(default_profile) _init_stylesheet(private_profile) + _update_stylesheet() elif option in ['content.headers.user_agent', 'content.headers.accept_language']: _set_http_headers(default_profile) diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 15466ab0d..22e17e2e7 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -533,7 +533,7 @@ class WebEngineTab(browsertab.AbstractTab): def _init_js(self): js_code = '\n'.join([ '"use strict";', - 'window._qutebrowser = {};', + 'window._qutebrowser = window._qutebrowser || {};', utils.read_file('javascript/scroll.js'), utils.read_file('javascript/webelem.js'), ]) diff --git a/qutebrowser/javascript/stylesheet.js b/qutebrowser/javascript/stylesheet.js new file mode 100644 index 000000000..3f2bc78a0 --- /dev/null +++ b/qutebrowser/javascript/stylesheet.js @@ -0,0 +1,142 @@ +/** + * Copyright 2017 Ulrik de Muelenaere + * + * This file is part of qutebrowser. + * + * qutebrowser 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. + * + * qutebrowser 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 qutebrowser. If not, see . + */ + +"use strict"; + +window._qutebrowser.stylesheet = (function() { + if (window._qutebrowser.stylesheet) { + return window._qutebrowser.stylesheet; + } + + var funcs = {}; + + var xhtml_ns = "http://www.w3.org/1999/xhtml"; + var svg_ns = "http://www.w3.org/2000/svg"; + + var root_elem; + var style_elem; + var css_content = ""; + + var root_observer; + var style_observer; + var initialized = false; + + // Watch for rewrites of the root element and changes to its children, + // then move the stylesheet to the end. Partially inspired by Stylus: + // https://github.com/openstyles/stylus/blob/1.1.4.2/content/apply.js#L235-L355 + function watch_root() { + if (root_elem !== document.documentElement) { + root_elem = document.documentElement; + root_observer.disconnect(); + root_observer.observe(document, {"childList": true}); + root_observer.observe(root_elem, {"childList": true}); + } + if (style_elem !== root_elem.lastChild) { + root_elem.appendChild(style_elem); + } + } + + function create_style() { + var ns = xhtml_ns; + if (document.documentElement.namespaceURI === svg_ns) { + ns = svg_ns; + } + style_elem = document.createElementNS(ns, "style"); + style_elem.textContent = css_content; + root_observer = new MutationObserver(watch_root); + watch_root(); + } + + // We should only inject the stylesheet if the document already has style + // information associated with it. Otherwise we wait until the browser + // rewrites it to an XHTML document showing the document tree. As a + // starting point for exploring the relevant code in Chromium, see + // https://github.com/qt/qtwebengine-chromium/blob/cfe8c60/chromium/third_party/WebKit/Source/core/xml/parser/XMLDocumentParser.cpp#L1539-L1540 + function check_style(node) { + var stylesheet = node.nodeType === Node.PROCESSING_INSTRUCTION_NODE && + node.target === "xml-stylesheet" && + node.parentNode === document; + var known_ns = node.nodeType === Node.ELEMENT_NODE && + (node.namespaceURI === xhtml_ns || + node.namespaceURI === svg_ns); + if (stylesheet || known_ns) { + create_style(); + return true; + } + return false; + } + + function check_added_style(mutations) { + for (var mi = 0; mi < mutations.length; ++mi) { + var nodes = mutations[mi].addedNodes; + for (var ni = 0; ni < nodes.length; ++ni) { + if (check_style(nodes[ni])) { + style_observer.disconnect(); + return; + } + } + } + } + + function init() { + initialized = true; + // Chromium will not rewrite a document inside a frame, so add the + // stylesheet even if the document is unstyled. + if (window !== window.top) { + create_style(); + return; + } + var iter = document.createNodeIterator(document); + var node; + while ((node = iter.nextNode())) { + if (check_style(node)) { + return; + } + } + style_observer = new MutationObserver(check_added_style); + style_observer.observe(document, {"childList": true, "subtree": true}); + } + + var doc = document; + + funcs.set_css = function(css) { + if (!initialized) { + init(); + } + if (style_elem) { + style_elem.textContent = css; + // The browser seems to rewrite the document in same-origin frames + // without notifying the mutation observer. Ensure that the + // stylesheet is in the current document. + watch_root(); + } else { + css_content = css; + } + // Propagate the new CSS to all child frames. + // FIXME:qtwebengine This does not work for cross-origin frames. + for (var i = 0; i < window.frames.length; ++i) { + var frame = window.frames[i]; + if (frame._qutebrowser && frame._qutebrowser.stylesheet) { + frame._qutebrowser.stylesheet.set_css(css); + } + } + }; + + return funcs; +})(); diff --git a/tests/end2end/data/downloads/mhtml/simple/simple-webengine.mht b/tests/end2end/data/downloads/mhtml/simple/simple-webengine.mht index d8bfdee70..79bd1ae50 100644 --- a/tests/end2end/data/downloads/mhtml/simple/simple-webengine.mht +++ b/tests/end2end/data/downloads/mhtml/simple/simple-webengine.mht @@ -16,11 +16,11 @@ Content-Location: http://localhost:(port)/data/downloads/mhtml/simple/simple.htm t/html; charset=3DUTF-8"> =20 Simple MHTML test - + normal link to another page =20 - + -----=_qute-UUID From 2fe1a1db89f97c23b3ca1aeac8fb33f4d8c5f940 Mon Sep 17 00:00:00 2001 From: Ulrik de Muelenaere Date: Sun, 29 Oct 2017 00:23:11 +0200 Subject: [PATCH 2/8] Remove unused variable --- qutebrowser/javascript/stylesheet.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/qutebrowser/javascript/stylesheet.js b/qutebrowser/javascript/stylesheet.js index 3f2bc78a0..095a7f6bb 100644 --- a/qutebrowser/javascript/stylesheet.js +++ b/qutebrowser/javascript/stylesheet.js @@ -113,8 +113,6 @@ window._qutebrowser.stylesheet = (function() { style_observer.observe(document, {"childList": true, "subtree": true}); } - var doc = document; - funcs.set_css = function(css) { if (!initialized) { init(); From 0540a43995d5115af22b80b70e1689d56a054ca0 Mon Sep 17 00:00:00 2001 From: Ulrik de Muelenaere Date: Mon, 30 Oct 2017 19:52:15 +0200 Subject: [PATCH 3/8] Check for deleted window --- qutebrowser/browser/webengine/webenginesettings.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py index d48c58cf4..ff0beb805 100644 --- a/qutebrowser/browser/webengine/webenginesettings.py +++ b/qutebrowser/browser/webengine/webenginesettings.py @@ -29,6 +29,7 @@ Module attributes: import os +import sip from PyQt5.QtGui import QFont from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile, QWebEngineScript) @@ -181,7 +182,10 @@ def _update_stylesheet(): """Update the custom stylesheet in existing tabs.""" css = shared.get_user_stylesheet() code = javascript.assemble('stylesheet', 'set_css', css) - for win_id in objreg.window_registry: + for win_id, window in objreg.window_registry.items(): + # We could be in the middle of destroying a window here + if sip.isdeleted(window): + continue tab_registry = objreg.get('tab-registry', scope='window', window=win_id) for tab in tab_registry.values(): From 34b27437d0e18c5ec832a78dfac09291187f4fa7 Mon Sep 17 00:00:00 2001 From: Ulrik de Muelenaere Date: Mon, 30 Oct 2017 19:55:37 +0200 Subject: [PATCH 4/8] Clarify function names in stylesheet.js --- qutebrowser/javascript/stylesheet.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/qutebrowser/javascript/stylesheet.js b/qutebrowser/javascript/stylesheet.js index 095a7f6bb..e363d296f 100644 --- a/qutebrowser/javascript/stylesheet.js +++ b/qutebrowser/javascript/stylesheet.js @@ -75,18 +75,15 @@ window._qutebrowser.stylesheet = (function() { var known_ns = node.nodeType === Node.ELEMENT_NODE && (node.namespaceURI === xhtml_ns || node.namespaceURI === svg_ns); - if (stylesheet || known_ns) { - create_style(); - return true; - } - return false; + return stylesheet || known_ns; } - function check_added_style(mutations) { + function watch_added_style(mutations) { for (var mi = 0; mi < mutations.length; ++mi) { var nodes = mutations[mi].addedNodes; for (var ni = 0; ni < nodes.length; ++ni) { if (check_style(nodes[ni])) { + create_style(); style_observer.disconnect(); return; } @@ -106,10 +103,11 @@ window._qutebrowser.stylesheet = (function() { var node; while ((node = iter.nextNode())) { if (check_style(node)) { + create_style(); return; } } - style_observer = new MutationObserver(check_added_style); + style_observer = new MutationObserver(watch_added_style); style_observer.observe(document, {"childList": true, "subtree": true}); } From 3adc2e0f83bbba3c7b6db0619f2ee2ae90703db2 Mon Sep 17 00:00:00 2001 From: Ulrik de Muelenaere Date: Mon, 30 Oct 2017 19:56:12 +0200 Subject: [PATCH 5/8] Add filter to NodeIterator checking for styled nodes --- qutebrowser/javascript/stylesheet.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qutebrowser/javascript/stylesheet.js b/qutebrowser/javascript/stylesheet.js index e363d296f..384138f2f 100644 --- a/qutebrowser/javascript/stylesheet.js +++ b/qutebrowser/javascript/stylesheet.js @@ -99,7 +99,9 @@ window._qutebrowser.stylesheet = (function() { create_style(); return; } - var iter = document.createNodeIterator(document); + var iter = document.createNodeIterator(document, + // eslint-disable-next-line no-bitwise + NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_ELEMENT); var node; while ((node = iter.nextNode())) { if (check_style(node)) { From 95b41b311f6199da976b7bafbd3148d2aa35287f Mon Sep 17 00:00:00 2001 From: Ulrik de Muelenaere Date: Mon, 30 Oct 2017 22:24:59 +0200 Subject: [PATCH 6/8] Disable ESLint no-bitwise rule --- qutebrowser/javascript/.eslintrc.yaml | 1 + qutebrowser/javascript/stylesheet.js | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/javascript/.eslintrc.yaml b/qutebrowser/javascript/.eslintrc.yaml index d75ef11d8..62ffe52d1 100644 --- a/qutebrowser/javascript/.eslintrc.yaml +++ b/qutebrowser/javascript/.eslintrc.yaml @@ -45,3 +45,4 @@ rules: no-multi-spaces: ["error", {"ignoreEOLComments": true}] function-paren-newline: "off" multiline-comment-style: "off" + no-bitwise: "off" diff --git a/qutebrowser/javascript/stylesheet.js b/qutebrowser/javascript/stylesheet.js index 384138f2f..b1cc70e9f 100644 --- a/qutebrowser/javascript/stylesheet.js +++ b/qutebrowser/javascript/stylesheet.js @@ -100,7 +100,6 @@ window._qutebrowser.stylesheet = (function() { return; } var iter = document.createNodeIterator(document, - // eslint-disable-next-line no-bitwise NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_ELEMENT); var node; while ((node = iter.nextNode())) { From ce1494e5ec9ecdb93f50eceac1c0a917bf12c80c Mon Sep 17 00:00:00 2001 From: Ulrik de Muelenaere Date: Fri, 3 Nov 2017 12:17:35 +0200 Subject: [PATCH 7/8] Update stylesheet.js to ES6 --- qutebrowser/javascript/stylesheet.js | 64 +++++++++++++--------------- 1 file changed, 30 insertions(+), 34 deletions(-) diff --git a/qutebrowser/javascript/stylesheet.js b/qutebrowser/javascript/stylesheet.js index b1cc70e9f..13741cf41 100644 --- a/qutebrowser/javascript/stylesheet.js +++ b/qutebrowser/javascript/stylesheet.js @@ -24,18 +24,17 @@ window._qutebrowser.stylesheet = (function() { return window._qutebrowser.stylesheet; } - var funcs = {}; + const funcs = {}; - var xhtml_ns = "http://www.w3.org/1999/xhtml"; - var svg_ns = "http://www.w3.org/2000/svg"; + const xhtml_ns = "http://www.w3.org/1999/xhtml"; + const svg_ns = "http://www.w3.org/2000/svg"; - var root_elem; - var style_elem; - var css_content = ""; + let root_elem; + let style_elem; + let css_content = ""; - var root_observer; - var style_observer; - var initialized = false; + let root_observer; + let initialized = false; // Watch for rewrites of the root element and changes to its children, // then move the stylesheet to the end. Partially inspired by Stylus: @@ -53,7 +52,7 @@ window._qutebrowser.stylesheet = (function() { } function create_style() { - var ns = xhtml_ns; + let ns = xhtml_ns; if (document.documentElement.namespaceURI === svg_ns) { ns = svg_ns; } @@ -69,28 +68,15 @@ window._qutebrowser.stylesheet = (function() { // starting point for exploring the relevant code in Chromium, see // https://github.com/qt/qtwebengine-chromium/blob/cfe8c60/chromium/third_party/WebKit/Source/core/xml/parser/XMLDocumentParser.cpp#L1539-L1540 function check_style(node) { - var stylesheet = node.nodeType === Node.PROCESSING_INSTRUCTION_NODE && - node.target === "xml-stylesheet" && - node.parentNode === document; - var known_ns = node.nodeType === Node.ELEMENT_NODE && - (node.namespaceURI === xhtml_ns || - node.namespaceURI === svg_ns); + const stylesheet = node.nodeType === Node.PROCESSING_INSTRUCTION_NODE && + node.target === "xml-stylesheet" && + node.parentNode === document; + const known_ns = node.nodeType === Node.ELEMENT_NODE && + (node.namespaceURI === xhtml_ns || + node.namespaceURI === svg_ns); return stylesheet || known_ns; } - function watch_added_style(mutations) { - for (var mi = 0; mi < mutations.length; ++mi) { - var nodes = mutations[mi].addedNodes; - for (var ni = 0; ni < nodes.length; ++ni) { - if (check_style(nodes[ni])) { - create_style(); - style_observer.disconnect(); - return; - } - } - } - } - function init() { initialized = true; // Chromium will not rewrite a document inside a frame, so add the @@ -99,16 +85,26 @@ window._qutebrowser.stylesheet = (function() { create_style(); return; } - var iter = document.createNodeIterator(document, + const iter = document.createNodeIterator(document, NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_ELEMENT); - var node; + let node; while ((node = iter.nextNode())) { if (check_style(node)) { create_style(); return; } } - style_observer = new MutationObserver(watch_added_style); + const style_observer = new MutationObserver((mutations) => { + for (const mutation of mutations) { + for (const added of mutation.addedNodes) { + if (check_style(added)) { + create_style(); + style_observer.disconnect(); + return; + } + } + } + }); style_observer.observe(document, {"childList": true, "subtree": true}); } @@ -127,8 +123,8 @@ window._qutebrowser.stylesheet = (function() { } // Propagate the new CSS to all child frames. // FIXME:qtwebengine This does not work for cross-origin frames. - for (var i = 0; i < window.frames.length; ++i) { - var frame = window.frames[i]; + for (let i = 0; i < window.frames.length; ++i) { + const frame = window.frames[i]; if (frame._qutebrowser && frame._qutebrowser.stylesheet) { frame._qutebrowser.stylesheet.set_css(css); } From b37517e55f6363722e576370e5fd6e9af8e007a7 Mon Sep 17 00:00:00 2001 From: Ulrik de Muelenaere Date: Thu, 9 Nov 2017 19:28:36 +0200 Subject: [PATCH 8/8] Fix error in stylesheet.js on older QtWebEngine --- qutebrowser/javascript/stylesheet.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qutebrowser/javascript/stylesheet.js b/qutebrowser/javascript/stylesheet.js index 13741cf41..b1cdeb26e 100644 --- a/qutebrowser/javascript/stylesheet.js +++ b/qutebrowser/javascript/stylesheet.js @@ -96,8 +96,9 @@ window._qutebrowser.stylesheet = (function() { } const style_observer = new MutationObserver((mutations) => { for (const mutation of mutations) { - for (const added of mutation.addedNodes) { - if (check_style(added)) { + const nodes = mutation.addedNodes; + for (let i = 0; i < nodes.length; ++i) { + if (check_style(nodes[i])) { create_style(); style_observer.disconnect(); return;