summaryrefslogtreecommitdiff
path: root/data/extensions/uBlock0@raymondhill.net/js/1p-filters.js
diff options
context:
space:
mode:
Diffstat (limited to 'data/extensions/uBlock0@raymondhill.net/js/1p-filters.js')
-rw-r--r--data/extensions/uBlock0@raymondhill.net/js/1p-filters.js385
1 files changed, 385 insertions, 0 deletions
diff --git a/data/extensions/uBlock0@raymondhill.net/js/1p-filters.js b/data/extensions/uBlock0@raymondhill.net/js/1p-filters.js
new file mode 100644
index 0000000..7e20a30
--- /dev/null
+++ b/data/extensions/uBlock0@raymondhill.net/js/1p-filters.js
@@ -0,0 +1,385 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2014-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, uBlockDashboard */
+
+import './codemirror/ubo-static-filtering.js';
+import { dom, qs$ } from './dom.js';
+import { i18n$ } from './i18n.js';
+import { onBroadcast } from './broadcast.js';
+
+/******************************************************************************/
+
+const cmEditor = new CodeMirror(qs$('#userFilters'), {
+ autoCloseBrackets: true,
+ autofocus: true,
+ extraKeys: {
+ 'Ctrl-Space': 'autocomplete',
+ 'Tab': 'toggleComment',
+ },
+ foldGutter: true,
+ gutters: [
+ 'CodeMirror-linenumbers',
+ { className: 'CodeMirror-lintgutter', style: 'width: 11px' },
+ ],
+ lineNumbers: true,
+ lineWrapping: true,
+ matchBrackets: true,
+ maxScanLines: 1,
+ styleActiveLine: {
+ nonEmpty: true,
+ },
+});
+
+uBlockDashboard.patchCodeMirrorEditor(cmEditor);
+
+/******************************************************************************/
+
+// Add auto-complete ability to the editor. Polling is used as the suggested
+// hints also depend on the tabs currently opened.
+
+{
+ let hintUpdateToken = 0;
+
+ const getHints = async function() {
+ const hints = await vAPI.messaging.send('dashboard', {
+ what: 'getAutoCompleteDetails',
+ hintUpdateToken
+ });
+ if ( hints instanceof Object === false ) { return; }
+ if ( hints.hintUpdateToken !== undefined ) {
+ cmEditor.setOption('uboHints', hints);
+ hintUpdateToken = hints.hintUpdateToken;
+ }
+ timer.on(2503);
+ };
+
+ const timer = vAPI.defer.create(( ) => {
+ getHints();
+ });
+
+ getHints();
+}
+
+vAPI.messaging.send('dashboard', {
+ what: 'getTrustedScriptletTokens',
+}).then(tokens => {
+ cmEditor.setOption('trustedScriptletTokens', tokens);
+});
+
+/******************************************************************************/
+
+let originalState = {
+ enabled: true,
+ trusted: false,
+ filters: '',
+};
+
+function getCurrentState() {
+ const enabled = qs$('#enableMyFilters input').checked;
+ return {
+ enabled,
+ trusted: qs$('#trustMyFilters input').checked,
+ filters: getEditorText(),
+ };
+}
+
+function rememberCurrentState() {
+ originalState = getCurrentState();
+}
+
+function currentStateChanged() {
+ return JSON.stringify(getCurrentState()) !== JSON.stringify(originalState);
+}
+
+function getEditorText() {
+ const text = cmEditor.getValue().trimEnd();
+ return text === '' ? text : `${text}\n`;
+}
+
+function setEditorText(text) {
+ cmEditor.setValue(`${text.trimEnd()}\n\n`);
+}
+
+/******************************************************************************/
+
+function userFiltersChanged(details = {}) {
+ const changed = typeof details.changed === 'boolean'
+ ? details.changed
+ : self.hasUnsavedData();
+ qs$('#userFiltersApply').disabled = !changed;
+ qs$('#userFiltersRevert').disabled = !changed;
+ const enabled = qs$('#enableMyFilters input').checked;
+ const trustedbefore = cmEditor.getOption('trustedSource');
+ const trustedAfter = enabled && qs$('#trustMyFilters input').checked;
+ if ( trustedAfter === trustedbefore ) { return; }
+ cmEditor.startOperation();
+ cmEditor.setOption('trustedSource', trustedAfter);
+ const doc = cmEditor.getDoc();
+ const history = doc.getHistory();
+ const selections = doc.listSelections();
+ doc.replaceRange(doc.getValue(),
+ { line: 0, ch: 0 },
+ { line: doc.lineCount(), ch: 0 }
+ );
+ doc.setSelections(selections);
+ doc.setHistory(history);
+ cmEditor.endOperation();
+ cmEditor.focus();
+}
+
+/******************************************************************************/
+
+// https://github.com/gorhill/uBlock/issues/3704
+// Merge changes to user filters occurring in the background with changes
+// made in the editor. The code assumes that no deletion occurred in the
+// background.
+
+function threeWayMerge(newContent) {
+ const prvContent = originalState.filters.trim().split(/\n/);
+ const differ = new self.diff_match_patch();
+ const newChanges = differ.diff(
+ prvContent,
+ newContent.trim().split(/\n/)
+ );
+ const usrChanges = differ.diff(
+ prvContent,
+ getEditorText().trim().split(/\n/)
+ );
+ const out = [];
+ let i = 0, j = 0, k = 0;
+ while ( i < prvContent.length ) {
+ for ( ; j < newChanges.length; j++ ) {
+ const change = newChanges[j];
+ if ( change[0] !== 1 ) { break; }
+ out.push(change[1]);
+ }
+ for ( ; k < usrChanges.length; k++ ) {
+ const change = usrChanges[k];
+ if ( change[0] !== 1 ) { break; }
+ out.push(change[1]);
+ }
+ if ( k === usrChanges.length || usrChanges[k][0] !== -1 ) {
+ out.push(prvContent[i]);
+ }
+ i += 1; j += 1; k += 1;
+ }
+ for ( ; j < newChanges.length; j++ ) {
+ const change = newChanges[j];
+ if ( change[0] !== 1 ) { continue; }
+ out.push(change[1]);
+ }
+ for ( ; k < usrChanges.length; k++ ) {
+ const change = usrChanges[k];
+ if ( change[0] !== 1 ) { continue; }
+ out.push(change[1]);
+ }
+ return out.join('\n');
+}
+
+/******************************************************************************/
+
+async function renderUserFilters(merge = false) {
+ const details = await vAPI.messaging.send('dashboard', {
+ what: 'readUserFilters',
+ });
+ if ( details instanceof Object === false || details.error ) { return; }
+
+ cmEditor.setOption('trustedSource', details.trusted);
+
+ qs$('#enableMyFilters input').checked = details.enabled;
+ qs$('#trustMyFilters input').checked = details.trusted;
+
+ const newContent = details.content.trim();
+
+ if ( merge && self.hasUnsavedData() ) {
+ setEditorText(threeWayMerge(newContent));
+ userFiltersChanged({ changed: true });
+ } else {
+ setEditorText(newContent);
+ userFiltersChanged({ changed: false });
+ }
+
+ rememberCurrentState();
+}
+
+/******************************************************************************/
+
+function handleImportFilePicker(ev) {
+ const file = ev.target.files[0];
+ if ( file === undefined || file.name === '' ) { return; }
+ if ( file.type.indexOf('text') !== 0 ) { return; }
+ const fr = new FileReader();
+ fr.onload = function() {
+ if ( typeof fr.result !== 'string' ) { return; }
+ const content = uBlockDashboard.mergeNewLines(getEditorText(), fr.result);
+ cmEditor.operation(( ) => {
+ const cmPos = cmEditor.getCursor();
+ setEditorText(content);
+ cmEditor.setCursor(cmPos);
+ cmEditor.focus();
+ });
+ };
+ fr.readAsText(file);
+}
+
+dom.on('#importFilePicker', 'change', handleImportFilePicker);
+
+function startImportFilePicker() {
+ const input = qs$('#importFilePicker');
+ // Reset to empty string, this will ensure an change event is properly
+ // triggered if the user pick a file, even if it is the same as the last
+ // one picked.
+ input.value = '';
+ input.click();
+}
+
+dom.on('#importUserFiltersFromFile', 'click', startImportFilePicker);
+
+/******************************************************************************/
+
+function exportUserFiltersToFile() {
+ const val = getEditorText();
+ if ( val === '' ) { return; }
+ const filename = i18n$('1pExportFilename')
+ .replace('{{datetime}}', uBlockDashboard.dateNowToSensibleString())
+ .replace(/ +/g, '_');
+ vAPI.download({
+ 'url': `data:text/plain;charset=utf-8,${encodeURIComponent(val)}`,
+ 'filename': filename
+ });
+}
+
+/******************************************************************************/
+
+async function applyChanges() {
+ const state = getCurrentState();
+ const details = await vAPI.messaging.send('dashboard', {
+ what: 'writeUserFilters',
+ content: state.filters,
+ enabled: state.enabled,
+ trusted: state.trusted,
+ });
+ if ( details instanceof Object === false || details.error ) { return; }
+ rememberCurrentState();
+ userFiltersChanged({ changed: false });
+ vAPI.messaging.send('dashboard', {
+ what: 'reloadAllFilters',
+ });
+}
+
+function revertChanges() {
+ qs$('#enableMyFilters input').checked = originalState.enabled;
+ qs$('#trustMyFilters input').checked = originalState.trusted;
+ setEditorText(originalState.filters);
+ userFiltersChanged();
+}
+
+/******************************************************************************/
+
+function getCloudData() {
+ return getEditorText();
+}
+
+function setCloudData(data, append) {
+ if ( typeof data !== 'string' ) { return; }
+ if ( append ) {
+ data = uBlockDashboard.mergeNewLines(getEditorText(), data);
+ }
+ cmEditor.setValue(data);
+}
+
+self.cloud.onPush = getCloudData;
+self.cloud.onPull = setCloudData;
+
+/******************************************************************************/
+
+self.wikilink = 'https://github.com/gorhill/uBlock/wiki/Dashboard:-My-filters';
+
+self.hasUnsavedData = function() {
+ return currentStateChanged();
+};
+
+/******************************************************************************/
+
+// Handle user interaction
+dom.on('#exportUserFiltersToFile', 'click', exportUserFiltersToFile);
+dom.on('#userFiltersApply', 'click', ( ) => { applyChanges(); });
+dom.on('#userFiltersRevert', 'click', revertChanges);
+dom.on('#enableMyFilters input', 'change', userFiltersChanged);
+dom.on('#trustMyFilters input', 'change', userFiltersChanged);
+
+(async ( ) => {
+ await renderUserFilters();
+
+ cmEditor.clearHistory();
+
+ // https://github.com/gorhill/uBlock/issues/3706
+ // Save/restore cursor position
+ {
+ const line = await vAPI.localStorage.getItemAsync('myFiltersCursorPosition');
+ if ( typeof line === 'number' ) {
+ cmEditor.setCursor(line, 0);
+ }
+ cmEditor.focus();
+ }
+
+ // https://github.com/gorhill/uBlock/issues/3706
+ // Save/restore cursor position
+ {
+ let curline = 0;
+ cmEditor.on('cursorActivity', ( ) => {
+ if ( timer.ongoing() ) { return; }
+ if ( cmEditor.getCursor().line === curline ) { return; }
+ timer.on(701);
+ });
+ const timer = vAPI.defer.create(( ) => {
+ curline = cmEditor.getCursor().line;
+ vAPI.localStorage.setItem('myFiltersCursorPosition', curline);
+ });
+ }
+
+ // https://github.com/gorhill/uBlock/issues/3704
+ // Merge changes to user filters occurring in the background
+ onBroadcast(msg => {
+ switch ( msg.what ) {
+ case 'userFiltersUpdated': {
+ cmEditor.startOperation();
+ const scroll = cmEditor.getScrollInfo();
+ const selections = cmEditor.listSelections();
+ renderUserFilters(true).then(( ) => {
+ cmEditor.clearHistory();
+ cmEditor.setSelection(selections[0].anchor, selections[0].head);
+ cmEditor.scrollTo(scroll.left, scroll.top);
+ cmEditor.endOperation();
+ });
+ break;
+ }
+ default:
+ break;
+ }
+ });
+})();
+
+cmEditor.on('changes', userFiltersChanged);
+CodeMirror.commands.save = applyChanges;
+
+/******************************************************************************/