1
0
mirror of https://github.com/gorhill/uBlock.git synced 2024-11-09 12:22:33 +01:00

Add ability to sort rules in _My rules_ pane

Related issue:
- https://github.com/uBlockOrigin/uBlock-issues/issues/1055
This commit is contained in:
Raymond Hill 2020-08-24 12:39:07 -04:00
parent 6284eca351
commit dd655473f6
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
4 changed files with 165 additions and 101 deletions

View File

@ -48,6 +48,23 @@
.cm-staticnetAllow { color: #004f00; } .cm-staticnetAllow { color: #004f00; }
.cm-staticOpt { background-color: #ddd; font-weight: bold; } .cm-staticOpt { background-color: #ddd; font-weight: bold; }
/* Rules */
.cm-s-default .cm-allowrule {
color: green;
font-weight: bold;
}
.cm-s-default .cm-blockrule {
color: red;
font-weight: bold;
}
.cm-s-default .cm-nooprule {
color: darkslategray;
font-weight: bold;
}
.cm-s-default .cm-sortkey {
color: #708;
}
div.CodeMirror span.CodeMirror-matchingbracket { div.CodeMirror span.CodeMirror-matchingbracket {
color: unset; color: unset;
} }
@ -111,11 +128,9 @@ div.CodeMirror span.CodeMirror-matchingbracket {
.CodeMirror-merge-l-deleted { .CodeMirror-merge-l-deleted {
background-image: none; background-image: none;
font-weight: bold;
} }
.CodeMirror-merge-l-inserted { .CodeMirror-merge-l-inserted {
background-image: none; background-image: none;
font-weight: bold;
} }
/* This probably needs to be added to CodeMirror repo */ /* This probably needs to be added to CodeMirror repo */
.CodeMirror-merge-gap { .CodeMirror-merge-gap {

View File

@ -34,7 +34,9 @@
<button type="button" class="iconifiable" id="importButton"><span class="fa">&#xf019;</span><span data-i18n="rulesImport"></span></button> <button type="button" class="iconifiable" id="importButton"><span class="fa">&#xf019;</span><span data-i18n="rulesImport"></span></button>
<button type="button" class="iconifiable important disabled" id="editSaveButton"><span class="fa">&#xf0c7;</span><span data-i18n="rulesEditSave"></span></button> <button type="button" class="iconifiable important disabled" id="editSaveButton"><span class="fa">&#xf0c7;</span><span data-i18n="rulesEditSave"></span></button>
</div> </div>
<div id="ruleFilter"><span class="fa">&#xf0b0;</span>&ensp;<input type="search" size="20"></div> <div id="ruleFilter">
<span><span class="fa-icon">filter</span>&nbsp;<input type="search" size="20"></span>&emsp;Sort: <select><option value="0">Rule type<option value="1" selected>Source<option value="2">Destination</select>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -57,7 +57,10 @@ CodeMirror.defineMode('ubo-dynamic-filtering', ( ) => {
'noop', 'noop',
]); ]);
const reIsNotHostname = /[:/#?*]/; const reIsNotHostname = /[:/#?*]/;
const slices = [];
let sliceIndex = 0;
const tokens = []; const tokens = [];
let tokenIndex = 0;
const isSwitchRule = ( ) => { const isSwitchRule = ( ) => {
const token = tokens[0]; const token = tokens[0];
@ -73,35 +76,54 @@ CodeMirror.defineMode('ubo-dynamic-filtering', ( ) => {
return style; return style;
}; };
const token = stream => { const token = function(stream) {
if ( stream.sol() ) { tokens.length = 0; } if ( stream.sol() ) {
stream.eatSpace(); slices.length = 0;
const match = stream.match(/\S+/); tokens.length = 0;
if ( Array.isArray(match) === false ) { const reTokens = /\S+/g;
return skipToEnd(stream); for (;;) {
const lastIndex = reTokens.lastIndex;
const match = reTokens.exec(stream.string);
if ( match === null ) { break; }
const l = match.index;
const r = reTokens.lastIndex;
if ( l !== lastIndex ) {
slices.push({ t: false, l: lastIndex, r: l });
} }
if ( tokens.length === 4 ) { slices.push({ t: true, l, r });
return skipToEnd(stream, 'error'); tokens.push(stream.string.slice(l, r));
} }
const token = match[0]; sliceIndex = tokenIndex = 0;
tokens.push(token); }
if ( sliceIndex >= slices.length ) {
return stream.skipToEnd(stream);
}
const slice = slices[sliceIndex++];
stream.pos = slice.r;
if ( slice.t !== true ) { return null; }
const token = tokens[tokenIndex++];
// Field 1: per-site switch or hostname // Field 1: per-site switch or hostname
if ( tokens.length === 1 ) { if ( tokenIndex === 1 ) {
if ( isSwitchRule(token) ) { if ( isSwitchRule(token) ) {
if ( validSwitches.has(token) === false ) { if ( validSwitches.has(token) === false ) {
return skipToEnd(stream, 'error'); return skipToEnd(stream, 'error');
} }
} else if ( reIsNotHostname.test(token) && token !== '*' ) { if ( this.sortType === 0 ) { return 'sortkey'; }
return null;
}
if ( reIsNotHostname.test(token) && token !== '*' ) {
return skipToEnd(stream, 'error'); return skipToEnd(stream, 'error');
} }
if ( this.sortType === 1 ) { return 'sortkey'; }
return null; return null;
} }
// Field 2: hostname or url // Field 2: hostname or url
if ( tokens.length === 2 ) { if ( tokenIndex === 2 ) {
if ( isSwitchRule(tokens[0]) ) { if ( isSwitchRule(tokens[0]) ) {
if ( reIsNotHostname.test(token) && token !== '*' ) { if ( reIsNotHostname.test(token) && token !== '*' ) {
return skipToEnd(stream, 'error'); return skipToEnd(stream, 'error');
} }
if ( this.sortType === 1 ) { return 'sortkey'; }
} }
if ( if (
reIsNotHostname.test(token) && reIsNotHostname.test(token) &&
@ -110,15 +132,18 @@ CodeMirror.defineMode('ubo-dynamic-filtering', ( ) => {
) { ) {
return skipToEnd(stream, 'error'); return skipToEnd(stream, 'error');
} }
if ( this.sortType === 2 ) { return 'sortkey'; }
return null; return null;
} }
// Field 3 // Field 3
if ( tokens.length === 3 ) { if ( tokenIndex === 3 ) {
// Switch rule // Switch rule
if ( isSwitchRule(tokens[0]) ) { if ( isSwitchRule(tokens[0]) ) {
if ( validSwitcheStates.has(token) === false ) { if ( validSwitcheStates.has(token) === false ) {
return skipToEnd(stream, 'error'); return skipToEnd(stream, 'error');
} }
if ( token === 'true' ) { return 'blockrule'; }
if ( token === 'false' ) { return 'allowrule'; }
return null; return null;
} }
// Hostname rule // Hostname rule
@ -141,61 +166,19 @@ CodeMirror.defineMode('ubo-dynamic-filtering', ( ) => {
return null; return null;
} }
// Field 4 // Field 4
if ( tokens.length === 4 ) { if ( tokenIndex === 4 ) {
if ( if (
isSwitchRule(tokens[0]) || isSwitchRule(tokens[0]) ||
validActions.has(token) === false validActions.has(token) === false
) { ) {
return skipToEnd(stream, 'error'); return skipToEnd(stream, 'error');
} }
return null; if ( token === 'allow' ) { return 'allowrule'; }
if ( token === 'block' ) { return 'blockrule'; }
return 'nooprule';
} }
return skipToEnd(stream); return skipToEnd(stream);
}; };
return { token }; return { token, sortType: 1 };
}); });
/*
Code below is to address
https://github.com/uBlockOrigin/uMatrix-issues/issues/128
But this needs fixing because glitchiness in some cases.
I may end up having to create a custom merge view rather
than using the existing CodeMirror one.
CodeMirror.registerHelper('fold', 'ubo-dynamic-filtering', (cm, start) => {
function isHeader(lineNo) {
const tokentype = cm.getTokenTypeAt(CodeMirror.Pos(lineNo, 0));
return tokentype && /\bheader\b/.test(tokentype);
}
function headerLevel(lineNo, line, nextLine) {
let match = line && line.match(/^#+/);
if (match && isHeader(lineNo)) return match[0].length;
match = nextLine && nextLine.match(/^[=\-]+\s*$/);
if (match && isHeader(lineNo + 1)) return nextLine[0] === '=' ? 1 : 2;
return 100;
}
const firstLine = cm.getLine(start.line);
let nextLine = cm.getLine(start.line + 1);
const level = headerLevel(start.line, firstLine, nextLine);
if ( level === 100 ) { return; }
const lastLineNo = cm.lastLine();
let end = start.line,
nextNextLine = cm.getLine(end + 2);
while ( end < lastLineNo ) {
if ( headerLevel(end + 1, nextLine, nextNextLine) <= level ) { break; }
++end;
nextLine = nextNextLine;
nextNextLine = cm.getLine(end + 2);
}
return {
from: CodeMirror.Pos(start.line, firstLine.length),
to: CodeMirror.Pos(end, cm.getLine(end).length),
};
});
*/

View File

@ -101,7 +101,7 @@ let differ;
// https://github.com/codemirror/CodeMirror/blob/3e1bb5fff682f8f6cbfaef0e56c61d62403d4798/addon/search/search.js#L22 // https://github.com/codemirror/CodeMirror/blob/3e1bb5fff682f8f6cbfaef0e56c61d62403d4798/addon/search/search.js#L22
// ... and modified as needed. // ... and modified as needed.
const updateOverlay = (function() { const updateOverlay = (( ) => {
let reFilter; let reFilter;
const mode = { const mode = {
token: function(stream) { token: function(stream) {
@ -135,11 +135,16 @@ const updateOverlay = (function() {
// - Minimum amount of text updated // - Minimum amount of text updated
const rulesToDoc = function(clearHistory) { const rulesToDoc = function(clearHistory) {
const orig = unfilteredRules.orig.doc;
const edit = unfilteredRules.edit.doc;
orig.startOperation();
edit.startOperation();
for ( const key in unfilteredRules ) { for ( const key in unfilteredRules ) {
if ( unfilteredRules.hasOwnProperty(key) === false ) { continue; } if ( unfilteredRules.hasOwnProperty(key) === false ) { continue; }
const doc = unfilteredRules[key].doc; const doc = unfilteredRules[key].doc;
const rules = filterRules(key); const rules = filterRules(key);
if ( if (
clearHistory ||
doc.lineCount() === 1 && doc.getValue() === '' || doc.lineCount() === 1 && doc.getValue() === '' ||
rules.length === 0 rules.length === 0
) { ) {
@ -157,27 +162,27 @@ const rulesToDoc = function(clearHistory) {
let afterText = rules.join('\n').trim(); let afterText = rules.join('\n').trim();
if ( afterText !== '' ) { afterText += '\n'; } if ( afterText !== '' ) { afterText += '\n'; }
const diffs = differ.diff_main(beforeText, afterText); const diffs = differ.diff_main(beforeText, afterText);
doc.startOperation(); let i = diffs.length;
let i = diffs.length, let iedit = beforeText.length;
iedit = beforeText.length;
while ( i-- ) { while ( i-- ) {
let diff = diffs[i]; const diff = diffs[i];
if ( diff[0] === 0 ) { if ( diff[0] === 0 ) {
iedit -= diff[1].length; iedit -= diff[1].length;
continue; continue;
} }
let end = doc.posFromIndex(iedit); const end = doc.posFromIndex(iedit);
if ( diff[0] === 1 ) { if ( diff[0] === 1 ) {
doc.replaceRange(diff[1], end, end); doc.replaceRange(diff[1], end, end);
continue; continue;
} }
/* diff[0] === -1 */ /* diff[0] === -1 */
iedit -= diff[1].length; iedit -= diff[1].length;
let beg = doc.posFromIndex(iedit); const beg = doc.posFromIndex(iedit);
doc.replaceRange('', beg, end); doc.replaceRange('', beg, end);
} }
doc.endOperation();
} }
orig.endOperation();
edit.endOperation();
cleanEditText = mergeView.editor().getValue().trim(); cleanEditText = mergeView.editor().getValue().trim();
cleanEditToken = mergeView.editor().changeGeneration(); cleanEditToken = mergeView.editor().changeGeneration();
if ( clearHistory ) { if ( clearHistory ) {
@ -205,27 +210,75 @@ const filterRules = function(key) {
/******************************************************************************/ /******************************************************************************/
const renderRules = (( ) => { const renderRules = (( ) => {
const reIsSwitchRule = /^[a-z-]+: /;
let firstVisit = true; let firstVisit = true;
let sortType = 1;
// Switches always listed at the top. const reSwRule = /^([^/]+): ([^/ ]+) ([^ ]+)/;
const customSort = (a, b) => { const reRule = /^([^ ]+) ([^/ ]+) ([^ ]+ [^ ]+)/;
const aIsSwitch = reIsSwitchRule.test(a); const reUrlRule = /^([^ ]+) ([^ ]+) ([^ ]+ [^ ]+)/;
if ( reIsSwitchRule.test(b) === aIsSwitch ) {
return a.localeCompare(b); const reverseHn = function(hn) {
} return hn.split('.').reverse().join('.');
return aIsSwitch ? -1 : 1;
}; };
return function(details) { const slotFromRule = function(rule) {
details.permanentRules.sort(customSort); let type, srcHn, desHn, extra = '';
details.sessionRules.sort(customSort); let match = reSwRule.exec(rule);
unfilteredRules.orig.rules = details.permanentRules; if ( match !== null ) {
unfilteredRules.edit.rules = details.sessionRules; type = ' ' + match[1];
rulesToDoc(firstVisit); srcHn = reverseHn(match[2]);
if ( firstVisit ) { desHn = srcHn;
} else if ( (match = reRule.exec(rule)) !== null ) {
type = '\x10FFFE';
srcHn = reverseHn(match[1]);
desHn = reverseHn(match[2]);
} else if ( (match = reUrlRule.exec(rule)) !== null ) {
type = '\x10FFFF';
srcHn = reverseHn(match[1]);
desHn = reverseHn(vAPI.hostnameFromURI(match[2]));
extra = rule;
}
if ( sortType === 0 ) {
return { rule, token: `${type} ${srcHn} ${desHn} ${extra}` };
}
if ( sortType === 1 ) {
return { rule, token: `${srcHn} ${type} ${desHn} ${extra}` };
}
return { rule, token: `${desHn} ${type} ${srcHn} ${extra}` };
};
const sort = rules => {
const slots = [];
for ( let i = 0; i < rules.length; i++ ) {
slots.push(slotFromRule(rules[i], 1));
}
slots.sort((a, b) => a.token.localeCompare(b.token));
for ( let i = 0; i < rules.length; i++ ) {
rules[i] = slots[i].rule;
}
};
return function(clearHistory = false) {
const select = document.querySelector('#ruleFilter select');
sortType = parseInt(select.value, 10) || 1;
unfilteredRules.orig.doc.getMode().sortType = sortType;
unfilteredRules.edit.doc.getMode().sortType = sortType;
sort(unfilteredRules.orig.rules);
sort(unfilteredRules.edit.rules);
rulesToDoc(firstVisit || clearHistory);
if ( firstVisit || clearHistory ) {
firstVisit = false; firstVisit = false;
mergeView.editor().execCommand('goNextDiff'); const chunks = mergeView.leftChunks();
if ( chunks.length !== 0 ) {
const ldoc = unfilteredRules.orig.doc;
const { clientHeight } = ldoc.getScrollInfo();
const line = Math.min(chunks[0].editFrom, chunks[0].origFrom);
ldoc.setCursor(line, 0);
ldoc.scrollIntoView(
{ line, ch: 0 },
(clientHeight - ldoc.defaultTextHeight()) / 2
);
}
} }
onTextChanged(true); onTextChanged(true);
}; };
@ -240,7 +293,9 @@ const applyDiff = async function(permanent, toAdd, toRemove) {
toAdd: toAdd, toAdd: toAdd,
toRemove: toRemove, toRemove: toRemove,
}); });
renderRules(details); unfilteredRules.orig.rules = details.permanentRules;
unfilteredRules.edit.rules = details.sessionRules;
renderRules();
}; };
/******************************************************************************/ /******************************************************************************/
@ -326,10 +381,10 @@ function exportUserRulesToFile() {
/******************************************************************************/ /******************************************************************************/
const onFilterChanged = (function() { const onFilterChanged = (( ) => {
let timer, let timer;
overlay = null, let overlay = null;
last = ''; let last = '';
const process = function() { const process = function() {
timer = undefined; timer = undefined;
@ -351,22 +406,28 @@ const onFilterChanged = (function() {
}; };
return function() { return function() {
if ( timer !== undefined ) { clearTimeout(timer); } if ( timer !== undefined ) { self.cancelIdleCallback(timer); }
timer = vAPI.setTimeout(process, 773); timer = self.requestIdleCallback(process, { timeout: 773 });
}; };
})(); })();
/******************************************************************************/ /******************************************************************************/
const onSortChanged = function() {
renderRules(true);
};
/******************************************************************************/
const onTextChanged = (( ) => { const onTextChanged = (( ) => {
let timer; let timer;
const process = now => { const process = details => {
timer = undefined; timer = undefined;
const diff = document.getElementById('diff'); const diff = document.getElementById('diff');
let isClean = mergeView.editor().isClean(cleanEditToken); let isClean = mergeView.editor().isClean(cleanEditToken);
if ( if (
now && details === undefined &&
isClean === false && isClean === false &&
mergeView.editor().getValue().trim() === cleanEditText mergeView.editor().getValue().trim() === cleanEditText
) { ) {
@ -388,8 +449,8 @@ const onTextChanged = (( ) => {
}; };
return function(now) { return function(now) {
if ( timer !== undefined ) { clearTimeout(timer); } if ( timer !== undefined ) { self.cancelIdleCallback(timer); }
timer = now ? process(now) : vAPI.setTimeout(process, 57); timer = now ? process() : self.requestIdleCallback(process, { timeout: 57 });
}; };
})(); })();
@ -483,7 +544,9 @@ self.hasUnsavedData = function() {
vAPI.messaging.send('dashboard', { vAPI.messaging.send('dashboard', {
what: 'getRules', what: 'getRules',
}).then(details => { }).then(details => {
renderRules(details); unfilteredRules.orig.rules = details.permanentRules;
unfilteredRules.edit.rules = details.sessionRules;
renderRules();
}); });
// Handle user interaction // Handle user interaction
@ -494,9 +557,10 @@ uDom('#revertButton').on('click', revertAllHandler);
uDom('#commitButton').on('click', commitAllHandler); uDom('#commitButton').on('click', commitAllHandler);
uDom('#editSaveButton').on('click', editSaveHandler); uDom('#editSaveButton').on('click', editSaveHandler);
uDom('#ruleFilter input').on('input', onFilterChanged); uDom('#ruleFilter input').on('input', onFilterChanged);
uDom('#ruleFilter select').on('input', onSortChanged);
// https://groups.google.com/forum/#!topic/codemirror/UQkTrt078Vs // https://groups.google.com/forum/#!topic/codemirror/UQkTrt078Vs
mergeView.editor().on('updateDiff', function() { onTextChanged(); }); mergeView.editor().on('updateDiff', ( ) => { onTextChanged(); });
/******************************************************************************/ /******************************************************************************/