mirror of https://github.com/gorhill/uBlock.git
Add ability to sort rules in _My rules_ pane
Related issue: - https://github.com/uBlockOrigin/uBlock-issues/issues/1055
This commit is contained in:
parent
6284eca351
commit
dd655473f6
|
@ -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 {
|
||||
|
|
|
@ -34,7 +34,9 @@
|
|||
<button type="button" class="iconifiable" id="importButton"><span class="fa"></span><span data-i18n="rulesImport"></span></button>
|
||||
<button type="button" class="iconifiable important disabled" id="editSaveButton"><span class="fa"></span><span data-i18n="rulesEditSave"></span></button>
|
||||
</div>
|
||||
<div id="ruleFilter"><span class="fa"></span> <input type="search" size="20"></div>
|
||||
<div id="ruleFilter">
|
||||
<span><span class="fa-icon">filter</span> <input type="search" size="20"></span> Sort: <select><option value="0">Rule type<option value="1" selected>Source<option value="2">Destination</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
});
|
||||
*/
|
||||
|
|
|
@ -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(); });
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
|
|
Loading…
Reference in New Issue