diff options
Diffstat (limited to 'data/extensions/uBlock0@raymondhill.net/js/1p-filters.js')
-rw-r--r-- | data/extensions/uBlock0@raymondhill.net/js/1p-filters.js | 385 |
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; + +/******************************************************************************/ |