diff --git a/src/js/epicker-ui.js b/src/js/epicker-ui.js index bc81e9e72..2d1767a0b 100644 --- a/src/js/epicker-ui.js +++ b/src/js/epicker-ui.js @@ -63,6 +63,7 @@ let netFilterCandidates = []; let cosmeticFilterCandidates = []; let computedCandidateSlot = 0; let computedCandidate = ''; +let computedSpecificityCandidates = []; let needBody = false; /******************************************************************************/ @@ -194,7 +195,13 @@ const candidateFromFilterChoice = function(filterChoice) { $stor(`#cosmeticFilters li:nth-of-type(${slot+1})`) .classList.add('active'); - const specificity = [ + return cosmeticCandidatesFromFilterChoice(filterChoice); +}; + +/******************************************************************************/ + +const cosmeticCandidatesFromFilterChoice = function(filterChoice) { + const specificities = [ 0b0000, // remove hierarchy; remove id, nth-of-type, attribute values 0b0010, // remove hierarchy; remove id, nth-of-type 0b0011, // remove hierarchy @@ -203,89 +210,100 @@ const candidateFromFilterChoice = function(filterChoice) { 0b1100, // remove id, nth-of-type, attribute values 0b1110, // remove id, nth-of-type 0b1111, // keep all = most specific - ][ parseInt($stor('#resultsetSpecificity input').value, 10) ]; + ]; - // Return path: the target element, then all siblings prepended - const paths = []; - for ( let i = slot; i < filters.length; i++ ) { - filter = filters[i].slice(2); - // Remove id, nth-of-type - // https://github.com/uBlockOrigin/uBlock-issues/issues/162 - // Mind escaped periods: they do not denote a class identifier. - if ( (specificity & 0b0001) === 0 ) { - filter = filter.replace(/:nth-of-type\(\d+\)/, ''); - if ( - filter.charAt(0) === '#' && ( - (specificity & 0b1000) === 0 || i === slot - ) - ) { - const pos = filter.search(/[^\\]\./); - if ( pos !== -1 ) { - filter = filter.slice(pos + 1); + const candidates = []; + + let { slot, filters } = filterChoice; + let filter = filters[slot]; + + for ( const specificity of specificities ) { + // Return path: the target element, then all siblings prepended + const paths = []; + for ( let i = slot; i < filters.length; i++ ) { + filter = filters[i].slice(2); + // Remove id, nth-of-type + // https://github.com/uBlockOrigin/uBlock-issues/issues/162 + // Mind escaped periods: they do not denote a class identifier. + if ( (specificity & 0b0001) === 0 ) { + filter = filter.replace(/:nth-of-type\(\d+\)/, ''); + if ( + filter.charAt(0) === '#' && ( + (specificity & 0b1000) === 0 || i === slot + ) + ) { + const pos = filter.search(/[^\\]\./); + if ( pos !== -1 ) { + filter = filter.slice(pos + 1); + } + } + } + // Remove attribute values. + if ( (specificity & 0b0010) === 0 ) { + const match = /^\[([^^=]+)\^?=.+\]$/.exec(filter); + if ( match !== null ) { + filter = `[${match[1]}]`; + } + } + // Remove all classes when an id exists. + // https://github.com/uBlockOrigin/uBlock-issues/issues/162 + // Mind escaped periods: they do not denote a class identifier. + if ( filter.charAt(0) === '#' ) { + filter = filter.replace(/([^\\])\..+$/, '$1'); + } + if ( paths.length !== 0 ) { + filter += ' > '; + } + paths.unshift(filter); + // Stop at any element with an id: these are unique in a web page + if ( (specificity & 0b1000) === 0 || filter.startsWith('#') ) { + break; + } + } + + // Trim hierarchy: remove generic elements from path + if ( (specificity & 0b1100) === 0b1000 ) { + let i = 0; + while ( i < paths.length - 1 ) { + if ( /^[a-z0-9]+ > $/.test(paths[i+1]) ) { + if ( paths[i].endsWith(' > ') ) { + paths[i] = paths[i].slice(0, -2); + } + paths.splice(i + 1, 1); + } else { + i += 1; } } } - // Remove attribute values. - if ( (specificity & 0b0010) === 0 ) { - const match = /^\[([^^=]+)\^?=.+\]$/.exec(filter); - if ( match !== null ) { - filter = `[${match[1]}]`; - } - } - // Remove all classes when an id exists. - // https://github.com/uBlockOrigin/uBlock-issues/issues/162 - // Mind escaped periods: they do not denote a class identifier. - if ( filter.charAt(0) === '#' ) { - filter = filter.replace(/([^\\])\..+$/, '$1'); - } - if ( paths.length !== 0 ) { - filter += ' > '; - } - paths.unshift(filter); - // Stop at any element with an id: these are unique in a web page - if ( (specificity & 0b1000) === 0 || filter.startsWith('#') ) { break; } - } - // Trim hierarchy: remove generic elements from path - if ( (specificity & 0b1100) === 0b1000 ) { - let i = 0; - while ( i < paths.length - 1 ) { - if ( /^[a-z0-9]+ > $/.test(paths[i+1]) ) { - if ( paths[i].endsWith(' > ') ) { - paths[i] = paths[i].slice(0, -2); - } - paths.splice(i + 1, 1); - } else { - i += 1; - } + if ( + needBody && + paths.length !== 0 && + paths[0].startsWith('#') === false && + (specificity & 0b1100) !== 0 + ) { + paths.unshift('body > '); } - } - if ( - needBody && - paths.length !== 0 && - paths[0].startsWith('#') === false && - (specificity & 0b1100) !== 0 - ) { - paths.unshift('body > '); + candidates.push(paths); } - if ( paths.length === 0 ) { return ''; } - renderRange('resultsetDepth', slot, true); renderRange('resultsetSpecificity'); vAPI.MessagingConnection.sendTo(epickerConnectionId, { - what: 'optimizeCandidate', - paths, + what: 'optimizeCandidates', + candidates, }); }; /******************************************************************************/ -const onCandidateOptimized = function(details) { +const onCandidatesOptimized = function(details) { $id('resultsetModifiers').classList.remove('hide'); - computedCandidate = details.filter; + const i = parseInt($stor('#resultsetSpecificity input').value, 10); + computedSpecificityCandidates = details.candidates; + computedCandidate = computedSpecificityCandidates[i]; cmEditor.setValue(computedCandidate); cmEditor.clearHistory(); onCandidateChanged(); @@ -501,13 +519,11 @@ const onDepthChanged = function() { /******************************************************************************/ const onSpecificityChanged = function() { + renderRange('resultsetSpecificity'); if ( rawFilterFromTextarea() !== computedCandidate ) { return; } - const text = candidateFromFilterChoice({ - filters: cosmeticFilterCandidates, - slot: computedCandidateSlot, - }); - if ( text === undefined ) { return; } - cmEditor.setValue(text); + const i = parseInt($stor('#resultsetSpecificity input').value, 10); + computedCandidate = computedSpecificityCandidates[i]; + cmEditor.setValue(computedCandidate); cmEditor.clearHistory(); onCandidateChanged(); }; @@ -808,8 +824,8 @@ const quitPicker = function() { const onPickerMessage = function(msg) { switch ( msg.what ) { - case 'candidateOptimized': - onCandidateOptimized(msg); + case 'candidatesOptimized': + onCandidatesOptimized(msg); break; case 'showDialog': showDialog(msg); diff --git a/src/js/scriptlets/epicker.js b/src/js/scriptlets/epicker.js index 2b4b98d98..4571ebba3 100644 --- a/src/js/scriptlets/epicker.js +++ b/src/js/scriptlets/epicker.js @@ -821,21 +821,41 @@ const filterToDOMInterface = (( ) => { /******************************************************************************/ -const onOptmizeCandidate = function(details) { - const { paths } = details; - let count = Number.MAX_SAFE_INTEGER; - let selector = ''; - for ( let i = 0, n = paths.length; i < n; i++ ) { - const s = paths.slice(n - i - 1).join(''); - const elems = document.querySelectorAll(s); - if ( elems.length < count ) { - selector = s; - count = elems.length; +const onOptmizeCandidates = function(details) { + const { candidates } = details; + const results = []; + for ( const paths of candidates ) { + let count = Number.MAX_SAFE_INTEGER; + let selector = ''; + for ( let i = 0, n = paths.length; i < n; i++ ) { + const s = paths.slice(n - i - 1).join(''); + const elems = document.querySelectorAll(s); + if ( elems.length < count ) { + selector = s; + count = elems.length; + } } + results.push({ selector: `##${selector}`, count }); + } + // Sort by most match count and shortest selector to least match count and + // longest selector. + results.sort((a, b) => { + const r = b.count - a.count; + if ( r !== 0 ) { return r; } + return a.selector.length - b.selector.length; + }); + // Discard selectors with same match count as shorter ones. + for ( let i = 0; i < results.length - 1; i++ ) { + const a = results[i+0]; + const b = results[i+1]; + if ( b.count !== a.count ) { continue; } + if ( b.selector.length === a.selector.length ) { continue; } + b.selector = a.selector; + b.count = a.count; } vAPI.MessagingConnection.sendTo(epickerConnectionId, { - what: 'candidateOptimized', - filter: `##${selector}`, + what: 'candidatesOptimized', + candidates: results.map(a => a.selector), }); }; @@ -1064,8 +1084,8 @@ const onDialogMessage = function(msg) { highlightElements([], true); } break; - case 'optimizeCandidate': - onOptmizeCandidate(msg); + case 'optimizeCandidates': + onOptmizeCandidates(msg); break; case 'dialogCreate': filterToDOMInterface.queryAll(msg);