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.
This commit is contained in:
Ulrik de Muelenaere 2017-10-28 22:16:29 +02:00
parent 0d1e716760
commit 51d48f6b00
4 changed files with 167 additions and 16 deletions

View File

@ -37,7 +37,7 @@ from qutebrowser.browser import shared
from qutebrowser.browser.webengine import spell from qutebrowser.browser.webengine import spell
from qutebrowser.config import config, websettings from qutebrowser.config import config, websettings
from qutebrowser.utils import (utils, standarddir, javascript, qtutils, from qutebrowser.utils import (utils, standarddir, javascript, qtutils,
message, log) message, log, objreg)
# The default QWebEngineProfile # The default QWebEngineProfile
default_profile = None default_profile = None
@ -153,33 +153,41 @@ class DictionaryLanguageSetter(DefaultProfileSetter):
def _init_stylesheet(profile): def _init_stylesheet(profile):
"""Initialize custom stylesheets. """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/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') old_script = profile.scripts().findScript('_qute_stylesheet')
if not old_script.isNull(): if not old_script.isNull():
profile.scripts().remove(old_script) profile.scripts().remove(old_script)
css = shared.get_user_stylesheet() css = shared.get_user_stylesheet()
source = """ source = '\n'.join([
(function() {{ '"use strict";',
var css = document.createElement('style'); 'window._qutebrowser = window._qutebrowser || {};',
css.setAttribute('type', 'text/css'); utils.read_file('javascript/stylesheet.js'),
css.appendChild(document.createTextNode('{}')); javascript.assemble('stylesheet', 'set_css', css),
document.getElementsByTagName('head')[0].appendChild(css); ])
}})()
""".format(javascript.string_escape(css))
script = QWebEngineScript() script = QWebEngineScript()
script.setName('_qute_stylesheet') script.setName('_qute_stylesheet')
script.setInjectionPoint(QWebEngineScript.DocumentReady) script.setInjectionPoint(QWebEngineScript.DocumentCreation)
script.setWorldId(QWebEngineScript.ApplicationWorld) script.setWorldId(QWebEngineScript.ApplicationWorld)
script.setRunsOnSubFrames(True) script.setRunsOnSubFrames(True)
script.setSourceCode(source) script.setSourceCode(source)
profile.scripts().insert(script) 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): def _set_http_headers(profile):
"""Set the user agent and accept-language for the given 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']: if option in ['scrolling.bar', 'content.user_stylesheets']:
_init_stylesheet(default_profile) _init_stylesheet(default_profile)
_init_stylesheet(private_profile) _init_stylesheet(private_profile)
_update_stylesheet()
elif option in ['content.headers.user_agent', elif option in ['content.headers.user_agent',
'content.headers.accept_language']: 'content.headers.accept_language']:
_set_http_headers(default_profile) _set_http_headers(default_profile)

View File

@ -533,7 +533,7 @@ class WebEngineTab(browsertab.AbstractTab):
def _init_js(self): def _init_js(self):
js_code = '\n'.join([ js_code = '\n'.join([
'"use strict";', '"use strict";',
'window._qutebrowser = {};', 'window._qutebrowser = window._qutebrowser || {};',
utils.read_file('javascript/scroll.js'), utils.read_file('javascript/scroll.js'),
utils.read_file('javascript/webelem.js'), utils.read_file('javascript/webelem.js'),
]) ])

View File

@ -0,0 +1,142 @@
/**
* Copyright 2017 Ulrik de Muelenaere <ulrikdem@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
"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;
})();

View File

@ -16,11 +16,11 @@ Content-Location: http://localhost:(port)/data/downloads/mhtml/simple/simple.htm
t/html; charset=3DUTF-8"> t/html; charset=3DUTF-8">
=20 =20
<title>Simple MHTML test</title> <title>Simple MHTML test</title>
<style type=3D"text/css"> </head>
html > ::-webkit-scrollbar { width: 0px; height: 0px; }</style></head>
<body> <body>
<a href=3D"http://localhost:(port)/">normal link to another page</a> <a href=3D"http://localhost:(port)/">normal link to another page</a>
=20 =20
</body></html> </body><style>
html > ::-webkit-scrollbar { width: 0px; height: 0px; }</style></html>
-----=_qute-UUID -----=_qute-UUID