| /** |
| @license |
| Copyright (c) 2017 The Polymer Project Authors. All rights reserved. |
| This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt |
| The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt |
| The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt |
| Code distributed by Google as part of the polymer project is also |
| subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt |
| */ |
| |
| /* |
| Extremely simple css parser. Intended to be not more than what we need |
| and definitely not necessarily correct =). |
| */ |
| |
| 'use strict'; |
| |
| /** @unrestricted */ |
| class StyleNode { |
| constructor() { |
| /** @type {number} */ |
| this['start'] = 0; |
| /** @type {number} */ |
| this['end'] = 0; |
| /** @type {StyleNode} */ |
| this['previous'] = null; |
| /** @type {StyleNode} */ |
| this['parent'] = null; |
| /** @type {Array<StyleNode>} */ |
| this['rules'] = null; |
| /** @type {string} */ |
| this['parsedCssText'] = ''; |
| /** @type {string} */ |
| this['cssText'] = ''; |
| /** @type {boolean} */ |
| this['atRule'] = false; |
| /** @type {number} */ |
| this['type'] = 0; |
| /** @type {string} */ |
| this['keyframesName'] = ''; |
| /** @type {string} */ |
| this['selector'] = ''; |
| /** @type {string} */ |
| this['parsedSelector'] = ''; |
| } |
| } |
| |
| export {StyleNode} |
| |
| // given a string of css, return a simple rule tree |
| /** |
| * @param {string} text |
| * @return {StyleNode} |
| */ |
| export function parse(text) { |
| text = clean(text); |
| return parseCss(lex(text), text); |
| } |
| |
| // remove stuff we don't care about that may hinder parsing |
| /** |
| * @param {string} cssText |
| * @return {string} |
| */ |
| function clean(cssText) { |
| return cssText.replace(RX.comments, '').replace(RX.port, ''); |
| } |
| |
| // super simple {...} lexer that returns a node tree |
| /** |
| * @param {string} text |
| * @return {StyleNode} |
| */ |
| function lex(text) { |
| let root = new StyleNode(); |
| root['start'] = 0; |
| root['end'] = text.length |
| let n = root; |
| for (let i = 0, l = text.length; i < l; i++) { |
| if (text[i] === OPEN_BRACE) { |
| if (!n['rules']) { |
| n['rules'] = []; |
| } |
| let p = n; |
| let previous = p['rules'][p['rules'].length - 1] || null; |
| n = new StyleNode(); |
| n['start'] = i + 1; |
| n['parent'] = p; |
| n['previous'] = previous; |
| p['rules'].push(n); |
| } else if (text[i] === CLOSE_BRACE) { |
| n['end'] = i + 1; |
| n = n['parent'] || root; |
| } |
| } |
| return root; |
| } |
| |
| // add selectors/cssText to node tree |
| /** |
| * @param {StyleNode} node |
| * @param {string} text |
| * @return {StyleNode} |
| */ |
| function parseCss(node, text) { |
| let t = text.substring(node['start'], node['end'] - 1); |
| node['parsedCssText'] = node['cssText'] = t.trim(); |
| if (node['parent']) { |
| let ss = node['previous'] ? node['previous']['end'] : node['parent']['start']; |
| t = text.substring(ss, node['start'] - 1); |
| t = _expandUnicodeEscapes(t); |
| t = t.replace(RX.multipleSpaces, ' '); |
| // TODO(sorvell): ad hoc; make selector include only after last ; |
| // helps with mixin syntax |
| t = t.substring(t.lastIndexOf(';') + 1); |
| let s = node['parsedSelector'] = node['selector'] = t.trim(); |
| node['atRule'] = (s.indexOf(AT_START) === 0); |
| // note, support a subset of rule types... |
| if (node['atRule']) { |
| if (s.indexOf(MEDIA_START) === 0) { |
| node['type'] = types.MEDIA_RULE; |
| } else if (s.match(RX.keyframesRule)) { |
| node['type'] = types.KEYFRAMES_RULE; |
| node['keyframesName'] = |
| node['selector'].split(RX.multipleSpaces).pop(); |
| } |
| } else { |
| if (s.indexOf(VAR_START) === 0) { |
| node['type'] = types.MIXIN_RULE; |
| } else { |
| node['type'] = types.STYLE_RULE; |
| } |
| } |
| } |
| let r$ = node['rules']; |
| if (r$) { |
| for (let i = 0, l = r$.length, r; |
| (i < l) && (r = r$[i]); i++) { |
| parseCss(r, text); |
| } |
| } |
| return node; |
| } |
| |
| /** |
| * conversion of sort unicode escapes with spaces like `\33 ` (and longer) into |
| * expanded form that doesn't require trailing space `\000033` |
| * @param {string} s |
| * @return {string} |
| */ |
| function _expandUnicodeEscapes(s) { |
| return s.replace(/\\([0-9a-f]{1,6})\s/gi, function() { |
| let code = arguments[1], |
| repeat = 6 - code.length; |
| while (repeat--) { |
| code = '0' + code; |
| } |
| return '\\' + code; |
| }); |
| } |
| |
| /** |
| * stringify parsed css. |
| * @param {StyleNode} node |
| * @param {boolean=} preserveProperties |
| * @param {string=} text |
| * @return {string} |
| */ |
| export function stringify(node, preserveProperties, text = '') { |
| // calc rule cssText |
| let cssText = ''; |
| if (node['cssText'] || node['rules']) { |
| let r$ = node['rules']; |
| if (r$ && !_hasMixinRules(r$)) { |
| for (let i = 0, l = r$.length, r; |
| (i < l) && (r = r$[i]); i++) { |
| cssText = stringify(r, preserveProperties, cssText); |
| } |
| } else { |
| cssText = preserveProperties ? node['cssText'] : |
| removeCustomProps(node['cssText']); |
| cssText = cssText.trim(); |
| if (cssText) { |
| cssText = ' ' + cssText + '\n'; |
| } |
| } |
| } |
| // emit rule if there is cssText |
| if (cssText) { |
| if (node['selector']) { |
| text += node['selector'] + ' ' + OPEN_BRACE + '\n'; |
| } |
| text += cssText; |
| if (node['selector']) { |
| text += CLOSE_BRACE + '\n\n'; |
| } |
| } |
| return text; |
| } |
| |
| /** |
| * @param {Array<StyleNode>} rules |
| * @return {boolean} |
| */ |
| function _hasMixinRules(rules) { |
| let r = rules[0]; |
| return Boolean(r) && Boolean(r['selector']) && r['selector'].indexOf(VAR_START) === 0; |
| } |
| |
| /** |
| * @param {string} cssText |
| * @return {string} |
| */ |
| function removeCustomProps(cssText) { |
| cssText = removeCustomPropAssignment(cssText); |
| return removeCustomPropApply(cssText); |
| } |
| |
| /** |
| * @param {string} cssText |
| * @return {string} |
| */ |
| export function removeCustomPropAssignment(cssText) { |
| return cssText |
| .replace(RX.customProp, '') |
| .replace(RX.mixinProp, ''); |
| } |
| |
| /** |
| * @param {string} cssText |
| * @return {string} |
| */ |
| function removeCustomPropApply(cssText) { |
| return cssText |
| .replace(RX.mixinApply, '') |
| .replace(RX.varApply, ''); |
| } |
| |
| /** @enum {number} */ |
| export const types = { |
| STYLE_RULE: 1, |
| KEYFRAMES_RULE: 7, |
| MEDIA_RULE: 4, |
| MIXIN_RULE: 1000 |
| } |
| |
| const OPEN_BRACE = '{'; |
| const CLOSE_BRACE = '}'; |
| |
| // helper regexp's |
| const RX = { |
| comments: /\/\*[^*]*\*+([^/*][^*]*\*+)*\//gim, |
| port: /@import[^;]*;/gim, |
| customProp: /(?:^[^;\-\s}]+)?--[^;{}]*?:[^{};]*?(?:[;\n]|$)/gim, |
| mixinProp: /(?:^[^;\-\s}]+)?--[^;{}]*?:[^{};]*?{[^}]*?}(?:[;\n]|$)?/gim, |
| mixinApply: /@apply\s*\(?[^);]*\)?\s*(?:[;\n]|$)?/gim, |
| varApply: /[^;:]*?:[^;]*?var\([^;]*\)(?:[;\n]|$)?/gim, |
| keyframesRule: /^@[^\s]*keyframes/, |
| multipleSpaces: /\s+/g |
| } |
| |
| const VAR_START = '--'; |
| const MEDIA_START = '@media'; |
| const AT_START = '@'; |