1/** 2@license 3Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 4This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt 5The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt 6The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt 7Code distributed by Google as part of the polymer project is also 8subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt 9*/ 10 11/* 12Extremely simple css parser. Intended to be not more than what we need 13and definitely not necessarily correct =). 14*/ 15 16'use strict'; 17 18/** @unrestricted */ 19class 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 48export {StyleNode} 49 50// given a string of css, return a simple rule tree 51/** 52 * @param {string} text 53 * @return {StyleNode} 54 */ 55export 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 */ 65function 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 */ 74function 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 */ 105function 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 */ 151function _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 */ 169export 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 */ 205function _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 */ 214function removeCustomProps(cssText) { 215 cssText = removeCustomPropAssignment(cssText); 216 return removeCustomPropApply(cssText); 217} 218 219/** 220 * @param {string} cssText 221 * @return {string} 222 */ 223export 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 */ 233function removeCustomPropApply(cssText) { 234 return cssText 235 .replace(RX.mixinApply, '') 236 .replace(RX.varApply, ''); 237} 238 239/** @enum {number} */ 240export const types = { 241 STYLE_RULE: 1, 242 KEYFRAMES_RULE: 7, 243 MEDIA_RULE: 4, 244 MIXIN_RULE: 1000 245} 246 247const OPEN_BRACE = '{'; 248const CLOSE_BRACE = '}'; 249 250// helper regexp's 251const 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 262const VAR_START = '--'; 263const MEDIA_START = '@media'; 264const AT_START = '@'; 265