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