diff options
Diffstat (limited to 'catapult/third_party/polymer/components/shadycss/src/css-parse.js')
-rw-r--r-- | catapult/third_party/polymer/components/shadycss/src/css-parse.js | 264 |
1 files changed, 264 insertions, 0 deletions
diff --git a/catapult/third_party/polymer/components/shadycss/src/css-parse.js b/catapult/third_party/polymer/components/shadycss/src/css-parse.js new file mode 100644 index 00000000..8a8fb1cd --- /dev/null +++ b/catapult/third_party/polymer/components/shadycss/src/css-parse.js @@ -0,0 +1,264 @@ +/** +@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 = '@'; |