Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Side by Side Diff: chrome/content/elemHideEmulation.js

Issue 29383960: Issue 3143 - Filter elements with :-abp-has() (Closed) Base URL: https://hg.adblockplus.org/adblockpluscore
Patch Set: Validate the syntax, remove unused flags Created April 10, 2017, 1:12 p.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | lib/filterClasses.js » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // We are currently limited to ECMAScript 5 in this file, because it is being 1 // We are currently limited to ECMAScript 5 in this file, because it is being
2 // used in the browser tests. See https://issues.adblockplus.org/ticket/4796 2 // used in the browser tests. See https://issues.adblockplus.org/ticket/4796
Wladimir Palant 2017/04/25 10:57:52 This is really a PITA. Given that https://issues.a
hub 2017/04/25 19:42:47 If issue 4796 lands before this, then I'll fix it.
Wladimir Palant 2017/04/26 00:03:09 As I suggested on IRC, it would probably be easies
hub 2017/04/27 17:43:04 Newer patch is now ES6 based on top of your patch.
3 3
4 var propertySelectorRegExp = /\[\-abp\-properties=(["'])([^"']+)\1\]/; 4 var propertySelectorRegExp = /\[-abp-properties=(["'])([^"']+)\1\]/;
5 var abpSelectorRegExp = /\[-abp-selector=(["'])(.+)\1\]/;
6 var pseudoClassHasSelectorRegExp = /:has\((.*)\)/;
7 var pseudoClassPropsSelectorRegExp = /:-abp-properties\((["'])([^"']+)\1\)/;
Wladimir Palant 2017/04/25 11:30:46 Having :has() but :-abp-properties() is inconsiste
hub 2017/04/25 19:42:47 OK.
5 8
6 function splitSelector(selector) 9 function splitSelector(selector)
7 { 10 {
8 if (selector.indexOf(",") == -1) 11 if (selector.indexOf(",") == -1)
9 return [selector]; 12 return [selector];
10 13
11 var selectors = []; 14 var selectors = [];
12 var start = 0; 15 var start = 0;
13 var level = 0; 16 var level = 0;
14 var sep = ""; 17 var sep = "";
(...skipping 19 matching lines...) Expand all
34 selectors.push(selector.substring(start, i)); 37 selectors.push(selector.substring(start, i));
35 start = i + 1; 38 start = i + 1;
36 } 39 }
37 } 40 }
38 } 41 }
39 42
40 selectors.push(selector.substring(start)); 43 selectors.push(selector.substring(start));
41 return selectors; 44 return selectors;
42 } 45 }
43 46
44 function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc) 47 const ABP_ATTR = "abp-0ac791f0-a03b-4f2c-a935-a285bc2e668e";
48 const ABP_ATTR_SEL = "[" + ABP_ATTR + "] ";
49
50 function matchChildren(e, selector)
51 {
52 var subElement;
53 var newSelector = ABP_ATTR_SEL + selector;
54 var parentNode = e.parentNode || document;
55
56 e.setAttribute(ABP_ATTR, "");
57 subElement = parentNode.querySelector(newSelector);
58 e.removeAttribute(ABP_ATTR);
Wladimir Palant 2017/04/25 10:57:52 This hack is problematic. Even if we make the attr
hub 2017/04/25 19:42:47 I was thinking we could generate an actually rando
hub 2017/04/27 17:43:04 On 2017/04/25 19:42:47, hub wrote: Newer patch re
59
60 return subElement != null;
61 }
62
63 function selectChildren(e, selector)
64 {
65 var subElements;
66 var newSelector = ABP_ATTR_SEL + selector;
67 var parentNode = e.parentNode || document;
68
69 e.setAttribute(ABP_ATTR, "");
70 subElements = parentNode.querySelectorAll(newSelector);
71 e.removeAttribute(ABP_ATTR);
72
73 return subElements;
74 }
75
76 /** Unwrap the pattern out of a [-abp-selector=''] if necessary
77 * @param {object} pattern - The pattern object to unwrap
78 * @return {object} the parsed pattern object or undefined if nothing parse.
79 */
80 function unwrapPattern(pattern)
Wladimir Palant 2017/04/25 10:57:53 This unwrapping belongs into lib/filterClasses.js
hub 2017/04/25 19:42:47 ok, moving it.
81 {
82 var match = abpSelectorRegExp.exec(pattern.selector);
83 if (match)
84 {
85 var prefix = pattern.selector.substr(0, match.index).trim();
86 var suffix = pattern.selector.substr(match.index + match[0].length).trim();
87 var selector = prefix + match[2] + suffix;
88 return parsePattern({selector: selector}, true);
89 }
90
91 return parsePattern(pattern, false);
92 }
93
94 function parsePattern(pattern, unwrapped)
95 {
96 var selector = pattern.selector;
97 var match;
98
99 if (unwrapped)
100 {
101 // Seems that the pattern should be unwrapped.
102 // This is a fundamental error.
103 if (selector.indexOf("[-abp-selector=") != -1)
104 return;
105
106 match = pseudoClassHasSelectorRegExp.exec(selector);
Wladimir Palant 2017/04/25 10:57:52 The syntax is getting too complex, parsing CSS wit
107 if (match)
108 return {
109 type: "has",
110 text: pattern.text,
111 elementMatcher: new PseudoHasMatcher(match[1]),
112 prefix: selector.substr(0, match.index).trim(),
113 suffix: selector.substr(match.index + match[0].length).trim()
114 };
115 match = pseudoClassPropsSelectorRegExp.exec(selector);
116 }
117
118 if (!match)
119 match = propertySelectorRegExp.exec(selector);
Wladimir Palant 2017/04/25 10:57:52 I don't think that we should have this special han
hub 2017/04/25 19:42:47 good idea. Like unwrap above.
120 if (match)
121 {
122 var regexpString;
123 var propertyExpression = match[2];
124 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" &&
125 propertyExpression[propertyExpression.length - 1] == "/")
126 regexpString = propertyExpression.slice(1, -1)
127 .replace("\\x7B ", "{").replace("\\x7D ", "}");
128 else
129 regexpString = filterToRegExp(propertyExpression);
130 return {
131 type: "props",
132 text: pattern.text,
133 regexp: new RegExp(regexpString, "i"),
134 prefix: selector.substr(0, match.index),
135 suffix: selector.substr(match.index + match[0].length)
136 };
137 }
138 }
139
140 function matchStyleProps(style, rule, pattern, selectors, filters)
141 {
142 if (pattern.regexp.test(style))
143 {
144 var subSelectors = splitSelector(rule.selectorText);
145 for (var i = 0; i < subSelectors.length; i++)
146 {
147 var subSelector = subSelectors[i];
148 selectors.push(pattern.prefix + subSelector + pattern.suffix);
149 filters.push(pattern.text);
150 }
151 }
152 }
153
154 function findPropsSelectors(stylesheet, patterns, selectors, filters)
155 {
156 var rules = stylesheet.cssRules;
157 if (!rules)
158 return;
159
160 for (var i = 0; i < rules.length; i++)
161 {
162 var rule = rules[i];
163 if (rule.type != rule.STYLE_RULE)
164 continue;
165
166 var style = stringifyStyle(rule.style);
167 for (var j = 0; j < patterns.length; j++)
168 {
169 matchStyleProps(style, rule, patterns[j], selectors, filters);
170 }
171 }
172 }
173
174 /**
175 * Match the selector @pattern containing :has() starting from @node.
176 * @param {string} pattern - the pattern to match
177 * @param {object} node - the top level node.
178 */
179 function pseudoClassHasMatch(pattern, node, stylesheets, elements, filters)
180 {
181 // select element for the prefix pattern. Or just use node.
182 var haveEl = pattern.prefix ? node.querySelectorAll(pattern.prefix) : [ node ] ;
Wladimir Palant 2017/04/25 10:57:53 This will work correctly for selectors like "div >
hub 2017/04/25 19:42:47 I naively assumed that since these are filters, we
Wladimir Palant 2017/04/26 00:03:09 Feel free to suggest a meaningful subset of the sy
183 for (var j = 0; j < haveEl.length; j++)
184 {
185 var matched = pattern.elementMatcher.match(haveEl[j], stylesheets);
186 if (!matched)
187 continue;
188
189 if (pattern.suffix)
190 {
191 var subElements = selectChildren(haveEl[j], pattern.suffix);
192 if (subElements)
193 {
194 for (var k = 0; k < subElements.length; k++)
195 {
196 elements.push(subElements[k]);
197 filters.push(pattern.text);
198 }
199 }
200 }
201 else
202 {
203 elements.push(haveEl[j]);
204 filters.push(pattern.text);
205 }
206 }
207 }
208
209 function stringifyStyle(style)
210 {
211 var styles = [];
212 for (var i = 0; i < style.length; i++)
213 {
214 var property = style.item(i);
215 var value = style.getPropertyValue(property);
216 var priority = style.getPropertyPriority(property);
217 styles.push(property + ": " + value + (priority ? " !" + priority : "") + "; ");
218 }
219 styles.sort();
220 return styles.join(" ");
221 }
222
223 /** matcher for the pseudo CSS4 class :has()
224 * For those browser that don't have it yet.
225 */
226 function PseudoHasMatcher(selector)
227 {
228 this.hasSelector = selector;
229 this.parsed = parsePattern({selector: this.hasSelector}, true);
230 }
231
232 PseudoHasMatcher.prototype = {
233 match: function(elem, stylesheets)
234 {
235 var selectors = [];
236
237 if (this.parsed)
238 {
239 var filters = []; // don't need this, we have a partial filter.
240 if (this.parsed.type == "has")
241 {
242 var child = elem.firstChild;
243 while (child)
244 {
245 if (child.nodeType === Node.ELEMENT_NODE)
246 {
247 var matches = [];
248 pseudoClassHasMatch(this.parsed, child, stylesheets, matches, filter s);
249 if (matches.length > 0)
250 return true;
251 }
252 child = child.nextSibling;
253 }
254 return false;
255 }
256 if (this.parsed.type == "props")
257 {
258 for (var i = 0; i < stylesheets.length; i++)
259 findPropsSelectors(stylesheets[i], [this.parsed], selectors, filters);
Wladimir Palant 2017/04/25 10:57:52 We will be going through the stylesheets multiple
260 }
261 }
262 else
263 {
264 selectors = [this.hasSelector];
265 }
266
267 var matched = false;
268 // look up for all elements that match the :has().
269 for (var k = 0; k < selectors.length; k++)
270 {
271 try
272 {
273 matched = matchChildren(elem, selectors[k]);
274 if (matched)
275 break;
276 }
277 catch (e)
278 {
279 console.error("Exception with querySelector()", selectors[k], e);
280 }
281 }
282 return matched;
283 }
284 };
285
286 function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc, hideElement sFunc)
45 { 287 {
46 this.window = window; 288 this.window = window;
47 this.getFiltersFunc = getFiltersFunc; 289 this.getFiltersFunc = getFiltersFunc;
48 this.addSelectorsFunc = addSelectorsFunc; 290 this.addSelectorsFunc = addSelectorsFunc;
291 this.hideElementsFunc = hideElementsFunc;
Wladimir Palant 2017/04/25 10:57:53 I guess that hideElementsFunc should do some kind
49 } 292 }
50 293
51 ElemHideEmulation.prototype = { 294 ElemHideEmulation.prototype = {
52 stringifyStyle: function(style)
53 {
54 var styles = [];
55 for (var i = 0; i < style.length; i++)
56 {
57 var property = style.item(i);
58 var value = style.getPropertyValue(property);
59 var priority = style.getPropertyPriority(property);
60 styles.push(property + ": " + value + (priority ? " !" + priority : "") + ";");
61 }
62 styles.sort();
63 return styles.join(" ");
64 },
65 295
66 isSameOrigin: function(stylesheet) 296 isSameOrigin: function(stylesheet)
67 { 297 {
68 try 298 try
69 { 299 {
70 return new URL(stylesheet.href).origin == this.window.location.origin; 300 return new URL(stylesheet.href).origin == this.window.location.origin;
71 } 301 }
72 catch (e) 302 catch (e)
73 { 303 {
74 // Invalid URL, assume that it is first-party. 304 // Invalid URL, assume that it is first-party.
75 return true; 305 return true;
76 } 306 }
77 }, 307 },
78 308
79 findSelectors: function(stylesheet, selectors, filters) 309 findPseudoClassHasElements: function(node, stylesheets, elements, filters)
80 { 310 {
81 // Explicitly ignore third-party stylesheets to ensure consistent behavior 311 for (var i = 0; i < this.pseudoHasPatterns.length; i++)
82 // between Firefox and Chrome. 312 {
83 if (!this.isSameOrigin(stylesheet)) 313 pseudoClassHasMatch(this.pseudoHasPatterns[i], node, stylesheets, elements , filters);
84 return;
85
86 var rules = stylesheet.cssRules;
87 if (!rules)
88 return;
89
90 for (var i = 0; i < rules.length; i++)
91 {
92 var rule = rules[i];
93 if (rule.type != rule.STYLE_RULE)
94 continue;
95
96 var style = this.stringifyStyle(rule.style);
97 for (var j = 0; j < this.patterns.length; j++)
98 {
99 var pattern = this.patterns[j];
100 if (pattern.regexp.test(style))
101 {
102 var subSelectors = splitSelector(rule.selectorText);
103 for (var k = 0; k < subSelectors.length; k++)
104 {
105 var subSelector = subSelectors[k];
106 selectors.push(pattern.prefix + subSelector + pattern.suffix);
107 filters.push(pattern.text);
108 }
109 }
110 }
111 } 314 }
112 }, 315 },
113 316
114 addSelectors: function(stylesheets) 317 addSelectors: function(stylesheets)
115 { 318 {
116 var selectors = []; 319 var selectors = [];
117 var filters = []; 320 var filters = [];
118 for (var i = 0; i < stylesheets.length; i++) 321 for (var i = 0; i < stylesheets.length; i++)
119 this.findSelectors(stylesheets[i], selectors, filters); 322 {
323 // Explicitly ignore third-party stylesheets to ensure consistent behavior
324 // between Firefox and Chrome.
325 if (!this.isSameOrigin(stylesheets[i]))
326 continue;
327 findPropsSelectors(stylesheets[i], this.propSelPatterns, selectors, filter s);
328 }
120 this.addSelectorsFunc(selectors, filters); 329 this.addSelectorsFunc(selectors, filters);
121 }, 330 },
122 331
332 hideElements: function(stylesheets)
333 {
334 var elements = [];
335 var filters = [];
336 this.findPseudoClassHasElements(document, stylesheets, elements, filters);
337 this.hideElementsFunc(elements, filters);
338 },
339
123 onLoad: function(event) 340 onLoad: function(event)
124 { 341 {
125 var stylesheet = event.target.sheet; 342 var stylesheet = event.target.sheet;
126 if (stylesheet) 343 if (stylesheet)
127 this.addSelectors([stylesheet]); 344 this.addSelectors([stylesheet]);
345 this.hideElements([stylesheet]);
128 }, 346 },
129 347
130 apply: function() 348 apply: function()
131 { 349 {
132 this.getFiltersFunc(function(patterns) 350 this.getFiltersFunc(function(patterns)
133 { 351 {
134 this.patterns = []; 352 this.propSelPatterns = [];
353 this.pseudoHasPatterns = [];
135 for (var i = 0; i < patterns.length; i++) 354 for (var i = 0; i < patterns.length; i++)
136 { 355 {
137 var pattern = patterns[i]; 356 var pattern = patterns[i];
138 var match = propertySelectorRegExp.exec(pattern.selector); 357 var parsed = unwrapPattern(pattern);
139 if (!match) 358 if (parsed == undefined)
140 continue; 359 continue;
141 360 if (parsed.type == "props")
142 var propertyExpression = match[2]; 361 {
143 var regexpString; 362 this.propSelPatterns.push(parsed);
144 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" && 363 }
145 propertyExpression[propertyExpression.length - 1] == "/") 364 else if (parsed.type == "has")
146 regexpString = propertyExpression.slice(1, -1) 365 {
147 .replace("\\x7B ", "{").replace("\\x7D ", "}"); 366 this.pseudoHasPatterns.push(parsed);
148 else 367 }
149 regexpString = filterToRegExp(propertyExpression); 368 }
150 369
151 this.patterns.push({ 370 if (this.pseudoHasPatterns.length > 0 || this.propSelPatterns.length > 0)
152 text: pattern.text,
153 regexp: new RegExp(regexpString, "i"),
154 prefix: pattern.selector.substr(0, match.index),
155 suffix: pattern.selector.substr(match.index + match[0].length)
156 });
157 }
158
159 if (this.patterns.length > 0)
160 { 371 {
161 var document = this.window.document; 372 var document = this.window.document;
162 this.addSelectors(document.styleSheets); 373 this.addSelectors(document.styleSheets);
374 this.hideElements(document.styleSheets);
163 document.addEventListener("load", this.onLoad.bind(this), true); 375 document.addEventListener("load", this.onLoad.bind(this), true);
164 } 376 }
165 }.bind(this)); 377 }.bind(this));
166 } 378 }
167 }; 379 };
OLDNEW
« no previous file with comments | « no previous file | lib/filterClasses.js » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld