16cf2ff3f2878130a8237399dab55d5a034a3b56
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Models / DOMNodeStyles.js
1 /*
2  * Copyright (C) 2013 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WI.DOMNodeStyles = class DOMNodeStyles extends WI.Object
27 {
28     constructor(node)
29     {
30         super();
31
32         console.assert(node);
33         this._node = node || null;
34
35         this._rulesMap = {};
36         this._styleDeclarationsMap = {};
37
38         this._matchedRules = [];
39         this._inheritedRules = [];
40         this._pseudoElements = {};
41         this._inlineStyle = null;
42         this._attributesStyle = null;
43         this._computedStyle = null;
44         this._orderedStyles = [];
45
46         this._propertyNameToEffectivePropertyMap = {};
47
48         this._pendingRefreshTask = null;
49         this.refresh();
50     }
51
52     // Public
53
54     get node()
55     {
56         return this._node;
57     }
58
59     get needsRefresh()
60     {
61         return this._pendingRefreshTask || this._needsRefresh;
62     }
63
64     refreshIfNeeded()
65     {
66         if (this._pendingRefreshTask)
67             return this._pendingRefreshTask;
68         if (!this._needsRefresh)
69             return Promise.resolve(this);
70         return this.refresh();
71     }
72
73     refresh()
74     {
75         if (this._pendingRefreshTask)
76             return this._pendingRefreshTask;
77
78         this._needsRefresh = false;
79
80         let fetchedMatchedStylesPromise = new WI.WrappedPromise;
81         let fetchedInlineStylesPromise = new WI.WrappedPromise;
82         let fetchedComputedStylesPromise = new WI.WrappedPromise;
83
84         // Ensure we resolve these promises even in the case of an error.
85         function wrap(func, promise) {
86             return (...args) => {
87                 try {
88                     func.apply(this, args);
89                 } catch (e) {
90                     console.error(e);
91                     promise.resolve();
92                 }
93             };
94         }
95
96         function parseRuleMatchArrayPayload(matchArray, node, inherited)
97         {
98             var result = [];
99
100             // Iterate in reverse order to match the cascade order.
101             var ruleOccurrences = {};
102             for (var i = matchArray.length - 1; i >= 0; --i) {
103                 var rule = this._parseRulePayload(matchArray[i].rule, matchArray[i].matchingSelectors, node, inherited, ruleOccurrences);
104                 if (!rule)
105                     continue;
106                 result.push(rule);
107             }
108
109             return result;
110         }
111
112         function fetchedMatchedStyles(error, matchedRulesPayload, pseudoElementRulesPayload, inheritedRulesPayload)
113         {
114             matchedRulesPayload = matchedRulesPayload || [];
115             pseudoElementRulesPayload = pseudoElementRulesPayload || [];
116             inheritedRulesPayload = inheritedRulesPayload || [];
117
118             // Move the current maps to previous.
119             this._previousRulesMap = this._rulesMap;
120             this._previousStyleDeclarationsMap = this._styleDeclarationsMap;
121
122             // Clear the current maps.
123             this._rulesMap = {};
124             this._styleDeclarationsMap = {};
125
126             this._matchedRules = parseRuleMatchArrayPayload.call(this, matchedRulesPayload, this._node);
127
128             this._pseudoElements = {};
129             for (var pseudoElementRulePayload of pseudoElementRulesPayload) {
130                 var pseudoElementRules = parseRuleMatchArrayPayload.call(this, pseudoElementRulePayload.matches, this._node);
131                 this._pseudoElements[pseudoElementRulePayload.pseudoId] = {matchedRules: pseudoElementRules};
132             }
133
134             this._inheritedRules = [];
135
136             var i = 0;
137             var currentNode = this._node.parentNode;
138             while (currentNode && i < inheritedRulesPayload.length) {
139                 var inheritedRulePayload = inheritedRulesPayload[i];
140
141                 var inheritedRuleInfo = {node: currentNode};
142                 inheritedRuleInfo.inlineStyle = inheritedRulePayload.inlineStyle ? this._parseStyleDeclarationPayload(inheritedRulePayload.inlineStyle, currentNode, true, WI.CSSStyleDeclaration.Type.Inline) : null;
143                 inheritedRuleInfo.matchedRules = inheritedRulePayload.matchedCSSRules ? parseRuleMatchArrayPayload.call(this, inheritedRulePayload.matchedCSSRules, currentNode, true) : [];
144
145                 if (inheritedRuleInfo.inlineStyle || inheritedRuleInfo.matchedRules.length)
146                     this._inheritedRules.push(inheritedRuleInfo);
147
148                 currentNode = currentNode.parentNode;
149                 ++i;
150             }
151
152             fetchedMatchedStylesPromise.resolve();
153         }
154
155         function fetchedInlineStyles(error, inlineStylePayload, attributesStylePayload)
156         {
157             this._inlineStyle = inlineStylePayload ? this._parseStyleDeclarationPayload(inlineStylePayload, this._node, false, WI.CSSStyleDeclaration.Type.Inline) : null;
158             this._attributesStyle = attributesStylePayload ? this._parseStyleDeclarationPayload(attributesStylePayload, this._node, false, WI.CSSStyleDeclaration.Type.Attribute) : null;
159
160             this._updateStyleCascade();
161
162             fetchedInlineStylesPromise.resolve();
163         }
164
165         function fetchedComputedStyle(error, computedPropertiesPayload)
166         {
167             var properties = [];
168             for (var i = 0; computedPropertiesPayload && i < computedPropertiesPayload.length; ++i) {
169                 var propertyPayload = computedPropertiesPayload[i];
170
171                 var canonicalName = WI.cssManager.canonicalNameForPropertyName(propertyPayload.name);
172                 propertyPayload.implicit = !this._propertyNameToEffectivePropertyMap[canonicalName];
173
174                 var property = this._parseStylePropertyPayload(propertyPayload, NaN, this._computedStyle);
175                 if (!property.implicit)
176                     property.implicit = !this._isPropertyFoundInMatchingRules(property.name);
177                 properties.push(property);
178             }
179
180             if (this._computedStyle)
181                 this._computedStyle.update(null, properties);
182             else
183                 this._computedStyle = new WI.CSSStyleDeclaration(this, null, null, WI.CSSStyleDeclaration.Type.Computed, this._node, false, null, properties);
184
185             let significantChange = false;
186             for (let key in this._styleDeclarationsMap) {
187                 // Check if the same key exists in the previous map and has the same style objects.
188                 if (key in this._previousStyleDeclarationsMap) {
189                     if (Array.shallowEqual(this._styleDeclarationsMap[key], this._previousStyleDeclarationsMap[key]))
190                         continue;
191
192                     // Some styles have selectors such that they will match with the DOM node twice (for example "::before, ::after").
193                     // In this case a second style for a second matching may be generated and added which will cause the shallowEqual
194                     // to not return true, so in this case we just want to ensure that all the current styles existed previously.
195                     let styleFound = false;
196                     for (let style of this._styleDeclarationsMap[key]) {
197                         if (this._previousStyleDeclarationsMap[key].includes(style)) {
198                             styleFound = true;
199                             break;
200                         }
201                     }
202
203                     if (styleFound)
204                         continue;
205                 }
206
207                 if (!this._includeUserAgentRulesOnNextRefresh) {
208                     // We can assume all the styles with the same key are from the same stylesheet and rule, so we only check the first.
209                     let firstStyle = this._styleDeclarationsMap[key][0];
210                     if (firstStyle && firstStyle.ownerRule && firstStyle.ownerRule.type === WI.CSSStyleSheet.Type.UserAgent) {
211                         // User Agent styles get different identifiers after some edits. This would cause us to fire a significant refreshed
212                         // event more than it is helpful. And since the user agent stylesheet is static it shouldn't match differently
213                         // between refreshes for the same node. This issue is tracked by: https://webkit.org/b/110055
214                         continue;
215                     }
216                 }
217
218                 // This key is new or has different style objects than before. This is a significant change.
219                 significantChange = true;
220                 break;
221             }
222
223             if (!significantChange) {
224                 for (var key in this._previousStyleDeclarationsMap) {
225                     // Check if the same key exists in current map. If it does exist it was already checked for equality above.
226                     if (key in this._styleDeclarationsMap)
227                         continue;
228
229                     if (!this._includeUserAgentRulesOnNextRefresh) {
230                         // See above for why we skip user agent style rules.
231                         var firstStyle = this._previousStyleDeclarationsMap[key][0];
232                         if (firstStyle && firstStyle.ownerRule && firstStyle.ownerRule.type === WI.CSSStyleSheet.Type.UserAgent)
233                             continue;
234                     }
235
236                     // This key no longer exists. This is a significant change.
237                     significantChange = true;
238                     break;
239                 }
240             }
241
242             delete this._includeUserAgentRulesOnNextRefresh;
243
244             // Delete the previous maps now that any reused rules and style have been moved over.
245             delete this._previousRulesMap;
246             delete this._previousStyleDeclarationsMap;
247
248             this.dispatchEventToListeners(WI.DOMNodeStyles.Event.Refreshed, {significantChange});
249
250             fetchedComputedStylesPromise.resolve();
251         }
252
253         // FIXME: Convert to pushing StyleSheet information to the frontend. <rdar://problem/13213680>
254         WI.cssManager.fetchStyleSheetsIfNeeded();
255
256         CSSAgent.getMatchedStylesForNode.invoke({nodeId: this._node.id, includePseudo: true, includeInherited: true}, wrap.call(this, fetchedMatchedStyles, fetchedMatchedStylesPromise));
257         CSSAgent.getInlineStylesForNode.invoke({nodeId: this._node.id}, wrap.call(this, fetchedInlineStyles, fetchedInlineStylesPromise));
258         CSSAgent.getComputedStyleForNode.invoke({nodeId: this._node.id}, wrap.call(this, fetchedComputedStyle, fetchedComputedStylesPromise));
259
260         this._pendingRefreshTask = Promise.all([fetchedMatchedStylesPromise.promise, fetchedInlineStylesPromise.promise, fetchedComputedStylesPromise.promise])
261         .then(() => {
262             this._pendingRefreshTask = null;
263             return this;
264         });
265
266         return this._pendingRefreshTask;
267     }
268
269     addRule(selector, text, styleSheetId)
270     {
271         selector = selector || this._node.appropriateSelectorFor(true);
272
273         function completed()
274         {
275             DOMAgent.markUndoableState();
276             this.refresh();
277         }
278
279         function styleChanged(error, stylePayload)
280         {
281             if (error)
282                 return;
283
284             completed.call(this);
285         }
286
287         function addedRule(error, rulePayload)
288         {
289             if (error)
290                 return;
291
292             if (!text || !text.length) {
293                 completed.call(this);
294                 return;
295             }
296
297             CSSAgent.setStyleText(rulePayload.style.styleId, text, styleChanged.bind(this));
298         }
299
300         // COMPATIBILITY (iOS 9): Before CSS.createStyleSheet, CSS.addRule could be called with a contextNode.
301         if (!CSSAgent.createStyleSheet) {
302             CSSAgent.addRule.invoke({contextNodeId: this._node.id, selector}, addedRule.bind(this));
303             return;
304         }
305
306         function inspectorStyleSheetAvailable(styleSheet)
307         {
308             if (!styleSheet)
309                 return;
310
311             CSSAgent.addRule(styleSheet.id, selector, addedRule.bind(this));
312         }
313
314         if (styleSheetId)
315             inspectorStyleSheetAvailable.call(this, WI.cssManager.styleSheetForIdentifier(styleSheetId));
316         else
317             WI.cssManager.preferredInspectorStyleSheetForFrame(this._node.frame, inspectorStyleSheetAvailable.bind(this));
318     }
319
320     rulesForSelector(selector)
321     {
322         selector = selector || this._node.appropriateSelectorFor(true);
323
324         function ruleHasSelector(rule) {
325             return !rule.mediaList.length && rule.selectorText === selector;
326         }
327
328         let rules = this._matchedRules.filter(ruleHasSelector);
329
330         for (let id in this._pseudoElements)
331             rules = rules.concat(this._pseudoElements[id].matchedRules.filter(ruleHasSelector));
332
333         return rules;
334     }
335
336     get matchedRules()
337     {
338         return this._matchedRules;
339     }
340
341     get inheritedRules()
342     {
343         return this._inheritedRules;
344     }
345
346     get inlineStyle()
347     {
348         return this._inlineStyle;
349     }
350
351     get attributesStyle()
352     {
353         return this._attributesStyle;
354     }
355
356     get pseudoElements()
357     {
358         return this._pseudoElements;
359     }
360
361     get computedStyle()
362     {
363         return this._computedStyle;
364     }
365
366     get orderedStyles()
367     {
368         return this._orderedStyles;
369     }
370
371     get uniqueOrderedStyles()
372     {
373         let uniqueStyles = [];
374
375         for (let style of this._orderedStyles) {
376             let rule = style.ownerRule;
377             if (!rule) {
378                 uniqueStyles.push(style);
379                 continue;
380             }
381
382             let found = false;
383             for (let existingStyle of uniqueStyles) {
384                 if (rule.isEqualTo(existingStyle.ownerRule)) {
385                     found = true;
386                     break;
387                 }
388             }
389             if (!found)
390                 uniqueStyles.push(style);
391         }
392
393         return uniqueStyles;
394     }
395
396     effectivePropertyForName(name)
397     {
398         let property = this._propertyNameToEffectivePropertyMap[name];
399         if (property)
400             return property;
401
402         let canonicalName = WI.cssManager.canonicalNameForPropertyName(name);
403         return this._propertyNameToEffectivePropertyMap[canonicalName] || null;
404     }
405
406     // Protected
407
408     mediaQueryResultDidChange()
409     {
410         this._markAsNeedsRefresh();
411     }
412
413     pseudoClassesDidChange(node)
414     {
415         this._includeUserAgentRulesOnNextRefresh = true;
416         this._markAsNeedsRefresh();
417     }
418
419     attributeDidChange(node, attributeName)
420     {
421         this._markAsNeedsRefresh();
422     }
423
424     changeRuleSelector(rule, selector)
425     {
426         selector = selector || "";
427         let result = new WI.WrappedPromise;
428
429         function ruleSelectorChanged(error, rulePayload)
430         {
431             if (error) {
432                 result.reject(error);
433                 return;
434             }
435
436             DOMAgent.markUndoableState();
437
438             // Do a full refresh incase the rule no longer matches the node or the
439             // matched selector indices changed.
440             this.refresh().then(() => {
441                 result.resolve(rulePayload);
442             });
443         }
444
445         this._needsRefresh = true;
446         this._ignoreNextContentDidChangeForStyleSheet = rule.ownerStyleSheet;
447
448         CSSAgent.setRuleSelector(rule.id, selector, ruleSelectorChanged.bind(this));
449         return result.promise;
450     }
451
452     changeStyleText(style, text)
453     {
454         if (!style.ownerStyleSheet || !style.styleSheetTextRange)
455             return;
456
457         text = text || "";
458
459         function styleChanged(error, stylePayload)
460         {
461             if (error)
462                 return;
463             this.refresh();
464         }
465
466         CSSAgent.setStyleText(style.id, text, styleChanged.bind(this));
467     }
468
469     // Private
470
471     _createSourceCodeLocation(sourceURL, sourceLine, sourceColumn)
472     {
473         if (!sourceURL)
474             return null;
475
476         var sourceCode;
477
478         // Try to use the node to find the frame which has the correct resource first.
479         if (this._node.ownerDocument) {
480             var mainResource = WI.networkManager.resourceForURL(this._node.ownerDocument.documentURL);
481             if (mainResource) {
482                 var parentFrame = mainResource.parentFrame;
483                 sourceCode = parentFrame.resourceForURL(sourceURL);
484             }
485         }
486
487         // If that didn't find the resource, then search all frames.
488         if (!sourceCode)
489             sourceCode = WI.networkManager.resourceForURL(sourceURL);
490
491         if (!sourceCode)
492             return null;
493
494         return sourceCode.createSourceCodeLocation(sourceLine || 0, sourceColumn || 0);
495     }
496
497     _parseSourceRangePayload(payload)
498     {
499         if (!payload)
500             return null;
501
502         return new WI.TextRange(payload.startLine, payload.startColumn, payload.endLine, payload.endColumn);
503     }
504
505     _parseStylePropertyPayload(payload, index, styleDeclaration, styleText)
506     {
507         var text = payload.text || "";
508         var name = payload.name;
509         var value = payload.value || "";
510         var priority = payload.priority || "";
511
512         var enabled = true;
513         var overridden = false;
514         var implicit = payload.implicit || false;
515         var anonymous = false;
516         var valid = "parsedOk" in payload ? payload.parsedOk : true;
517
518         switch (payload.status || "style") {
519         case "active":
520             enabled = true;
521             break;
522         case "inactive":
523             overridden = true;
524             enabled = true;
525             break;
526         case "disabled":
527             enabled = false;
528             break;
529         case "style":
530             // FIXME: Is this still needed? This includes UserAgent styles and HTML attribute styles.
531             anonymous = true;
532             break;
533         }
534
535         var styleSheetTextRange = this._parseSourceRangePayload(payload.range);
536
537         if (styleDeclaration) {
538             // Use propertyForName when the index is NaN since propertyForName is fast in that case.
539             var property = isNaN(index) ? styleDeclaration.propertyForName(name, true) : styleDeclaration.enabledProperties[index];
540
541             // Reuse a property if the index and name matches. Otherwise it is a different property
542             // and should be created from scratch. This works in the simple cases where only existing
543             // properties change in place and no properties are inserted or deleted at the beginning.
544             // FIXME: This could be smarter by ignoring index and just go by name. However, that gets
545             // tricky for rules that have more than one property with the same name.
546             if (property && property.name === name && (property.index === index || (isNaN(property.index) && isNaN(index)))) {
547                 property.update(text, name, value, priority, enabled, overridden, implicit, anonymous, valid, styleSheetTextRange);
548                 return property;
549             }
550
551             // Reuse a pending property with the same name. These properties are pending being committed,
552             // so if we find a match that likely means it got committed and we should use it.
553             var pendingProperties = styleDeclaration.pendingProperties;
554             for (var i = 0; i < pendingProperties.length; ++i) {
555                 var pendingProperty = pendingProperties[i];
556                 if (pendingProperty.name === name && isNaN(pendingProperty.index)) {
557                     pendingProperty.index = index;
558                     pendingProperty.update(text, name, value, priority, enabled, overridden, implicit, anonymous, valid, styleSheetTextRange);
559                     return pendingProperty;
560                 }
561             }
562         }
563
564         return new WI.CSSProperty(index, text, name, value, priority, enabled, overridden, implicit, anonymous, valid, styleSheetTextRange);
565     }
566
567     _parseStyleDeclarationPayload(payload, node, inherited, type, rule, updateAllStyles)
568     {
569         if (!payload)
570             return null;
571
572         rule = rule || null;
573         inherited = inherited || false;
574
575         var id = payload.styleId;
576         var mapKey = id ? id.styleSheetId + ":" + id.ordinal : null;
577
578         if (type === WI.CSSStyleDeclaration.Type.Attribute)
579             mapKey = node.id + ":attribute";
580
581         var styleDeclaration = rule ? rule.style : null;
582         var styleDeclarations = [];
583
584         // Look for existing styles in the previous map if there is one, otherwise use the current map.
585         var previousStyleDeclarationsMap = this._previousStyleDeclarationsMap || this._styleDeclarationsMap;
586         if (mapKey && mapKey in previousStyleDeclarationsMap) {
587             styleDeclarations = previousStyleDeclarationsMap[mapKey];
588
589             // If we need to update all styles, then stop here and call _parseStyleDeclarationPayload for each style.
590             // We need to parse multiple times so we reuse the right properties from each style.
591             if (updateAllStyles && styleDeclarations.length) {
592                 for (var i = 0; i < styleDeclarations.length; ++i) {
593                     var styleDeclaration = styleDeclarations[i];
594                     this._parseStyleDeclarationPayload(payload, styleDeclaration.node, styleDeclaration.inherited, styleDeclaration.type, styleDeclaration.ownerRule);
595                 }
596
597                 return null;
598             }
599
600             if (!styleDeclaration) {
601                 var filteredStyleDeclarations = styleDeclarations.filter(function(styleDeclaration) {
602                     // This case only applies for styles that are not part of a rule.
603                     if (styleDeclaration.ownerRule) {
604                         console.assert(!rule);
605                         return false;
606                     }
607
608                     if (styleDeclaration.node !== node)
609                         return false;
610
611                     if (styleDeclaration.inherited !== inherited)
612                         return false;
613
614                     return true;
615                 });
616
617                 console.assert(filteredStyleDeclarations.length <= 1);
618                 styleDeclaration = filteredStyleDeclarations[0] || null;
619             }
620         }
621
622         if (previousStyleDeclarationsMap !== this._styleDeclarationsMap) {
623             // If the previous and current maps differ then make sure the found styleDeclaration is added to the current map.
624             styleDeclarations = mapKey && mapKey in this._styleDeclarationsMap ? this._styleDeclarationsMap[mapKey] : [];
625
626             if (styleDeclaration && !styleDeclarations.includes(styleDeclaration)) {
627                 styleDeclarations.push(styleDeclaration);
628                 this._styleDeclarationsMap[mapKey] = styleDeclarations;
629             }
630         }
631
632         var shorthands = {};
633         for (var i = 0; payload.shorthandEntries && i < payload.shorthandEntries.length; ++i) {
634             var shorthand = payload.shorthandEntries[i];
635             shorthands[shorthand.name] = shorthand.value;
636         }
637
638         var text = payload.cssText;
639
640         var inheritedPropertyCount = 0;
641
642         var properties = [];
643         for (var i = 0; payload.cssProperties && i < payload.cssProperties.length; ++i) {
644             var propertyPayload = payload.cssProperties[i];
645
646             if (inherited && WI.CSSProperty.isInheritedPropertyName(propertyPayload.name))
647                 ++inheritedPropertyCount;
648
649             var property = this._parseStylePropertyPayload(propertyPayload, i, styleDeclaration, text);
650             properties.push(property);
651         }
652
653         var styleSheetTextRange = this._parseSourceRangePayload(payload.range);
654
655         if (styleDeclaration) {
656             styleDeclaration.update(text, properties, styleSheetTextRange);
657             return styleDeclaration;
658         }
659
660         var styleSheet = id ? WI.cssManager.styleSheetForIdentifier(id.styleSheetId) : null;
661         if (styleSheet) {
662             if (type === WI.CSSStyleDeclaration.Type.Inline)
663                 styleSheet.markAsInlineStyleAttributeStyleSheet();
664             styleSheet.addEventListener(WI.CSSStyleSheet.Event.ContentDidChange, this._styleSheetContentDidChange, this);
665         }
666
667         if (inherited && !inheritedPropertyCount)
668             return null;
669
670         styleDeclaration = new WI.CSSStyleDeclaration(this, styleSheet, id, type, node, inherited, text, properties, styleSheetTextRange);
671
672         if (mapKey) {
673             styleDeclarations.push(styleDeclaration);
674             this._styleDeclarationsMap[mapKey] = styleDeclarations;
675         }
676
677         return styleDeclaration;
678     }
679
680     _parseSelectorListPayload(selectorList)
681     {
682         var selectors = selectorList.selectors;
683         if (!selectors.length)
684             return [];
685
686         // COMPATIBILITY (iOS 8): The selectorList payload was an array of selector text strings.
687         // Now they are CSSSelector objects with multiple properties.
688         if (typeof selectors[0] === "string") {
689             return selectors.map(function(selectorText) {
690                 return new WI.CSSSelector(selectorText);
691             });
692         }
693
694         return selectors.map(function(selectorPayload) {
695             return new WI.CSSSelector(selectorPayload.text, selectorPayload.specificity, selectorPayload.dynamic);
696         });
697     }
698
699     _parseRulePayload(payload, matchedSelectorIndices, node, inherited, ruleOccurrences)
700     {
701         if (!payload)
702             return null;
703
704         // User and User Agent rules don't have 'ruleId' in the payload. However, their style's have 'styleId' and
705         // 'styleId' is the same identifier the backend uses for Author rule identifiers, so do the same here.
706         // They are excluded by the backend because they are not editable, however our front-end does not determine
707         // editability solely based on the existence of the id like the open source front-end does.
708         var id = payload.ruleId || payload.style.styleId;
709
710         var mapKey = id ? id.styleSheetId + ":" + id.ordinal + ":" + (inherited ? "I" : "N") + ":" + node.id : null;
711
712         // Rules can match multiple times if they have multiple selectors or because of inheritance. We keep a count
713         // of occurrences so we have unique rules per occurrence, that way properties will be correctly marked as overridden.
714         var occurrence = 0;
715         if (mapKey) {
716             if (mapKey in ruleOccurrences)
717                 occurrence = ++ruleOccurrences[mapKey];
718             else
719                 ruleOccurrences[mapKey] = occurrence;
720
721             // Append the occurrence number to the map key for lookup in the rules map.
722             mapKey += ":" + occurrence;
723         }
724
725         var rule = null;
726
727         // Look for existing rules in the previous map if there is one, otherwise use the current map.
728         var previousRulesMap = this._previousRulesMap || this._rulesMap;
729         if (mapKey && mapKey in previousRulesMap) {
730             rule = previousRulesMap[mapKey];
731
732             if (previousRulesMap !== this._rulesMap) {
733                 // If the previous and current maps differ then make sure the found rule is added to the current map.
734                 this._rulesMap[mapKey] = rule;
735             }
736         }
737
738         var style = this._parseStyleDeclarationPayload(payload.style, node, inherited, WI.CSSStyleDeclaration.Type.Rule, rule);
739         if (!style)
740             return null;
741
742         var styleSheet = id ? WI.cssManager.styleSheetForIdentifier(id.styleSheetId) : null;
743
744         var selectorText = payload.selectorList.text;
745         var selectors = this._parseSelectorListPayload(payload.selectorList);
746         var type = WI.CSSManager.protocolStyleSheetOriginToEnum(payload.origin);
747
748         var sourceCodeLocation = null;
749         var sourceRange = payload.selectorList.range;
750         if (sourceRange)
751             sourceCodeLocation = this._createSourceCodeLocation(payload.sourceURL, sourceRange.startLine, sourceRange.startColumn);
752         else {
753             // FIXME: Is it possible for a CSSRule to have a sourceLine without its selectorList having a sourceRange? Fall back just in case.
754             sourceCodeLocation = this._createSourceCodeLocation(payload.sourceURL, payload.sourceLine);
755         }
756
757         if (styleSheet) {
758             if (!sourceCodeLocation && styleSheet.isInspectorStyleSheet())
759                 sourceCodeLocation = styleSheet.createSourceCodeLocation(sourceRange.startLine, sourceRange.startColumn);
760
761             sourceCodeLocation = styleSheet.offsetSourceCodeLocation(sourceCodeLocation);
762         }
763
764         var mediaList = [];
765         for (var i = 0; payload.media && i < payload.media.length; ++i) {
766             var mediaItem = payload.media[i];
767             var mediaType = WI.CSSManager.protocolMediaSourceToEnum(mediaItem.source);
768             var mediaText = mediaItem.text;
769             var mediaSourceCodeLocation = this._createSourceCodeLocation(mediaItem.sourceURL, mediaItem.sourceLine);
770             if (styleSheet)
771                 mediaSourceCodeLocation = styleSheet.offsetSourceCodeLocation(mediaSourceCodeLocation);
772
773             mediaList.push(new WI.CSSMedia(mediaType, mediaText, mediaSourceCodeLocation));
774         }
775
776         if (rule) {
777             rule.update(sourceCodeLocation, selectorText, selectors, matchedSelectorIndices, style, mediaList);
778             return rule;
779         }
780
781         if (styleSheet)
782             styleSheet.addEventListener(WI.CSSStyleSheet.Event.ContentDidChange, this._styleSheetContentDidChange, this);
783
784         rule = new WI.CSSRule(this, styleSheet, id, type, sourceCodeLocation, selectorText, selectors, matchedSelectorIndices, style, mediaList);
785
786         if (mapKey)
787             this._rulesMap[mapKey] = rule;
788
789         return rule;
790     }
791
792     _markAsNeedsRefresh()
793     {
794         this._needsRefresh = true;
795         this.dispatchEventToListeners(WI.DOMNodeStyles.Event.NeedsRefresh);
796     }
797
798     _styleSheetContentDidChange(event)
799     {
800         var styleSheet = event.target;
801         console.assert(styleSheet);
802         if (!styleSheet)
803             return;
804
805         // Ignore the stylesheet we know we just changed and handled above.
806         if (styleSheet === this._ignoreNextContentDidChangeForStyleSheet) {
807             delete this._ignoreNextContentDidChangeForStyleSheet;
808             return;
809         }
810
811         this._markAsNeedsRefresh();
812     }
813
814     _updateStyleCascade()
815     {
816         var cascadeOrderedStyleDeclarations = this._collectStylesInCascadeOrder(this._matchedRules, this._inlineStyle, this._attributesStyle);
817
818         for (var i = 0; i < this._inheritedRules.length; ++i) {
819             var inheritedStyleInfo = this._inheritedRules[i];
820             var inheritedCascadeOrder = this._collectStylesInCascadeOrder(inheritedStyleInfo.matchedRules, inheritedStyleInfo.inlineStyle, null);
821             cascadeOrderedStyleDeclarations = cascadeOrderedStyleDeclarations.concat(inheritedCascadeOrder);
822         }
823
824         this._orderedStyles = cascadeOrderedStyleDeclarations;
825
826         this._propertyNameToEffectivePropertyMap = {};
827
828         this._markOverriddenProperties(cascadeOrderedStyleDeclarations, this._propertyNameToEffectivePropertyMap);
829         this._associateRelatedProperties(cascadeOrderedStyleDeclarations, this._propertyNameToEffectivePropertyMap);
830
831         for (var pseudoIdentifier in this._pseudoElements) {
832             var pseudoElementInfo = this._pseudoElements[pseudoIdentifier];
833             pseudoElementInfo.orderedStyles = this._collectStylesInCascadeOrder(pseudoElementInfo.matchedRules, null, null);
834             this._markOverriddenProperties(pseudoElementInfo.orderedStyles);
835             this._associateRelatedProperties(pseudoElementInfo.orderedStyles);
836         }
837     }
838
839     _collectStylesInCascadeOrder(matchedRules, inlineStyle, attributesStyle)
840     {
841         var result = [];
842
843         // Inline style has the greatest specificity. So it goes first in the cascade order.
844         if (inlineStyle)
845             result.push(inlineStyle);
846
847         var userAndUserAgentStyles = [];
848
849         for (var i = 0; i < matchedRules.length; ++i) {
850             var rule = matchedRules[i];
851
852             // Only append to the result array here for author and inspector rules since attribute
853             // styles come between author rules and user/user agent rules.
854             switch (rule.type) {
855             case WI.CSSStyleSheet.Type.Inspector:
856             case WI.CSSStyleSheet.Type.Author:
857                 result.push(rule.style);
858                 break;
859
860             case WI.CSSStyleSheet.Type.User:
861             case WI.CSSStyleSheet.Type.UserAgent:
862                 userAndUserAgentStyles.push(rule.style);
863                 break;
864             }
865         }
866
867         // Style properties from HTML attributes are next.
868         if (attributesStyle)
869             result.push(attributesStyle);
870
871         // Finally add the user and user stylesheet's matched style rules we collected earlier.
872         result = result.concat(userAndUserAgentStyles);
873
874         return result;
875     }
876
877     _markOverriddenProperties(styles, propertyNameToEffectiveProperty)
878     {
879         propertyNameToEffectiveProperty = propertyNameToEffectiveProperty || {};
880
881         for (var i = 0; i < styles.length; ++i) {
882             var style = styles[i];
883             var properties = style.enabledProperties;
884
885             for (var j = 0; j < properties.length; ++j) {
886                 var property = properties[j];
887                 if (!property.attached || !property.valid) {
888                     property.overridden = false;
889                     continue;
890                 }
891
892                 if (style.inherited && !property.inherited) {
893                     property.overridden = false;
894                     continue;
895                 }
896
897                 var canonicalName = property.canonicalName;
898                 if (canonicalName in propertyNameToEffectiveProperty) {
899                     var effectiveProperty = propertyNameToEffectiveProperty[canonicalName];
900
901                     if (effectiveProperty.ownerStyle === property.ownerStyle) {
902                         if (effectiveProperty.important && !property.important) {
903                             property.overridden = true;
904                             continue;
905                         }
906                     } else if (effectiveProperty.important || !property.important || effectiveProperty.ownerStyle.node !== property.ownerStyle.node) {
907                         property.overridden = true;
908                         continue;
909                     }
910
911                     if (!property.anonymous)
912                         effectiveProperty.overridden = true;
913                 }
914
915                 property.overridden = false;
916
917                 propertyNameToEffectiveProperty[canonicalName] = property;
918             }
919         }
920     }
921
922     _associateRelatedProperties(styles, propertyNameToEffectiveProperty)
923     {
924         for (var i = 0; i < styles.length; ++i) {
925             var properties = styles[i].enabledProperties;
926
927             var knownShorthands = {};
928
929             for (var j = 0; j < properties.length; ++j) {
930                 var property = properties[j];
931
932                 if (!property.valid)
933                     continue;
934
935                 if (!WI.CSSCompletions.cssNameCompletions.isShorthandPropertyName(property.name))
936                     continue;
937
938                 if (knownShorthands[property.canonicalName] && !knownShorthands[property.canonicalName].overridden) {
939                     console.assert(property.overridden);
940                     continue;
941                 }
942
943                 knownShorthands[property.canonicalName] = property;
944             }
945
946             for (var j = 0; j < properties.length; ++j) {
947                 var property = properties[j];
948
949                 if (!property.valid)
950                     continue;
951
952                 var shorthandProperty = null;
953
954                 if (!isEmptyObject(knownShorthands)) {
955                     var possibleShorthands = WI.CSSCompletions.cssNameCompletions.shorthandsForLonghand(property.canonicalName);
956                     for (var k = 0; k < possibleShorthands.length; ++k) {
957                         if (possibleShorthands[k] in knownShorthands) {
958                             shorthandProperty = knownShorthands[possibleShorthands[k]];
959                             break;
960                         }
961                     }
962                 }
963
964                 if (!shorthandProperty || shorthandProperty.overridden !== property.overridden) {
965                     property.relatedShorthandProperty = null;
966                     property.clearRelatedLonghandProperties();
967                     continue;
968                 }
969
970                 shorthandProperty.addRelatedLonghandProperty(property);
971                 property.relatedShorthandProperty = shorthandProperty;
972
973                 if (propertyNameToEffectiveProperty && propertyNameToEffectiveProperty[shorthandProperty.canonicalName] === shorthandProperty)
974                     propertyNameToEffectiveProperty[property.canonicalName] = property;
975             }
976         }
977     }
978
979     _isPropertyFoundInMatchingRules(propertyName)
980     {
981         return this._orderedStyles.some((style) => {
982             return style.enabledProperties.some((property) => property.name === propertyName);
983         });
984     }
985 };
986
987 WI.DOMNodeStyles.Event = {
988     NeedsRefresh: "dom-node-styles-needs-refresh",
989     Refreshed: "dom-node-styles-refreshed"
990 };