| OLD | NEW |
| (Empty) | |
| 1 /** |
| 2 @license |
| 3 Copyright (c) 2017 The Polymer Project Authors. All rights reserved. |
| 4 This code may only be used under the BSD style license found at http://polymer.g
ithub.io/LICENSE.txt |
| 5 The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt |
| 6 The complete set of contributors may be found at http://polymer.github.io/CONTRI
BUTORS.txt |
| 7 Code distributed by Google as part of the polymer project is also |
| 8 subject to an additional IP rights grant found at http://polymer.github.io/PATEN
TS.txt |
| 9 */ |
| 10 |
| 11 /* |
| 12 Extremely simple css parser. Intended to be not more than what we need |
| 13 and definitely not necessarily correct =). |
| 14 */ |
| 15 |
| 16 'use strict'; |
| 17 |
| 18 /** @unrestricted */ |
| 19 class StyleNode { |
| 20 constructor() { |
| 21 /** @type {number} */ |
| 22 this['start'] = 0; |
| 23 /** @type {number} */ |
| 24 this['end'] = 0; |
| 25 /** @type {StyleNode} */ |
| 26 this['previous'] = null; |
| 27 /** @type {StyleNode} */ |
| 28 this['parent'] = null; |
| 29 /** @type {Array<StyleNode>} */ |
| 30 this['rules'] = null; |
| 31 /** @type {string} */ |
| 32 this['parsedCssText'] = ''; |
| 33 /** @type {string} */ |
| 34 this['cssText'] = ''; |
| 35 /** @type {boolean} */ |
| 36 this['atRule'] = false; |
| 37 /** @type {number} */ |
| 38 this['type'] = 0; |
| 39 /** @type {string} */ |
| 40 this['keyframesName'] = ''; |
| 41 /** @type {string} */ |
| 42 this['selector'] = ''; |
| 43 /** @type {string} */ |
| 44 this['parsedSelector'] = ''; |
| 45 } |
| 46 } |
| 47 |
| 48 export {StyleNode} |
| 49 |
| 50 // given a string of css, return a simple rule tree |
| 51 /** |
| 52 * @param {string} text |
| 53 * @return {StyleNode} |
| 54 */ |
| 55 export function parse(text) { |
| 56 text = clean(text); |
| 57 return parseCss(lex(text), text); |
| 58 } |
| 59 |
| 60 // remove stuff we don't care about that may hinder parsing |
| 61 /** |
| 62 * @param {string} cssText |
| 63 * @return {string} |
| 64 */ |
| 65 function clean(cssText) { |
| 66 return cssText.replace(RX.comments, '').replace(RX.port, ''); |
| 67 } |
| 68 |
| 69 // super simple {...} lexer that returns a node tree |
| 70 /** |
| 71 * @param {string} text |
| 72 * @return {StyleNode} |
| 73 */ |
| 74 function lex(text) { |
| 75 let root = new StyleNode(); |
| 76 root['start'] = 0; |
| 77 root['end'] = text.length |
| 78 let n = root; |
| 79 for (let i = 0, l = text.length; i < l; i++) { |
| 80 if (text[i] === OPEN_BRACE) { |
| 81 if (!n['rules']) { |
| 82 n['rules'] = []; |
| 83 } |
| 84 let p = n; |
| 85 let previous = p['rules'][p['rules'].length - 1] || null; |
| 86 n = new StyleNode(); |
| 87 n['start'] = i + 1; |
| 88 n['parent'] = p; |
| 89 n['previous'] = previous; |
| 90 p['rules'].push(n); |
| 91 } else if (text[i] === CLOSE_BRACE) { |
| 92 n['end'] = i + 1; |
| 93 n = n['parent'] || root; |
| 94 } |
| 95 } |
| 96 return root; |
| 97 } |
| 98 |
| 99 // add selectors/cssText to node tree |
| 100 /** |
| 101 * @param {StyleNode} node |
| 102 * @param {string} text |
| 103 * @return {StyleNode} |
| 104 */ |
| 105 function parseCss(node, text) { |
| 106 let t = text.substring(node['start'], node['end'] - 1); |
| 107 node['parsedCssText'] = node['cssText'] = t.trim(); |
| 108 if (node['parent']) { |
| 109 let ss = node['previous'] ? node['previous']['end'] : node['parent']['start'
]; |
| 110 t = text.substring(ss, node['start'] - 1); |
| 111 t = _expandUnicodeEscapes(t); |
| 112 t = t.replace(RX.multipleSpaces, ' '); |
| 113 // TODO(sorvell): ad hoc; make selector include only after last ; |
| 114 // helps with mixin syntax |
| 115 t = t.substring(t.lastIndexOf(';') + 1); |
| 116 let s = node['parsedSelector'] = node['selector'] = t.trim(); |
| 117 node['atRule'] = (s.indexOf(AT_START) === 0); |
| 118 // note, support a subset of rule types... |
| 119 if (node['atRule']) { |
| 120 if (s.indexOf(MEDIA_START) === 0) { |
| 121 node['type'] = types.MEDIA_RULE; |
| 122 } else if (s.match(RX.keyframesRule)) { |
| 123 node['type'] = types.KEYFRAMES_RULE; |
| 124 node['keyframesName'] = |
| 125 node['selector'].split(RX.multipleSpaces).pop(); |
| 126 } |
| 127 } else { |
| 128 if (s.indexOf(VAR_START) === 0) { |
| 129 node['type'] = types.MIXIN_RULE; |
| 130 } else { |
| 131 node['type'] = types.STYLE_RULE; |
| 132 } |
| 133 } |
| 134 } |
| 135 let r$ = node['rules']; |
| 136 if (r$) { |
| 137 for (let i = 0, l = r$.length, r; |
| 138 (i < l) && (r = r$[i]); i++) { |
| 139 parseCss(r, text); |
| 140 } |
| 141 } |
| 142 return node; |
| 143 } |
| 144 |
| 145 /** |
| 146 * conversion of sort unicode escapes with spaces like `\33 ` (and longer) into |
| 147 * expanded form that doesn't require trailing space `\000033` |
| 148 * @param {string} s |
| 149 * @return {string} |
| 150 */ |
| 151 function _expandUnicodeEscapes(s) { |
| 152 return s.replace(/\\([0-9a-f]{1,6})\s/gi, function() { |
| 153 let code = arguments[1], |
| 154 repeat = 6 - code.length; |
| 155 while (repeat--) { |
| 156 code = '0' + code; |
| 157 } |
| 158 return '\\' + code; |
| 159 }); |
| 160 } |
| 161 |
| 162 /** |
| 163 * stringify parsed css. |
| 164 * @param {StyleNode} node |
| 165 * @param {boolean=} preserveProperties |
| 166 * @param {string=} text |
| 167 * @return {string} |
| 168 */ |
| 169 export function stringify(node, preserveProperties, text = '') { |
| 170 // calc rule cssText |
| 171 let cssText = ''; |
| 172 if (node['cssText'] || node['rules']) { |
| 173 let r$ = node['rules']; |
| 174 if (r$ && !_hasMixinRules(r$)) { |
| 175 for (let i = 0, l = r$.length, r; |
| 176 (i < l) && (r = r$[i]); i++) { |
| 177 cssText = stringify(r, preserveProperties, cssText); |
| 178 } |
| 179 } else { |
| 180 cssText = preserveProperties ? node['cssText'] : |
| 181 removeCustomProps(node['cssText']); |
| 182 cssText = cssText.trim(); |
| 183 if (cssText) { |
| 184 cssText = ' ' + cssText + '\n'; |
| 185 } |
| 186 } |
| 187 } |
| 188 // emit rule if there is cssText |
| 189 if (cssText) { |
| 190 if (node['selector']) { |
| 191 text += node['selector'] + ' ' + OPEN_BRACE + '\n'; |
| 192 } |
| 193 text += cssText; |
| 194 if (node['selector']) { |
| 195 text += CLOSE_BRACE + '\n\n'; |
| 196 } |
| 197 } |
| 198 return text; |
| 199 } |
| 200 |
| 201 /** |
| 202 * @param {Array<StyleNode>} rules |
| 203 * @return {boolean} |
| 204 */ |
| 205 function _hasMixinRules(rules) { |
| 206 let r = rules[0]; |
| 207 return Boolean(r) && Boolean(r['selector']) && r['selector'].indexOf(VAR_START
) === 0; |
| 208 } |
| 209 |
| 210 /** |
| 211 * @param {string} cssText |
| 212 * @return {string} |
| 213 */ |
| 214 function removeCustomProps(cssText) { |
| 215 cssText = removeCustomPropAssignment(cssText); |
| 216 return removeCustomPropApply(cssText); |
| 217 } |
| 218 |
| 219 /** |
| 220 * @param {string} cssText |
| 221 * @return {string} |
| 222 */ |
| 223 export function removeCustomPropAssignment(cssText) { |
| 224 return cssText |
| 225 .replace(RX.customProp, '') |
| 226 .replace(RX.mixinProp, ''); |
| 227 } |
| 228 |
| 229 /** |
| 230 * @param {string} cssText |
| 231 * @return {string} |
| 232 */ |
| 233 function removeCustomPropApply(cssText) { |
| 234 return cssText |
| 235 .replace(RX.mixinApply, '') |
| 236 .replace(RX.varApply, ''); |
| 237 } |
| 238 |
| 239 /** @enum {number} */ |
| 240 export const types = { |
| 241 STYLE_RULE: 1, |
| 242 KEYFRAMES_RULE: 7, |
| 243 MEDIA_RULE: 4, |
| 244 MIXIN_RULE: 1000 |
| 245 } |
| 246 |
| 247 const OPEN_BRACE = '{'; |
| 248 const CLOSE_BRACE = '}'; |
| 249 |
| 250 // helper regexp's |
| 251 const RX = { |
| 252 comments: /\/\*[^*]*\*+([^/*][^*]*\*+)*\//gim, |
| 253 port: /@import[^;]*;/gim, |
| 254 customProp: /(?:^[^;\-\s}]+)?--[^;{}]*?:[^{};]*?(?:[;\n]|$)/gim, |
| 255 mixinProp: /(?:^[^;\-\s}]+)?--[^;{}]*?:[^{};]*?{[^}]*?}(?:[;\n]|$)?/gim, |
| 256 mixinApply: /@apply\s*\(?[^);]*\)?\s*(?:[;\n]|$)?/gim, |
| 257 varApply: /[^;:]*?:[^;]*?var\([^;]*\)(?:[;\n]|$)?/gim, |
| 258 keyframesRule: /^@[^\s]*keyframes/, |
| 259 multipleSpaces: /\s+/g |
| 260 } |
| 261 |
| 262 const VAR_START = '--'; |
| 263 const MEDIA_START = '@media'; |
| 264 const AT_START = '@'; |
| OLD | NEW |