From 73a69711f2c415e2f437455aead329c73a6d7bd8 Mon Sep 17 00:00:00 2001 From: gorhill Date: Sun, 25 Dec 2016 16:56:39 -0500 Subject: [PATCH] add chainable and recursive cosmetic procedural filters --- src/epicker.html | 13 +- src/js/contentscript.js | 426 ++++++++++++++++----------- src/js/cosmetic-filtering.js | 305 +++++++++++++------ src/js/messaging.js | 4 + src/js/reverselookup-worker.js | 17 +- src/js/scriptlets/cosmetic-logger.js | 54 ++-- src/js/scriptlets/dom-inspector.js | 14 +- src/js/scriptlets/element-picker.js | 318 +++++++++----------- 8 files changed, 654 insertions(+), 497 deletions(-) diff --git a/src/epicker.html b/src/epicker.html index f3e038d33..fc1f34e52 100644 --- a/src/epicker.html +++ b/src/epicker.html @@ -63,7 +63,10 @@ section > div:first-child { margin: 0; position: relative; } -section > div > textarea { +section.invalidFilter > div:first-child { + border-color: red; +} +section > div:first-child > textarea { background-color: #fff; border: none; box-sizing: border-box; @@ -75,10 +78,7 @@ section > div > textarea { resize: none; width: 100%; } -section > div > textarea.invalidFilter { - background-color: #fee; -} -section > div > textarea + div { +section > div:first-child > textarea + div { background-color: #aaa; bottom: 0; color: white; @@ -86,6 +86,9 @@ section > div > textarea + div { position: absolute; right: 0; } +section.invalidFilter > div:first-child > textarea + div { + background-color: red; +} section > div:first-child + div { direction: ltr; margin: 2px 0; diff --git a/src/js/contentscript.js b/src/js/contentscript.js index 91ef518a9..33f241aca 100644 --- a/src/js/contentscript.js +++ b/src/js/contentscript.js @@ -137,19 +137,9 @@ vAPI.domFilterer = (function() { /******************************************************************************/ -var jobQueue = [ - { t: 'css-hide', _0: [] }, // to inject in style tag - { t: 'css-style', _0: [] }, // to inject in style tag - { t: 'css-ssel', _0: [] }, // to manually hide (incremental) - { t: 'css-csel', _0: [] } // to manually hide (not incremental) -]; - -var reParserEx = /:(?:has|matches-css|matches-css-before|matches-css-after|style|xpath)\(.+?\)$/; - var allExceptions = createSet(), allSelectors = createSet(), - stagedNodes = [], - matchesProp = vAPI.matchesProp; + stagedNodes = []; // Complex selectors, due to their nature may need to be "de-committed". A // Set() is used to implement this functionality. @@ -308,100 +298,179 @@ var platformHideNode = vAPI.hideNode, /******************************************************************************/ -var runSimpleSelectorJob = function(job, root, fn) { - if ( job._1 === undefined ) { - job._1 = job._0.join(cssNotHiddenId + ','); - } - if ( root[matchesProp](job._1) ) { - fn(root); - } - var nodes = root.querySelectorAll(job._1), - i = nodes.length; - while ( i-- ) { - fn(nodes[i], job); - } -}; +// 'P' stands for 'Procedural' -var runComplexSelectorJob = function(job, fn) { - if ( job._1 === undefined ) { - job._1 = job._0.join(','); - } - var nodes = document.querySelectorAll(job._1), - i = nodes.length; - while ( i-- ) { - fn(nodes[i], job); - } +var PSelectorHasTask = function(task) { + this.selector = task[1]; }; - -var runHasJob = function(job, fn) { - var nodes = document.querySelectorAll(job._0), - i = nodes.length, node; - while ( i-- ) { - node = nodes[i]; - if ( node.querySelector(job._1) !== null ) { - fn(node, job); +PSelectorHasTask.prototype.exec = function(input) { + var output = []; + for ( var i = 0, n = input.length; i < n; i++ ) { + if ( input[i].querySelector(this.selector) !== null ) { + output.push(input[i]); } } + return output; }; -// '/' = ascii 0x2F */ - -var parseMatchesCSSJob = function(raw) { - var prop = raw.trim(); - if ( prop === '' ) { return null; } - var pos = prop.indexOf(':'), - v = pos !== -1 ? prop.slice(pos + 1).trim() : '', - vlen = v.length; - if ( - vlen > 1 && - v.charCodeAt(0) === 0x2F && - v.charCodeAt(vlen-1) === 0x2F - ) { - try { v = new RegExp(v.slice(1, -1)); } catch(ex) { return null; } - } - return { k: prop.slice(0, pos).trim(), v: v }; +var PSelectorHasTextTask = function(task) { + this.needle = new RegExp(task[1]); }; - -var runMatchesCSSJob = function(job, fn) { - var nodes = document.querySelectorAll(job._0), - i = nodes.length; - if ( i === 0 ) { return; } - if ( typeof job._1 === 'string' ) { - job._1 = parseMatchesCSSJob(job._1); - } - if ( job._1 === null ) { return; } - var k = job._1.k, - v = job._1.v, - node, style, match; - while ( i-- ) { - node = nodes[i]; - style = window.getComputedStyle(node, job._2); - if ( style === null ) { continue; } /* FF */ - if ( v instanceof RegExp ) { - match = v.test(style[k]); - } else { - match = style[k] === v; - } - if ( match ) { - fn(node, job); +PSelectorHasTextTask.prototype.exec = function(input) { + var output = []; + for ( var i = 0, n = input.length; i < n; i++ ) { + if ( this.needle.test(input[i].textContent) ) { + output.push(input[i]); } } + return output; }; -var runXpathJob = function(job, fn) { - if ( job._1 === undefined ) { - job._1 = document.createExpression(job._0, null); +var PSelectorIfTask = function(task) { + this.pselector = new PSelector(task[1]); +}; +PSelectorIfTask.prototype.target = true; +PSelectorIfTask.prototype.exec = function(input) { + var output = []; + for ( var i = 0, n = input.length; i < n; i++ ) { + if ( this.pselector.test(input[i]) === this.target ) { + output.push(input[i]); + } } - var xpr = job._2 = job._1.evaluate( - document, - XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, - job._2 || null - ); - var i = xpr.snapshotLength, node; + return output; +}; + +var PSelectorIfNotTask = function(task) { + PSelectorIfTask.call(this, task); + this.target = false; +}; +PSelectorIfNotTask.prototype = Object.create(PSelectorIfTask.prototype); +PSelectorIfNotTask.prototype.constructor = PSelectorIfNotTask; + +var PSelectorMatchesCSSTask = function(task) { + this.name = task[1].name; + this.value = new RegExp(task[1].value); +}; +PSelectorMatchesCSSTask.prototype.pseudo = null; +PSelectorMatchesCSSTask.prototype.exec = function(input) { + var output = [], style; + for ( var i = 0, n = input.length; i < n; i++ ) { + style = window.getComputedStyle(input[i], this.pseudo); + if ( style === null ) { return null; } /* FF */ + if ( this.value.test(style[this.name]) ) { + output.push(input[i]); + } + } + return output; +}; + +var PSelectorMatchesCSSAfterTask = function(task) { + PSelectorMatchesCSSTask.call(this, task); + this.pseudo = ':after'; +}; +PSelectorMatchesCSSAfterTask.prototype = Object.create(PSelectorMatchesCSSTask.prototype); +PSelectorMatchesCSSAfterTask.prototype.constructor = PSelectorMatchesCSSAfterTask; + +var PSelectorMatchesCSSBeforeTask = function(task) { + PSelectorMatchesCSSTask.call(this, task); + this.pseudo = ':before'; +}; +PSelectorMatchesCSSBeforeTask.prototype = Object.create(PSelectorMatchesCSSTask.prototype); +PSelectorMatchesCSSBeforeTask.prototype.constructor = PSelectorMatchesCSSBeforeTask; + +var PSelectorXpathTask = function(task) { + this.xpe = document.createExpression(task[1], null); + this.xpr = null; +}; +PSelectorXpathTask.prototype.exec = function(input) { + var output = [], j, node; + for ( var i = 0, n = input.length; i < n; i++ ) { + this.xpr = this.xpe.evaluate( + input[i], + XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, + this.xpr + ); + j = this.xpr.snapshotLength; + while ( j-- ) { + node = this.xpr.snapshotItem(j); + if ( node.nodeType === 1 ) { + output.push(node); + } + } + } + return output; +}; + +var PSelector = function(o) { + if ( PSelector.prototype.operatorToTaskMap === undefined ) { + PSelector.prototype.operatorToTaskMap = new Map([ + [ ':has', PSelectorHasTask ], + [ ':has-text', PSelectorHasTextTask ], + [ ':if', PSelectorIfTask ], + [ ':if-not', PSelectorIfNotTask ], + [ ':matches-css', PSelectorMatchesCSSTask ], + [ ':matches-css-after', PSelectorMatchesCSSAfterTask ], + [ ':matches-css-before', PSelectorMatchesCSSBeforeTask ], + [ ':xpath', PSelectorXpathTask ] + ]); + } + this.raw = o.raw; + this.selector = o.selector; + this.tasks = []; + var tasks = o.tasks, task, ctor; + for ( var i = 0; i < tasks.length; i++ ) { + task = tasks[i]; + ctor = this.operatorToTaskMap.get(task[0]); + this.tasks.push(new ctor(task)); + } +}; +PSelector.prototype.operatorToTaskMap = undefined; +PSelector.prototype.prime = function(input) { + var root = input || document; + if ( this.selector !== '' ) { + return root.querySelectorAll(this.selector); + } + return [ root ]; +}; +PSelector.prototype.exec = function(input) { + //var t0 = window.performance.now(); + var tasks = this.tasks, nodes = this.prime(input); + for ( var i = 0, n = tasks.length; i < n && nodes.length !== 0; i++ ) { + nodes = tasks[i].exec(nodes); + } + //console.log('%s: %s ms', this.raw, (window.performance.now() - t0).toFixed(2)); + return nodes; +}; +PSelector.prototype.test = function(input) { + //var t0 = window.performance.now(); + var tasks = this.tasks, nodes = this.prime(input), aa0 = [ null ], aa; + for ( var i = 0, ni = nodes.length; i < ni; i++ ) { + aa0[0] = nodes[i]; aa = aa0; + for ( var j = 0, nj = tasks.length; j < nj && aa.length !== 0; j++ ) { + aa = tasks[i].exec(aa); + } + if ( aa.length !== 0 ) { return true; } + } + //console.log('%s: %s ms', this.raw, (window.performance.now() - t0).toFixed(2)); + return false; +}; + +var PSelectors = function() { + this.entries = []; +}; +PSelectors.prototype.add = function(o) { + this.entries.push(new PSelector(o)); +}; +PSelectors.prototype.forEachNode = function(callback) { + var pfilters = this.entries, + i = pfilters.length, + pfilter, nodes, j; while ( i-- ) { - node = xpr.snapshotItem(i); - if ( node.nodeType === 1 ) { - fn(node, job); + pfilter = pfilters[i]; + nodes = pfilter.exec(); + j = nodes.length; + while ( j-- ) { + callback(nodes[j], pfilter); } } }; @@ -418,14 +487,52 @@ var domFilterer = { hiddenNodeCount: 0, hiddenNodeEnforcer: false, loggerEnabled: undefined, - styleTags: [], - jobQueue: jobQueue, - // Stock jobs. - job0: jobQueue[0], - job1: jobQueue[1], - job2: jobQueue[2], - job3: jobQueue[3], + newHideSelectorBuffer: [], // Hide style filter buffer + newStyleRuleBuffer: [], // Non-hide style filter buffer + simpleHideSelectors: { // Hiding filters: simple selectors + entries: [], + matchesProp: vAPI.matchesProp, + selector: undefined, + add: function(selector) { + this.entries.push(selector); + this.selector = undefined; + }, + forEachNodeOfSelector: function(/*callback, root, extra*/) { + }, + forEachNode: function(callback, root, extra) { + if ( this.selector === undefined ) { + this.selector = this.entries.join(extra + ',') + extra; + } + if ( root[this.matchesProp](this.selector) ) { + callback(root); + } + var nodes = root.querySelectorAll(this.selector), + i = nodes.length; + while ( i-- ) { + callback(nodes[i]); + } + } + }, + complexHideSelectors: { // Hiding filters: complex selectors + entries: [], + selector: undefined, + add: function(selector) { + this.entries.push(selector); + this.selector = undefined; + }, + forEachNode: function(callback) { + if ( this.selector === undefined ) { + this.selector = this.entries.join(','); + } + var nodes = document.querySelectorAll(this.selector), + i = nodes.length; + while ( i-- ) { + callback(nodes[i]); + } + } + }, + proceduralSelectors: new PSelectors(), // Hiding filters: procedural addExceptions: function(aa) { for ( var i = 0, n = aa.length; i < n; i++ ) { @@ -433,58 +540,28 @@ var domFilterer = { } }, - // Job: - // Stock jobs in job queue: - // 0 = css rules/css declaration to remove visibility - // 1 = css rules/any css declaration - // 2 = simple css selectors/hide - // 3 = complex css selectors/hide - // Custom jobs: - // matches-css/hide - // has/hide - // xpath/hide - - addSelector: function(s) { - if ( allSelectors.has(s) || allExceptions.has(s) ) { + addSelector: function(selector) { + if ( allSelectors.has(selector) || allExceptions.has(selector) ) { return; } - allSelectors.add(s); - var sel0 = s, sel1 = ''; - if ( s.charCodeAt(s.length - 1) === 0x29 ) { - var parts = reParserEx.exec(s); - if ( parts !== null ) { - sel1 = parts[0]; - } - } - if ( sel1 === '' ) { - this.job0._0.push(sel0); - if ( sel0.indexOf(' ') === -1 ) { - this.job2._0.push(sel0); - this.job2._1 = undefined; + allSelectors.add(selector); + if ( selector.charCodeAt(0) !== 0x7B /* '{' */ ) { + this.newHideSelectorBuffer.push(selector); + if ( selector.indexOf(' ') === -1 ) { + this.simpleHideSelectors.add(selector); } else { - this.job3._0.push(sel0); - this.job3._1 = undefined; + this.complexHideSelectors.add(selector); } return; } - sel0 = sel0.slice(0, sel0.length - sel1.length); - if ( sel1.lastIndexOf(':has', 0) === 0 ) { - this.jobQueue.push({ t: 'has-hide', raw: s, _0: sel0, _1: sel1.slice(5, -1) }); - } else if ( sel1.lastIndexOf(':matches-css', 0) === 0 ) { - if ( sel1.lastIndexOf(':matches-css-before', 0) === 0 ) { - this.jobQueue.push({ t: 'matches-css-hide', raw: s, _0: sel0, _1: sel1.slice(20, -1), _2: ':before' }); - } else if ( sel1.lastIndexOf(':matches-css-after', 0) === 0 ) { - this.jobQueue.push({ t: 'matches-css-hide', raw: s, _0: sel0, _1: sel1.slice(19, -1), _2: ':after' }); - } else { - this.jobQueue.push({ t: 'matches-css-hide', raw: s, _0: sel0, _1: sel1.slice(13, -1), _2: null }); - } - } else if ( sel1.lastIndexOf(':style', 0) === 0 ) { - this.job1._0.push(sel0 + ' { ' + sel1.slice(7, -1) + ' }'); - this.job1._1 = undefined; - } else if ( sel1.lastIndexOf(':xpath', 0) === 0 ) { - this.jobQueue.push({ t: 'xpath-hide', raw: s, _0: sel1.slice(7, -1) }); + var o = JSON.parse(selector); + if ( o.style ) { + this.newStyleRuleBuffer.push(o.parts.join(' ')); + return; + } + if ( o.procedural ) { + this.proceduralSelectors.add(o); } - return; }, addSelectors: function(aa) { @@ -497,27 +574,27 @@ var domFilterer = { this.commitTimer.clear(); var beforeHiddenNodeCount = this.hiddenNodeCount, - styleText = '', i, n; + styleText = '', i; - // Stock job 0 = css rules/hide - if ( this.job0._0.length ) { - styleText = '\n:root ' + this.job0._0.join(',\n:root ') + '\n{ display: none !important; }'; - this.job0._0.length = 0; + // CSS rules/hide + if ( this.newHideSelectorBuffer.length ) { + styleText = '\n:root ' + this.newHideSelectorBuffer.join(',\n:root ') + '\n{ display: none !important; }'; + this.newHideSelectorBuffer.length = 0; } - // Stock job 1 = css rules/any css declaration - if ( this.job1._0.length ) { - styleText += '\n' + this.job1._0.join('\n'); - this.job1._0.length = 0; + // CSS rules/any css declaration + if ( this.newStyleRuleBuffer.length ) { + styleText += '\n' + this.newStyleRuleBuffer.join('\n'); + this.newStyleRuleBuffer.length = 0; } // Simple selectors: incremental. - // Stock job 2 = simple css selectors/hide - if ( this.job2._0.length ) { + // Simple css selectors/hide + if ( this.simpleHideSelectors.entries.length ) { i = stagedNodes.length; while ( i-- ) { - runSimpleSelectorJob(this.job2, stagedNodes[i], hideNode); + this.simpleHideSelectors.forEachNode(hideNode, stagedNodes[i], cssNotHiddenId); } } stagedNodes = []; @@ -526,17 +603,16 @@ var domFilterer = { complexSelectorsOldResultSet = complexSelectorsCurrentResultSet; complexSelectorsCurrentResultSet = createSet('object'); - // Stock job 3 = complex css selectors/hide + // Complex css selectors/hide // The handling of these can be considered optional, since they are // also applied declaratively using a style tag. - if ( this.job3._0.length ) { - runComplexSelectorJob(this.job3, complexHideNode); + if ( this.complexHideSelectors.entries.length ) { + this.complexHideSelectors.forEachNode(complexHideNode); } - // Custom jobs. No optional since they can't be applied in a - // declarative way. - for ( i = 4, n = this.jobQueue.length; i < n; i++ ) { - this.runJob(this.jobQueue[i], complexHideNode); + // Procedural cosmetic filters + if ( this.proceduralSelectors.entries.length ) { + this.proceduralSelectors.forEachNode(complexHideNode); } // https://github.com/gorhill/uBlock/issues/1912 @@ -595,6 +671,10 @@ var domFilterer = { this.commitTimer.start(); }, + createProceduralFilter: function(o) { + return new PSelector(o); + }, + getExcludeId: function() { if ( this.excludeId === undefined ) { this.excludeId = vAPI.randomToken(); @@ -616,20 +696,6 @@ var domFilterer = { this.commitTimer = new vAPI.SafeAnimationFrame(this.commit_.bind(this)); }, - runJob: function(job, fn) { - switch ( job.t ) { - case 'has-hide': - runHasJob(job, fn); - break; - case 'matches-css-hide': - runMatchesCSSJob(job, fn); - break; - case 'xpath-hide': - runXpathJob(job, fn); - break; - } - }, - showNode: function(node) { node.hidden = false; platformUnhideNode(node); @@ -1248,14 +1314,14 @@ vAPI.domSurveyor = (function() { // Need to do this before committing DOM filterer, as needed info // will no longer be there after commit. - if ( firstSurvey || domFilterer.job0._0.length ) { + if ( firstSurvey || domFilterer.newHideSelectorBuffer.length ) { messaging.send( 'contentscript', { what: 'cosmeticFiltersInjected', type: 'cosmetic', hostname: window.location.hostname, - selectors: domFilterer.job0._0, + selectors: domFilterer.newHideSelectorBuffer, first: firstSurvey, cost: surveyCost } @@ -1263,7 +1329,7 @@ vAPI.domSurveyor = (function() { } // Shutdown surveyor if too many consecutive empty resultsets. - if ( domFilterer.job0._0.length === 0 ) { + if ( domFilterer.newHideSelectorBuffer.length === 0 ) { cosmeticSurveyingMissCount += 1; } else { cosmeticSurveyingMissCount = 0; diff --git a/src/js/cosmetic-filtering.js b/src/js/cosmetic-filtering.js index ebfdc7392..6d693adb5 100644 --- a/src/js/cosmetic-filtering.js +++ b/src/js/cosmetic-filtering.js @@ -39,6 +39,35 @@ var µb = µBlock; var encode = JSON.stringify; var decode = JSON.parse; +var isValidCSSSelector = (function() { + var div = document.createElement('div'), + matchesFn; + // Keep in mind: + // https://github.com/gorhill/uBlock/issues/693 + // https://github.com/gorhill/uBlock/issues/1955 + if ( div.matches instanceof Function ) { + matchesFn = div.matches.bind(div); + } else if ( div.mozMatchesSelector instanceof Function ) { + matchesFn = div.mozMatchesSelector.bind(div); + } else if ( div.webkitMatchesSelector instanceof Function ) { + matchesFn = div.webkitMatchesSelector.bind(div); + } else if ( div.msMatchesSelector instanceof Function ) { + matchesFn = div.msMatchesSelector.bind(div); + } else { + matchesFn = div.querySelector.bind(div); + } + return function(s) { + try { + matchesFn(s + ', ' + s + ':not(#foo)'); + } catch (ex) { + return false; + } + return true; + }; +})(); + +var reIsRegexLiteral = /^\/.+\/$/; + var isBadRegex = function(s) { try { void new RegExp(s); @@ -218,7 +247,7 @@ var FilterParser = function() { this.hostnames = []; this.invalid = false; this.cosmetic = true; - this.reNeedHostname = /^(?:script:contains|script:inject|.+?:has|.+?:matches-css(?:-before|-after)?|:xpath)\(.+?\)$/; + this.reNeedHostname = /^(?:script:contains|script:inject|.+?:has|.+?:has-text|.+?:if|.+?:if-not|.+?:matches-css(?:-before|-after)?|.*?:xpath)\(.+\)$/; }; /******************************************************************************/ @@ -331,7 +360,12 @@ FilterParser.prototype.parse = function(raw) { // ##script:contains(...) // ##script:inject(...) // ##.foo:has(...) + // ##.foo:has-text(...) + // ##.foo:if(...) + // ##.foo:if-not(...) // ##.foo:matches-css(...) + // ##.foo:matches-css-after(...) + // ##.foo:matches-css-before(...) // ##:xpath(...) if ( this.hostnames.length === 0 && @@ -698,91 +732,178 @@ FilterContainer.prototype.freeze = function() { // implemented (if ever). Unlikely, see: // https://github.com/gorhill/uBlock/issues/1752 -FilterContainer.prototype.isValidSelector = (function() { - var div = document.createElement('div'); - var matchesProp = (function() { - if ( typeof div.matches === 'function' ) { - return 'matches'; - } - if ( typeof div.mozMatchesSelector === 'function' ) { - return 'mozMatchesSelector'; - } - if ( typeof div.webkitMatchesSelector === 'function' ) { - return 'webkitMatchesSelector'; - } - return ''; - })(); - // Not all browsers support `Element.matches`: - // http://caniuse.com/#feat=matchesselector - if ( matchesProp === '' ) { - return function() { - return true; - }; - } - - var reHasSelector = /^(.+?):has\((.+?)\)$/, - reMatchesCSSSelector = /^(.+?):matches-css(?:-before|-after)?\((.+?)\)$/, - reXpathSelector = /^:xpath\((.+?)\)$/, - reStyleSelector = /^(.+?):style\((.+?)\)$/, +FilterContainer.prototype.compileSelector = (function() { + var reStyleSelector = /^(.+?):style\((.+?)\)$/, reStyleBad = /url\([^)]+\)/, reScriptSelector = /^script:(contains|inject)\((.+)\)$/; - // Keep in mind: - // https://github.com/gorhill/uBlock/issues/693 - // https://github.com/gorhill/uBlock/issues/1955 - var isValidCSSSelector = function(s) { - try { - div[matchesProp](s + ', ' + s + ':not(#foo)'); - } catch (ex) { - return false; + return function(raw) { + if ( isValidCSSSelector(raw) && raw.indexOf('[-abp-properties=') === -1 ) { + return raw; } - return true; - }; - return function(s) { - if ( isValidCSSSelector(s) && s.indexOf('[-abp-properties=') === -1 ) { - return true; - } - // We reach this point very rarely. + // We rarely reach this point. var matches; - // Future `:has`-based filter? If so, validate both parts of the whole - // selector. - matches = reHasSelector.exec(s); - if ( matches !== null ) { - return isValidCSSSelector(matches[1]) && isValidCSSSelector(matches[2]); - } - // Custom `:matches-css`-based filter? - matches = reMatchesCSSSelector.exec(s); - if ( matches !== null ) { - return isValidCSSSelector(matches[1]); - } - // Custom `:xpath`-based filter? - matches = reXpathSelector.exec(s); - if ( matches !== null ) { - try { - return document.createExpression(matches[1], null) instanceof XPathExpression; - } catch (e) { - } - return false; - } // `:style` selector? - matches = reStyleSelector.exec(s); - if ( matches !== null ) { - return isValidCSSSelector(matches[1]) && reStyleBad.test(matches[2]) === false; - } - // Special `script:` filter? - matches = reScriptSelector.exec(s); - if ( matches !== null ) { - if ( matches[1] === 'inject' ) { - return true; + if ( (matches = reStyleSelector.exec(raw)) !== null ) { + if ( isValidCSSSelector(matches[1]) && reStyleBad.test(matches[2]) === false ) { + return JSON.stringify({ + style: true, + raw: raw, + parts: [ matches[1], '{' + matches[2] + '}' ] + }); } - return matches[2].startsWith('/') === false || - matches[2].endsWith('/') === false || - isBadRegex(matches[2].slice(1, -1)) === false; + return; } - µb.logger.writeOne('', 'error', 'Cosmetic filtering – invalid filter: ' + s); - return false; + + // `script:` filter? + if ( (matches = reScriptSelector.exec(raw)) !== null ) { + // :inject + if ( matches[1] === 'inject' ) { + return raw; + } + // :contains + if ( reIsRegexLiteral.test(matches[2]) === false || isBadRegex(matches[2].slice(1, -1)) === false ) { + return raw; + } + return; + } + + // Procedural selector? + var compiled; + if ( (compiled = this.compileProceduralSelector(raw)) ) { + return compiled; + } + + µb.logger.writeOne('', 'error', 'Cosmetic filtering – invalid filter: ' + raw); + return; + }; +})(); + +/******************************************************************************/ + +FilterContainer.prototype.compileProceduralSelector = (function() { + var reParserEx = /(:(?:has|has-text|if|if-not|matches-css|matches-css-after|matches-css-before|xpath))\(.+\)$/, + reFirstParentheses = /^\(*/, + reLastParentheses = /\)*$/, + reEscapeRegex = /[.*+?^${}()|[\]\\]/g; + + var lastProceduralSelector = '', + lastProceduralSelectorCompiled; + + var compileCSSSelector = function(s) { + if ( isValidCSSSelector(s) ) { + return s; + } + }; + + var compileText = function(s) { + if ( reIsRegexLiteral.test(s) ) { + s = s.slice(1, -1); + if ( isBadRegex(s) ) { return; } + } else { + s = s.replace(reEscapeRegex, '\\$&'); + } + return s; + }; + + var compileCSSDeclaration = function(s) { + var name, value, + pos = s.indexOf(':'); + if ( pos === -1 ) { return; } + name = s.slice(0, pos).trim(); + value = s.slice(pos + 1).trim(); + if ( reIsRegexLiteral.test(value) ) { + value = value.slice(1, -1); + if ( isBadRegex(value) ) { return; } + } else { + value = value.replace(reEscapeRegex, '\\$&'); + } + return { name: name, value: value }; + }; + + var compileConditionalSelector = function(s) { + return compile(s); + }; + + var compileXpathExpression = function(s) { + var dummy; + try { + dummy = document.createExpression(s, null) instanceof XPathExpression; + } catch (e) { + return; + } + return s; + }; + + var compileArgument = new Map([ + [ ':has', compileCSSSelector ], + [ ':has-text', compileText ], + [ ':if', compileConditionalSelector ], + [ ':if-not', compileConditionalSelector ], + [ ':matches-css', compileCSSDeclaration ], + [ ':matches-css-after', compileCSSDeclaration ], + [ ':matches-css-before', compileCSSDeclaration ], + [ ':xpath', compileXpathExpression ] + ]); + + var compile = function(raw) { + var matches = reParserEx.exec(raw); + if ( matches === null ) { return; } + var tasks = [], + firstOperand = raw.slice(0, matches.index), + currentOperator = matches[1], + selector = raw.slice(matches.index + currentOperator.length), + currentArgument = '', nextOperand, nextOperator, + depth = 0, opening, closing; + if ( firstOperand !== '' && isValidCSSSelector(firstOperand) === false ) { return; } + for (;;) { + matches = reParserEx.exec(selector); + if ( matches !== null ) { + nextOperand = selector.slice(0, matches.index); + nextOperator = matches[1]; + } else { + nextOperand = selector; + nextOperator = ''; + } + opening = reFirstParentheses.exec(nextOperand)[0].length; + closing = reLastParentheses.exec(nextOperand)[0].length; + if ( opening > closing ) { + if ( depth === 0 ) { currentArgument = ''; } + depth += 1; + } else if ( closing > opening && depth > 0 ) { + depth -= 1; + if ( depth === 0 ) { nextOperand = currentArgument + nextOperand; } + } + if ( depth !== 0 ) { + currentArgument += nextOperand + nextOperator; + } else { + currentArgument = compileArgument.get(currentOperator)(nextOperand.slice(1, -1)); + if ( currentArgument === undefined ) { return; } + tasks.push([ currentOperator, currentArgument ]); + currentOperator = nextOperator; + } + if ( nextOperator === '' ) { break; } + selector = selector.slice(matches.index + nextOperator.length); + } + if ( tasks.length === 0 || depth !== 0 ) { return; } + return { selector: firstOperand, tasks: tasks }; + }; + + return function(raw) { + if ( raw === lastProceduralSelector ) { + return lastProceduralSelectorCompiled; + } + lastProceduralSelector = raw; + var compiled = compile(raw); + if ( compiled !== undefined ) { + compiled.procedural = true; + compiled.raw = raw; + compiled = JSON.stringify(compiled); + } + lastProceduralSelectorCompiled = compiled; + return compiled; }; })(); @@ -843,7 +964,7 @@ FilterContainer.prototype.compile = function(s, out) { // still the most common, and can easily be tested using a plain regex. if ( this.reClassOrIdSelector.test(parsed.suffix) === false && - this.isValidSelector(parsed.suffix) === false + this.compileSelector(parsed.suffix) === undefined ) { return true; } @@ -895,15 +1016,15 @@ FilterContainer.prototype.compileGenericHideSelector = function(parsed, out) { return; } // Composite CSS rule. - if ( this.isValidSelector(selector) ) { + if ( this.compileSelector(selector) ) { out.push('c\vlg+\v' + key + '\v' + selector); } return; } - if ( this.isValidSelector(selector) !== true ) { - return; - } + var compiled = this.compileSelector(selector); + if ( compiled === undefined ) { return; } + // TODO: Detect and error on procedural cosmetic filters. // ["title"] and ["alt"] will go in high-low generic bin. if ( this.reHighLow.test(selector) ) { @@ -948,10 +1069,6 @@ FilterContainer.prototype.compileGenericHideSelector = function(parsed, out) { FilterContainer.prototype.compileGenericUnhideSelector = function(parsed, out) { var selector = parsed.suffix; - if ( this.isValidSelector(selector) !== true ) { - return; - } - // script:contains(...) // script:inject(...) if ( this.reScriptSelector.test(selector) ) { @@ -959,10 +1076,14 @@ FilterContainer.prototype.compileGenericUnhideSelector = function(parsed, out) { return; } + // Procedural cosmetic filters are acceptable as generic exception filters. + var compiled = this.compileSelector(selector); + if ( compiled === undefined ) { return; } + // https://github.com/chrisaljoudi/uBlock/issues/497 // All generic exception filters are put in the same bucket: they are // expected to be very rare. - out.push('c\vg1\v' + selector); + out.push('c\vg1\v' + compiled); }; /******************************************************************************/ @@ -980,20 +1101,24 @@ FilterContainer.prototype.compileHostnameSelector = function(hostname, parsed, o hostname = this.punycode.toASCII(hostname); } - var domain = this.µburi.domainFromHostname(hostname), + var selector = parsed.suffix, + domain = this.µburi.domainFromHostname(hostname), hash; // script:contains(...) // script:inject(...) - if ( this.reScriptSelector.test(parsed.suffix) ) { + if ( this.reScriptSelector.test(selector) ) { hash = domain !== '' ? domain : this.noDomainHash; if ( unhide ) { hash = '!' + hash; } - out.push('c\vjs\v' + hash + '\v' + hostname + '\v' + parsed.suffix); + out.push('c\vjs\v' + hash + '\v' + hostname + '\v' + selector); return; } + var compiled = this.compileSelector(selector); + if ( compiled === undefined ) { return; } + // https://github.com/chrisaljoudi/uBlock/issues/188 // If not a real domain as per PSL, assign a synthetic one if ( hostname.endsWith('.*') === false ) { @@ -1005,7 +1130,7 @@ FilterContainer.prototype.compileHostnameSelector = function(hostname, parsed, o hash = '!' + hash; } - out.push('c\vh\v' + hash + '\v' + hostname + '\v' + parsed.suffix); + out.push('c\vh\v' + hash + '\v' + hostname + '\v' + compiled); }; /******************************************************************************/ diff --git a/src/js/messaging.js b/src/js/messaging.js index b422d1abc..c6ade1f4f 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -100,6 +100,10 @@ var onMessage = function(request, sender, callback) { µb.mouseURL = request.url; break; + case 'compileCosmeticFilterSelector': + response = µb.cosmeticFilteringEngine.compileSelector(request.selector); + break; + case 'cosmeticFiltersInjected': µb.cosmeticFilteringEngine.addToSelectorCache(request); /* falls through */ diff --git a/src/js/reverselookup-worker.js b/src/js/reverselookup-worker.js index 176a2896c..ff827fdcd 100644 --- a/src/js/reverselookup-worker.js +++ b/src/js/reverselookup-worker.js @@ -134,14 +134,23 @@ var fromCosmeticFilter = function(details) { } candidates[details.rawFilter] = new RegExp(reStr.join('\\v') + '(?:\\n|$)'); + // Procedural filters, which are pre-compiled, make thing sort of + // complicated. We are going to also search for one portion of the + // compiled form of a filter. + var filterEx = '(' + + reEscape(filter) + + '|[^\\v]+' + + reEscape(JSON.stringify({ raw: filter }).slice(1,-1)) + + '[^\\v]+)'; + // Second step: find hostname-based versions. // Reference: FilterContainer.compileHostnameSelector(). - var pos; - var hostname = details.hostname; + var pos, + hostname = details.hostname; if ( hostname !== '' ) { for ( ;; ) { candidates[hostname + '##' + filter] = new RegExp( - ['c', 'h', '[^\\v]+', reEscape(hostname), reEscape(filter)].join('\\v') + + ['c', 'h', '[^\\v]+', reEscape(hostname), filterEx].join('\\v') + '(?:\\n|$)' ); pos = hostname.indexOf('.'); @@ -159,7 +168,7 @@ var fromCosmeticFilter = function(details) { if ( pos !== -1 ) { var entity = domain.slice(0, pos) + '.*'; candidates[entity + '##' + filter] = new RegExp( - ['c', 'h', '[^\\v]+', reEscape(entity), reEscape(filter)].join('\\v') + + ['c', 'h', '[^\\v]+', reEscape(entity), filterEx].join('\\v') + '(?:\\n|$)' ); } diff --git a/src/js/scriptlets/cosmetic-logger.js b/src/js/scriptlets/cosmetic-logger.js index 29ac23477..51c67d6fe 100644 --- a/src/js/scriptlets/cosmetic-logger.js +++ b/src/js/scriptlets/cosmetic-logger.js @@ -31,38 +31,36 @@ if ( typeof vAPI !== 'object' || !vAPI.domFilterer ) { return; } -var df = vAPI.domFilterer, - loggedSelectors = vAPI.loggedSelectors || {}, - matchedSelectors = [], - selectors, i, selector; +var loggedSelectors = vAPI.loggedSelectors || {}, + matchedSelectors = []; -// CSS selectors. -selectors = df.jobQueue[2]._0.concat(df.jobQueue[3]._0); -i = selectors.length; -while ( i-- ) { - selector = selectors[i]; - if ( loggedSelectors.hasOwnProperty(selector) ) { - continue; +var evaluateSelector = function(selector) { + if ( + loggedSelectors.hasOwnProperty(selector) === false && + document.querySelector(selector) !== null + ) { + loggedSelectors[selector] = true; + matchedSelectors.push(selector); } - if ( document.querySelector(selector) === null ) { - continue; - } - loggedSelectors[selector] = true; - matchedSelectors.push(selector); -} - -// Non-CSS selectors. -var logHit = function(node, job) { - if ( !job.raw || loggedSelectors.hasOwnProperty(job.raw) ) { - return; - } - loggedSelectors[job.raw] = true; - matchedSelectors.push(job.raw); }; -for ( i = 4; i < df.jobQueue.length; i++ ) { - df.runJob(df.jobQueue[i], logHit); -} + +// Simple CSS selector-based cosmetic filters. +vAPI.domFilterer.simpleHideSelectors.entries.forEach(evaluateSelector); + +// Complex CSS selector-based cosmetic filters. +vAPI.domFilterer.complexHideSelectors.entries.forEach(evaluateSelector); + +// Procedural cosmetic filters. +vAPI.domFilterer.proceduralSelectors.entries.forEach(function(pfilter) { + if ( + loggedSelectors.hasOwnProperty(pfilter.raw) === false && + pfilter.exec().length !== 0 + ) { + loggedSelectors[pfilter.raw] = true; + matchedSelectors.push(pfilter.raw); + } +}); vAPI.loggedSelectors = loggedSelectors; diff --git a/src/js/scriptlets/dom-inspector.js b/src/js/scriptlets/dom-inspector.js index a31f6988a..5ef7658b6 100644 --- a/src/js/scriptlets/dom-inspector.js +++ b/src/js/scriptlets/dom-inspector.js @@ -693,7 +693,7 @@ var cosmeticFilterMapper = (function() { i, j; // CSS-based selectors: simple one. - selectors = vAPI.domFilterer.job2._0; + selectors = vAPI.domFilterer.simpleHideSelectors.entries; i = selectors.length; while ( i-- ) { selector = selectors[i]; @@ -709,9 +709,9 @@ var cosmeticFilterMapper = (function() { } } } - + // CSS-based selectors: complex one (must query from doc root). - selectors = vAPI.domFilterer.job3._0; + selectors = vAPI.domFilterer.complexHideSelectors.entries; i = selectors.length; while ( i-- ) { selector = selectors[i]; @@ -726,14 +726,12 @@ var cosmeticFilterMapper = (function() { } // Non-CSS selectors. - var runJobCallback = function(node, job) { + var runJobCallback = function(node, pfilter) { if ( filterMap.has(node) === false ) { - filterMap.set(node, job.raw); + filterMap.set(node, pfilter.raw); } }; - for ( i = 4; i < vAPI.domFilterer.jobQueue.length; i++ ) { - vAPI.domFilterer.runJob(vAPI.domFilterer.jobQueue[i], runJobCallback); - } + vAPI.domFilterer.proceduralSelectors.forEachNode(runJobCallback); }; var incremental = function(rootNode) { diff --git a/src/js/scriptlets/element-picker.js b/src/js/scriptlets/element-picker.js index fc77cbdd0..1181f77f5 100644 --- a/src/js/scriptlets/element-picker.js +++ b/src/js/scriptlets/element-picker.js @@ -177,6 +177,14 @@ var safeQuerySelectorAll = function(node, selector) { /******************************************************************************/ +var rawFilterFromTextarea = function() { + var s = taCandidate.value, + pos = s.indexOf('\n'); + return pos === -1 ? s.trim() : s.slice(0, pos).trim(); +}; + +/******************************************************************************/ + var getElementBoundingClientRect = function(elem) { var rect = typeof elem.getBoundingClientRect === 'function' ? elem.getBoundingClientRect() : @@ -635,7 +643,9 @@ var filtersFrom = function(x, y) { filterToDOMInterface.set @desc Look-up all the HTML elements matching the filter passed in argument. - @param string, a cosmetic of network filter. + @param string, a cosmetic or network filter. + @param function, called once all items matching the filter have been + collected. @return array, or undefined if the filter is invalid. filterToDOMInterface.preview @@ -733,16 +743,15 @@ var filterToDOMInterface = (function() { // ways to compose a valid href to the same effective URL. One idea is to // normalize all a[href] on the page, but for now I will wait and see, as I // prefer to refrain from tampering with the page content if I can avoid it. - var fromCosmeticFilter = function(filter) { + var fromPlainCosmeticFilter = function(filter) { var elems; try { elems = document.querySelectorAll(filter); } catch (e) { - return fromProceduralCosmeticFilter(filter); + return; } - var out = [], - iElem = elems.length; + var out = [], iElem = elems.length; while ( iElem-- ) { out.push({ type: 'cosmetic', elem: elems[iElem]}); } @@ -751,108 +760,27 @@ var filterToDOMInterface = (function() { // https://github.com/gorhill/uBlock/issues/1772 // Handle procedural cosmetic filters. - var fromProceduralCosmeticFilter = function(filter) { - if ( filter.charCodeAt(filter.length - 1) === 0x29 /* ')' */ ) { - var parts = reProceduralCosmeticFilter.exec(filter); - if ( - parts !== null && - proceduralCosmeticFilterFunctions.hasOwnProperty(parts[2]) - ) { - return proceduralCosmeticFilterFunctions[parts[2]]( - parts[1].trim(), - parts[3].trim() - ); - } + var fromCompiledCosmeticFilter = function(raw) { + if ( typeof raw !== 'string' ) { return; } + var o; + try { + o = JSON.parse(raw); + } catch(ex) { + return; } - }; - - var reProceduralCosmeticFilter = /^(.*?):(matches-css|has|style|xpath)\((.+?)\)$/; - - // Collection of handlers for procedural cosmetic filters. - var proceduralCosmeticFilterFunctions = { - 'has': function(selector, arg) { - if ( selector === '' ) { return; } - var elems; - try { - elems = document.querySelectorAll(selector); - document.querySelector(arg); - } catch(ex) { - return; - } - var out = [], elem; - for ( var i = 0, n = elems.length; i < n; i++ ) { - elem = elems[i]; - if ( elem.querySelector(arg) ) { - out.push({ type: 'cosmetic', elem: elem }); - } - } - return out; - }, - 'matches-css': function(selector, arg) { - if ( selector === '' ) { return; } - var elems; - try { - elems = document.querySelectorAll(selector); - } catch(ex) { - return; - } - var out = [], elem, style, - pos = arg.indexOf(':'); - if ( pos === -1 ) { return; } - var prop = arg.slice(0, pos).trim(), - reText = arg.slice(pos + 1).trim(); - if ( reText === '' ) { return; } - var re = reText !== '*' ? - new RegExp('^' + reText.replace(/[.+?${}()|[\]\\^]/g, '\\$&').replace(/\*+/g, '.*?') + '$') : - /./; - for ( var i = 0, n = elems.length; i < n; i++ ) { - elem = elems[i]; - style = window.getComputedStyle(elem, null); - if ( re.test(style[prop]) ) { - out.push({ type: 'cosmetic', elem: elem }); - } - } - return out; - }, - 'style': function(selector, arg) { - if ( selector === '' || arg === '' ) { return; } - var elems; - try { - elems = document.querySelectorAll(selector); - } catch(ex) { - return; - } - var out = []; - for ( var i = 0, n = elems.length; i < n; i++ ) { - out.push({ type: 'cosmetic', elem: elems[i] }); - } - lastAction = selector + ' { ' + arg + ' }'; - return out; - }, - 'xpath': function(selector, arg) { - if ( selector !== '' ) { return []; } - var result; - try { - result = document.evaluate( - arg, - document, - null, - XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, - null - ); - } catch(ex) { - return; - } - if ( result === undefined ) { return []; } - var out = [], elem, i = result.snapshotLength; - while ( i-- ) { - elem = result.snapshotItem(i); - if ( elem.nodeType === 1 ) { - out.push({ type: 'cosmetic', elem: elem }); - } - } - return out; + var elems; + if ( o.style ) { + elems = document.querySelectorAll(o.parts[0]); + lastAction = o.parts.join(' '); + } else if ( o.procedural ) { + elems = vAPI.domFilterer.createProceduralFilter(o).exec(); } + if ( !elems ) { return; } + var out = []; + for ( var i = 0, n = elems.length; i < n; i++ ) { + out.push({ type: 'cosmetic', elem: elems[i] }); + } + return out; }; var lastFilter, @@ -862,26 +790,44 @@ var filterToDOMInterface = (function() { applied = false, previewing = false; - var queryAll = function(filter) { + var queryAll = function(filter, callback) { filter = filter.trim(); if ( filter === lastFilter ) { - return lastResultset; + callback(lastResultset); + return; } unapply(); if ( filter === '' ) { lastFilter = ''; lastResultset = []; - } else { - lastFilter = filter; - lastAction = undefined; - lastResultset = filter.lastIndexOf('##', 0) === 0 ? - fromCosmeticFilter(filter.slice(2)) : - fromNetworkFilter(filter); - if ( previewing ) { - apply(filter); - } + callback(lastResultset); + return; } - return lastResultset; + lastFilter = filter; + lastAction = undefined; + if ( filter.lastIndexOf('##', 0) === -1 ) { + lastResultset = fromNetworkFilter(filter); + if ( previewing ) { apply(); } + callback(lastResultset); + return; + } + var selector = filter.slice(2); + lastResultset = fromPlainCosmeticFilter(selector); + if ( lastResultset ) { + if ( previewing ) { apply(); } + callback(lastResultset); + return; + } + // Procedural cosmetic filter + vAPI.messaging.send( + 'elementPicker', + { what: 'compileCosmeticFilterSelector', selector: selector }, + function(response) { + lastResultset = fromCompiledCosmeticFilter(response); + if ( previewing ) { apply(); } + callback(lastResultset); + } + ); }; var applyHide = function() { @@ -983,9 +929,11 @@ var filterToDOMInterface = (function() { var preview = function(filter) { previewing = filter !== false; if ( previewing ) { - if ( queryAll(filter) !== undefined ) { - apply(); - } + queryAll(filter, function(items) { + if ( items !== undefined ) { + apply(); + } + }); } else { unapply(); } @@ -999,70 +947,75 @@ var filterToDOMInterface = (function() { }; })(); -// https://www.youtube.com/watch?v=nuUXJ6RfIik - /******************************************************************************/ -var userFilterFromCandidate = function() { - var v = taCandidate.value; - var items = filterToDOMInterface.set(v); - if ( !items || items.length === 0 ) { - return false; - } - - // https://github.com/gorhill/uBlock/issues/738 - // Trim dots. - var hostname = window.location.hostname; - if ( hostname.slice(-1) === '.' ) { - hostname = hostname.slice(0, -1); - } - - // Cosmetic filter? - if ( v.lastIndexOf('##', 0) === 0 ) { - return hostname + v; - } - - // Assume net filter - var opts = []; - - // If no domain included in filter, we need domain option - if ( v.lastIndexOf('||', 0) === -1 ) { - opts.push('domain=' + hostname); - } - - var item = items[0]; - if ( item.opts ) { - opts.push(item.opts); - } - - if ( opts.length ) { - v += '$' + opts.join(','); - } - - return v; -}; - -/******************************************************************************/ - -var onCandidateChanged = function() { - var elems = [], - items = filterToDOMInterface.set(taCandidate.value), - valid = items !== undefined; - if ( valid ) { - for ( var i = 0; i < items.length; i++ ) { - elems.push(items[i].elem); +var userFilterFromCandidate = function(callback) { + var v = rawFilterFromTextarea(); + filterToDOMInterface.set(v, function(items) { + if ( !items || items.length === 0 ) { + callback(); + return; } - } - pickerBody.querySelector('body section textarea + div').textContent = valid ? - items.length.toLocaleString() : - '0'; - taCandidate.classList.toggle('invalidFilter', !valid); - dialog.querySelector('#create').disabled = elems.length === 0; - highlightElements(elems, true); + + // https://github.com/gorhill/uBlock/issues/738 + // Trim dots. + var hostname = window.location.hostname; + if ( hostname.slice(-1) === '.' ) { + hostname = hostname.slice(0, -1); + } + + // Cosmetic filter? + if ( v.lastIndexOf('##', 0) === 0 ) { + callback(hostname + v); + return; + } + + // Assume net filter + var opts = []; + + // If no domain included in filter, we need domain option + if ( v.lastIndexOf('||', 0) === -1 ) { + opts.push('domain=' + hostname); + } + + var item = items[0]; + if ( item.opts ) { + opts.push(item.opts); + } + + if ( opts.length ) { + v += '$' + opts.join(','); + } + + callback(v); + }); }; /******************************************************************************/ +var onCandidateChanged = (function() { + var process = function(items) { + var elems = [], valid = items !== undefined; + if ( valid ) { + for ( var i = 0; i < items.length; i++ ) { + elems.push(items[i].elem); + } + } + pickerBody.querySelector('body section textarea + div').textContent = valid ? + items.length.toLocaleString() : + 'ε'; + dialog.querySelector('section').classList.toggle('invalidFilter', !valid); + dialog.querySelector('#create').disabled = elems.length === 0; + highlightElements(elems, true); + }; + + return function() { + filterToDOMInterface.set(rawFilterFromTextarea(), process); + }; +})(); + +/******************************************************************************/ + var candidateFromFilterChoice = function(filterChoice) { var slot = filterChoice.slot; var filters = filterChoice.filters; @@ -1132,8 +1085,8 @@ var onDialogClicked = function(ev) { // We have to exit from preview mode: this guarantees matching elements // will be found for the candidate filter. filterToDOMInterface.preview(false); - var filter = userFilterFromCandidate(); - if ( filter ) { + userFilterFromCandidate(function(filter) { + if ( !filter ) { return; } var d = new Date(); vAPI.messaging.send( 'elementPicker', @@ -1143,9 +1096,9 @@ var onDialogClicked = function(ev) { pageDomain: window.location.hostname } ); - filterToDOMInterface.preview(taCandidate.value); + filterToDOMInterface.preview(rawFilterFromTextarea()); stopPicker(); - } + }); } else if ( ev.target.id === 'pick' ) { @@ -1161,7 +1114,7 @@ var onDialogClicked = function(ev) { if ( filterToDOMInterface.previewing() ) { filterToDOMInterface.preview(false); } else { - filterToDOMInterface.preview(taCandidate.value); + filterToDOMInterface.preview(rawFilterFromTextarea()); } highlightElements(targetElements, true); } @@ -1300,6 +1253,7 @@ var onKeyPressed = function(ev) { if ( ev.which === 27 ) { ev.stopPropagation(); ev.preventDefault(); + filterToDOMInterface.preview(false); stopPicker(); } };