Mon, 22 Feb 2016 19:57:58 +0100
Continued porting the web browser.
- continued adding the GreaseMonkey stuff
--- a/Preferences/__init__.py Sun Feb 21 19:54:14 2016 +0100 +++ b/Preferences/__init__.py Mon Feb 22 19:57:58 2016 +0100 @@ -1026,6 +1026,8 @@ # search engine name) "SearchLanguage": QLocale().language(), "RssFeeds": [], + # Grease Monkey + "GreaseMonkeyDisabledScripts": [], # Flash Cookie Manager: identical to helpDefaults # PIM: identical to helpDefaults # VirusTotal: identical to helpDefaults @@ -2767,9 +2769,10 @@ ## "ClickToFlashWhitelist", "SendRefererWhitelist", ## "GreaseMonkeyDisabledScripts", "NoCacheHosts", ## "FlashCookiesWhitelist", "FlashCookiesBlacklist", -## ]: -## return toList(prefClass.settings.value( -## "WebBrowser/" + key, prefClass.helpDefaults[key])) + elif key in ["GreaseMonkeyDisabledScripts", + ]: + return toList(prefClass.settings.value( + "WebBrowser/" + key, prefClass.helpDefaults[key])) else: return prefClass.settings.value("WebBrowser/" + key, prefClass.webBrowserDefaults[key])
--- a/WebBrowser/GreaseMonkey/GreaseMonkeyManager.py Sun Feb 21 19:54:14 2016 +0100 +++ b/WebBrowser/GreaseMonkey/GreaseMonkeyManager.py Mon Feb 22 19:57:58 2016 +0100 @@ -18,6 +18,9 @@ import Utilities import Preferences +from WebBrowser.WebBrowserWindow import WebBrowserWindow +from .GreaseMonkeyUrlInterceptor import GreaseMonkeyUrlInterceptor + class GreaseMonkeyManager(QObject): """ @@ -34,22 +37,21 @@ super(GreaseMonkeyManager, self).__init__(parent) self.__disabledScripts = [] - self.__endScripts = [] - self.__startScripts = [] + self.__scripts = [] self.__downloaders = [] + self.__interceptor = GreaseMonkeyUrlInterceptor(self) + WebBrowserWindow.networkManager().installUrlInterceptor( + self.__interceptor) + QTimer.singleShot(0, self.__load) -## , m_interceptor(new GM_UrlInterceptor(this)) -##{ -## mApp->networkManager()->installUrlInterceptor(m_interceptor); -## -## QTimer::singleShot(0, this, SLOT(load())); -##} -## -##GM_Manager::~GM_Manager() -##{ -## mApp->networkManager()->removeUrlInterceptor(m_interceptor); -##} + + def __del__(self): + """ + Special method called during object destruction. + """ + WebBrowserWindow.networkManager().removeUrlInterceptor( + self.__interceptor) def showConfigurationDialog(self, parent=None): """ @@ -143,7 +145,7 @@ @return list of all scripts (list of GreaseMonkeyScript) """ - return self.__startScripts[:] + self.__endScripts[:] + return self.__scripts[:] def containsScript(self, fullName): """ @@ -152,12 +154,10 @@ @param fullName full name of the script (string) @return flag indicating the existence (boolean) """ - for script in self.__startScripts: + for script in self.__scripts: if script.fullName() == fullName: return True - for script in self.__endScripts: - if script.fullName() == fullName: - return True + return False def enableScript(self, script): @@ -170,14 +170,9 @@ fullName = script.fullName() if fullName in self.__disabledScripts: self.__disabledScripts.remove(fullName) -##void GM_Manager::enableScript(GM_Script* script) -##{ -## script->setEnabled(true); -## m_disabledScripts.removeOne(script->fullName()); -## -## QWebEngineScriptCollection *collection = mApp->webProfile()->scripts(); -## collection->insert(script->webScript()); -##} + + collection = WebBrowserWindow.webProfile().scripts() + collection.insert(script.webScript()) def disableScript(self, script): """ @@ -189,13 +184,9 @@ fullName = script.fullName() if fullName not in self.__disabledScripts: self.__disabledScripts.append(fullName) -##void GM_Manager::disableScript(GM_Script* script) -##{ -## script->setEnabled(false); -## m_disabledScripts.append(script->fullName()); -## -## QWebEngineScriptCollection *collection = mApp->webProfile()->scripts(); -## collection->remove(collection->findScript(script->fullName())); + + collection = WebBrowserWindow.webProfile().scripts() + collection.remove(collection.findScript(fullName)) def addScript(self, script): """ @@ -204,83 +195,48 @@ @param script script to be added (GreaseMonkeyScript) @return flag indicating success (boolean) """ - if not script: + if not script or not script.isValid(): return False - from .GreaseMonkeyScript import GreaseMonkeyScript - if script.startAt() == GreaseMonkeyScript.DocumentStart: - self.__startScripts.append(script) - else: - self.__endScripts.append(script) + self.__scripts.append(script) + script.scriptChanged.connect(self.__scriptChanged) + + collection = WebBrowserWindow.webProfile().scripts() + collection.insert(script.webScript()) self.scriptsChanged.emit() return True -##bool GM_Manager::addScript(GM_Script* script) -##{ -## if (!script || !script->isValid()) { -## return false; -## } -## -## m_scripts.append(script); -## connect(script, &GM_Script::scriptChanged, this, &GM_Manager::scriptChanged); -## -## QWebEngineScriptCollection *collection = mApp->webProfile()->scripts(); -## collection->insert(script->webScript()); -## -## emit scriptsChanged(); -## return true; -##} - def removeScript(self, script): + def removeScript(self, script, removeFile=True): """ Public method to remove a script. @param script script to be removed (GreaseMonkeyScript) + @param removeFile flag indicating to remove the script file as well + (bool) @return flag indicating success (boolean) """ if not script: return False - from .GreaseMonkeyScript import GreaseMonkeyScript - if script.startAt() == GreaseMonkeyScript.DocumentStart: - try: - self.__startScripts.remove(script) - except ValueError: - pass - else: - try: - self.__endScripts.remove(script) - except ValueError: - pass + try: + self.__scripts.remove(script) + except ValueError: + pass fullName = script.fullName() + collection = WebBrowserWindow.webProfile().scripts() + collection.remove(collection.findScript(fullName)) + if fullName in self.__disabledScripts: self.__disabledScripts.remove(fullName) - QFile.remove(script.fileName()) + + if removeFile: + QFile.remove(script.fileName()) + del script self.scriptsChanged.emit() return True -##bool GM_Manager::removeScript(GM_Script* script, bool removeFile) -##{ -## if (!script) { -## return false; -## } -## -## m_scripts.removeOne(script); -## -## QWebEngineScriptCollection *collection = mApp->webProfile()->scripts(); -## collection->remove(collection->findScript(script->fullName())); -## -## m_disabledScripts.removeOne(script->fullName()); -## -## if (removeFile) { -## QFile::remove(script->fileName()); -## delete script; -## } -## -## emit scriptsChanged(); -## return true; -##} def canRunOnScheme(self, scheme): """ @@ -291,32 +247,6 @@ """ return scheme in ["http", "https", "data", "ftp"] -## def pageLoadStarted(self): -## """ -## Public slot to handle the start of loading a page. -## """ -## frame = self.sender() -## if not frame: -## return -## -## urlScheme = frame.url().scheme() -## urlString = bytes(frame.url().toEncoded()).decode() -## -## if not self.canRunOnScheme(urlScheme): -## return -## -## from .GreaseMonkeyJavaScript import bootstrap_js -## for script in self.__startScripts: -## if script.match(urlString): -## frame.evaluateJavaScript(bootstrap_js + script.script()) -## -## for script in self.__endScripts: -## if script.match(urlString): -## javascript = 'window.addEventListener("DOMContentLoaded",' \ -## 'function(e) {{ {0} }}, false);'.format( -## bootstrap_js + script.script()) -## frame.evaluateJavaScript(javascript) - def __load(self): """ Private slot to load the available scripts into the manager. @@ -329,101 +259,34 @@ scriptsDir.mkdir("requires") self.__disabledScripts = \ - Preferences.getHelp("GreaseMonkeyDisabledScripts") + Preferences.getWebBrowser("GreaseMonkeyDisabledScripts") from .GreaseMonkeyScript import GreaseMonkeyScript for fileName in scriptsDir.entryList(["*.js"], QDir.Files): absolutePath = scriptsDir.absoluteFilePath(fileName) script = GreaseMonkeyScript(self, absolutePath) + if not script.isValid(): + del script + continue + + self.__scripts.append(script) + if script.fullName() in self.__disabledScripts: script.setEnabled(False) - - if script.startAt() == GreaseMonkeyScript.DocumentStart: - self.__startScripts.append(script) else: - self.__endScripts.append(script) -##void GM_Manager::load() -##{ -## QDir gmDir(m_settingsPath + QL1S("/greasemonkey")); -## if (!gmDir.exists()) { -## gmDir.mkdir(m_settingsPath + QL1S("/greasemonkey")); -## } -## -## if (!gmDir.exists("requires")) { -## gmDir.mkdir("requires"); -## } -## -## m_bootstrapScript = QzTools::readAllFileContents(":gm/data/bootstrap.min.js"); -## m_valuesScript = QzTools::readAllFileContents(":gm/data/values.min.js"); -## -## QSettings settings(m_settingsPath + QL1S("/extensions.ini"), QSettings::IniFormat); -## settings.beginGroup("GreaseMonkey"); -## m_disabledScripts = settings.value("disabledScripts", QStringList()).toStringList(); -## -## foreach (const QString &fileName, gmDir.entryList(QStringList("*.js"), QDir::Files)) { -## const QString absolutePath = gmDir.absoluteFilePath(fileName); -## GM_Script* script = new GM_Script(this, absolutePath); -## -## if (!script->isValid()) { -## delete script; -## continue; -## } -## -## m_scripts.append(script); -## -## if (m_disabledScripts.contains(script->fullName())) { -## script->setEnabled(false); -## } -## else { -## mApp->webProfile()->scripts()->insert(script->webScript()); -## } -## } -##} + collection = WebBrowserWindow.webProfile().scripts() + collection.insert(script.webScript()) def __scriptChanged(self): """ Private slot handling a changed script. """ -##void GM_Manager::scriptChanged() -##{ -## GM_Script *script = qobject_cast<GM_Script*>(sender()); -## if (!script) -## return; -## -## QWebEngineScriptCollection *collection = mApp->webProfile()->scripts(); -## collection->remove(collection->findScript(script->fullName())); -## collection->insert(script->webScript()); -##} - -## def connectPage(self, page): -## """ -## Public method to allow the GreaseMonkey manager to connect to the page. -## -## @param page reference to the web page (HelpWebPage) -## """ -## page.mainFrame().javaScriptWindowObjectCleared.connect( -## self.pageLoadStarted) -## -## def createRequest(self, op, request, outgoingData=None): -## """ -## Public method to create a request. -## -## @param op the operation to be performed -## (QNetworkAccessManager.Operation) -## @param request reference to the request object (QNetworkRequest) -## @param outgoingData reference to an IODevice containing data to be sent -## (QIODevice) -## @return reference to the created reply object (QNetworkReply) -## """ -## if op == QNetworkAccessManager.GetOperation and \ -## request.rawHeader(b"X-Eric6-UserLoadAction") == QByteArray(b"1"): -## urlString = request.url().toString( -## QUrl.RemoveFragment | QUrl.RemoveQuery) -## if urlString.endswith(".user.js"): -## self.downloadScript(request) -## from Helpviewer.Network.EmptyNetworkReply import \ -## EmptyNetworkReply -## return EmptyNetworkReply(self) -## -## return None + script = self.sender() + if not script: + return + + fullName = script.fullName() + collection = WebBrowserWindow.webProfile().scripts() + collection.remove(collection.findScript(fullName)) + collection.insert(script.webScript())
--- a/WebBrowser/GreaseMonkey/GreaseMonkeyScript.py Sun Feb 21 19:54:14 2016 +0100 +++ b/WebBrowser/GreaseMonkey/GreaseMonkeyScript.py Mon Feb 22 19:57:58 2016 +0100 @@ -9,18 +9,25 @@ from __future__ import unicode_literals -from PyQt5.QtCore import QUrl, QRegExp +from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl, QRegExp, \ + QByteArray, QCryptographicHash +from PyQt5.QtWebEngineWidgets import QWebEngineScript from .GreaseMonkeyUrlMatcher import GreaseMonkeyUrlMatcher +from .GreaseMonkeyJavaScript import bootstrap_js, values_js + +from ..Tools.DelayedFileWatcher import DelayedFileWatcher -class GreaseMonkeyScript(object): +class GreaseMonkeyScript(QObject): """ Class implementing the GreaseMonkey script. """ DocumentStart = 0 DocumentEnd = 1 + scriptChanged = pyqtSignal() + def __init__(self, manager, path): """ Constructor @@ -28,7 +35,10 @@ @param manager reference to the manager object (GreaseMonkeyManager) @param path path of the Javascript file (string) """ + super(GreaseMonkeyScript, self).__init__(manager) + self.__manager = manager + self.__fileWatcher = DelayedFileWatcher(parent=None) self.__name = "" self.__namespace = "GreaseMonkeyNS" @@ -39,6 +49,7 @@ self.__exclude = [] self.__downloadUrl = QUrl() + self.__updateUrl = QUrl() self.__startAt = GreaseMonkeyScript.DocumentEnd self.__script = "" @@ -46,10 +57,12 @@ self.__enabled = True self.__valid = False self.__metaData = "" + self.__noFrames = False - self.__parseScript(path) -## , m_fileWatcher(new DelayedFileWatcher(this)) -## connect(m_fileWatcher, SIGNAL(delayedFileChanged(QString)), this, SLOT(watchedFileChanged(QString))); + self.__parseScript() + + self.__fileWatcher.delayedFileChanged.connect( + self.__watchedFileChanged) def isValid(self): """ @@ -107,6 +120,14 @@ """ return QUrl(self.__downloadUrl) + def updateUrl(self): + """ + Public method to get the update URL of the script. + + @return update URL of the script (QUrl) + """ + return QUrl(self.__updateUrl) + def startAt(self): """ Public method to get the start point of the script. @@ -115,13 +136,22 @@ """ return self.__startAt + def noFrames(self): + """ + Public method to get the noFrames flag. + + @return flag indicating to not run on sub frames + @rtype bool + """ + return self.__noFrames + def isEnabled(self): """ Public method to check, if the script is enabled. @return flag indicating an enabled state (boolean) """ - return self.__enabled + return self.__enabled and self.__valid def setEnabled(self, enable): """ @@ -161,10 +191,14 @@ """ return self.__script -##QString GM_Script::metaData() const -##{ -## return m_metadata; -##} + def metaData(self): + """ + Public method to get the script meta information. + + @return script meta information + @rtype str + """ + return self.__metaData def fileName(self): """ @@ -181,7 +215,7 @@ @param urlString URL (string) @return flag indicating a match (boolean) """ - if not self.__enabled: + if not self.isEnabled(): return False for matcher in self.__exclude: @@ -194,6 +228,22 @@ return False + @pyqtSlot(str) + def __watchedFileChanged(self, fileName): + """ + Private slot handling changes of the script file. + + @param fileName path of the script file + @type str + """ + if self.__fileName == fileName: + self.__parseScript() + + self.__manager.removeScript(self, False) + self.__manager.addScript(self) + + self.scriptChanged.emit() + def __parseScript(self, path): """ Private method to parse the given script and populate the data @@ -201,6 +251,24 @@ @param path path of the Javascript file (string) """ + self.__name = "" + self.__namespace = "GreaseMonkeyNS" + self.__description = "" + self.__version = "" + + self.__include = [] + self.__exclude = [] + + self.__downloadUrl = QUrl() + self.__updateUrl = QUrl() + self.__startAt = GreaseMonkeyScript.DocumentEnd + + self.__script = "" + self.__enabled = True + self.__valid = False + self.__metaData = "" + self.__noFrames = False + try: f = open(path, "r", encoding="utf-8") fileData = f.read() @@ -209,6 +277,9 @@ # silently ignore because it shouldn't happen return + if self.__fileName not in self.__fileWatcher.files(): + self.__fileWatcher.addPath(self.__fileName) + rx = QRegExp("// ==UserScript==(.*)// ==/UserScript==") rx.indexIn(fileData) metaDataBlock = rx.cap(1).strip() @@ -219,6 +290,9 @@ requireList = [] for line in metaDataBlock.splitlines(): + if not line.strip(): + continue + if not line.startswith("// @"): continue @@ -247,9 +321,9 @@ elif key == "@version": self.__version = value - elif key == "@updateURL": - self.__downloadUrl = QUrl(value) - +## elif key == "@updateURL": +## self.__downloadUrl = QUrl(value) +## elif key in ["@include", "@match"]: self.__include.append(GreaseMonkeyUrlMatcher(value)) @@ -267,6 +341,9 @@ elif key == "@downloadURL" and self.__downloadUrl.isEmpty(): self.__downloadUrl = QUrl(value) + + elif key == "@updateURL" and self.__updateUrl.isEmpty(): + self.__updateUrl = QUrl(value) if not self.__include: self.__include.append(GreaseMonkeyUrlMatcher("*")) @@ -275,31 +352,32 @@ index = fileData.find(marker) + len(marker) self.__metaData = fileData[:index] script = fileData[index:].strip() - script = "{0}{1}".format( - self.__manager.requireScripts(requireList), - script) - self.__script = "(function(){{{0}}})();".format(script) - self.__valid = len(script) > 0 -## const QString nspace = QCryptographicHash::hash(fullName().toUtf8(), QCryptographicHash::Md4).toHex(); -## const QString gmValues = m_manager->valuesScript().arg(nspace); -## -## m_script = QSL("(function(){%1\n%2\n%3\n})();").arg(gmValues, m_manager->requireScripts(requireList), script); -## m_valid = true; + + nspace = bytes(QCryptographicHash.hash( + QByteArray(self.fullName().encode("utf-8")), + QCryptographicHash.Md4).toHex()).decode("ascii") + valuesScript = values_js.format(nspace) + self.__script = "(function(){{{0}\n{1}\n{2}\n}})();".format( + valuesScript, self.__manager.requireScripts(requireList), script + ) + self.__valid = True def webScript(self): """ - Public method to create a script object + Public method to create a script object. @return prepared script object @rtype QWebEngineScript """ -##QWebEngineScript GM_Script::webScript() const -##{ -## QWebEngineScript script; -## script.setName(fullName()); -## script.setInjectionPoint(startAt() == DocumentStart ? QWebEngineScript::DocumentCreation : QWebEngineScript::DocumentReady); -## script.setWorldId(QWebEngineScript::MainWorld); -## script.setRunsOnSubFrames(!m_noframes); -## script.setSourceCode(QSL("%1\n%2\n%3").arg(m_metadata, m_manager->bootstrapScript(), m_script)); -## return script; -##} + script = QWebEngineScript() + script.setName(self.fullName()) + if self.startAt() == GreaseMonkeyScript.DocumentStart: + script.setInjectionPoint(QWebEngineScript.DocumentCreation) + else: + script.setInjectionPoint(QWebEngineScript.DocumentReady) + script.setWorldId(QWebEngineScript.MainWorld) + script.setRunsOnSubFrames(not self.__noFrames) + script.setSourceCode("{0}\n{1}\n{2}".format( + self.__metaData, bootstrap_js, self.__script + )) + return script
--- a/WebBrowser/Network/NetworkManager.py Sun Feb 21 19:54:14 2016 +0100 +++ b/WebBrowser/Network/NetworkManager.py Mon Feb 22 19:57:58 2016 +0100 @@ -159,3 +159,11 @@ ## from WebBrowser.WebBrowserWindow import WebBrowserWindow ## WebBrowserWindow.webProfile().setHttpAcceptLanguage( ## self.__acceptLanguage) + + def installUrlInterceptor(self, interceptor): + # TODO: Qt 5.6, URL Interceptor + pass + + def removeUrlInterceptor(self, interceptor): + # TODO: Qt 5.6, URL Interceptor + pass
--- a/WebBrowser/Tools/DelayedFileWatcher.py Sun Feb 21 19:54:14 2016 +0100 +++ b/WebBrowser/Tools/DelayedFileWatcher.py Mon Feb 22 19:57:58 2016 +0100 @@ -3,71 +3,78 @@ # Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de> # +""" +Module implementing a file system watcher with a delay. +""" + from __future__ import unicode_literals -##class DelayedFileWatcher : public QFileSystemWatcher -##{ -## Q_OBJECT -## -##public: -## explicit DelayedFileWatcher(QObject* parent = 0); -## explicit DelayedFileWatcher(const QStringList &paths, QObject* parent = 0); -## -##signals: -## void delayedDirectoryChanged(const QString &path); -## void delayedFileChanged(const QString &path); -## -##private slots: -## void slotDirectoryChanged(const QString &path); -## void slotFileChanged(const QString &path); -## -## void dequeueDirectory(); -## void dequeueFile(); -## -##private: -## void init(); -## -## QQueue<QString> m_dirQueue; -## QQueue<QString> m_fileQueue; -##}; -## -## -##DelayedFileWatcher::DelayedFileWatcher(QObject* parent) -## : QFileSystemWatcher(parent) -##{ -## init(); -##} -## -##DelayedFileWatcher::DelayedFileWatcher(const QStringList &paths, QObject* parent) -## : QFileSystemWatcher(paths, parent) -##{ -## init(); -##} -## -##void DelayedFileWatcher::init() -##{ -## connect(this, SIGNAL(directoryChanged(QString)), this, SLOT(slotDirectoryChanged(QString))); -## connect(this, SIGNAL(fileChanged(QString)), this, SLOT(slotFileChanged(QString))); -##} -## -##void DelayedFileWatcher::slotDirectoryChanged(const QString &path) -##{ -## m_dirQueue.enqueue(path); -## QTimer::singleShot(500, this, SLOT(dequeueDirectory())); -##} -## -##void DelayedFileWatcher::slotFileChanged(const QString &path) -##{ -## m_fileQueue.enqueue(path); -## QTimer::singleShot(500, this, SLOT(dequeueFile())); -##} -## -##void DelayedFileWatcher::dequeueDirectory() -##{ -## emit delayedDirectoryChanged(m_dirQueue.dequeue()); -##} -## -##void DelayedFileWatcher::dequeueFile() -##{ -## emit delayedFileChanged(m_fileQueue.dequeue()); -##} +from PyQt5.QtCore import pyqtSignal, pyqtSlot, QFileSystemWatcher, QTimer + + +class DelayedFileWatcher(QFileSystemWatcher): + """ + Class implementing a file system watcher with a delay. + + @signal delayedDirectoryChanged(path) emitted to indicate a changed + directory + @signal delayedFileChanged(path) emitted to indicate a changed file + """ + delayedDirectoryChanged = pyqtSignal(str) + delayedFileChanged = pyqtSignal(str) + + def __init__(self, paths=None, parent=None): + """ + Constructor + + @param paths list of paths to be watched + @type list of str + @param parent reference to the parent object + @type QObject + """ + if paths: + super(DelayedFileWatcher, self).__init__(paths, parent) + else: + super(DelayedFileWatcher, self).__init__(parent) + + self.__dirQueue = [] + self.__fileQueue = [] + + self.directoryChanged.connect(self.__directoryChanged) + self.fileChanged.connect(self.__fileChanged) + + @pyqtSlot(str) + def __directoryChanged(self, path): + """ + Private slot handling a changed directory. + + @param path name of the changed directory + @type str + """ + self.__dirQueue.append(path) + QTimer.singleShot(500, self.__dequeueDirectory) + + @pyqtSlot(str) + def __fileChanged(self, path): + """ + Private slot handling a changed file. + + @param path name of the changed file + @type str + """ + self.__fileQueue.append(path) + QTimer.singleShot(500, self.__dequeueFile) + + @pyqtSlot() + def __dequeueDirectory(self): + """ + Private slot to signal a directory change. + """ + self.delayedDirectoryChanged.emit(self.__dirQueue.pop(0)) + + @pyqtSlot() + def __dequeueFile(self): + """ + Private slot to signal a file change. + """ + self.delayedFileChanged.emit(self.__fileQueue.pop(0))
--- a/WebBrowser/Tools/Scripts.py Sun Feb 21 19:54:14 2016 +0100 +++ b/WebBrowser/Tools/Scripts.py Mon Feb 22 19:57:58 2016 +0100 @@ -54,7 +54,7 @@ registerExternal(channel.objects.eric_object); }}); - }})()""" + }})()""" return source.format(readAllFileContents(":/javascript/qwebchannel.js")) @@ -226,7 +226,7 @@ var val; {1} form.submit(); - }})()""" + }})()""" valueSource = """ val = document.createElement('input'); @@ -311,7 +311,7 @@ }); observer.observe(document.documentElement, { childList: true }); - })()""" + })()""" return source @@ -346,7 +346,7 @@ }} }} - }})()""" + }})()""" data = bytes(data).decode("utf-8") data = data.replace("'", "\\'")