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-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 {
color: unset;
}
@ -111,11 +128,9 @@ div.CodeMirror span.CodeMirror-matchingbracket {
.CodeMirror-merge-l-deleted {
background-image: none;
font-weight: bold;
}
.CodeMirror-merge-l-inserted {
background-image: none;
font-weight: bold;
}
/* This probably needs to be added to CodeMirror repo */
.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 important disabled" id="editSaveButton"><span class="fa">&#xf0c7;</span><span data-i18n="rulesEditSave"></span></button>
</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>

View File

@ -57,7 +57,10 @@ CodeMirror.defineMode('ubo-dynamic-filtering', ( ) => {
'noop',
]);
const reIsNotHostname = /[:/#?*]/;
const slices = [];
let sliceIndex = 0;
const tokens = [];
let tokenIndex = 0;
const isSwitchRule = ( ) => {
const token = tokens[0];
@ -73,35 +76,54 @@ CodeMirror.defineMode('ubo-dynamic-filtering', ( ) => {
return style;
};
const token = stream => {
if ( stream.sol() ) { tokens.length = 0; }
stream.eatSpace();
const match = stream.match(/\S+/);
if ( Array.isArray(match) === false ) {
return skipToEnd(stream);
const token = function(stream) {
if ( stream.sol() ) {
slices.length = 0;
tokens.length = 0;
const reTokens = /\S+/g;
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 });
}
slices.push({ t: true, l, r });
tokens.push(stream.string.slice(l, r));
}
sliceIndex = tokenIndex = 0;
}
if ( tokens.length === 4 ) {
return skipToEnd(stream, 'error');
if ( sliceIndex >= slices.length ) {
return stream.skipToEnd(stream);
}
const token = match[0];
tokens.push(token);
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
if ( tokens.length === 1 ) {
if ( tokenIndex === 1 ) {
if ( isSwitchRule(token) ) {
if ( validSwitches.has(token) === false ) {
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');
}
if ( this.sortType === 1 ) { return 'sortkey'; }
return null;
}
// Field 2: hostname or url
if ( tokens.length === 2 ) {
if ( tokenIndex === 2 ) {
if ( isSwitchRule(tokens[0]) ) {
if ( reIsNotHostname.test(token) && token !== '*' ) {
return skipToEnd(stream, 'error');
}
if ( this.sortType === 1 ) { return 'sortkey'; }
}
if (
reIsNotHostname.test(token) &&
@ -110,15 +132,18 @@ CodeMirror.defineMode('ubo-dynamic-filtering', ( ) => {
) {
return skipToEnd(stream, 'error');
}
if ( this.sortType === 2 ) { return 'sortkey'; }
return null;
}
// Field 3
if ( tokens.length === 3 ) {
if ( tokenIndex === 3 ) {
// Switch rule
if ( isSwitchRule(tokens[0]) ) {
if ( validSwitcheStates.has(token) === false ) {
return skipToEnd(stream, 'error');
}
if ( token === 'true' ) { return 'blockrule'; }
if ( token === 'false' ) { return 'allowrule'; }
return null;
}
// Hostname rule
@ -141,61 +166,19 @@ CodeMirror.defineMode('ubo-dynamic-filtering', ( ) => {
return null;
}
// Field 4
if ( tokens.length === 4 ) {
if ( tokenIndex === 4 ) {
if (
isSwitchRule(tokens[0]) ||
validActions.has(token) === false
) {
return skipToEnd(stream, 'error');
}
return null;
if ( token === 'allow' ) { return 'allowrule'; }
if ( token === 'block' ) { return 'blockrule'; }
return 'nooprule';
}
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
// ... and modified as needed.
const updateOverlay = (function() {
const updateOverlay = (( ) => {
let reFilter;
const mode = {
token: function(stream) {
@ -135,11 +135,16 @@ const updateOverlay = (function() {
// - Minimum amount of text updated
const rulesToDoc = function(clearHistory) {
const orig = unfilteredRules.orig.doc;
const edit = unfilteredRules.edit.doc;
orig.startOperation();
edit.startOperation();
for ( const key in unfilteredRules ) {
if ( unfilteredRules.hasOwnProperty(key) === false ) { continue; }
const doc = unfilteredRules[key].doc;
const rules = filterRules(key);
if (
clearHistory ||
doc.lineCount() === 1 && doc.getValue() === '' ||
rules.length === 0
) {
@ -157,27 +162,27 @@ const rulesToDoc = function(clearHistory) {
let afterText = rules.join('\n').trim();
if ( afterText !== '' ) { afterText += '\n'; }
const diffs = differ.diff_main(beforeText, afterText);
doc.startOperation();
let i = diffs.length,
iedit = beforeText.length;
let i = diffs.length;
let iedit = beforeText.length;
while ( i-- ) {
let diff = diffs[i];
const diff = diffs[i];
if ( diff[0] === 0 ) {
iedit -= diff[1].length;
continue;
}
let end = doc.posFromIndex(iedit);
const end = doc.posFromIndex(iedit);
if ( diff[0] === 1 ) {
doc.replaceRange(diff[1], end, end);
continue;
}
/* diff[0] === -1 */
iedit -= diff[1].length;
let beg = doc.posFromIndex(iedit);
const beg = doc.posFromIndex(iedit);
doc.replaceRange('', beg, end);
}
doc.endOperation();
}
orig.endOperation();
edit.endOperation();
cleanEditText = mergeView.editor().getValue().trim();
cleanEditToken = mergeView.editor().changeGeneration();
if ( clearHistory ) {
@ -205,27 +210,75 @@ const filterRules = function(key) {
/******************************************************************************/
const renderRules = (( ) => {
const reIsSwitchRule = /^[a-z-]+: /;
let firstVisit = true;
let sortType = 1;
// Switches always listed at the top.
const customSort = (a, b) => {
const aIsSwitch = reIsSwitchRule.test(a);
if ( reIsSwitchRule.test(b) === aIsSwitch ) {
return a.localeCompare(b);
}
return aIsSwitch ? -1 : 1;
const reSwRule = /^([^/]+): ([^/ ]+) ([^ ]+)/;
const reRule = /^([^ ]+) ([^/ ]+) ([^ ]+ [^ ]+)/;
const reUrlRule = /^([^ ]+) ([^ ]+) ([^ ]+ [^ ]+)/;
const reverseHn = function(hn) {
return hn.split('.').reverse().join('.');
};
return function(details) {
details.permanentRules.sort(customSort);
details.sessionRules.sort(customSort);
unfilteredRules.orig.rules = details.permanentRules;
unfilteredRules.edit.rules = details.sessionRules;
rulesToDoc(firstVisit);
if ( firstVisit ) {
const slotFromRule = function(rule) {
let type, srcHn, desHn, extra = '';
let match = reSwRule.exec(rule);
if ( match !== null ) {
type = ' ' + match[1];
srcHn = reverseHn(match[2]);
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;
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);
};
@ -240,7 +293,9 @@ const applyDiff = async function(permanent, toAdd, toRemove) {
toAdd: toAdd,
toRemove: toRemove,
});
renderRules(details);
unfilteredRules.orig.rules = details.permanentRules;
unfilteredRules.edit.rules = details.sessionRules;
renderRules();
};
/******************************************************************************/
@ -326,10 +381,10 @@ function exportUserRulesToFile() {
/******************************************************************************/
const onFilterChanged = (function() {
let timer,
overlay = null,
last = '';
const onFilterChanged = (( ) => {
let timer;
let overlay = null;
let last = '';
const process = function() {
timer = undefined;
@ -351,22 +406,28 @@ const onFilterChanged = (function() {
};
return function() {
if ( timer !== undefined ) { clearTimeout(timer); }
timer = vAPI.setTimeout(process, 773);
if ( timer !== undefined ) { self.cancelIdleCallback(timer); }
timer = self.requestIdleCallback(process, { timeout: 773 });
};
})();
/******************************************************************************/
const onSortChanged = function() {
renderRules(true);
};
/******************************************************************************/
const onTextChanged = (( ) => {
let timer;
const process = now => {
const process = details => {
timer = undefined;
const diff = document.getElementById('diff');
let isClean = mergeView.editor().isClean(cleanEditToken);
if (
now &&
details === undefined &&
isClean === false &&
mergeView.editor().getValue().trim() === cleanEditText
) {
@ -388,8 +449,8 @@ const onTextChanged = (( ) => {
};
return function(now) {
if ( timer !== undefined ) { clearTimeout(timer); }
timer = now ? process(now) : vAPI.setTimeout(process, 57);
if ( timer !== undefined ) { self.cancelIdleCallback(timer); }
timer = now ? process() : self.requestIdleCallback(process, { timeout: 57 });
};
})();
@ -483,7 +544,9 @@ self.hasUnsavedData = function() {
vAPI.messaging.send('dashboard', {
what: 'getRules',
}).then(details => {
renderRules(details);
unfilteredRules.orig.rules = details.permanentRules;
unfilteredRules.edit.rules = details.sessionRules;
renderRules();
});
// Handle user interaction
@ -494,9 +557,10 @@ uDom('#revertButton').on('click', revertAllHandler);
uDom('#commitButton').on('click', commitAllHandler);
uDom('#editSaveButton').on('click', editSaveHandler);
uDom('#ruleFilter input').on('input', onFilterChanged);
uDom('#ruleFilter select').on('input', onSortChanged);
// https://groups.google.com/forum/#!topic/codemirror/UQkTrt078Vs
mergeView.editor().on('updateDiff', function() { onTextChanged(); });
mergeView.editor().on('updateDiff', ( ) => { onTextChanged(); });
/******************************************************************************/