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 the filter syntax Created April 10, 2017, 10:03 a.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') | lib/filterClasses.js » ('J')
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 abpSelectorRegExp = /\[-abp-selector=(["'])(.+)\1\]/;
6 var pseudoClassHasSelectorRegExp = /:has\((.*)\)/;
7 var pseudoClassPropsSelectorRegExp = /:-abp-properties\((["'])([^"']+)\1\)/;
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);
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)
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});
89 }
90
91 return parsePattern(pattern);
92 }
93
94 function parsePattern(pattern)
95 {
96 var selector = pattern.selector;
97
98 var match = pseudoClassHasSelectorRegExp.exec(selector);
Felix Dahlke 2017/04/10 10:55:12 How I understand the code, :has and :-abp-properti
hub 2017/04/10 12:29:27 My question is do we still support [-abp-propertie
Felix Dahlke 2017/04/10 12:35:41 Lonely :-abp-properties (pseudo class syntax!) sho
hub 2017/04/10 12:41:02 Ah right. The pseudo class syntax. Good catch.
99 if (match)
100 return {
101 type: "has",
102 text: pattern.text,
103 elementMatcher: new PseudoHasMatcher(match[1]),
104 prefix: selector.substr(0, match.index).trim(),
105 suffix: selector.substr(match.index + match[0].length).trim()
106 };
107
108 match = pseudoClassPropsSelectorRegExp.exec(selector);
109 if (!match)
110 match = propertySelectorRegExp.exec(selector);
111 if (match)
112 {
113 var regexpString;
114 var propertyExpression = match[2];
115 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" &&
116 propertyExpression[propertyExpression.length - 1] == "/")
117 regexpString = propertyExpression.slice(1, -1)
118 .replace("\\x7B ", "{").replace("\\x7D ", "}");
119 else
120 regexpString = filterToRegExp(propertyExpression);
121 return {
122 type: "props",
123 text: pattern.text,
124 regexp: new RegExp(regexpString, "i"),
125 prefix: selector.substr(0, match.index),
126 suffix: selector.substr(match.index + match[0].length)
127 };
128 }
129 }
130
131 function matchStyleProps(style, rule, pattern, selectors, filters)
132 {
133 if (pattern.regexp.test(style))
134 {
135 var subSelectors = splitSelector(rule.selectorText);
136 for (var i = 0; i < subSelectors.length; i++)
137 {
138 var subSelector = subSelectors[i];
139 selectors.push(pattern.prefix + subSelector + pattern.suffix);
140 filters.push(pattern.text);
141 }
142 }
143 }
144
145 function findPropsSelectors(stylesheet, patterns, selectors, filters)
146 {
147 var rules = stylesheet.cssRules;
148 if (!rules)
149 return;
150
151 for (var i = 0; i < rules.length; i++)
152 {
153 var rule = rules[i];
154 if (rule.type != rule.STYLE_RULE)
155 continue;
156
157 var style = stringifyStyle(rule.style);
158 for (var j = 0; j < patterns.length; j++)
159 {
160 matchStyleProps(style, rule, patterns[j], selectors, filters);
161 }
162 }
163 }
164
165 /**
166 * Match the selector @pattern containing :has() starting from @node.
167 * @param {string} pattern - the pattern to match
168 * @param {object} node - the top level node.
169 */
170 function pseudoClassHasMatch(pattern, node, stylesheets, elements, filters)
171 {
172 // select element for the prefix pattern. Or just use node.
173 var haveEl = pattern.prefix ? node.querySelectorAll(pattern.prefix) : [ node ] ;
174 for (var j = 0; j < haveEl.length; j++)
175 {
176 var matched = pattern.elementMatcher.match(haveEl[j], stylesheets);
177 if (!matched)
178 continue;
179
180 if (pattern.suffix)
181 {
182 var subElements = selectChildren(haveEl[j], pattern.suffix);
183 if (subElements)
184 {
185 for (var k = 0; k < subElements.length; k++)
186 {
187 elements.push(subElements[k]);
188 filters.push(pattern.text);
189 }
190 }
191 }
192 else
193 {
194 elements.push(haveEl[j]);
195 filters.push(pattern.text);
196 }
197 }
198 }
199
200 function stringifyStyle(style)
201 {
202 var styles = [];
203 for (var i = 0; i < style.length; i++)
204 {
205 var property = style.item(i);
206 var value = style.getPropertyValue(property);
207 var priority = style.getPropertyPriority(property);
208 styles.push(property + ": " + value + (priority ? " !" + priority : "") + "; ");
209 }
210 styles.sort();
211 return styles.join(" ");
212 }
213
214 /** matcher for the pseudo CSS4 class :has()
215 * For those browser that don't have it yet.
216 */
217 function PseudoHasMatcher(selector)
218 {
219 this.hasSelector = selector;
220 this.parsed = parsePattern({selector: this.hasSelector});
221 }
222
223 PseudoHasMatcher.prototype = {
224 match: function(elem, stylesheets)
225 {
226 var selectors = [];
227
228 if (this.parsed)
229 {
230 var filters = []; // don't need this, we have a partial filter.
231 if (this.parsed.type == "has")
232 {
233 var child = elem.firstChild;
234 while (child)
235 {
236 if (child.nodeType === Node.ELEMENT_NODE)
237 {
238 var matches = [];
239 pseudoClassHasMatch(this.parsed, child, stylesheets, matches, filter s);
240 if (matches.length > 0)
241 return true;
242 }
243 child = child.nextSibling;
244 }
245 return false;
246 }
247 if (this.parsed.type == "props")
248 {
249 for (var i = 0; i < stylesheets.length; i++)
250 findPropsSelectors(stylesheets[i], [this.parsed], selectors, filters);
251 }
252 }
253 else
254 {
255 selectors = [this.hasSelector];
256 }
257
258 var matched = false;
259 // look up for all elements that match the :has().
260 for (var k = 0; k < selectors.length; k++)
261 {
262 try
263 {
264 matched = matchChildren(elem, selectors[k]);
265 if (matched)
266 break;
267 }
268 catch (e)
269 {
270 console.error("Exception with querySelector()", selectors[k], e);
271 }
272 }
273 return matched;
274 }
275 };
276
277 function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc, hideElement sFunc)
45 { 278 {
46 this.window = window; 279 this.window = window;
47 this.getFiltersFunc = getFiltersFunc; 280 this.getFiltersFunc = getFiltersFunc;
48 this.addSelectorsFunc = addSelectorsFunc; 281 this.addSelectorsFunc = addSelectorsFunc;
282 this.hideElementsFunc = hideElementsFunc;
49 } 283 }
50 284
51 ElemHideEmulation.prototype = { 285 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 286
66 isSameOrigin: function(stylesheet) 287 isSameOrigin: function(stylesheet)
67 { 288 {
68 try 289 try
69 { 290 {
70 return new URL(stylesheet.href).origin == this.window.location.origin; 291 return new URL(stylesheet.href).origin == this.window.location.origin;
71 } 292 }
72 catch (e) 293 catch (e)
73 { 294 {
74 // Invalid URL, assume that it is first-party. 295 // Invalid URL, assume that it is first-party.
75 return true; 296 return true;
76 } 297 }
77 }, 298 },
78 299
79 findSelectors: function(stylesheet, selectors, filters) 300 findPseudoClassHasElements: function(node, stylesheets, elements, filters)
80 { 301 {
81 // Explicitly ignore third-party stylesheets to ensure consistent behavior 302 for (var i = 0; i < this.pseudoHasPatterns.length; i++)
82 // between Firefox and Chrome. 303 {
83 if (!this.isSameOrigin(stylesheet)) 304 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 } 305 }
112 }, 306 },
113 307
114 addSelectors: function(stylesheets) 308 addSelectors: function(stylesheets)
115 { 309 {
116 var selectors = []; 310 var selectors = [];
117 var filters = []; 311 var filters = [];
118 for (var i = 0; i < stylesheets.length; i++) 312 for (var i = 0; i < stylesheets.length; i++)
119 this.findSelectors(stylesheets[i], selectors, filters); 313 {
314 // Explicitly ignore third-party stylesheets to ensure consistent behavior
315 // between Firefox and Chrome.
316 if (!this.isSameOrigin(stylesheets[i]))
317 continue;
318 findPropsSelectors(stylesheets[i], this.propSelPatterns, selectors, filter s);
319 }
120 this.addSelectorsFunc(selectors, filters); 320 this.addSelectorsFunc(selectors, filters);
121 }, 321 },
122 322
323 hideElements: function(stylesheets)
324 {
325 var elements = [];
326 var filters = [];
327 this.findPseudoClassHasElements(document, stylesheets, elements, filters);
328 this.hideElementsFunc(elements, filters);
329 },
330
123 onLoad: function(event) 331 onLoad: function(event)
124 { 332 {
125 var stylesheet = event.target.sheet; 333 var stylesheet = event.target.sheet;
126 if (stylesheet) 334 if (stylesheet)
127 this.addSelectors([stylesheet]); 335 this.addSelectors([stylesheet]);
336 this.hideElements([stylesheet]);
128 }, 337 },
129 338
130 apply: function() 339 apply: function()
131 { 340 {
132 this.getFiltersFunc(function(patterns) 341 this.getFiltersFunc(function(patterns)
133 { 342 {
134 this.patterns = []; 343 this.propSelPatterns = [];
344 this.pseudoHasPatterns = [];
135 for (var i = 0; i < patterns.length; i++) 345 for (var i = 0; i < patterns.length; i++)
136 { 346 {
137 var pattern = patterns[i]; 347 var pattern = patterns[i];
138 var match = propertySelectorRegExp.exec(pattern.selector); 348 var parsed = unwrapPattern(pattern);
139 if (!match) 349 if (parsed == undefined)
140 continue; 350 continue;
141 351 if (parsed.type == "props")
142 var propertyExpression = match[2]; 352 {
143 var regexpString; 353 this.propSelPatterns.push(parsed);
144 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" && 354 }
145 propertyExpression[propertyExpression.length - 1] == "/") 355 else if (parsed.type == "has")
146 regexpString = propertyExpression.slice(1, -1) 356 {
147 .replace("\\x7B ", "{").replace("\\x7D ", "}"); 357 this.pseudoHasPatterns.push(parsed);
148 else 358 }
149 regexpString = filterToRegExp(propertyExpression); 359 }
150 360
151 this.patterns.push({ 361 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 { 362 {
161 var document = this.window.document; 363 var document = this.window.document;
162 this.addSelectors(document.styleSheets); 364 this.addSelectors(document.styleSheets);
365 this.hideElements(document.styleSheets);
163 document.addEventListener("load", this.onLoad.bind(this), true); 366 document.addEventListener("load", this.onLoad.bind(this), true);
164 } 367 }
165 }.bind(this)); 368 }.bind(this));
166 } 369 }
167 }; 370 };
OLDNEW
« no previous file with comments | « no previous file | lib/filterClasses.js » ('j') | lib/filterClasses.js » ('J')

Powered by Google App Engine
This is Rietveld