09d6104842171291e92f22d2fce5a778e7504c34
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Models / CSSCompletions.js
1 /*
2  * Copyright (C) 2010 Nikita Vasilyev. All rights reserved.
3  * Copyright (C) 2010 Joseph Pecoraro. All rights reserved.
4  * Copyright (C) 2010 Google Inc. All rights reserved.
5  * Copyright (C) 2013 Apple Inc. All rights reserved.
6  *
7  * Redistribution and use in source and binary forms, with or without
8  * modification, are permitted provided that the following conditions are
9  * met:
10  *
11  *     * Redistributions of source code must retain the above copyright
12  * notice, this list of conditions and the following disclaimer.
13  *     * Redistributions in binary form must reproduce the above
14  * copyright notice, this list of conditions and the following disclaimer
15  * in the documentation and/or other materials provided with the
16  * distribution.
17  *     * Neither the name of Google Inc. nor the names of its
18  * contributors may be used to endorse or promote products derived from
19  * this software without specific prior written permission.
20  *
21  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
24  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
25  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
26  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
27  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
28  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
29  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
30  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
31  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32  */
33
34 WI.CSSCompletions = class CSSCompletions
35 {
36     constructor(properties, acceptEmptyPrefix)
37     {
38         this._values = [];
39         this._shorthands = {};
40
41         // The `properties` parameter can be either a list of objects with 'name' / 'longhand'
42         // properties when initialized from the protocol for CSSCompletions.cssNameCompletions.
43         // Or it may just a list of strings when quickly initialized for other completion purposes.
44         if (properties.length && typeof properties[0] === "string")
45             this._values = this._values.concat(properties);
46         else {
47             for (var property of properties) {
48                 var propertyName = property.name;
49                 console.assert(propertyName);
50
51                 this._values.push(propertyName);
52
53                 let aliases = property.aliases;
54                 if (aliases)
55                     this._values = this._values.concat(aliases);
56
57                 var longhands = property.longhands;
58                 if (longhands) {
59                     for (var j = 0; j < longhands.length; ++j) {
60                         var longhandName = longhands[j];
61
62                         var shorthands = this._shorthands[longhandName];
63                         if (!shorthands) {
64                             shorthands = [];
65                             this._shorthands[longhandName] = shorthands;
66                         }
67
68                         shorthands.push(propertyName);
69                     }
70                 }
71             }
72         }
73
74         this._values.sort();
75
76         this._acceptEmptyPrefix = acceptEmptyPrefix;
77     }
78
79     // Static
80
81     static initializeCSSCompletions(target)
82     {
83         console.assert(target.CSSAgent);
84
85         if (WI.CSSCompletions.cssNameCompletions)
86             return;
87
88         function propertyNamesCallback(error, names)
89         {
90             if (error)
91                 return;
92
93             WI.CSSCompletions.cssNameCompletions = new WI.CSSCompletions(names, false);
94
95             WI.CSSKeywordCompletions.addCustomCompletions(names);
96
97             // CodeMirror is not included by tests so we shouldn't assume it always exists.
98             // If it isn't available we skip MIME type associations.
99             if (!window.CodeMirror)
100                 return;
101
102             var propertyNamesForCodeMirror = {};
103             var valueKeywordsForCodeMirror = {"inherit": true, "initial": true, "unset": true, "revert": true, "var": true, "env": true};
104             var colorKeywordsForCodeMirror = {};
105
106             function nameForCodeMirror(name)
107             {
108                 // CodeMirror parses the vendor prefix separate from the property or keyword name,
109                 // so we need to strip vendor prefixes from our names. Also strip function parenthesis.
110                 return name.replace(/^-[^-]+-/, "").replace(/\(\)$/, "").toLowerCase();
111             }
112
113             function collectPropertyNameForCodeMirror(propertyName)
114             {
115                 // Properties can also be value keywords, like when used in a transition.
116                 // So we add them to both lists.
117                 var codeMirrorPropertyName = nameForCodeMirror(propertyName);
118                 propertyNamesForCodeMirror[codeMirrorPropertyName] = true;
119                 valueKeywordsForCodeMirror[codeMirrorPropertyName] = true;
120             }
121
122             for (var property of names)
123                 collectPropertyNameForCodeMirror(property.name);
124
125             for (var propertyName in WI.CSSKeywordCompletions._propertyKeywordMap) {
126                 var keywords = WI.CSSKeywordCompletions._propertyKeywordMap[propertyName];
127                 for (var i = 0; i < keywords.length; ++i) {
128                     // Skip numbers, like the ones defined for font-weight.
129                     if (keywords[i] === WI.CSSKeywordCompletions.AllPropertyNamesPlaceholder || !isNaN(Number(keywords[i])))
130                         continue;
131                     valueKeywordsForCodeMirror[nameForCodeMirror(keywords[i])] = true;
132                 }
133             }
134
135             WI.CSSKeywordCompletions._colors.forEach(function(colorName) {
136                 colorKeywordsForCodeMirror[nameForCodeMirror(colorName)] = true;
137             });
138
139             function updateCodeMirrorCSSMode(mimeType)
140             {
141                 var modeSpec = CodeMirror.resolveMode(mimeType);
142
143                 console.assert(modeSpec.propertyKeywords);
144                 console.assert(modeSpec.valueKeywords);
145                 console.assert(modeSpec.colorKeywords);
146
147                 modeSpec.propertyKeywords = propertyNamesForCodeMirror;
148                 modeSpec.valueKeywords = valueKeywordsForCodeMirror;
149                 modeSpec.colorKeywords = colorKeywordsForCodeMirror;
150
151                 CodeMirror.defineMIME(mimeType, modeSpec);
152             }
153
154             updateCodeMirrorCSSMode("text/css");
155             updateCodeMirrorCSSMode("text/x-scss");
156         }
157
158         function fontFamilyNamesCallback(error, fontFamilyNames)
159         {
160             if (error)
161                 return;
162
163             WI.CSSKeywordCompletions.addPropertyCompletionValues("font-family", fontFamilyNames);
164             WI.CSSKeywordCompletions.addPropertyCompletionValues("font", fontFamilyNames);
165         }
166
167         target.CSSAgent.getSupportedCSSProperties(propertyNamesCallback);
168
169         // COMPATIBILITY (iOS 9): CSS.getSupportedSystemFontFamilyNames did not exist.
170         if (target.CSSAgent.getSupportedSystemFontFamilyNames)
171             target.CSSAgent.getSupportedSystemFontFamilyNames(fontFamilyNamesCallback);
172     }
173
174     // Public
175
176     get values()
177     {
178         return this._values;
179     }
180
181     startsWith(prefix)
182     {
183         var firstIndex = this._firstIndexOfPrefix(prefix);
184         if (firstIndex === -1)
185             return [];
186
187         var results = [];
188         while (firstIndex < this._values.length && this._values[firstIndex].startsWith(prefix))
189             results.push(this._values[firstIndex++]);
190         return results;
191     }
192
193     _firstIndexOfPrefix(prefix)
194     {
195         if (!this._values.length)
196             return -1;
197         if (!prefix)
198             return this._acceptEmptyPrefix ? 0 : -1;
199
200         var maxIndex = this._values.length - 1;
201         var minIndex = 0;
202         var foundIndex;
203
204         do {
205             var middleIndex = (maxIndex + minIndex) >> 1;
206             if (this._values[middleIndex].startsWith(prefix)) {
207                 foundIndex = middleIndex;
208                 break;
209             }
210             if (this._values[middleIndex] < prefix)
211                 minIndex = middleIndex + 1;
212             else
213                 maxIndex = middleIndex - 1;
214         } while (minIndex <= maxIndex);
215
216         if (foundIndex === undefined)
217             return -1;
218
219         while (foundIndex && this._values[foundIndex - 1].startsWith(prefix))
220             foundIndex--;
221
222         return foundIndex;
223     }
224
225     next(str, prefix)
226     {
227         return this._closest(str, prefix, 1);
228     }
229
230     previous(str, prefix)
231     {
232         return this._closest(str, prefix, -1);
233     }
234
235     _closest(str, prefix, shift)
236     {
237         if (!str)
238             return "";
239
240         var index = this._values.indexOf(str);
241         if (index === -1)
242             return "";
243
244         if (!prefix) {
245             index = (index + this._values.length + shift) % this._values.length;
246             return this._values[index];
247         }
248
249         var propertiesWithPrefix = this.startsWith(prefix);
250         var j = propertiesWithPrefix.indexOf(str);
251         j = (j + propertiesWithPrefix.length + shift) % propertiesWithPrefix.length;
252         return propertiesWithPrefix[j];
253     }
254
255     isShorthandPropertyName(shorthand)
256     {
257         return WI.CSSKeywordCompletions.LonghandNamesForShorthandProperty.has(shorthand);
258     }
259
260     shorthandsForLonghand(longhand)
261     {
262         return this._shorthands[longhand] || [];
263     }
264
265     isValidPropertyName(name)
266     {
267         return this._values.includes(name);
268     }
269 };
270
271 WI.CSSCompletions.cssNameCompletions = null;