summaryrefslogtreecommitdiffstats
path: root/src/js/codemirror
diff options
context:
space:
mode:
Diffstat (limited to 'src/js/codemirror')
-rw-r--r--src/js/codemirror/search-thread.js199
-rw-r--r--src/js/codemirror/search.js504
-rw-r--r--src/js/codemirror/ubo-dynamic-filtering.js239
-rw-r--r--src/js/codemirror/ubo-static-filtering.js1200
4 files changed, 2142 insertions, 0 deletions
diff --git a/src/js/codemirror/search-thread.js b/src/js/codemirror/search-thread.js
new file mode 100644
index 0000000..3a4416f
--- /dev/null
+++ b/src/js/codemirror/search-thread.js
@@ -0,0 +1,199 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2020-present Raymond Hill
+
+ 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 {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+*/
+
+'use strict';
+
+/******************************************************************************/
+
+(( ) => {
+// >>>>> start of local scope
+
+/******************************************************************************/
+
+// Worker context
+
+if (
+ self.WorkerGlobalScope instanceof Object &&
+ self instanceof self.WorkerGlobalScope
+) {
+ let content = '';
+
+ const doSearch = function(details) {
+ const reEOLs = /\n\r|\r\n|\n|\r/g;
+ const t1 = Date.now() + 750;
+
+ let reSearch;
+ try {
+ reSearch = new RegExp(details.pattern, details.flags);
+ } catch(ex) {
+ return;
+ }
+
+ const response = [];
+ const maxOffset = content.length;
+ let iLine = 0;
+ let iOffset = 0;
+ let size = 0;
+ while ( iOffset < maxOffset ) {
+ // Find next match
+ const match = reSearch.exec(content);
+ if ( match === null ) { break; }
+ // Find number of line breaks between last and current match.
+ reEOLs.lastIndex = 0;
+ const eols = content.slice(iOffset, match.index).match(reEOLs);
+ if ( Array.isArray(eols) ) {
+ iLine += eols.length;
+ }
+ // Store line
+ response.push(iLine);
+ size += 1;
+ // Find next line break.
+ reEOLs.lastIndex = reSearch.lastIndex;
+ const eol = reEOLs.exec(content);
+ iOffset = eol !== null
+ ? reEOLs.lastIndex
+ : content.length;
+ reSearch.lastIndex = iOffset;
+ iLine += 1;
+ // Quit if this takes too long
+ if ( (size & 0x3FF) === 0 && Date.now() >= t1 ) { break; }
+ }
+
+ return response;
+ };
+
+ self.onmessage = function(e) {
+ const msg = e.data;
+
+ switch ( msg.what ) {
+ case 'setHaystack':
+ content = msg.content;
+ break;
+
+ case 'doSearch':
+ const response = doSearch(msg);
+ self.postMessage({ id: msg.id, response });
+ break;
+ }
+ };
+
+ return;
+}
+
+/******************************************************************************/
+
+// Main context
+
+{
+ const workerTTL = { min: 5 };
+ const pendingResponses = new Map();
+ const workerTTLTimer = vAPI.defer.create(( ) => {
+ shutdown();
+ });
+
+ let worker;
+ let messageId = 1;
+
+ const onWorkerMessage = function(e) {
+ const msg = e.data;
+ const resolver = pendingResponses.get(msg.id);
+ if ( resolver === undefined ) { return; }
+ pendingResponses.delete(msg.id);
+ resolver(msg.response);
+ };
+
+ const cancelPendingTasks = function() {
+ for ( const resolver of pendingResponses.values() ) {
+ resolver();
+ }
+ pendingResponses.clear();
+ };
+
+ const destroy = function() {
+ shutdown();
+ self.searchThread = undefined;
+ };
+
+ const shutdown = function() {
+ if ( worker === undefined ) { return; }
+ workerTTLTimer.off();
+ worker.terminate();
+ worker.onmessage = undefined;
+ worker = undefined;
+ cancelPendingTasks();
+ };
+
+ const init = function() {
+ if ( self.searchThread instanceof Object === false ) { return; }
+ if ( worker === undefined ) {
+ worker = new Worker('js/codemirror/search-thread.js');
+ worker.onmessage = onWorkerMessage;
+ }
+ workerTTLTimer.offon(workerTTL);
+ };
+
+ const needHaystack = function() {
+ return worker instanceof Object === false;
+ };
+
+ const setHaystack = function(content) {
+ init();
+ worker.postMessage({ what: 'setHaystack', content });
+ };
+
+ const search = function(query, overwrite = true) {
+ init();
+ if ( worker instanceof Object === false ) {
+ return Promise.resolve();
+ }
+ if ( overwrite ) {
+ cancelPendingTasks();
+ }
+ const id = messageId++;
+ worker.postMessage({
+ what: 'doSearch',
+ id,
+ pattern: query.source,
+ flags: query.flags,
+ isRE: query instanceof RegExp
+ });
+ return new Promise(resolve => {
+ pendingResponses.set(id, resolve);
+ });
+ };
+
+ self.addEventListener(
+ 'beforeunload',
+ ( ) => { destroy(); },
+ { once: true }
+ );
+
+ self.searchThread = { needHaystack, setHaystack, search, shutdown };
+}
+
+/******************************************************************************/
+
+// <<<<< end of local scope
+})();
+
+/******************************************************************************/
+
+void 0;
diff --git a/src/js/codemirror/search.js b/src/js/codemirror/search.js
new file mode 100644
index 0000000..477e9cc
--- /dev/null
+++ b/src/js/codemirror/search.js
@@ -0,0 +1,504 @@
+// The following code is heavily based on the standard CodeMirror
+// search addon found at: https://codemirror.net/addon/search/search.js
+// I added/removed and modified code in order to get a closer match to a
+// browser's built-in find-in-page feature which are just enough for
+// uBlock Origin.
+//
+// This file was originally wholly imported from:
+// https://github.com/codemirror/CodeMirror/blob/3e1bb5fff682f8f6cbfaef0e56c61d62403d4798/addon/search/search.js
+//
+// And has been modified over time to better suit uBO's usage and coding style:
+// https://github.com/gorhill/uBlock/commits/master/src/js/codemirror/search.js
+//
+// The original copyright notice is reproduced below:
+
+// =====
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+// Define search commands. Depends on dialog.js or another
+// implementation of the openDialog method.
+
+// Replace works a little oddly -- it will do the replace on the next
+// Ctrl-G (or whatever is bound to findNext) press. You prevent a
+// replace by making sure the match is no longer selected when hitting
+// Ctrl-G.
+// =====
+
+'use strict';
+
+import { dom, qs$ } from '../dom.js';
+import { i18n$ } from '../i18n.js';
+
+{
+ const CodeMirror = self.CodeMirror;
+
+ const searchOverlay = function(query, caseInsensitive) {
+ if ( typeof query === 'string' )
+ query = new RegExp(
+ query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'),
+ caseInsensitive ? 'gi' : 'g'
+ );
+ else if ( !query.global )
+ query = new RegExp(query.source, query.ignoreCase ? 'gi' : 'g');
+
+ return {
+ token: function(stream) {
+ query.lastIndex = stream.pos;
+ const match = query.exec(stream.string);
+ if ( match && match.index === stream.pos ) {
+ stream.pos += match[0].length || 1;
+ return 'searching';
+ } else if ( match ) {
+ stream.pos = match.index;
+ } else {
+ stream.skipToEnd();
+ }
+ }
+ };
+ };
+
+ const searchWidgetKeydownHandler = function(cm, ev) {
+ const keyName = CodeMirror.keyName(ev);
+ if ( !keyName ) { return; }
+ CodeMirror.lookupKey(
+ keyName,
+ cm.getOption('keyMap'),
+ function(command) {
+ if ( widgetCommandHandler(cm, command) ) {
+ ev.preventDefault();
+ ev.stopPropagation();
+ }
+ }
+ );
+ };
+
+ const searchWidgetInputHandler = function(cm, ev) {
+ const state = getSearchState(cm);
+ if ( ev.isTrusted !== true ) {
+ if ( state.queryText === '' ) {
+ clearSearch(cm);
+ } else {
+ cm.operation(function() {
+ startSearch(cm, state);
+ });
+ }
+ return;
+ }
+ if ( queryTextFromSearchWidget(cm) === state.queryText ) { return; }
+ state.queryTimer.offon(350);
+ };
+
+ const searchWidgetClickHandler = function(cm, ev) {
+ const tcl = ev.target.classList;
+ if ( tcl.contains('cm-search-widget-up') ) {
+ findNext(cm, -1);
+ } else if ( tcl.contains('cm-search-widget-down') ) {
+ findNext(cm, 1);
+ } else if ( tcl.contains('cm-linter-widget-up') ) {
+ findNextError(cm, -1);
+ } else if ( tcl.contains('cm-linter-widget-down') ) {
+ findNextError(cm, 1);
+ }
+ if ( ev.target.localName !== 'input' ) {
+ ev.preventDefault();
+ } else {
+ ev.stopImmediatePropagation();
+ }
+ };
+
+ const queryTextFromSearchWidget = function(cm) {
+ return getSearchState(cm).widget.querySelector('input[type="search"]').value;
+ };
+
+ const queryTextToSearchWidget = function(cm, q) {
+ const input = getSearchState(cm).widget.querySelector('input[type="search"]');
+ if ( typeof q === 'string' && q !== input.value ) {
+ input.value = q;
+ }
+ input.setSelectionRange(0, input.value.length);
+ input.focus();
+ };
+
+ const SearchState = function(cm) {
+ this.query = null;
+ this.panel = null;
+ const widgetParent = document.querySelector('.cm-search-widget-template').cloneNode(true);
+ this.widget = widgetParent.children[0];
+ this.widget.addEventListener('keydown', searchWidgetKeydownHandler.bind(null, cm));
+ this.widget.addEventListener('input', searchWidgetInputHandler.bind(null, cm));
+ this.widget.addEventListener('mousedown', searchWidgetClickHandler.bind(null, cm));
+ if ( typeof cm.addPanel === 'function' ) {
+ this.panel = cm.addPanel(this.widget);
+ }
+ this.queryText = '';
+ this.dirty = true;
+ this.lines = [];
+ cm.on('changes', (cm, changes) => {
+ for ( const change of changes ) {
+ if ( change.text.length !== 0 || change.removed !== 0 ) {
+ this.dirty = true;
+ break;
+ }
+ }
+ });
+ cm.on('cursorActivity', cm => {
+ updateCount(cm);
+ });
+ this.queryTimer = vAPI.defer.create(( ) => {
+ findCommit(cm, 0);
+ });
+ };
+
+ // We want the search widget to behave as if the focus was on the
+ // CodeMirror editor.
+
+ const reSearchCommands = /^(?:find|findNext|findPrev|newlineAndIndent)$/;
+
+ const widgetCommandHandler = function(cm, command) {
+ if ( reSearchCommands.test(command) === false ) { return false; }
+ const queryText = queryTextFromSearchWidget(cm);
+ if ( command === 'find' ) {
+ queryTextToSearchWidget(cm);
+ return true;
+ }
+ if ( queryText.length !== 0 ) {
+ findNext(cm, command === 'findPrev' ? -1 : 1);
+ }
+ return true;
+ };
+
+ const getSearchState = function(cm) {
+ return cm.state.search || (cm.state.search = new SearchState(cm));
+ };
+
+ const queryCaseInsensitive = function(query) {
+ return typeof query === 'string' && query === query.toLowerCase();
+ };
+
+ // Heuristic: if the query string is all lowercase, do a case insensitive search.
+ const getSearchCursor = function(cm, query, pos) {
+ return cm.getSearchCursor(
+ query,
+ pos,
+ { caseFold: queryCaseInsensitive(query), multiline: false }
+ );
+ };
+
+ // https://github.com/uBlockOrigin/uBlock-issues/issues/658
+ // Modified to backslash-escape ONLY widely-used control characters.
+ const parseString = function(string) {
+ return string.replace(/\\[nrt\\]/g, match => {
+ if ( match === '\\n' ) { return '\n'; }
+ if ( match === '\\r' ) { return '\r'; }
+ if ( match === '\\t' ) { return '\t'; }
+ if ( match === '\\\\' ) { return '\\'; }
+ return match;
+ });
+ };
+
+ const reEscape = /[.*+\-?^${}()|[\]\\]/g;
+
+ // Must always return a RegExp object.
+ //
+ // Assume case-sensitivity if there is at least one uppercase in plain
+ // query text.
+ const parseQuery = function(query) {
+ let flags = 'i';
+ let reParsed = query.match(/^\/(.+)\/([iu]*)$/);
+ if ( reParsed !== null ) {
+ try {
+ const re = new RegExp(reParsed[1], reParsed[2]);
+ query = re.source;
+ flags = re.flags;
+ }
+ catch (e) {
+ reParsed = null;
+ }
+ }
+ if ( reParsed === null ) {
+ if ( /[A-Z]/.test(query) ) { flags = ''; }
+ query = parseString(query).replace(reEscape, '\\$&');
+ }
+ if ( typeof query === 'string' ? query === '' : query.test('') ) {
+ query = 'x^';
+ }
+ return new RegExp(query, 'gm' + flags);
+ };
+
+ let intlNumberFormat;
+
+ const formatNumber = function(n) {
+ if ( intlNumberFormat === undefined ) {
+ intlNumberFormat = null;
+ if ( Intl.NumberFormat instanceof Function ) {
+ const intl = new Intl.NumberFormat(undefined, {
+ notation: 'compact',
+ maximumSignificantDigits: 3
+ });
+ if (
+ intl.resolvedOptions instanceof Function &&
+ intl.resolvedOptions().hasOwnProperty('notation')
+ ) {
+ intlNumberFormat = intl;
+ }
+ }
+ }
+ return n > 10000 && intlNumberFormat instanceof Object
+ ? intlNumberFormat.format(n)
+ : n.toLocaleString();
+ };
+
+ const updateCount = function(cm) {
+ const state = getSearchState(cm);
+ const lines = state.lines;
+ const current = cm.getCursor().line;
+ let l = 0;
+ let r = lines.length;
+ let i = -1;
+ while ( l < r ) {
+ i = l + r >>> 1;
+ const candidate = lines[i];
+ if ( current === candidate ) { break; }
+ if ( current < candidate ) {
+ r = i;
+ } else /* if ( current > candidate ) */ {
+ l = i + 1;
+ }
+ }
+ let text = '';
+ if ( i !== -1 ) {
+ text = formatNumber(i + 1);
+ if ( lines[i] !== current ) {
+ text = '~' + text;
+ }
+ text = text + '\xA0/\xA0';
+ }
+ const count = lines.length;
+ text += formatNumber(count);
+ const span = state.widget.querySelector('.cm-search-widget-count');
+ span.textContent = text;
+ span.title = count.toLocaleString();
+ };
+
+ const startSearch = function(cm, state) {
+ state.query = parseQuery(state.queryText);
+ if ( state.overlay !== undefined ) {
+ cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query));
+ }
+ state.overlay = searchOverlay(state.query, queryCaseInsensitive(state.query));
+ cm.addOverlay(state.overlay);
+ if ( state.dirty || self.searchThread.needHaystack() ) {
+ self.searchThread.setHaystack(cm.getValue());
+ state.dirty = false;
+ }
+ self.searchThread.search(state.query).then(lines => {
+ if ( Array.isArray(lines) === false ) { return; }
+ state.lines = lines;
+ const count = lines.length;
+ updateCount(cm);
+ if ( state.annotate !== undefined ) {
+ state.annotate.clear();
+ state.annotate = undefined;
+ }
+ if ( count === 0 ) { return; }
+ state.annotate = cm.annotateScrollbar('CodeMirror-search-match');
+ const annotations = [];
+ let lineBeg = -1;
+ let lineEnd = -1;
+ for ( const line of lines ) {
+ if ( lineBeg === -1 ) {
+ lineBeg = line;
+ lineEnd = line + 1;
+ continue;
+ } else if ( line === lineEnd ) {
+ lineEnd = line + 1;
+ continue;
+ }
+ annotations.push({
+ from: { line: lineBeg, ch: 0 },
+ to: { line: lineEnd, ch: 0 }
+ });
+ lineBeg = -1;
+ }
+ if ( lineBeg !== -1 ) {
+ annotations.push({
+ from: { line: lineBeg, ch: 0 },
+ to: { line: lineEnd, ch: 0 }
+ });
+ }
+ state.annotate.update(annotations);
+ });
+ state.widget.setAttribute('data-query', state.queryText);
+ // Ensure the caret is visible
+ const input = state.widget.querySelector('.cm-search-widget-input input');
+ input.selectionStart = input.selectionStart;
+ };
+
+ const findNext = function(cm, dir, callback) {
+ cm.operation(function() {
+ const state = getSearchState(cm);
+ if ( !state.query ) { return; }
+ let cursor = getSearchCursor(
+ cm,
+ state.query,
+ dir <= 0 ? cm.getCursor('from') : cm.getCursor('to')
+ );
+ const previous = dir < 0;
+ if (!cursor.find(previous)) {
+ cursor = getSearchCursor(
+ cm,
+ state.query,
+ previous
+ ? CodeMirror.Pos(cm.lastLine())
+ : CodeMirror.Pos(cm.firstLine(), 0)
+ );
+ if (!cursor.find(previous)) return;
+ }
+ cm.setSelection(cursor.from(), cursor.to());
+ const { clientHeight } = cm.getScrollInfo();
+ cm.scrollIntoView(
+ { from: cursor.from(), to: cursor.to() },
+ clientHeight >>> 1
+ );
+ if (callback) callback(cursor.from(), cursor.to());
+ });
+ };
+
+ const findNextError = function(cm, dir) {
+ const doc = cm.getDoc();
+ const cursor = cm.getCursor('from');
+ const cursorLine = cursor.line;
+ const start = dir < 0 ? 0 : cursorLine + 1;
+ const end = dir < 0 ? cursorLine : doc.lineCount();
+ let found = -1;
+ doc.eachLine(start, end, lineHandle => {
+ const markers = lineHandle.gutterMarkers || null;
+ if ( markers === null ) { return; }
+ const marker = markers['CodeMirror-lintgutter'];
+ if ( marker === undefined ) { return; }
+ if ( marker.dataset.error !== 'y' ) { return; }
+ const line = lineHandle.lineNo();
+ if ( dir < 0 ) {
+ found = line;
+ return;
+ }
+ found = line;
+ return true;
+ });
+ if ( found === -1 || found === cursorLine ) { return; }
+ cm.getDoc().setCursor(found);
+ const { clientHeight } = cm.getScrollInfo();
+ cm.scrollIntoView({ line: found, ch: 0 }, clientHeight >>> 1);
+ };
+
+ const clearSearch = function(cm, hard) {
+ cm.operation(function() {
+ const state = getSearchState(cm);
+ if ( state.query ) {
+ state.query = state.queryText = null;
+ }
+ state.lines = [];
+ if ( state.overlay !== undefined ) {
+ cm.removeOverlay(state.overlay);
+ state.overlay = undefined;
+ }
+ if ( state.annotate ) {
+ state.annotate.clear();
+ state.annotate = undefined;
+ }
+ state.widget.removeAttribute('data-query');
+ if ( hard ) {
+ state.panel.clear();
+ state.panel = null;
+ state.widget = null;
+ cm.state.search = null;
+ }
+ });
+ };
+
+ const findCommit = function(cm, dir) {
+ const state = getSearchState(cm);
+ state.queryTimer.off();
+ const queryText = queryTextFromSearchWidget(cm);
+ if ( queryText === state.queryText ) { return; }
+ state.queryText = queryText;
+ if ( state.queryText === '' ) {
+ clearSearch(cm);
+ } else {
+ cm.operation(function() {
+ startSearch(cm, state);
+ findNext(cm, dir);
+ });
+ }
+ };
+
+ const findCommand = function(cm) {
+ let queryText = cm.getSelection() || undefined;
+ if ( !queryText ) {
+ const word = cm.findWordAt(cm.getCursor());
+ queryText = cm.getRange(word.anchor, word.head);
+ if ( /^\W|\W$/.test(queryText) ) {
+ queryText = undefined;
+ }
+ cm.setCursor(word.anchor);
+ }
+ queryTextToSearchWidget(cm, queryText);
+ findCommit(cm, 1);
+ };
+
+ const findNextCommand = function(cm) {
+ const state = getSearchState(cm);
+ if ( state.query ) { return findNext(cm, 1); }
+ };
+
+ const findPrevCommand = function(cm) {
+ const state = getSearchState(cm);
+ if ( state.query ) { return findNext(cm, -1); }
+ };
+
+ {
+ const searchWidgetTemplate =
+ '<div class="cm-search-widget-template" style="display:none;">' +
+ '<div class="cm-search-widget">' +
+ '<span class="cm-search-widget-input">' +
+ '<span class="fa-icon fa-icon-ro">search</span>&ensp;' +
+ '<input type="search" spellcheck="false">&emsp;' +
+ '<span class="cm-search-widget-up cm-search-widget-button fa-icon">angle-up</span>&nbsp;' +
+ '<span class="cm-search-widget-down cm-search-widget-button fa-icon fa-icon-vflipped">angle-up</span>&emsp;' +
+ '<span class="cm-search-widget-count"></span>' +
+ '</span>' +
+ '<span class="cm-linter-widget" data-lint="0">' +
+ '<span class="cm-linter-widget-count"></span>&emsp;' +
+ '<span class="cm-linter-widget-up cm-search-widget-button fa-icon">angle-up</span>&nbsp;' +
+ '<span class="cm-linter-widget-down cm-search-widget-button fa-icon fa-icon-vflipped">angle-up</span>&emsp;' +
+ '</span>' +
+ '<span>' +
+ '<a class="fa-icon sourceURL" href>external-link</a>' +
+ '</span>' +
+ '</div>' +
+ '</div>';
+ const domParser = new DOMParser();
+ const doc = domParser.parseFromString(searchWidgetTemplate, 'text/html');
+ const widgetTemplate = document.adoptNode(doc.body.firstElementChild);
+ document.body.appendChild(widgetTemplate);
+ }
+
+ CodeMirror.commands.find = findCommand;
+ CodeMirror.commands.findNext = findNextCommand;
+ CodeMirror.commands.findPrev = findPrevCommand;
+
+ CodeMirror.defineInitHook(function(cm) {
+ getSearchState(cm);
+ cm.on('linterDone', details => {
+ const linterWidget = qs$('.cm-linter-widget');
+ const count = details.errorCount;
+ if ( linterWidget.dataset.lint === `${count}` ) { return; }
+ linterWidget.dataset.lint = `${count}`;
+ dom.text(
+ qs$(linterWidget, '.cm-linter-widget-count'),
+ i18n$('linterMainReport').replace('{{count}}', count.toLocaleString())
+ );
+ });
+ });
+}
diff --git a/src/js/codemirror/ubo-dynamic-filtering.js b/src/js/codemirror/ubo-dynamic-filtering.js
new file mode 100644
index 0000000..d0709a4
--- /dev/null
+++ b/src/js/codemirror/ubo-dynamic-filtering.js
@@ -0,0 +1,239 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2019-present Raymond Hill
+
+ 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 {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+*/
+
+/* global CodeMirror */
+
+'use strict';
+
+CodeMirror.defineMode('ubo-dynamic-filtering', ( ) => {
+
+ const validSwitches = new Set([
+ 'no-strict-blocking:',
+ 'no-popups:',
+ 'no-cosmetic-filtering:',
+ 'no-remote-fonts:',
+ 'no-large-media:',
+ 'no-csp-reports:',
+ 'no-scripting:',
+ ]);
+ const validSwitcheStates = new Set([
+ 'true',
+ 'false',
+ ]);
+ const validHnRuleTypes = new Set([
+ '*',
+ '3p',
+ 'image',
+ 'inline-script',
+ '1p-script',
+ '3p-script',
+ '3p-frame',
+ ]);
+ const invalidURLRuleTypes = new Set([
+ 'doc',
+ 'main_frame',
+ ]);
+ const validActions = new Set([
+ 'block',
+ 'allow',
+ 'noop',
+ ]);
+ const hnValidator = new URL(self.location.href);
+ const reBadHn = /[%]|^\.|\.$/;
+ const slices = [];
+ let sliceIndex = 0;
+ let sliceCount = 0;
+ let hostnameToDomainMap = new Map();
+ let psl;
+
+ const isValidHostname = hnin => {
+ if ( hnin === '*' ) { return true; }
+ hnValidator.hostname = '_';
+ try {
+ hnValidator.hostname = hnin;
+ } catch(_) {
+ return false;
+ }
+ const hnout = hnValidator.hostname;
+ return hnout !== '_' && hnout !== '' && reBadHn.test(hnout) === false;
+ };
+
+ const addSlice = (len, style = null) => {
+ let i = sliceCount;
+ if ( i === slices.length ) {
+ slices[i] = { len: 0, style: null };
+ }
+ const entry = slices[i];
+ entry.len = len;
+ entry.style = style;
+ sliceCount += 1;
+ };
+
+ const addMatchSlice = (match, style = null) => {
+ const len = match !== null ? match[0].length : 0;
+ addSlice(len, style);
+ return match !== null ? match.input.slice(len) : '';
+ };
+
+ const addMatchHnSlices = (match, style = null) => {
+ const hn = match[0];
+ if ( hn === '*' ) {
+ return addMatchSlice(match, style);
+ }
+ let dn = hostnameToDomainMap.get(hn) || '';
+ if ( dn === '' && psl !== undefined ) {
+ dn = /(\d|\])$/.test(hn) ? hn : (psl.getDomain(hn) || hn);
+ }
+ const entityBeg = hn.length - dn.length;
+ if ( entityBeg !== 0 ) {
+ addSlice(entityBeg, style);
+ }
+ let entityEnd = dn.indexOf('.');
+ if ( entityEnd === -1 ) { entityEnd = dn.length; }
+ addSlice(entityEnd, style !== null ? `${style} strong` : 'strong');
+ if ( entityEnd < dn.length ) {
+ addSlice(dn.length - entityEnd, style);
+ }
+ return match.input.slice(hn.length);
+ };
+
+ const makeSlices = (stream, opts) => {
+ sliceIndex = 0;
+ sliceCount = 0;
+ let { string } = stream;
+ if ( string === '...' ) { return; }
+ const { sortType } = opts;
+ const reNotToken = /^\s+/;
+ const reToken = /^\S+/;
+ const tokens = [];
+ // leading whitespaces
+ let match = reNotToken.exec(string);
+ if ( match !== null ) {
+ string = addMatchSlice(match);
+ }
+ // first token
+ match = reToken.exec(string);
+ if ( match === null ) { return; }
+ tokens.push(match[0]);
+ // hostname or switch
+ const isSwitchRule = validSwitches.has(match[0]);
+ if ( isSwitchRule ) {
+ string = addMatchSlice(match, sortType === 0 ? 'sortkey' : null);
+ } else if ( isValidHostname(match[0]) ) {
+ if ( sortType === 1 ) {
+ string = addMatchHnSlices(match, 'sortkey');
+ } else {
+ string = addMatchHnSlices(match, null);
+ }
+ } else {
+ string = addMatchSlice(match, 'error');
+ }
+ // whitespaces before second token
+ match = reNotToken.exec(string);
+ if ( match === null ) { return; }
+ string = addMatchSlice(match);
+ // second token
+ match = reToken.exec(string);
+ if ( match === null ) { return; }
+ tokens.push(match[0]);
+ // hostname or url
+ const isURLRule = isSwitchRule === false && match[0].indexOf('://') > 0;
+ if ( isURLRule ) {
+ string = addMatchSlice(match, sortType === 2 ? 'sortkey' : null);
+ } else if ( isValidHostname(match[0]) === false ) {
+ string = addMatchSlice(match, 'error');
+ } else if ( sortType === 1 && isSwitchRule || sortType === 2 ) {
+ string = addMatchHnSlices(match, 'sortkey');
+ } else {
+ string = addMatchHnSlices(match, null);
+ }
+ // whitespaces before third token
+ match = reNotToken.exec(string);
+ if ( match === null ) { return; }
+ string = addMatchSlice(match);
+ // third token
+ match = reToken.exec(string);
+ if ( match === null ) { return; }
+ tokens.push(match[0]);
+ // rule type or switch state
+ if ( isSwitchRule ) {
+ string = validSwitcheStates.has(match[0])
+ ? addMatchSlice(match, match[0] === 'true' ? 'blockrule' : 'allowrule')
+ : addMatchSlice(match, 'error');
+ } else if ( isURLRule ) {
+ string = invalidURLRuleTypes.has(match[0])
+ ? addMatchSlice(match, 'error')
+ : addMatchSlice(match);
+ } else if ( tokens[1] === '*' ) {
+ string = validHnRuleTypes.has(match[0])
+ ? addMatchSlice(match)
+ : addMatchSlice(match, 'error');
+ } else {
+ string = match[0] === '*'
+ ? addMatchSlice(match)
+ : addMatchSlice(match, 'error');
+ }
+ // whitespaces before fourth token
+ match = reNotToken.exec(string);
+ if ( match === null ) { return; }
+ string = addMatchSlice(match);
+ // fourth token
+ match = reToken.exec(string);
+ if ( match === null ) { return; }
+ tokens.push(match[0]);
+ string = isSwitchRule || validActions.has(match[0]) === false
+ ? addMatchSlice(match, 'error')
+ : addMatchSlice(match, `${match[0]}rule`);
+ // whitespaces before end of line
+ match = reNotToken.exec(string);
+ if ( match === null ) { return; }
+ string = addMatchSlice(match);
+ // any token beyond fourth token is invalid
+ match = reToken.exec(string);
+ if ( match !== null ) {
+ string = addMatchSlice(null, 'error');
+ }
+ };
+
+ const token = function(stream) {
+ if ( stream.sol() ) {
+ makeSlices(stream, this);
+ }
+ if ( sliceIndex >= sliceCount ) {
+ stream.skipToEnd(stream);
+ return null;
+ }
+ const { len, style } = slices[sliceIndex++];
+ if ( len === 0 ) {
+ stream.skipToEnd();
+ } else {
+ stream.pos += len;
+ }
+ return style;
+ };
+
+ return {
+ token,
+ sortType: 1,
+ setHostnameToDomainMap: a => { hostnameToDomainMap = a; },
+ setPSL: a => { psl = a; },
+ };
+});
diff --git a/src/js/codemirror/ubo-static-filtering.js b/src/js/codemirror/ubo-static-filtering.js
new file mode 100644
index 0000000..ac1b048
--- /dev/null
+++ b/src/js/codemirror/ubo-static-filtering.js
@@ -0,0 +1,1200 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2018-present Raymond Hill
+
+ 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 {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+*/
+
+/* global CodeMirror */
+
+'use strict';
+
+/******************************************************************************/
+
+import * as sfp from '../static-filtering-parser.js';
+import { dom, qs$ } from '../dom.js';
+
+/******************************************************************************/
+
+const redirectNames = new Map();
+const scriptletNames = new Map();
+const preparseDirectiveEnv = [];
+const preparseDirectiveHints = [];
+const originHints = [];
+let hintHelperRegistered = false;
+
+/******************************************************************************/
+
+CodeMirror.defineOption('trustedSource', false, (cm, state) => {
+ if ( typeof state !== 'boolean' ) { return; }
+ self.dispatchEvent(new CustomEvent('trustedSource', {
+ detail: state,
+ }));
+});
+
+CodeMirror.defineOption('trustedScriptletTokens', undefined, (cm, tokens) => {
+ if ( tokens === undefined || tokens === null ) { return; }
+ if ( typeof tokens[Symbol.iterator] !== 'function' ) { return; }
+ self.dispatchEvent(new CustomEvent('trustedScriptletTokens', {
+ detail: new Set(tokens),
+ }));
+});
+
+/******************************************************************************/
+
+CodeMirror.defineMode('ubo-static-filtering', function() {
+ const astParser = new sfp.AstFilterParser({
+ interactive: true,
+ nativeCssHas: vAPI.webextFlavor.env.includes('native_css_has'),
+ });
+ const astWalker = astParser.getWalker();
+ let currentWalkerNode = 0;
+ let lastNetOptionType = 0;
+
+ const redirectTokenStyle = node => {
+ const rawToken = astParser.getNodeString(node || currentWalkerNode);
+ const { token } = sfp.parseRedirectValue(rawToken);
+ return redirectNames.has(token) ? 'value' : 'value warning';
+ };
+
+ const nodeHasError = node => {
+ return astParser.getNodeFlags(
+ node || currentWalkerNode, sfp.NODE_FLAG_ERROR
+ ) !== 0;
+ };
+
+ const colorFromAstNode = function() {
+ if ( astParser.nodeIsEmptyString(currentWalkerNode) ) { return '+'; }
+ if ( nodeHasError() ) { return 'error'; }
+ const nodeType = astParser.getNodeType(currentWalkerNode);
+ switch ( nodeType ) {
+ case sfp.NODE_TYPE_WHITESPACE:
+ return '';
+ case sfp.NODE_TYPE_COMMENT:
+ if ( astWalker.canGoDown() ) { break; }
+ return 'comment';
+ case sfp.NODE_TYPE_COMMENT_URL:
+ return 'comment link';
+ case sfp.NODE_TYPE_IGNORE:
+ return 'comment';
+ case sfp.NODE_TYPE_PREPARSE_DIRECTIVE:
+ case sfp.NODE_TYPE_PREPARSE_DIRECTIVE_VALUE:
+ return 'directive';
+ case sfp.NODE_TYPE_PREPARSE_DIRECTIVE_IF_VALUE: {
+ const raw = astParser.getNodeString(currentWalkerNode);
+ const state = sfp.utils.preparser.evaluateExpr(raw, preparseDirectiveEnv);
+ return state ? 'positive strong' : 'negative strong';
+ }
+ case sfp.NODE_TYPE_EXT_OPTIONS_ANCHOR:
+ return astParser.getFlags(sfp.AST_FLAG_IS_EXCEPTION)
+ ? 'tag strong'
+ : 'def strong';
+ case sfp.NODE_TYPE_EXT_DECORATION:
+ return 'def';
+ case sfp.NODE_TYPE_EXT_PATTERN_RAW:
+ if ( astWalker.canGoDown() ) { break; }
+ return 'variable';
+ case sfp.NODE_TYPE_EXT_PATTERN_COSMETIC:
+ case sfp.NODE_TYPE_EXT_PATTERN_HTML:
+ return 'variable';
+ case sfp.NODE_TYPE_EXT_PATTERN_RESPONSEHEADER:
+ case sfp.NODE_TYPE_EXT_PATTERN_SCRIPTLET:
+ if ( astWalker.canGoDown() ) { break; }
+ return 'variable';
+ case sfp.NODE_TYPE_EXT_PATTERN_SCRIPTLET_TOKEN: {
+ const token = astParser.getNodeString(currentWalkerNode);
+ if ( scriptletNames.has(token) === false ) {
+ return 'warning';
+ }
+ return 'variable';
+ }
+ case sfp.NODE_TYPE_EXT_PATTERN_SCRIPTLET_ARG:
+ return 'variable';
+ case sfp.NODE_TYPE_NET_EXCEPTION:
+ return 'tag strong';
+ case sfp.NODE_TYPE_NET_PATTERN:
+ if ( astWalker.canGoDown() ) { break; }
+ if ( astParser.isRegexPattern() ) {
+ if ( astParser.getNodeFlags(currentWalkerNode, sfp.NODE_FLAG_PATTERN_UNTOKENIZABLE) !== 0 ) {
+ return 'variable warning';
+ }
+ return 'variable notice';
+ }
+ return 'variable';
+ case sfp.NODE_TYPE_NET_PATTERN_PART:
+ return 'variable';
+ case sfp.NODE_TYPE_NET_PATTERN_PART_SPECIAL:
+ return 'keyword strong';
+ case sfp.NODE_TYPE_NET_PATTERN_PART_UNICODE:
+ return 'variable unicode';
+ case sfp.NODE_TYPE_NET_PATTERN_LEFT_HNANCHOR:
+ case sfp.NODE_TYPE_NET_PATTERN_LEFT_ANCHOR:
+ case sfp.NODE_TYPE_NET_PATTERN_RIGHT_ANCHOR:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_NOT:
+ return 'keyword strong';
+ case sfp.NODE_TYPE_NET_OPTIONS_ANCHOR:
+ case sfp.NODE_TYPE_NET_OPTION_SEPARATOR:
+ lastNetOptionType = 0;
+ return 'def strong';
+ case sfp.NODE_TYPE_NET_OPTION_NAME_UNKNOWN:
+ lastNetOptionType = 0;
+ return 'error';
+ case sfp.NODE_TYPE_NET_OPTION_NAME_1P:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_STRICT1P:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_3P:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_STRICT3P:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_ALL:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_BADFILTER:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_CNAME:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_CSP:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_CSS:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_DENYALLOW:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_DOC:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_EHIDE:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_EMPTY:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_FONT:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_FRAME:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_FROM:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_GENERICBLOCK:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_GHIDE:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_HEADER:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_IMAGE:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_IMPORTANT:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_INLINEFONT:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_INLINESCRIPT:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_MATCHCASE:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_MEDIA:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_METHOD:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_MP4:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_NOOP:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_OBJECT:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_OTHER:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_PING:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_POPUNDER:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_POPUP:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_REDIRECT:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_REDIRECTRULE:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_REMOVEPARAM:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_SCRIPT:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_SHIDE:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_TO:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_XHR:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_WEBRTC:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_WEBSOCKET:
+ lastNetOptionType = nodeType;
+ return 'def';
+ case sfp.NODE_TYPE_NET_OPTION_ASSIGN:
+ return 'def';
+ case sfp.NODE_TYPE_NET_OPTION_VALUE:
+ if ( astWalker.canGoDown() ) { break; }
+ switch ( lastNetOptionType ) {
+ case sfp.NODE_TYPE_NET_OPTION_NAME_REDIRECT:
+ case sfp.NODE_TYPE_NET_OPTION_NAME_REDIRECTRULE:
+ return redirectTokenStyle();
+ default:
+ break;
+ }
+ return 'value';
+ case sfp.NODE_TYPE_OPTION_VALUE_NOT:
+ return 'keyword strong';
+ case sfp.NODE_TYPE_OPTION_VALUE_DOMAIN:
+ return 'value';
+ case sfp.NODE_TYPE_OPTION_VALUE_SEPARATOR:
+ return 'def';
+ default:
+ break;
+ }
+ return '+';
+ };
+
+ self.addEventListener('trustedSource', ev => {
+ astParser.options.trustedSource = ev.detail;
+ });
+
+ self.addEventListener('trustedScriptletTokens', ev => {
+ astParser.options.trustedScriptletTokens = ev.detail;
+ });
+
+ return {
+ lineComment: '!',
+ token: function(stream) {
+ if ( stream.sol() ) {
+ astParser.parse(stream.string);
+ if ( astParser.getFlags(sfp.AST_FLAG_UNSUPPORTED) !== 0 ) {
+ stream.skipToEnd();
+ return 'error';
+ }
+ if ( astParser.getType() === sfp.AST_TYPE_NONE ) {
+ stream.skipToEnd();
+ return 'comment';
+ }
+ currentWalkerNode = astWalker.reset();
+ } else if ( nodeHasError() ) {
+ currentWalkerNode = astWalker.right();
+ } else {
+ currentWalkerNode = astWalker.next();
+ }
+ let style = '';
+ while ( currentWalkerNode !== 0 ) {
+ style = colorFromAstNode(stream);
+ if ( style !== '+' ) { break; }
+ currentWalkerNode = astWalker.next();
+ }
+ if ( style === '+' ) {
+ stream.skipToEnd();
+ return null;
+ }
+ stream.pos = astParser.getNodeStringEnd(currentWalkerNode);
+ if ( astParser.isNetworkFilter() ) {
+ return style ? `line-cm-net ${style}` : 'line-cm-net';
+ }
+ if ( astParser.isExtendedFilter() ) {
+ let flavor = '';
+ if ( astParser.isCosmeticFilter() ) {
+ flavor = 'line-cm-ext-dom';
+ } else if ( astParser.isScriptletFilter() ) {
+ flavor = 'line-cm-ext-js';
+ } else if ( astParser.isHtmlFilter() ) {
+ flavor = 'line-cm-ext-html';
+ }
+ if ( flavor !== '' ) {
+ style = `${flavor} ${style}`;
+ }
+ }
+ style = style.trim();
+ return style !== '' ? style : null;
+ },
+ parser: astParser,
+ };
+});
+
+/******************************************************************************/
+
+// Following code is for auto-completion. Reference:
+// https://codemirror.net/demo/complete.html
+
+CodeMirror.defineOption('uboHints', null, (cm, hints) => {
+ if ( hints instanceof Object === false ) { return; }
+ if ( Array.isArray(hints.redirectResources) ) {
+ for ( const [ name, desc ] of hints.redirectResources ) {
+ const displayText = desc.aliasOf !== ''
+ ? `${name} (${desc.aliasOf})`
+ : '';
+ if ( desc.canRedirect ) {
+ redirectNames.set(name, displayText);
+ }
+ if ( desc.canInject && name.endsWith('.js') ) {
+ scriptletNames.set(name.slice(0, -3), displayText);
+ }
+ }
+ }
+ if ( Array.isArray(hints.preparseDirectiveEnv)) {
+ preparseDirectiveEnv.length = 0;
+ preparseDirectiveEnv.push(...hints.preparseDirectiveEnv);
+ }
+ if ( Array.isArray(hints.preparseDirectiveHints)) {
+ preparseDirectiveHints.push(...hints.preparseDirectiveHints);
+ }
+ if ( Array.isArray(hints.originHints) ) {
+ originHints.length = 0;
+ for ( const hint of hints.originHints ) {
+ originHints.push(hint);
+ }
+ }
+ if ( hintHelperRegistered ) { return; }
+ hintHelperRegistered = true;
+ initHints();
+});
+
+function initHints() {
+ const astParser = new sfp.AstFilterParser({
+ interactive: true,
+ nativeCssHas: vAPI.webextFlavor.env.includes('native_css_has'),
+ });
+ const proceduralOperatorNames = new Map(
+ Array.from(sfp.proceduralOperatorTokens)
+ .filter(item => (item[1] & 0b01) !== 0)
+ );
+ const excludedHints = new Set([
+ 'genericblock',
+ 'object-subrequest',
+ 'rewrite',
+ 'webrtc',
+ ]);
+
+ const pickBestHints = function(cursor, seedLeft, seedRight, hints) {
+ const seed = (seedLeft + seedRight).trim();
+ const out = [];
+ // First, compare against whole seed
+ for ( const hint of hints ) {
+ const text = hint instanceof Object
+ ? hint.displayText || hint.text
+ : hint;
+ if ( text.startsWith(seed) === false ) { continue; }
+ out.push(hint);
+ }
+ if ( out.length !== 0 ) {
+ return {
+ from: { line: cursor.line, ch: cursor.ch - seedLeft.length },
+ to: { line: cursor.line, ch: cursor.ch + seedRight.length },
+ list: out,
+ };
+ }
+ // If no match, try again with a different heuristic: valid hints are
+ // those matching left seed, not matching right seed but right seed is
+ // found to be a valid hint. This is to take care of cases like:
+ //
+ // *$script,redomain=example.org
+ // ^
+ // + cursor position
+ //
+ // In such case, [ redirect=, redirect-rule= ] should be returned
+ // as valid hints.
+ for ( const hint of hints ) {
+ const text = hint instanceof Object
+ ? hint.displayText || hint.text
+ : hint;
+ if ( seedLeft.length === 0 ) { continue; }
+ if ( text.startsWith(seedLeft) === false ) { continue; }
+ if ( hints.includes(seedRight) === false ) { continue; }
+ out.push(hint);
+ }
+ if ( out.length !== 0 ) {
+ return {
+ from: { line: cursor.line, ch: cursor.ch - seedLeft.length },
+ to: { line: cursor.line, ch: cursor.ch },
+ list: out,
+ };
+ }
+ // If no match, try again with a different heuristic: valid hints are
+ // those containing seed as a substring. This is to take care of cases
+ // like:
+ //
+ // *$script,redirect=gif
+ // ^
+ // + cursor position
+ //
+ // In such case, [ 1x1.gif, 1x1-transparent.gif ] should be returned
+ // as valid hints.
+ for ( const hint of hints ) {
+ const text = hint instanceof Object
+ ? hint.displayText || hint.text
+ : hint;
+ if ( seedLeft.length === 1 ) {
+ if ( text.startsWith(seedLeft) === false ) { continue; }
+ } else if ( text.includes(seed) === false ) { continue; }
+ out.push(hint);
+ }
+ if ( out.length !== 0 ) {
+ return {
+ from: { line: cursor.line, ch: cursor.ch - seedLeft.length },
+ to: { line: cursor.line, ch: cursor.ch + seedRight.length },
+ list: out,
+ };
+ }
+ // If still no match, try again with a different heuristic: valid hints
+ // are those containing left seed as a substring. This is to take care
+ // of cases like:
+ //
+ // *$script,redirect=gifdomain=example.org
+ // ^
+ // + cursor position
+ //
+ // In such case, [ 1x1.gif, 1x1-transparent.gif ] should be returned
+ // as valid hints.
+ for ( const hint of hints ) {
+ const text = hint instanceof Object
+ ? hint.displayText || hint.text
+ : hint;
+ if ( text.includes(seedLeft) === false ) { continue; }
+ out.push(hint);
+ }
+ if ( out.length !== 0 ) {
+ return {
+ from: { line: cursor.line, ch: cursor.ch - seedLeft.length },
+ to: { line: cursor.line, ch: cursor.ch },
+ list: out,
+ };
+ }
+ };
+
+ const getOriginHints = function(cursor, line, suffix = '') {
+ const beg = cursor.ch;
+ const matchLeft = /[^,|=~]*$/.exec(line.slice(0, beg));
+ const matchRight = /^[^#,|]*/.exec(line.slice(beg));
+ if ( matchLeft === null || matchRight === null ) { return; }
+ const hints = [];
+ for ( const text of originHints ) {
+ hints.push(text + suffix);
+ }
+ return pickBestHints(cursor, matchLeft[0], matchRight[0], hints);
+ };
+
+ const getNetPatternHints = function(cursor, line) {
+ if ( /\|\|[\w.-]*$/.test(line.slice(0, cursor.ch)) ) {
+ return getOriginHints(cursor, line, '^');
+ }
+ // Maybe a static extended filter is meant to be crafted.
+ if ( /[^\w\x80-\xF4#,.-]/.test(line) === false ) {
+ return getOriginHints(cursor, line);
+ }
+ };
+
+ const getNetOptionHints = function(cursor, seedLeft, seedRight) {
+ const isNegated = seedLeft.startsWith('~');
+ if ( isNegated ) {
+ seedLeft = seedLeft.slice(1);
+ }
+ const assignPos = seedRight.indexOf('=');
+ if ( assignPos !== -1 ) { seedRight = seedRight.slice(0, assignPos); }
+ const isException = astParser.isException();
+ const hints = [];
+ for ( let [ text, desc ] of sfp.netOptionTokenDescriptors ) {
+ if ( excludedHints.has(text) ) { continue; }
+ if ( isNegated && desc.canNegate !== true ) { continue; }
+ if ( isException ) {
+ if ( desc.blockOnly ) { continue; }
+ } else {
+ if ( desc.allowOnly ) { continue; }
+ if ( (assignPos === -1) && desc.mustAssign ) {
+ text += '=';
+ }
+ }
+ hints.push(text);
+ }
+ return pickBestHints(cursor, seedLeft, seedRight, hints);
+ };
+
+ const getNetRedirectHints = function(cursor, seedLeft, seedRight) {
+ const hints = [];
+ for ( const text of redirectNames.keys() ) {
+ if ( text.startsWith('abp-resource:') ) { continue; }
+ hints.push(text);
+ }
+ return pickBestHints(cursor, seedLeft, seedRight, hints);
+ };
+
+ const getNetHints = function(cursor, line) {
+ const patternNode = astParser.getBranchFromType(sfp.NODE_TYPE_NET_PATTERN_RAW);
+ if ( patternNode === 0 ) { return; }
+ const patternEnd = astParser.getNodeStringEnd(patternNode);
+ const beg = cursor.ch;
+ if ( beg <= patternEnd ) {
+ return getNetPatternHints(cursor, line);
+ }
+ const lineBefore = line.slice(0, beg);
+ const lineAfter = line.slice(beg);
+ let matchLeft = /[^$,]*$/.exec(lineBefore);
+ let matchRight = /^[^,]*/.exec(lineAfter);
+ if ( matchLeft === null || matchRight === null ) { return; }
+ const assignPos = matchLeft[0].indexOf('=');
+ if ( assignPos === -1 ) {
+ return getNetOptionHints(cursor, matchLeft[0], matchRight[0]);
+ }
+ if ( /^(redirect(-rule)?|rewrite)=/.test(matchLeft[0]) ) {
+ return getNetRedirectHints(
+ cursor,
+ matchLeft[0].slice(assignPos + 1),
+ matchRight[0]
+ );
+ }
+ if ( /^(domain|from)=/.test(matchLeft[0]) ) {
+ return getOriginHints(cursor, line);
+ }
+ };
+
+ const getExtSelectorHints = function(cursor, line) {
+ const beg = cursor.ch;
+ // Special selector case: `^responseheader`
+ {
+ const match = /#\^([a-z]+)$/.exec(line.slice(0, beg));
+ if (
+ match !== null &&
+ 'responseheader'.startsWith(match[1]) &&
+ line.slice(beg) === ''
+ ) {
+ return pickBestHints(
+ cursor,
+ match[1],
+ '',
+ [ 'responseheader()' ]
+ );
+ }
+ }
+ // Procedural operators
+ const matchLeft = /#\^?.*:([^:]*)$/.exec(line.slice(0, beg));
+ const matchRight = /^([a-z-]*)\(?/.exec(line.slice(beg));
+ if ( matchLeft === null || matchRight === null ) { return; }
+ const isStaticDOM = matchLeft[0].indexOf('^') !== -1;
+ const hints = [];
+ for ( let [ text, bits ] of proceduralOperatorNames ) {
+ if ( isStaticDOM && (bits & 0b10) !== 0 ) { continue; }
+ hints.push(text);
+ }
+ return pickBestHints(cursor, matchLeft[1], matchRight[1], hints);
+ };
+
+ const getExtHeaderHints = function(cursor, line) {
+ const beg = cursor.ch;
+ const matchLeft = /#\^responseheader\((.*)$/.exec(line.slice(0, beg));
+ const matchRight = /^([^)]*)/.exec(line.slice(beg));
+ if ( matchLeft === null || matchRight === null ) { return; }
+ const hints = [];
+ for ( const hint of sfp.removableHTTPHeaders ) {
+ hints.push(hint);
+ }
+ return pickBestHints(cursor, matchLeft[1], matchRight[1], hints);
+ };
+
+ const getExtScriptletHints = function(cursor, line) {
+ const beg = cursor.ch;
+ const matchLeft = /#\+\js\(([^,]*)$/.exec(line.slice(0, beg));
+ const matchRight = /^([^,)]*)/.exec(line.slice(beg));
+ if ( matchLeft === null || matchRight === null ) { return; }
+ const hints = [];
+ for ( const [ text, displayText ] of scriptletNames ) {
+ const hint = { text };
+ if ( displayText !== '' ) {
+ hint.displayText = displayText;
+ }
+ hints.push(hint);
+ }
+ return pickBestHints(cursor, matchLeft[1], matchRight[1], hints);
+ };
+
+ const getCommentHints = function(cursor, line) {
+ const beg = cursor.ch;
+ if ( line.startsWith('!#if ') ) {
+ const matchLeft = /^!#if !?(\w*)$/.exec(line.slice(0, beg));
+ const matchRight = /^\w*/.exec(line.slice(beg));
+ if ( matchLeft === null || matchRight === null ) { return; }
+ return pickBestHints(
+ cursor,
+ matchLeft[1],
+ matchRight[0],
+ preparseDirectiveHints
+ );
+ }
+ if ( line.startsWith('!#') && line !== '!#endif' ) {
+ const matchLeft = /^!#(\w*)$/.exec(line.slice(0, beg));
+ const matchRight = /^\w*/.exec(line.slice(beg));
+ if ( matchLeft === null || matchRight === null ) { return; }
+ const hints = [ 'if ', 'endif\n', 'include ' ];
+ return pickBestHints(cursor, matchLeft[1], matchRight[0], hints);
+ }
+ };
+
+ CodeMirror.registerHelper('hint', 'ubo-static-filtering', function(cm) {
+ const cursor = cm.getCursor();
+ const line = cm.getLine(cursor.line);
+ astParser.parse(line);
+ if ( astParser.isExtendedFilter() ) {
+ const anchorNode = astParser.getBranchFromType(sfp.NODE_TYPE_EXT_OPTIONS_ANCHOR);
+ if ( anchorNode === 0 ) { return; }
+ let hints;
+ if ( cursor.ch <= astParser.getNodeStringBeg(anchorNode) ) {
+ hints = getOriginHints(cursor, line);
+ } else if ( astParser.isScriptletFilter() ) {
+ hints = getExtScriptletHints(cursor, line);
+ } else if ( astParser.isResponseheaderFilter() ) {
+ hints = getExtHeaderHints(cursor, line);
+ } else {
+ hints = getExtSelectorHints(cursor, line);
+ }
+ return hints;
+ }
+ if ( astParser.isNetworkFilter() ) {
+ return getNetHints(cursor, line);
+ }
+ if ( astParser.isComment() ) {
+ return getCommentHints(cursor, line);
+ }
+ return getOriginHints(cursor, line);
+ });
+}
+
+/******************************************************************************/
+
+CodeMirror.registerHelper('fold', 'ubo-static-filtering', (( ) => {
+ const foldIfEndif = function(startLineNo, startLine, cm) {
+ const lastLineNo = cm.lastLine();
+ let endLineNo = startLineNo;
+ let depth = 1;
+ while ( endLineNo < lastLineNo ) {
+ endLineNo += 1;
+ const line = cm.getLine(endLineNo);
+ if ( line.startsWith('!#endif') ) {
+ depth -= 1;
+ if ( depth === 0 ) {
+ return {
+ from: CodeMirror.Pos(startLineNo, startLine.length),
+ to: CodeMirror.Pos(endLineNo, 0)
+ };
+ }
+ }
+ if ( line.startsWith('!#if') ) {
+ depth += 1;
+ }
+ }
+ };
+
+ const foldInclude = function(startLineNo, startLine, cm) {
+ const lastLineNo = cm.lastLine();
+ let endLineNo = startLineNo + 1;
+ if ( endLineNo >= lastLineNo ) { return; }
+ if ( cm.getLine(endLineNo).startsWith('! >>>>>>>> ') === false ) {
+ return;
+ }
+ while ( endLineNo < lastLineNo ) {
+ endLineNo += 1;
+ const line = cm.getLine(endLineNo);
+ if ( line.startsWith('! <<<<<<<< ') ) {
+ return {
+ from: CodeMirror.Pos(startLineNo, startLine.length),
+ to: CodeMirror.Pos(endLineNo, line.length)
+ };
+ }
+ }
+ };
+
+ return function(cm, start) {
+ const startLineNo = start.line;
+ const startLine = cm.getLine(startLineNo);
+ if ( startLine.startsWith('!#if') ) {
+ return foldIfEndif(startLineNo, startLine, cm);
+ }
+ if ( startLine.startsWith('!#include ') ) {
+ return foldInclude(startLineNo, startLine, cm);
+ }
+ };
+})());
+
+/******************************************************************************/
+
+// Linter
+
+{
+ const astParser = new sfp.AstFilterParser({
+ interactive: true,
+ nativeCssHas: vAPI.webextFlavor.env.includes('native_css_has'),
+ });
+
+ const changeset = [];
+ let changesetTimer;
+
+ const includeset = new Set();
+ let errorCount = 0;
+
+ const ifendifSet = new Set();
+ let ifendifSetChanged = false;
+
+ const extractMarkerDetails = (doc, lineHandle) => {
+ if ( astParser.isUnsupported() ) {
+ return { lint: 'error', msg: 'Unsupported filter syntax' };
+ }
+ if ( astParser.hasError() ) {
+ let msg = 'Invalid filter';
+ switch ( astParser.astError ) {
+ case sfp.AST_ERROR_UNSUPPORTED:
+ msg = `${msg}: Unsupported filter syntax`;
+ break;
+ case sfp.AST_ERROR_REGEX:
+ msg = `${msg}: Bad regular expression`;
+ break;
+ case sfp.AST_ERROR_PATTERN:
+ msg = `${msg}: Bad pattern`;
+ break;
+ case sfp.AST_ERROR_DOMAIN_NAME:
+ msg = `${msg}: Bad domain name`;
+ break;
+ case sfp.AST_ERROR_OPTION_BADVALUE:
+ msg = `${msg}: Bad value assigned to a valid option`;
+ break;
+ case sfp.AST_ERROR_OPTION_DUPLICATE:
+ msg = `${msg}: Duplicate filter option`;
+ break;
+ case sfp.AST_ERROR_OPTION_UNKNOWN:
+ msg = `${msg}: Unsupported filter option`;
+ break;
+ case sfp.AST_ERROR_IF_TOKEN_UNKNOWN:
+ msg = `${msg}: Unknown preparsing token`;
+ break;
+ case sfp.AST_ERROR_UNTRUSTED_SOURCE:
+ msg = `${msg}: Filter requires trusted source`;
+ break;
+ default:
+ if ( astParser.isCosmeticFilter() && astParser.result.error ) {
+ msg = `${msg}: ${astParser.result.error}`;
+ }
+ break;
+ }
+ return { lint: 'error', msg };
+ }
+ if ( astParser.astType !== sfp.AST_TYPE_COMMENT ) { return; }
+ if ( astParser.astTypeFlavor !== sfp.AST_TYPE_COMMENT_PREPARSER ) {
+ if ( astParser.raw.startsWith('! <<<<<<<< ') === false ) { return; }
+ for ( const include of includeset ) {
+ if ( astParser.raw.endsWith(include) === false ) { continue; }
+ includeset.delete(include);
+ return { lint: 'include-end' };
+ }
+ return;
+ }
+ if ( /^\s*!#if \S+/.test(astParser.raw) ) {
+ return {
+ lint: 'if-start',
+ data: {
+ state: sfp.utils.preparser.evaluateExpr(
+ astParser.getTypeString(sfp.NODE_TYPE_PREPARSE_DIRECTIVE_IF_VALUE),
+ preparseDirectiveEnv
+ ) ? 'y' : 'n'
+ }
+ };
+ }
+ if ( /^\s*!#endif\b/.test(astParser.raw) ) {
+ return { lint: 'if-end' };
+ }
+ const match = /^\s*!#include\s*(\S+)/.exec(astParser.raw);
+ if ( match === null ) { return; }
+ const nextLineHandle = doc.getLineHandle(lineHandle.lineNo() + 1);
+ if ( nextLineHandle === undefined ) { return; }
+ if ( nextLineHandle.text.startsWith('! >>>>>>>> ') === false ) { return; }
+ const includeToken = `/${match[1]}`;
+ if ( nextLineHandle.text.endsWith(includeToken) === false ) { return; }
+ includeset.add(includeToken);
+ return { lint: 'include-start' };
+ };
+
+ const extractMarker = lineHandle => {
+ const markers = lineHandle.gutterMarkers || null;
+ return markers !== null
+ ? markers['CodeMirror-lintgutter'] || null
+ : null;
+ };
+
+ const markerTemplates = {
+ 'error': {
+ node: null,
+ html: [
+ '<div class="CodeMirror-lintmarker" data-lint="error" data-error="y">&nbsp;',
+ '<span class="msg"></span>',
+ '</div>',
+ ],
+ },
+ 'if-start': {
+ node: null,
+ html: [
+ '<div class="CodeMirror-lintmarker" data-lint="if" data-fold="start" data-state="">&nbsp;',
+ '<svg viewBox="0 0 100 100">',
+ '<polygon points="0,0 100,0 50,100" />',
+ '</svg>',
+ '<span class="msg">Mismatched if-endif directive</span>',
+ '</div>',
+ ],
+ },
+ 'if-end': {
+ node: null,
+ html: [
+ '<div class="CodeMirror-lintmarker" data-lint="if" data-fold="end">&nbsp;',
+ '<svg viewBox="0 0 100 100">',
+ '<polygon points="50,0 100,100 0,100" />',
+ '</svg>',
+ '<span class="msg">Mismatched if-endif directive</span>',
+ '</div>',
+ ],
+ },
+ 'include-start': {
+ node: null,
+ html: [
+ '<div class="CodeMirror-lintmarker" data-lint="include" data-fold="start">&nbsp;',
+ '<svg viewBox="0 0 100 100">',
+ '<polygon points="0,0 100,0 50,100" />',
+ '</svg>',
+ '</div>',
+ ],
+ },
+ 'include-end': {
+ node: null,
+ html: [
+ '<div class="CodeMirror-lintmarker" data-lint="include" data-fold="end">&nbsp;',
+ '<svg viewBox="0 0 100 100">',
+ '<polygon points="50,0 100,100 0,100" />',
+ '</svg>',
+ '</div>',
+ ],
+ },
+ };
+
+ const markerFromTemplate = details => {
+ const template = markerTemplates[details.lint];
+ if ( template.node === null ) {
+ const domParser = new DOMParser();
+ const doc = domParser.parseFromString(template.html.join(''), 'text/html');
+ template.node = document.adoptNode(qs$(doc, '.CodeMirror-lintmarker'));
+ }
+ const node = template.node.cloneNode(true);
+ if ( details.data instanceof Object ) {
+ for ( const [ k, v ] of Object.entries(details.data) ) {
+ node.dataset[k] = `${v}`;
+ }
+ }
+ return node;
+ };
+
+ const addMarker = (doc, lineHandle, marker, details) => {
+ if ( marker && marker.dataset.lint !== details.lint ) {
+ doc.setGutterMarker(lineHandle, 'CodeMirror-lintgutter', null);
+ if ( marker.dataset.error === 'y' ) {
+ errorCount -= 1;
+ }
+ if ( marker.dataset.lint === 'if' ) {
+ ifendifSet.delete(lineHandle);
+ ifendifSetChanged = true;
+ }
+ marker = null;
+ }
+ if ( marker === null ) {
+ marker = markerFromTemplate(details);
+ doc.setGutterMarker(lineHandle, 'CodeMirror-lintgutter', marker);
+ if ( marker.dataset.error === 'y' ) {
+ errorCount += 1;
+ }
+ if ( marker.dataset.lint === 'if' ) {
+ ifendifSet.add(lineHandle);
+ ifendifSetChanged = true;
+ }
+ }
+ if ( typeof details.msg !== 'string' || details.msg === '' ) { return; }
+ const msgElem = qs$(marker, '.msg');
+ if ( msgElem === null ) { return; }
+ msgElem.textContent = details.msg;
+ };
+
+ const removeMarker = (doc, lineHandle, marker) => {
+ doc.setGutterMarker(lineHandle, 'CodeMirror-lintgutter', null);
+ if ( marker.dataset.error === 'y' ) {
+ errorCount -= 1;
+ }
+ if ( marker.dataset.lint === 'if' ) {
+ ifendifSet.delete(lineHandle);
+ ifendifSetChanged = true;
+ }
+ };
+
+ // Analyze whether all if-endif are properly paired
+ const processIfendifs = ( ) => {
+ if ( ifendifSet.size === 0 ) { return; }
+ if ( ifendifSetChanged !== true ) { return; }
+ const sortFn = (a, b) => a.lineNo() - b.lineNo();
+ const sorted = Array.from(ifendifSet).sort(sortFn);
+ const bad = [];
+ const stack = [];
+ for ( const line of sorted ) {
+ const marker = extractMarker(line);
+ const fold = marker.dataset.fold;
+ if ( fold === 'start' ) {
+ stack.push(line);
+ } else if ( fold === 'end' ) {
+ if ( stack.length !== 0 ) {
+ if ( marker.dataset.error === 'y' ) {
+ marker.dataset.error = '';
+ errorCount -= 1;
+ }
+ const ifstart = extractMarker(stack.pop());
+ if ( ifstart.dataset.error === 'y' ) {
+ ifstart.dataset.error = '';
+ errorCount -= 1;
+ }
+ } else {
+ bad.push(line);
+ }
+ }
+ }
+ bad.push(...stack);
+ for ( const line of bad ) {
+ const marker = extractMarker(line);
+ marker.dataset.error = 'y';
+ errorCount += 1;
+ }
+ ifendifSetChanged = false;
+ };
+
+ const processDeletion = (doc, change) => {
+ let { from, to } = change;
+ doc.eachLine(from.line, to.line, lineHandle => {
+ const marker = extractMarker(lineHandle);
+ if ( marker === null ) { return; }
+ if ( marker.dataset.error === 'y' ) {
+ marker.dataset.error = '';
+ errorCount -= 1;
+ }
+ ifendifSet.delete(lineHandle);
+ ifendifSetChanged = true;
+ });
+ };
+
+ const processInsertion = (doc, deadline, change) => {
+ let { from, to } = change;
+ doc.eachLine(from, to, lineHandle => {
+ astParser.parse(lineHandle.text);
+ const markerDetails = extractMarkerDetails(doc, lineHandle);
+ const marker = extractMarker(lineHandle);
+ if ( markerDetails !== undefined ) {
+ addMarker(doc, lineHandle, marker, markerDetails);
+ } else if ( marker !== null ) {
+ removeMarker(doc, lineHandle, marker);
+ }
+ from += 1;
+ if ( (from & 0x0F) !== 0 ) { return; }
+ if ( deadline.timeRemaining() !== 0 ) { return; }
+ return true;
+ });
+ if ( from !== to ) {
+ return { from, to };
+ }
+ };
+
+ const processChangeset = (doc, deadline) => {
+ const cm = doc.getEditor();
+ cm.startOperation();
+ while ( changeset.length !== 0 ) {
+ const change = processInsertion(doc, deadline, changeset.shift());
+ if ( change === undefined ) { continue; }
+ changeset.unshift(change);
+ break;
+ }
+ cm.endOperation();
+ if ( changeset.length !== 0 ) {
+ return processChangesetAsync(doc);
+ }
+ includeset.clear();
+ processIfendifs(doc);
+ CodeMirror.signal(doc.getEditor(), 'linterDone', { errorCount });
+ };
+
+ const processChangesetAsync = doc => {
+ if ( changesetTimer !== undefined ) { return; }
+ changesetTimer = self.requestIdleCallback(deadline => {
+ changesetTimer = undefined;
+ processChangeset(doc, deadline);
+ });
+ };
+
+ const onChanges = (cm, changes) => {
+ if ( changes.length === 0 ) { return; }
+ const doc = cm.getDoc();
+ for ( const change of changes ) {
+ const from = change.from.line;
+ const to = from + change.text.length;
+ changeset.push({ from, to });
+ }
+ processChangesetAsync(doc);
+ };
+
+ const onBeforeChanges = (cm, change) => {
+ const doc = cm.getDoc();
+ processDeletion(doc, change);
+ };
+
+ const foldRangeFinder = (cm, from) => {
+ const lineNo = from.line;
+ const lineHandle = cm.getDoc().getLineHandle(lineNo);
+ const marker = extractMarker(lineHandle);
+ if ( marker === null ) { return; }
+ if ( marker.dataset.fold === undefined ) { return; }
+ const foldName = marker.dataset.lint;
+ from.ch = lineHandle.text.length;
+ const to = { line: 0, ch: 0 };
+ const doc = cm.getDoc();
+ let depth = 0;
+ doc.eachLine(from.line, doc.lineCount(), lineHandle => {
+ const marker = extractMarker(lineHandle);
+ if ( marker === null ) { return; }
+ if ( marker.dataset.lint === foldName && marker.dataset.fold === 'start' ) {
+ depth += 1;
+ return;
+ }
+ if ( marker.dataset.lint !== foldName ) { return; }
+ if ( marker.dataset.fold !== 'end' ) { return; }
+ depth -= 1;
+ if ( depth !== 0 ) { return; }
+ to.line = lineHandle.lineNo();
+ return true;
+ });
+ return { from, to };
+ };
+
+ const onGutterClick = (cm, lineNo, gutterId, ev) => {
+ if ( ev.button !== 0 ) { return; }
+ if ( gutterId !== 'CodeMirror-lintgutter' ) { return; }
+ const doc = cm.getDoc();
+ const lineHandle = doc.getLineHandle(lineNo);
+ const marker = extractMarker(lineHandle);
+ if ( marker === null ) { return; }
+ if ( marker.dataset.fold === 'start' ) {
+ if ( ev.ctrlKey ) {
+ if ( dom.cl.has(marker, 'folded') ) {
+ CodeMirror.commands.unfoldAll(cm);
+ } else {
+ CodeMirror.commands.foldAll(cm);
+ }
+ doc.setCursor(lineNo);
+ return;
+ }
+ cm.foldCode(lineNo, {
+ widget: '\u00A0\u22EF\u00A0',
+ rangeFinder: foldRangeFinder,
+ });
+ return;
+ }
+ if ( marker.dataset.fold === 'end' ) {
+ let depth = 1;
+ let lineNo = lineHandle.lineNo();
+ while ( lineNo-- ) {
+ const prevLineHandle = doc.getLineHandle(lineNo);
+ const markerFrom = extractMarker(prevLineHandle);
+ if ( markerFrom === null ) { continue; }
+ if ( markerFrom.dataset.fold === 'end' ) {
+ depth += 1;
+ } else if ( markerFrom.dataset.fold === 'start' ) {
+ depth -= 1;
+ if ( depth === 0 ) {
+ doc.setCursor(lineNo);
+ break;
+ }
+ }
+ }
+ return;
+ }
+ };
+
+ self.addEventListener('trustedSource', ev => {
+ astParser.options.trustedSource = ev.detail;
+ });
+
+ self.addEventListener('trustedScriptletTokens', ev => {
+ astParser.options.trustedScriptletTokens = ev.detail;
+ });
+
+ CodeMirror.defineInitHook(cm => {
+ cm.on('changes', onChanges);
+ cm.on('beforeChange', onBeforeChanges);
+ cm.on('gutterClick', onGutterClick);
+ cm.on('fold', function(cm, from) {
+ const doc = cm.getDoc();
+ const lineHandle = doc.getLineHandle(from.line);
+ const marker = extractMarker(lineHandle);
+ dom.cl.add(marker, 'folded');
+ });
+ cm.on('unfold', function(cm, from) {
+ const doc = cm.getDoc();
+ const lineHandle = doc.getLineHandle(from.line);
+ const marker = extractMarker(lineHandle);
+ dom.cl.remove(marker, 'folded');
+ });
+ });
+}
+
+/******************************************************************************/
+
+// Enhanced word selection
+
+{
+ const selectWordAt = function(cm, pos) {
+ const { line, ch } = pos;
+ const s = cm.getLine(line);
+ const { type: token } = cm.getTokenAt(pos);
+ let beg, end;
+
+ // Select URL in comments
+ if ( /\bcomment\b/.test(token) && /\blink\b/.test(token) ) {
+ const l = /\S+$/.exec(s.slice(0, ch));
+ if ( l && /^https?:\/\//.test(s.slice(l.index)) ) {
+ const r = /^\S+/.exec(s.slice(ch));
+ if ( r ) {
+ beg = l.index;
+ end = ch + r[0].length;
+ }
+ }
+ }
+
+ // Better word selection for extended filters: prefix
+ else if (
+ /\bline-cm-ext-(?:dom|html|js)\b/.test(token) &&
+ /\bvalue\b/.test(token)
+ ) {
+ const l = /[^,.]*$/i.exec(s.slice(0, ch));
+ const r = /^[^#,]*/i.exec(s.slice(ch));
+ if ( l && r ) {
+ beg = l.index;
+ end = ch + r[0].length;
+ }
+ }
+
+ // Better word selection for cosmetic and HTML filters: suffix
+ else if ( /\bline-cm-ext-(?:dom|html)\b/.test(token) ) {
+ const l = /[#.]?[a-z0-9_-]+$/i.exec(s.slice(0, ch));
+ const r = /^[a-z0-9_-]+/i.exec(s.slice(ch));
+ if ( l && r ) {
+ beg = l.index;
+ end = ch + r[0].length;
+ if ( /\bdef\b/.test(cm.getTokenTypeAt({ line, ch: beg + 1 })) ) {
+ beg += 1;
+ }
+ }
+ }
+
+ // Better word selection for network filters
+ else if ( /\bline-cm-net\b/.test(token) ) {
+ if ( /\bvalue\b/.test(token) ) {
+ const l = /[^ ,.=|]*$/i.exec(s.slice(0, ch));
+ const r = /^[^ #,|]*/i.exec(s.slice(ch));
+ if ( l && r ) {
+ beg = l.index;
+ end = ch + r[0].length;
+ }
+ } else if ( /\bdef\b/.test(token) ) {
+ const l = /[a-z0-9-]+$/i.exec(s.slice(0, ch));
+ const r = /^[^,]*=[^,]+/i.exec(s.slice(ch));
+ if ( l && r ) {
+ beg = l.index;
+ end = ch + r[0].length;
+ }
+ }
+ }
+
+ if ( beg === undefined ) {
+ const { anchor, head } = cm.findWordAt(pos);
+ return { from: anchor, to: head };
+ }
+
+ return {
+ from: { line, ch: beg },
+ to: { line, ch: end },
+ };
+ };
+
+ CodeMirror.defineInitHook(cm => {
+ cm.setOption('configureMouse', function(cm, repeat) {
+ return {
+ unit: repeat === 'double' ? selectWordAt : null,
+ };
+ });
+ });
+}
+
+/******************************************************************************/