Web Inspector: Styles: variable swatch not shown for var() with a fallback
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / SpreadsheetStyleProperty.js
1 /*
2  * Copyright (C) 2017 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.SpreadsheetStyleProperty = class SpreadsheetStyleProperty extends WI.Object
27 {
28     constructor(delegate, property, options = {})
29     {
30         super();
31
32         console.assert(property instanceof WI.CSSProperty);
33
34         this._delegate = delegate || null;
35         this._property = property;
36         this._readOnly = options.readOnly || false;
37         this._element = document.createElement("div");
38
39         this._contentElement = null;
40         this._nameElement = null;
41         this._valueElement = null;
42         this._jumpToEffectivePropertyButton = null;
43
44         this._nameTextField = null;
45         this._valueTextField = null;
46
47         this._selected = false;
48         this._hasInvalidVariableValue = false;
49
50         this.update();
51         property.addEventListener(WI.CSSProperty.Event.OverriddenStatusChanged, this.updateStatus, this);
52         property.addEventListener(WI.CSSProperty.Event.Changed, this.updateStatus, this);
53
54         if (!this._readOnly) {
55             this._element.tabIndex = -1;
56             property.addEventListener(WI.CSSProperty.Event.ModifiedChanged, this.updateStatus, this);
57
58             this._element.addEventListener("blur", (event) => {
59                 // Keep selection after tabbing out of Web Inspector window and back.
60                 if (document.activeElement === this._element)
61                     return;
62
63                 if (this._delegate.spreadsheetStylePropertyBlur)
64                     this._delegate.spreadsheetStylePropertyBlur(event, this);
65             });
66
67             this._element.addEventListener("mouseenter", (event) => {
68                 if (this._delegate.spreadsheetStylePropertyMouseEnter)
69                     this._delegate.spreadsheetStylePropertyMouseEnter(event, this);
70             });
71
72             new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl, WI.KeyboardShortcut.Key.Slash, () => {
73                 this._toggle();
74                 this._select();
75             }, this._element);
76         }
77     }
78
79     // Public
80
81     get element() { return this._element; }
82     get property() { return this._property; }
83     get enabled() { return this._property.enabled; }
84
85     set index(index)
86     {
87         this._element.dataset.propertyIndex = index;
88     }
89
90     get selected()
91     {
92         return this._selected;
93     }
94
95     set selected(value)
96     {
97         if (value === this._selected)
98             return;
99
100         this._selected = value;
101         this.updateStatus();
102     }
103
104     startEditingName()
105     {
106         if (!this._nameTextField)
107             return;
108
109         this._nameTextField.startEditing();
110     }
111
112     startEditingValue()
113     {
114         if (!this._valueTextField)
115             return;
116
117         this._valueTextField.startEditing();
118     }
119
120     detached()
121     {
122         if (this._nameTextField)
123             this._nameTextField.detached();
124
125         if (this._valueTextField)
126             this._valueTextField.detached();
127     }
128
129     hidden()
130     {
131         if (this._nameTextField && this._nameTextField.editing)
132             this._nameTextField.element.blur();
133         else if (this._valueTextField && this._valueTextField.editing)
134             this._valueTextField.element.blur();
135     }
136
137     remove(replacement = null)
138     {
139         if (this._delegate && typeof this._delegate.spreadsheetStylePropertyWillRemove === "function")
140             this._delegate.spreadsheetStylePropertyWillRemove(this);
141
142         this.element.remove();
143
144         if (replacement)
145             this._property.replaceWithText(replacement);
146         else
147             this._property.remove();
148
149         this.detached();
150     }
151
152     update()
153     {
154         this.element.removeChildren();
155
156         if (this._isEditable()) {
157             this._checkboxElement = this.element.appendChild(document.createElement("input"));
158             this._checkboxElement.classList.add("property-toggle");
159             this._checkboxElement.type = "checkbox";
160             this._checkboxElement.checked = this._property.enabled;
161             this._checkboxElement.tabIndex = -1;
162             this._checkboxElement.addEventListener("click", (event) => {
163                 event.stopPropagation();
164                 this._toggle();
165                 console.assert(this._checkboxElement.checked === this._property.enabled);
166             });
167         }
168
169         this._contentElement = this.element.appendChild(document.createElement("span"));
170         this._contentElement.className = "content";
171
172         if (!this._property.enabled)
173             this._contentElement.append("/* ");
174
175         this._nameElement = this._contentElement.appendChild(document.createElement("span"));
176         this._nameElement.classList.add("name");
177         this._nameElement.textContent = this._property.name;
178
179         let colonElement = this._contentElement.appendChild(document.createElement("span"));
180         colonElement.classList.add("colon");
181         colonElement.textContent = ": ";
182
183         this._valueElement = this._contentElement.appendChild(document.createElement("span"));
184         this._valueElement.classList.add("value");
185         this._renderValue(this._property.rawValue);
186
187         if (this._isEditable() && this._property.enabled) {
188             this._nameElement.tabIndex = 0;
189             this._nameElement.addEventListener("beforeinput", this._handleNameBeforeInput.bind(this));
190             this._nameElement.addEventListener("paste", this._handleNamePaste.bind(this));
191
192             this._nameTextField = new WI.SpreadsheetTextField(this, this._nameElement, this._nameCompletionDataProvider.bind(this));
193
194             this._valueElement.tabIndex = 0;
195             this._valueElement.addEventListener("beforeinput", this._handleValueBeforeInput.bind(this));
196
197             this._valueTextField = new WI.SpreadsheetTextField(this, this._valueElement, this._valueCompletionDataProvider.bind(this));
198         }
199
200         if (this._isEditable()) {
201             this._setupJumpToSymbol(this._nameElement);
202             this._setupJumpToSymbol(this._valueElement);
203         }
204
205         let semicolonElement = this._contentElement.appendChild(document.createElement("span"));
206         semicolonElement.classList.add("semicolon");
207         semicolonElement.textContent = ";";
208
209         if (this._property.enabled) {
210             this._warningElement = this.element.appendChild(document.createElement("span"));
211             this._warningElement.className = "warning";
212         } else
213             this._contentElement.append(" */");
214
215         if (!this._property.implicit && this._property.ownerStyle.type === WI.CSSStyleDeclaration.Type.Computed) {
216             let effectiveProperty = this._property.ownerStyle.nodeStyles.effectivePropertyForName(this._property.name);
217             if (effectiveProperty && !effectiveProperty.styleSheetTextRange)
218                 effectiveProperty = effectiveProperty.relatedShorthandProperty;
219
220             let ownerRule = effectiveProperty ? effectiveProperty.ownerStyle.ownerRule : null;
221
222             let arrowElement = this._contentElement.appendChild(WI.createGoToArrowButton());
223             arrowElement.addEventListener("click", (event) => {
224                 if (!effectiveProperty || !ownerRule || !event.altKey) {
225                     if (this._delegate.spreadsheetStylePropertyShowProperty)
226                         this._delegate.spreadsheetStylePropertyShowProperty(this, this._property);
227                     return;
228                 }
229
230                 let sourceCode = ownerRule.sourceCodeLocation.sourceCode;
231                 let {startLine, startColumn} = effectiveProperty.styleSheetTextRange;
232                 WI.showSourceCodeLocation(sourceCode.createSourceCodeLocation(startLine, startColumn), {
233                     ignoreNetworkTab: true,
234                     ignoreSearchTab: true,
235                 });
236             });
237
238             if (effectiveProperty && ownerRule)
239                 arrowElement.title = WI.UIString("Option-click to show source");
240         }
241
242         this.updateStatus();
243     }
244
245     updateStatus()
246     {
247         let duplicatePropertyExistsBelow = (cssProperty) => {
248             let propertyFound = false;
249
250             for (let property of this._property.ownerStyle.enabledProperties) {
251                 if (property === cssProperty)
252                     propertyFound = true;
253                 else if (property.name === cssProperty.name && propertyFound)
254                     return true;
255             }
256
257             return false;
258         };
259
260         let classNames = [WI.SpreadsheetStyleProperty.StyleClassName];
261         let elementTitle = "";
262
263         if (this._property.overridden) {
264             if (!this._jumpToEffectivePropertyButton && this._delegate && this._delegate.spreadsheetStylePropertySelectByProperty && WI.settings.experimentalEnableStylesJumpToEffective.value) {
265                 console.assert(this._property.overridingProperty, `Overridden property is missing overridingProperty: ${this._property.formattedText}`);
266                 if (this._property.overridingProperty) {
267                     this._jumpToEffectivePropertyButton = WI.createGoToArrowButton();
268                     this._jumpToEffectivePropertyButton.classList.add("select-effective-property");
269                     this._jumpToEffectivePropertyButton.dataset.value = this._property.overridingProperty.rawValue;
270                     this._element.append(this._jumpToEffectivePropertyButton);
271
272                     this._jumpToEffectivePropertyButton.addEventListener("click", (event) => {
273                         console.assert(this._property.overridingProperty);
274                         event.stop();
275                         this._delegate.spreadsheetStylePropertySelectByProperty(this._property.overridingProperty);
276                     });
277                 }
278             }
279
280             classNames.push("overridden");
281             if (duplicatePropertyExistsBelow(this._property)) {
282                 classNames.push("has-warning");
283                 elementTitle = WI.UIString("Duplicate property");
284             }
285         }
286
287         if (this._property.implicit)
288             classNames.push("implicit");
289
290         if (this._property.ownerStyle.inherited && !this._property.inherited)
291             classNames.push("not-inherited");
292
293         if (!this._property.valid && this._property.hasOtherVendorNameOrKeyword())
294             classNames.push("other-vendor");
295         else if (this._hasInvalidVariableValue || (!this._property.valid && this._property.value !== "")) {
296             let propertyNameIsValid = false;
297             if (WI.CSSCompletions.cssNameCompletions)
298                 propertyNameIsValid = WI.CSSCompletions.cssNameCompletions.isValidPropertyName(this._property.name);
299
300             classNames.push("has-warning");
301
302             if (!propertyNameIsValid) {
303                 classNames.push("invalid-name");
304                 elementTitle = WI.UIString("Unsupported property name");
305             } else {
306                 classNames.push("invalid-value");
307                 elementTitle = WI.UIString("Unsupported property value");
308             }
309         }
310
311         if (!this._property.enabled)
312             classNames.push("disabled");
313
314         if (this._property.modified && this._property.name && this._property.rawValue)
315             classNames.push("modified");
316
317         if (this._selected)
318             classNames.push("selected");
319
320         this._element.className = classNames.join(" ");
321         this._element.title = elementTitle;
322     }
323
324     applyFilter(filterText)
325     {
326         let matchesName = this._nameElement.textContent.includes(filterText);
327         this._nameElement.classList.toggle(WI.GeneralStyleDetailsSidebarPanel.FilterMatchSectionClassName, !!matchesName);
328
329         let matchesValue = this._valueElement.textContent.includes(filterText);
330         this._valueElement.classList.toggle(WI.GeneralStyleDetailsSidebarPanel.FilterMatchSectionClassName, !!matchesValue);
331
332         let matches = matchesName || matchesValue;
333         this._contentElement.classList.toggle(WI.GeneralStyleDetailsSidebarPanel.NoFilterMatchInPropertyClassName, !matches);
334         return matches;
335     }
336
337     // SpreadsheetTextField delegate
338
339     spreadsheetTextFieldWillStartEditing(textField)
340     {
341         let isEditingName = textField === this._nameTextField;
342         textField.value = isEditingName ? this._property.name : this._property.rawValue;
343     }
344
345     spreadsheetTextFieldDidChange(textField)
346     {
347         if (textField === this._valueTextField)
348             this._handleValueChange();
349         else if (textField === this._nameTextField)
350             this._handleNameChange();
351     }
352
353     spreadsheetTextFieldDidCommit(textField, {direction})
354     {
355         let willRemoveProperty = false;
356         let isEditingName = textField === this._nameTextField;
357
358         if (!this._property.name || (!this._property.rawValue && !isEditingName && direction === "forward"))
359             willRemoveProperty = true;
360
361         if (!isEditingName && !willRemoveProperty)
362             this._renderValue(this._property.rawValue);
363
364         if (direction === "forward") {
365             if (isEditingName && !willRemoveProperty) {
366                 // Move focus from the name to the value.
367                 this._valueTextField.startEditing();
368                 return;
369             }
370         } else {
371             if (!isEditingName) {
372                 // Move focus from the value to the name.
373                 this._nameTextField.startEditing();
374                 return;
375             }
376         }
377
378         if (typeof this._delegate.spreadsheetStylePropertyFocusMoved === "function") {
379             // Move focus away from the current property, to the next or previous one, if exists, or to the next or previous rule, if exists.
380             this._delegate.spreadsheetStylePropertyFocusMoved(this, {direction, willRemoveProperty});
381         }
382
383         if (willRemoveProperty)
384             this.remove();
385     }
386
387     spreadsheetTextFieldDidBlur(textField, event, changed)
388     {
389         let focusedOutsideThisProperty = event.relatedTarget !== this._nameElement && event.relatedTarget !== this._valueElement;
390         if (focusedOutsideThisProperty && (!this._nameTextField.value.trim() || !this._valueTextField.value.trim())) {
391             this.remove();
392             return;
393         }
394
395         if (textField === this._valueTextField)
396             this._renderValue(this._property.rawValue);
397
398         if (typeof this._delegate.spreadsheetStylePropertyFocusMoved === "function")
399             this._delegate.spreadsheetStylePropertyFocusMoved(this, {direction: null});
400
401         if (changed && window.DOMAgent)
402             DOMAgent.markUndoableState();
403     }
404
405     spreadsheetTextFieldDidBackspace(textField)
406     {
407         if (textField === this._nameTextField)
408             this.spreadsheetTextFieldDidCommit(textField, {direction: "backward"});
409         else if (textField === this._valueTextField)
410             this._nameTextField.startEditing();
411     }
412
413     spreadsheetTextFieldDidPressEsc(textField, textBeforeEditing)
414     {
415         let isNewProperty = !textBeforeEditing;
416         if (isNewProperty)
417             this.remove();
418         else if (this._delegate.spreadsheetStylePropertyDidPressEsc)
419             this._delegate.spreadsheetStylePropertyDidPressEsc(this);
420     }
421
422     // Private
423
424     _toggle()
425     {
426         this._property.commentOut(this.property.enabled);
427         this.update();
428     }
429
430     _select()
431     {
432         if (this._delegate && this._delegate.spreadsheetStylePropertySelect) {
433             let index = parseInt(this._element.dataset.propertyIndex);
434             this._delegate.spreadsheetStylePropertySelect(index);
435         }
436     }
437
438     _isEditable()
439     {
440         return !this._readOnly && this._property.editable;
441     }
442
443     _renderValue(value)
444     {
445         this._hasInvalidVariableValue = false;
446
447         const maxValueLength = 150;
448         let tokens = WI.tokenizeCSSValue(value);
449
450         if (this._property.enabled)
451             tokens = this._replaceSpecialTokens(tokens);
452
453         tokens = tokens.map((token) => {
454             if (token instanceof Element)
455                 return token;
456
457             let className = "";
458
459             if (token.type) {
460                 if (token.type.includes("string"))
461                     className = "token-string";
462                 else if (token.type.includes("link"))
463                     className = "token-link";
464                 else if (token.type.includes("comment"))
465                     className = "token-comment";
466             }
467
468             if (className) {
469                 let span = document.createElement("span");
470                 span.classList.add(className);
471                 span.textContent = token.value.truncateMiddle(maxValueLength);
472
473                 if (token.type && token.type.includes("link"))
474                     span.addEventListener("contextmenu", this._handleLinkContextMenu.bind(this, token));
475
476                 return span;
477             }
478
479             return token.value;
480         });
481
482         this._valueElement.removeChildren();
483         this._valueElement.append(...tokens);
484     }
485
486     _createInlineSwatch(type, contents, valueObject)
487     {
488         let tokenElement = document.createElement("span");
489         let innerElement = document.createElement("span");
490         for (let item of contents) {
491             if (item instanceof Node)
492                 innerElement.appendChild(item);
493             else if (typeof item === "object")
494                 innerElement.append(item.value);
495             else
496                 innerElement.append(item);
497         }
498
499         let readOnly = !this._isEditable();
500         let swatch = new WI.InlineSwatch(type, valueObject, readOnly);
501
502         swatch.addEventListener(WI.InlineSwatch.Event.ValueChanged, (event) => {
503             let value = event.data.value && event.data.value.toString();
504             if (!value)
505                 return;
506
507             innerElement.textContent = value;
508             this._handleValueChange();
509
510             if (type === WI.InlineSwatch.Type.Variable)
511                 this._renderValue(this._property.rawValue);
512         }, this);
513
514         if (type === WI.InlineSwatch.Type.Variable) {
515             swatch.value = () => {
516                 return this._property.ownerStyle.nodeStyles.computedStyle.resolveVariableValue(innerElement.textContent);
517             };
518         }
519
520         if (this._delegate && typeof this._delegate.stylePropertyInlineSwatchActivated === "function") {
521             swatch.addEventListener(WI.InlineSwatch.Event.Activated, () => {
522                 this._delegate.stylePropertyInlineSwatchActivated();
523             });
524         }
525
526         if (this._delegate && typeof this._delegate.stylePropertyInlineSwatchDeactivated === "function") {
527             swatch.addEventListener(WI.InlineSwatch.Event.Deactivated, () => {
528                 this._delegate.stylePropertyInlineSwatchDeactivated();
529             });
530         }
531
532         tokenElement.append(swatch.element, innerElement);
533
534         return tokenElement;
535     }
536
537     _replaceSpecialTokens(tokens)
538     {
539         // FIXME: <https://webkit.org/b/178636> Web Inspector: Styles: Make inline widgets work with CSS functions (var(), calc(), etc.)
540
541         tokens = this._addVariableTokens(tokens);
542
543         if (this._property.variable || WI.CSSKeywordCompletions.isColorAwareProperty(this._property.name)) {
544             tokens = this._addGradientTokens(tokens);
545             tokens = this._addColorTokens(tokens);
546         }
547
548         if (this._property.variable || WI.CSSKeywordCompletions.isTimingFunctionAwareProperty(this._property.name)) {
549             tokens = this._addTimingFunctionTokens(tokens, "cubic-bezier");
550             tokens = this._addTimingFunctionTokens(tokens, "spring");
551         }
552
553         return tokens;
554     }
555
556     _addGradientTokens(tokens)
557     {
558         let gradientRegex = /^(repeating-)?(linear|radial)-gradient$/i;
559         let newTokens = [];
560         let gradientStartIndex = NaN;
561         let openParenthesis = 0;
562
563         for (let i = 0; i < tokens.length; i++) {
564             let token = tokens[i];
565             if (token.type && token.type.includes("atom") && gradientRegex.test(token.value)) {
566                 gradientStartIndex = i;
567                 openParenthesis = 0;
568             } else if (token.value === "(" && !isNaN(gradientStartIndex))
569                 openParenthesis++;
570             else if (token.value === ")" && !isNaN(gradientStartIndex)) {
571                 openParenthesis--;
572                 if (openParenthesis > 0) {
573                     // Matched a CSS function inside of the gradient.
574                     continue;
575                 }
576
577                 let rawTokens = tokens.slice(gradientStartIndex, i + 1);
578                 let text = rawTokens.map((token) => token.value).join("");
579                 let gradient = WI.Gradient.fromString(text);
580                 if (gradient)
581                     newTokens.push(this._createInlineSwatch(WI.InlineSwatch.Type.Gradient, rawTokens, gradient));
582                 else
583                     newTokens.push(...rawTokens);
584
585                 gradientStartIndex = NaN;
586             } else if (isNaN(gradientStartIndex))
587                 newTokens.push(token);
588         }
589
590         return newTokens;
591     }
592
593     _addColorTokens(tokens)
594     {
595         let newTokens = [];
596
597         let pushPossibleColorToken = (text, ...rawTokens) => {
598             let color = WI.Color.fromString(text);
599             if (color)
600                 newTokens.push(this._createInlineSwatch(WI.InlineSwatch.Type.Color, rawTokens, color));
601             else
602                 newTokens.push(...rawTokens);
603         };
604
605         let colorFunctionStartIndex = NaN;
606
607         for (let i = 0; i < tokens.length; i++) {
608             let token = tokens[i];
609             if (token.type && token.type.includes("hex-color")) {
610                 // Hex
611                 pushPossibleColorToken(token.value, token);
612             } else if (WI.Color.FunctionNames.has(token.value) && token.type && (token.type.includes("atom") || token.type.includes("keyword"))) {
613                 // Color Function start
614                 colorFunctionStartIndex = i;
615             } else if (isNaN(colorFunctionStartIndex) && token.type && (token.type.includes("atom") || token.type.includes("keyword"))) {
616                 // Color keyword
617                 pushPossibleColorToken(token.value, token);
618             } else if (!isNaN(colorFunctionStartIndex)) {
619                 // Color Function end
620                 if (token.value !== ")")
621                     continue;
622
623                 let rawTokens = tokens.slice(colorFunctionStartIndex, i + 1);
624                 let text = rawTokens.map((token) => token.value).join("");
625                 pushPossibleColorToken(text, ...rawTokens);
626                 colorFunctionStartIndex = NaN;
627             } else
628                 newTokens.push(token);
629         }
630
631         return newTokens;
632     }
633
634     _addTimingFunctionTokens(tokens, tokenType)
635     {
636         let newTokens = [];
637         let startIndex = NaN;
638         let openParenthesis = 0;
639
640         for (let i = 0; i < tokens.length; i++) {
641             let token = tokens[i];
642             if (token.value === tokenType && token.type && token.type.includes("atom")) {
643                 startIndex = i;
644                 openParenthesis = 0;
645             } else if (token.value === "(" && !isNaN(startIndex))
646                 openParenthesis++;
647             else if (token.value === ")" && !isNaN(startIndex)) {
648
649                 openParenthesis--;
650                 if (openParenthesis > 0)
651                     continue;
652
653                 let rawTokens = tokens.slice(startIndex, i + 1);
654                 let text = rawTokens.map((token) => token.value).join("");
655
656                 let valueObject;
657                 let inlineSwatchType;
658                 if (tokenType === "cubic-bezier") {
659                     valueObject = WI.CubicBezier.fromString(text);
660                     inlineSwatchType = WI.InlineSwatch.Type.Bezier;
661                 } else if (tokenType === "spring") {
662                     valueObject = WI.Spring.fromString(text);
663                     inlineSwatchType = WI.InlineSwatch.Type.Spring;
664                 }
665
666                 if (valueObject)
667                     newTokens.push(this._createInlineSwatch(inlineSwatchType, rawTokens, valueObject));
668                 else
669                     newTokens.push(...rawTokens);
670
671                 startIndex = NaN;
672             } else if (isNaN(startIndex))
673                 newTokens.push(token);
674         }
675
676         return newTokens;
677     }
678
679     _addVariableTokens(tokens)
680     {
681         let newTokens = [];
682         let startIndex = NaN;
683         let openParenthesis = 0;
684
685         for (let i = 0; i < tokens.length; i++) {
686             let token = tokens[i];
687             if (token.value === "var" && token.type && token.type.includes("atom")) {
688                 if (isNaN(startIndex)) {
689                     startIndex = i;
690                     openParenthesis = 0;
691                 }
692             } else if (token.value === "(" && !isNaN(startIndex))
693                 ++openParenthesis;
694             else if (token.value === ")" && !isNaN(startIndex)) {
695                 --openParenthesis;
696                 if (openParenthesis > 0)
697                     continue;
698
699                 let rawTokens = tokens.slice(startIndex, i + 1);
700                 let variableNameIndex = rawTokens.findIndex((token) => token.value.startsWith("--") && /\bvariable-2\b/.test(token.type));
701                 if (variableNameIndex !== -1) {
702                     let contents = [];
703                     let fallbackStartIndex = rawTokens.findIndex((value, i) => i > variableNameIndex + 1 && /\bm-css\b/.test(value.type));
704                     if (fallbackStartIndex !== -1) {
705                         contents = contents.concat(rawTokens.slice(0, fallbackStartIndex));
706                         contents = contents.concat(this._replaceSpecialTokens(rawTokens.slice(fallbackStartIndex, i)));
707                     } else
708                         contents = contents.concat(rawTokens.slice(0, i));
709                     contents.push(token);
710
711                     let text = rawTokens.reduce((accumulator, token) => accumulator + token.value, "");
712                     if (this._property.ownerStyle.nodeStyles.computedStyle.resolveVariableValue(text))
713                         newTokens.push(this._createInlineSwatch(WI.InlineSwatch.Type.Variable, contents));
714                     else
715                         newTokens = newTokens.concat(contents);
716                 } else {
717                     this._hasInvalidVariableValue = true;
718                     newTokens.push(...rawTokens);
719                 }
720
721                 startIndex = NaN;
722             } else if (isNaN(startIndex))
723                 newTokens.push(token);
724         }
725
726         return newTokens;
727     }
728
729     _handleNameChange()
730     {
731         this._property.name = this._nameElement.textContent.trim();
732     }
733
734     _handleValueChange()
735     {
736         this._property.rawValue = this._valueElement.textContent.trim();
737     }
738
739     _handleNameBeforeInput(event)
740     {
741         if (event.data !== ":" || event.inputType !== "insertText")
742             return;
743
744         event.preventDefault();
745         this._nameTextField.discardCompletion();
746         this._valueTextField.startEditing();
747     }
748
749     _handleNamePaste(event)
750     {
751         let text = event.clipboardData.getData("text/plain");
752         if (!text || !text.includes(":"))
753             return;
754
755         event.preventDefault();
756
757         this.remove(text);
758
759         if (this._delegate.spreadsheetStylePropertyAddBlankPropertySoon) {
760             this._delegate.spreadsheetStylePropertyAddBlankPropertySoon(this, {
761                 index: parseInt(this._element.dataset.propertyIndex) + 1,
762             });
763         }
764     }
765
766     _nameCompletionDataProvider(prefix, options = {})
767     {
768         let completions;
769         if (!prefix && options.allowEmptyPrefix)
770             completions = WI.CSSCompletions.cssNameCompletions.values;
771         else
772             completions = WI.CSSCompletions.cssNameCompletions.startsWith(prefix);
773         return {prefix, completions};
774     }
775
776     _handleValueBeforeInput(event)
777     {
778         if (event.data !== ";" || event.inputType !== "insertText")
779             return;
780
781         let text = this._valueTextField.valueWithoutSuggestion();
782         let selection = window.getSelection();
783         if (!selection.rangeCount || selection.getRangeAt(0).endOffset !== text.length)
784             return;
785
786         let unbalancedCharacters = WI.CSSCompletions.completeUnbalancedValue(text);
787         if (unbalancedCharacters)
788             return;
789
790         event.preventDefault();
791         this._valueTextField.stopEditing();
792         this.spreadsheetTextFieldDidCommit(this._valueTextField, {direction: "forward"});
793     }
794
795     _valueCompletionDataProvider(prefix)
796     {
797         // For "border: 1px so|", we want to suggest "solid" based on "so" prefix.
798         let match = prefix.match(/[a-z0-9()-]+$/i);
799
800         // Clicking on the value of `height: 100%` shouldn't show any completions.
801         if (!match && prefix)
802             return {completions: [], prefix: ""};
803
804         prefix = match ? match[0] : "";
805         let propertyName = this._nameElement.textContent.trim();
806         return {
807             prefix,
808             completions: WI.CSSKeywordCompletions.forProperty(propertyName).startsWith(prefix)
809         };
810     }
811
812     _setupJumpToSymbol(element)
813     {
814         element.addEventListener("mousedown", (event) => {
815             if (event.button !== 0)
816                 return;
817
818             if (!WI.modifierKeys.metaKey)
819                 return;
820
821             if (element.isContentEditable)
822                 return;
823
824             let sourceCodeLocation = null;
825             if (this._property.ownerStyle.ownerRule)
826                 sourceCodeLocation = this._property.ownerStyle.ownerRule.sourceCodeLocation;
827
828             if (!sourceCodeLocation)
829                 return;
830
831             let range = this._property.styleSheetTextRange;
832             const options = {
833                 ignoreNetworkTab: true,
834                 ignoreSearchTab: true,
835             };
836             let sourceCode = sourceCodeLocation.sourceCode;
837             WI.showSourceCodeLocation(sourceCode.createSourceCodeLocation(range.startLine, range.startColumn), options);
838         });
839     }
840
841     _handleLinkContextMenu(token, event)
842     {
843         let contextMenu = WI.ContextMenu.createFromEvent(event);
844
845         let resolveURL = (url) => {
846             let ownerStyle = this._property.ownerStyle;
847             if (!ownerStyle)
848                 return url;
849
850             let ownerStyleSheet = ownerStyle.ownerStyleSheet;
851             if (!ownerStyleSheet) {
852                 let ownerRule = ownerStyle.ownerRule;
853                 if (ownerRule)
854                     ownerStyleSheet = ownerRule.ownerStyleSheet;
855             }
856             if (ownerStyleSheet) {
857                 if (ownerStyleSheet.url)
858                     return absoluteURL(url, ownerStyleSheet.url);
859
860                 let parentFrame = ownerStyleSheet.parentFrame;
861                 if (parentFrame)
862                     return absoluteURL(url, parentFrame.url);
863             }
864
865             let node = ownerStyle.node;
866             if (!node) {
867                 let nodeStyles = ownerStyle.nodeStyles;
868                 if (!nodeStyles) {
869                     let ownerRule = ownerStyle.ownerRule;
870                     if (ownerRule)
871                         nodeStyles = ownerRule.nodeStyles;
872                 }
873                 if (nodeStyles)
874                     node = nodeStyles.node;
875             }
876             if (node) {
877                 let ownerDocument = node.ownerDocument;
878                 if (ownerDocument)
879                     return absoluteURL(url, node.ownerDocument.documentURL);
880             }
881
882             return url;
883         };
884
885         WI.appendContextMenuItemsForURL(contextMenu, resolveURL(token.value));
886     }
887 };
888
889 WI.SpreadsheetStyleProperty.StyleClassName = "property";