Left: | ||
Right: |
OLD | NEW |
---|---|
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 Loading... | |
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 }; |
OLD | NEW |