|
1 /* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
2 /* vim: set ts=2 et sw=2 tw=80: */ |
|
3 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
4 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
6 |
|
7 "use strict"; |
|
8 |
|
9 const cssTokenizer = require("devtools/sourceeditor/css-tokenizer"); |
|
10 |
|
11 /** |
|
12 * Returns the string enclosed in quotes |
|
13 */ |
|
14 function quoteString(string) { |
|
15 let hasDoubleQuotes = string.contains('"'); |
|
16 let hasSingleQuotes = string.contains("'"); |
|
17 |
|
18 if (hasDoubleQuotes && !hasSingleQuotes) { |
|
19 // In this case, no escaping required, just enclose in single-quotes |
|
20 return "'" + string + "'"; |
|
21 } |
|
22 |
|
23 // In all other cases, enclose in double-quotes, and escape any double-quote |
|
24 // that may be in the string |
|
25 return '"' + string.replace(/"/g, '\"') + '"'; |
|
26 } |
|
27 |
|
28 /** |
|
29 * Returns an array of CSS declarations given an string. |
|
30 * For example, parseDeclarations("width: 1px; height: 1px") would return |
|
31 * [{name:"width", value: "1px"}, {name: "height", "value": "1px"}] |
|
32 * |
|
33 * The input string is assumed to only contain declarations so { and } characters |
|
34 * will be treated as part of either the property or value, depending where it's |
|
35 * found. |
|
36 * |
|
37 * @param {string} inputString |
|
38 * An input string of CSS |
|
39 * @return {Array} an array of objects with the following signature: |
|
40 * [{"name": string, "value": string, "priority": string}, ...] |
|
41 */ |
|
42 function parseDeclarations(inputString) { |
|
43 let tokens = cssTokenizer(inputString); |
|
44 |
|
45 let declarations = [{name: "", value: "", priority: ""}]; |
|
46 |
|
47 let current = "", hasBang = false, lastProp; |
|
48 for (let token of tokens) { |
|
49 lastProp = declarations[declarations.length - 1]; |
|
50 |
|
51 if (token.tokenType === ":") { |
|
52 if (!lastProp.name) { |
|
53 // Set the current declaration name if there's no name yet |
|
54 lastProp.name = current.trim(); |
|
55 current = ""; |
|
56 hasBang = false; |
|
57 } else { |
|
58 // Otherwise, just append ':' to the current value (declaration value |
|
59 // with colons) |
|
60 current += ":"; |
|
61 } |
|
62 } else if (token.tokenType === ";") { |
|
63 lastProp.value = current.trim(); |
|
64 current = ""; |
|
65 hasBang = false; |
|
66 declarations.push({name: "", value: "", priority: ""}); |
|
67 } else { |
|
68 switch(token.tokenType) { |
|
69 case "IDENT": |
|
70 if (token.value === "important" && hasBang) { |
|
71 lastProp.priority = "important"; |
|
72 hasBang = false; |
|
73 } else { |
|
74 if (hasBang) { |
|
75 current += "!"; |
|
76 } |
|
77 current += token.value; |
|
78 } |
|
79 break; |
|
80 case "WHITESPACE": |
|
81 current += " "; |
|
82 break; |
|
83 case "DIMENSION": |
|
84 current += token.repr; |
|
85 break; |
|
86 case "HASH": |
|
87 current += "#" + token.value; |
|
88 break; |
|
89 case "URL": |
|
90 current += "url(" + quoteString(token.value) + ")"; |
|
91 break; |
|
92 case "FUNCTION": |
|
93 current += token.value + "("; |
|
94 break; |
|
95 case ")": |
|
96 current += token.tokenType; |
|
97 break; |
|
98 case "EOF": |
|
99 break; |
|
100 case "DELIM": |
|
101 if (token.value === "!") { |
|
102 hasBang = true; |
|
103 } else { |
|
104 current += token.value; |
|
105 } |
|
106 break; |
|
107 case "STRING": |
|
108 current += quoteString(token.value); |
|
109 break; |
|
110 case "{": |
|
111 case "}": |
|
112 current += token.tokenType; |
|
113 break; |
|
114 default: |
|
115 current += token.value; |
|
116 break; |
|
117 } |
|
118 } |
|
119 } |
|
120 |
|
121 // Handle whatever trailing properties or values might still be there |
|
122 if (current) { |
|
123 if (!lastProp.name) { |
|
124 // Trailing property found, e.g. p1:v1;p2:v2;p3 |
|
125 lastProp.name = current.trim(); |
|
126 } else { |
|
127 // Trailing value found, i.e. value without an ending ; |
|
128 lastProp.value += current.trim(); |
|
129 } |
|
130 } |
|
131 |
|
132 // Remove declarations that have neither a name nor a value |
|
133 declarations = declarations.filter(prop => prop.name || prop.value); |
|
134 |
|
135 return declarations; |
|
136 }; |
|
137 exports.parseDeclarations = parseDeclarations; |
|
138 |
|
139 /** |
|
140 * Expects a single CSS value to be passed as the input and parses the value |
|
141 * and priority. |
|
142 * |
|
143 * @param {string} value The value from the text editor. |
|
144 * @return {object} an object with 'value' and 'priority' properties. |
|
145 */ |
|
146 function parseSingleValue(value) { |
|
147 let declaration = parseDeclarations("a: " + value + ";")[0]; |
|
148 return { |
|
149 value: declaration ? declaration.value : "", |
|
150 priority: declaration ? declaration.priority : "" |
|
151 }; |
|
152 }; |
|
153 exports.parseSingleValue = parseSingleValue; |