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: Updated patch following feedback. Created March 29, 2017, 2 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
3 3
4 var propertySelectorRegExp = /\[\-abp\-properties=(["'])([^"']+)\1\]/; 4 var propertySelectorRegExp = /\[\-abp\-properties=(["'])([^"']+)\1\]/;
5 var pseudoClassHasSelectorRegExp = /:has\((.*)\)/;
6
7 // polyfill as PhantomJS doesn't have matches().
8 // Until https://issues.adblockplus.org/ticket/4796
9 if (!Element.prototype.matches) {
10 Element.prototype.matches =
11 Element.prototype.webkitMatchesSelector
12 }
13
14 // return the index were the simple-selector ends
15 function findFirstSelector(selector)
16 {
17 for (var i = 0; i < selector.length; i++)
18 {
19 var chr = selector[i];
20 if (chr == ' ' || chr == '>' || chr == '+' || chr == '~')
21 return i;
22 }
23 return -1;
24 }
25
26 function extractFirstSelector(selector)
27 {
28 var sepIndex = findFirstSelector(selector);
29
30 if (sepIndex == -1)
31 return selector;
32
33 return selector.substr(0, sepIndex);
34 }
5 35
6 function splitSelector(selector) 36 function splitSelector(selector)
7 { 37 {
8 if (selector.indexOf(",") == -1) 38 if (selector.indexOf(",") == -1)
9 return [selector]; 39 return [selector];
10 40
11 var selectors = []; 41 var selectors = [];
12 var start = 0; 42 var start = 0;
13 var level = 0; 43 var level = 0;
14 var sep = ""; 44 var sep = "";
(...skipping 19 matching lines...) Expand all
34 selectors.push(selector.substring(start, i)); 64 selectors.push(selector.substring(start, i));
35 start = i + 1; 65 start = i + 1;
36 } 66 }
37 } 67 }
38 } 68 }
39 69
40 selectors.push(selector.substring(start)); 70 selectors.push(selector.substring(start));
41 return selectors; 71 return selectors;
42 } 72 }
43 73
44 function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc) 74 function selectChildren(e, selector)
75 {
76 var sel = selector;
77 // XXX we should have a more elegant way
78 // also startsWith isn't available in PhantomJS.
79 var combinator = sel.substr(0, 1);
80 var subElements;
81 var nextEl = e;
82 sel = sel.substr(1).trim();
83 switch (combinator)
84 {
85 case ">":
86 subElements = e.querySelectorAll(sel);
87 break;
88
89 case "+":
90 do
91 {
92 nextEl = nextEl.nextSibling;
93 }
94 while (nextEl && nextEl.nodeType != 1);
95
96 var siblingSel = extractFirstSelector(sel);
97 var idx = findFirstSelector(sel);
98 var childSel = idx != -1 ? sel.substr(idx + 1).trim() : "";
99
100 if (nextEl && nextEl.matches(siblingSel))
101 {
102 if (childSel != "")
103 subElements = selectChildren(nextEl, childSel);
104 else
105 subElements = [ nextEl ];
106 }
107 break;
108
109 case "~":
110 do
111 {
112 nextEl = nextEl.nextSibling;
113 if (nextEl && nextEl.nodeType == 1 && nextEl.matches(sel))
114 {
115 subElements = nextEl.querySelectorAll(sel);
116 break;
117 }
118 }
119 while (nextEl);
120
121 break;
122 }
123 return subElements;
124 }
125
126 function parsePattern(pattern)
127 {
128 // we should catch the :has() pseudo class first.
129 var match = pseudoClassHasSelectorRegExp.exec(pattern.selector);
130 if (match)
131 {
132 return {
133 type: "has",
134 text: pattern.text,
135 elementMatcher: new PseudoHasMatcher(match[1]),
136 prefix: pattern.selector.substr(0, match.index).trim(),
137 suffix: pattern.selector.substr(match.index + match[0].length).trim()
138 };
139 }
140
141 match = propertySelectorRegExp.exec(pattern.selector);
142 if (match)
143 {
144 var regexpString;
145 var propertyExpression = match[2];
146 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" &&
147 propertyExpression[propertyExpression.length - 1] == "/")
148 regexpString = propertyExpression.slice(1, -1)
149 .replace("\\x7B ", "{").replace("\\x7D ", "}");
150 else
151 regexpString = filterToRegExp(propertyExpression);
152 return {
153 type: "props",
154 text: pattern.text,
155 regexp: new RegExp(regexpString, "i"),
156 prefix: pattern.selector.substr(0, match.index),
157 suffix: pattern.selector.substr(match.index + match[0].length)
158 };
159 }
160 }
161
162 function matchStyleProps(style, rule, pattern, selectors, filters)
163 {
164 if (pattern.regexp.test(style))
165 {
166 var subSelectors = splitSelector(rule.selectorText);
167 for (var i = 0; i < subSelectors.length; i++)
168 {
169 var subSelector = subSelectors[i];
170 selectors.push(pattern.prefix + subSelector + pattern.suffix);
171 filters.push(pattern.text);
172 }
173 }
174 }
175
176 function findPropsSelectors(stylesheet, patterns, selectors, filters)
177 {
178 var rules = stylesheet.cssRules;
179 if (!rules)
180 return;
181
182 for (var i = 0; i < rules.length; i++)
183 {
184 var rule = rules[i];
185 if (rule.type != rule.STYLE_RULE)
186 continue;
187
188 var style = stringifyStyle(rule.style);
189 for (var j = 0; j < patterns.length; j++)
190 {
191 matchStyleProps(style, rule, patterns[j], selectors, filters);
192 }
193 }
194 }
195
196 function pseudoClassHasMatch(pattern, node, stylesheets, elements, filters)
197 {
198 var haveEl = pattern.prefix ? node.querySelectorAll(pattern.prefix) : [ node ] ;
199 for (var j = 0; j < haveEl.length; j++)
200 {
201 var matched = pattern.elementMatcher.match(haveEl[j], stylesheets, !pattern. suffix);
202 if (matched.length == 0)
203 continue;
204
205 if (pattern.suffix)
206 {
207 matched.forEach(function(e)
208 {
209 var subElements = selectChildren(e, pattern.suffix);
210 if (subElements)
211 {
212 for (var k = 0; k < subElements.length; k++)
213 {
214 elements.push(subElements[k]);
215 filters.push(pattern.text);
216 }
217 }
218 });
219 }
220 else
221 {
222 elements.push(haveEl[j]);
223 filters.push(pattern.text);
224 }
225 }
226 }
227
228 function stringifyStyle(style)
229 {
230 var styles = [];
231 for (var i = 0; i < style.length; i++)
232 {
233 var property = style.item(i);
234 var value = style.getPropertyValue(property);
235 var priority = style.getPropertyPriority(property);
236 styles.push(property + ": " + value + (priority ? " !" + priority : "") + "; ");
237 }
238 styles.sort();
239 return styles.join(" ");
240 }
241
242 // matcher for the pseudo CSS4 class :has
243 // For those browser that don't have it yet.
244 function PseudoHasMatcher(selector)
245 {
246 this.hasSelector = selector;
247 this.parsed = parsePattern({ selector: this.hasSelector });
248 }
249
250 PseudoHasMatcher.prototype = {
251 match: function(elem, stylesheets, firstOnly)
252 {
253 var matches = [];
254 var selectors = [];
255
256 if (this.parsed)
257 {
258 var filters = []; // don't need this
259 if (this.parsed.type == "has")
260 {
261 pseudoClassHasMatch(this.parsed, elem, stylesheets, matches, filters)
262 return matches;
263 }
264 if (this.parsed.type == "props")
265 {
266 for (var i = 0; i < stylesheets.length; i++)
267 findPropsSelectors(stylesheets[i], [this.parsed], selectors, filters);
268 }
269 }
270 else
271 {
272 selectors = [this.hasSelector];
273 }
274
275 // look up for all elements that match the :has().
276 for (var k = 0; k < selectors.length; k++)
277 {
278 try
279 {
280 var hasElem = elem.querySelector(selectors[k]);
281 if (hasElem)
282 {
283 matches.push(hasElem);
284 if (firstOnly)
285 break;
286 }
287 }
288 catch(e)
289 {
290 console.log("Exception with querySelector()", selectors[k]);
291 }
292 }
293 return matches;
294 }
295 };
296
297 function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc, hideElement sFunc)
45 { 298 {
46 this.window = window; 299 this.window = window;
47 this.getFiltersFunc = getFiltersFunc; 300 this.getFiltersFunc = getFiltersFunc;
48 this.addSelectorsFunc = addSelectorsFunc; 301 this.addSelectorsFunc = addSelectorsFunc;
302 this.hideElementsFunc = hideElementsFunc;
49 } 303 }
50 304
51 ElemHideEmulation.prototype = { 305 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 306
66 isSameOrigin: function(stylesheet) 307 isSameOrigin: function(stylesheet)
67 { 308 {
68 try 309 try
69 { 310 {
70 return new URL(stylesheet.href).origin == this.window.location.origin; 311 return new URL(stylesheet.href).origin == this.window.location.origin;
71 } 312 }
72 catch (e) 313 catch (e)
73 { 314 {
74 // Invalid URL, assume that it is first-party. 315 // Invalid URL, assume that it is first-party.
75 return true; 316 return true;
76 } 317 }
77 }, 318 },
78 319
79 findSelectors: function(stylesheet, selectors, filters) 320 findPseudoClassHasElements: function(node, stylesheets, elements, filters)
80 { 321 {
81 // Explicitly ignore third-party stylesheets to ensure consistent behavior 322 for (var i = 0; i < this.pseudoHasPatterns.length; i++)
82 // between Firefox and Chrome. 323 {
83 if (!this.isSameOrigin(stylesheet)) 324 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 } 325 }
112 }, 326 },
113 327
114 addSelectors: function(stylesheets) 328 addSelectors: function(stylesheets)
115 { 329 {
116 var selectors = []; 330 var selectors = [];
117 var filters = []; 331 var filters = [];
118 for (var i = 0; i < stylesheets.length; i++) 332 for (var i = 0; i < stylesheets.length; i++)
119 this.findSelectors(stylesheets[i], selectors, filters); 333 {
334 // Explicitly ignore third-party stylesheets to ensure consistent behavior
335 // between Firefox and Chrome.
336 if (!this.isSameOrigin(stylesheets[i]))
337 continue;
338 findPropsSelectors(stylesheets[i], this.propSelPatterns, selectors, filter s);
339 }
120 this.addSelectorsFunc(selectors, filters); 340 this.addSelectorsFunc(selectors, filters);
121 }, 341 },
122 342
343 hideElements: function(stylesheets)
344 {
345 var elements = [];
346 var filters = [];
347 this.findPseudoClassHasElements(document, stylesheets, elements, filters);
348 this.hideElementsFunc(elements, filters);
349 },
350
123 onLoad: function(event) 351 onLoad: function(event)
124 { 352 {
125 var stylesheet = event.target.sheet; 353 var stylesheet = event.target.sheet;
126 if (stylesheet) 354 if (stylesheet)
127 this.addSelectors([stylesheet]); 355 this.addSelectors([stylesheet]);
356 this.hideElements([stylesheet]);
128 }, 357 },
129 358
130 apply: function() 359 apply: function()
131 { 360 {
132 this.getFiltersFunc(function(patterns) 361 this.getFiltersFunc(function(patterns)
133 { 362 {
134 this.patterns = []; 363 this.propSelPatterns = [];
364 this.pseudoHasPatterns = [];
135 for (var i = 0; i < patterns.length; i++) 365 for (var i = 0; i < patterns.length; i++)
136 { 366 {
137 var pattern = patterns[i]; 367 var pattern = patterns[i];
138 var match = propertySelectorRegExp.exec(pattern.selector); 368 var parsed = parsePattern(pattern);
139 if (!match) 369 if (parsed == undefined)
140 continue; 370 continue;
141 371 if (parsed.type == "props")
142 var propertyExpression = match[2]; 372 {
143 var regexpString; 373 this.propSelPatterns.push(parsed);
144 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" && 374 }
145 propertyExpression[propertyExpression.length - 1] == "/") 375 else if (parsed.type == "has")
146 regexpString = propertyExpression.slice(1, -1) 376 {
147 .replace("\\x7B ", "{").replace("\\x7D ", "}"); 377 this.pseudoHasPatterns.push(parsed);
148 else 378 }
149 regexpString = filterToRegExp(propertyExpression); 379 }
150 380
151 this.patterns.push({ 381 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 { 382 {
161 var document = this.window.document; 383 var document = this.window.document;
162 this.addSelectors(document.styleSheets); 384 this.addSelectors(document.styleSheets);
385 this.hideElements(document.styleSheets);
163 document.addEventListener("load", this.onLoad.bind(this), true); 386 document.addEventListener("load", this.onLoad.bind(this), true);
164 } 387 }
165 }.bind(this)); 388 }.bind(this));
166 } 389 }
167 }; 390 };
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