diff --git a/src/css-parse.js b/src/css-parse.js new file mode 100644 index 00000000..684fa4ba --- /dev/null +++ b/src/css-parse.js @@ -0,0 +1,603 @@ +// http://www.w3.org/TR/CSS21/grammar.html +// https://github.com/visionmedia/css-parse/pull/49#issuecomment-30088027 +var commentre = /\/\*[^*]*\*+([^/*][^*]*\*+)*\//g + +module.exports = function(css, options){ + options = options || {}; + + /** + * Positional. + */ + + var lineno = 1; + var column = 1; + + /** + * Update lineno and column based on `str`. + */ + + function updatePosition(str) { + var lines = str.match(/\n/g); + if (lines) lineno += lines.length; + var i = str.lastIndexOf('\n'); + column = ~i ? str.length - i : column + str.length; + } + + /** + * Mark position and patch `node.position`. + */ + + function position() { + var start = { line: lineno, column: column }; + return function(node){ + node.position = new Position(start); + whitespace(); + return node; + }; + } + + /** + * Store position information for a node + */ + + function Position(start) { + this.start = start; + this.end = { line: lineno, column: column }; + this.source = options.source; + } + + /** + * Non-enumerable source string + */ + + Position.prototype.content = css; + + /** + * Error `msg`. + */ + + var errorsList = []; + + function error(msg) { + var err = new Error(options.source + ':' + lineno + ':' + column + ': ' + msg); + err.reason = msg; + err.filename = options.source; + err.line = lineno; + err.column = column; + err.source = css; + + if (options.silent) { + errorsList.push(err); + } else { + throw err; + } + } + + /** + * Parse stylesheet. + */ + + function stylesheet() { + var rulesList = rules(); + + return { + type: 'stylesheet', + stylesheet: { + source: options.source, + rules: rulesList, + parsingErrors: errorsList + } + }; + } + + /** + * Opening brace. + */ + + function open() { + return match(/^{\s*/); + } + + /** + * Closing brace. + */ + + function close() { + return match(/^}/); + } + + /** + * Parse ruleset. + */ + + function rules() { + var node; + var rules = []; + whitespace(); + comments(rules); + while (css.length && css.charAt(0) != '}' && (node = atrule() || rule())) { + if (node !== false) { + rules.push(node); + comments(rules); + } + } + return rules; + } + + /** + * Match `re` and return captures. + */ + + function match(re) { + var m = re.exec(css); + if (!m) return; + var str = m[0]; + updatePosition(str); + css = css.slice(str.length); + return m; + } + + /** + * Parse whitespace. + */ + + function whitespace() { + match(/^\s*/); + } + + /** + * Parse comments; + */ + + function comments(rules) { + var c; + rules = rules || []; + while (c = comment()) { + if (c !== false) { + rules.push(c); + } + } + return rules; + } + + /** + * Parse comment. + */ + + function comment() { + var pos = position(); + if ('/' != css.charAt(0) || '*' != css.charAt(1)) return; + + var i = 2; + while ("" != css.charAt(i) && ('*' != css.charAt(i) || '/' != css.charAt(i + 1))) ++i; + i += 2; + + if ("" === css.charAt(i-1)) { + return error('End of comment missing'); + } + + var str = css.slice(2, i - 2); + column += 2; + updatePosition(str); + css = css.slice(i); + column += 2; + + return pos({ + type: 'comment', + comment: str + }); + } + + /** + * Parse selector. + */ + + function selector() { + var m = match(/^([^{]+)/); + if (!m) return; + /* @fix Remove all comments from selectors + * http://ostermiller.org/findcomment.html */ + return trim(m[0]) + .replace(/\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*\/+/g, '') + .replace(/"(?:\\"|[^"])*"|'(?:\\'|[^'])*'/g, function(m) { + return m.replace(/,/g, '\u200C'); + }) + .split(/\s*(?![^(]*\)),\s*/) + .map(function(s) { + return s.replace(/\u200C/g, ','); + }); + } + + /** + * Parse declaration. + */ + + function declaration() { + var pos = position(); + + // prop + var prop = match(/^(\*?[-#\/\*\\\w]+(\[[0-9a-z_-]+\])?)\s*/); + if (!prop) return; + prop = trim(prop[0]); + + // : + if (!match(/^:\s*/)) return error("property missing ':'"); + + // val + var val = match(/^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^\)]*?\)|[^};])+)/); + + var ret = pos({ + type: 'declaration', + property: prop.replace(commentre, ''), + value: val ? trim(val[0]).replace(commentre, '') : '' + }); + + // ; + match(/^[;\s]*/); + + return ret; + } + + /** + * Parse declarations. + */ + + function declarations() { + var decls = []; + + if (!open()) return error("missing '{'"); + comments(decls); + + // declarations + var decl; + while (decl = declaration()) { + if (decl !== false) { + decls.push(decl); + comments(decls); + } + } + + if (!close()) return error("missing '}'"); + return decls; + } + + /** + * Parse keyframe. + */ + + function keyframe() { + var m; + var vals = []; + var pos = position(); + + while (m = match(/^((\d+\.\d+|\.\d+|\d+)%?|[a-z]+)\s*/)) { + vals.push(m[1]); + match(/^,\s*/); + } + + if (!vals.length) return; + + return pos({ + type: 'keyframe', + values: vals, + declarations: declarations() + }); + } + + /** + * Parse keyframes. + */ + + function atkeyframes() { + var pos = position(); + var m = match(/^@([-\w]+)?keyframes\s*/); + + if (!m) return; + var vendor = m[1]; + + // identifier + var m = match(/^([-\w]+)\s*/); + if (!m) return error("@keyframes missing name"); + var name = m[1]; + + if (!open()) return error("@keyframes missing '{'"); + + var frame; + var frames = comments(); + while (frame = keyframe()) { + frames.push(frame); + frames = frames.concat(comments()); + } + + if (!close()) return error("@keyframes missing '}'"); + + return pos({ + type: 'keyframes', + name: name, + vendor: vendor, + keyframes: frames + }); + } + + /** + * Parse supports. + */ + + function atsupports() { + var pos = position(); + var m = match(/^@supports *([^{]+)/); + + if (!m) return; + var supports = trim(m[1]); + + if (!open()) return error("@supports missing '{'"); + + var style = comments().concat(rules()); + + if (!close()) return error("@supports missing '}'"); + + return pos({ + type: 'supports', + supports: supports, + rules: style + }); + } + + /** + * Parse host. + */ + + function athost() { + var pos = position(); + var m = match(/^@host\s*/); + + if (!m) return; + + if (!open()) return error("@host missing '{'"); + + var style = comments().concat(rules()); + + if (!close()) return error("@host missing '}'"); + + return pos({ + type: 'host', + rules: style + }); + } + + /** + * Parse media. + */ + + function atmedia() { + var pos = position(); + var m = match(/^@media *([^{]+)/); + + if (!m) return; + var media = trim(m[1]); + + if (!open()) return error("@media missing '{'"); + + var style = comments().concat(rules()); + + if (!close()) return error("@media missing '}'"); + + return pos({ + type: 'media', + media: media, + rules: style + }); + } + + + /** + * Parse custom-media. + */ + + function atcustommedia() { + var pos = position(); + var m = match(/^@custom-media\s+(--[^\s]+)\s*([^{;]+);/); + if (!m) return; + + return pos({ + type: 'custom-media', + name: trim(m[1]), + media: trim(m[2]) + }); + } + + /** + * Parse paged media. + */ + + function atpage() { + var pos = position(); + var m = match(/^@page */); + if (!m) return; + + var sel = selector() || []; + + if (!open()) return error("@page missing '{'"); + var decls = comments(); + + // declarations + var decl; + while (decl = declaration()) { + decls.push(decl); + decls = decls.concat(comments()); + } + + if (!close()) return error("@page missing '}'"); + + return pos({ + type: 'page', + selectors: sel, + declarations: decls + }); + } + + /** + * Parse document. + */ + + function atdocument() { + var pos = position(); + var m = match(/^@([-\w]+)?document *([^{]+)/); + if (!m) return; + + var vendor = trim(m[1]); + var doc = trim(m[2]); + + if (!open()) return error("@document missing '{'"); + + var style = comments().concat(rules()); + + if (!close()) return error("@document missing '}'"); + + return pos({ + type: 'document', + document: doc, + vendor: vendor, + rules: style + }); + } + + /** + * Parse font-face. + */ + + function atfontface() { + var pos = position(); + var m = match(/^@font-face\s*/); + if (!m) return; + + if (!open()) return error("@font-face missing '{'"); + var decls = comments(); + + // declarations + var decl; + while (decl = declaration()) { + decls.push(decl); + decls = decls.concat(comments()); + } + + if (!close()) return error("@font-face missing '}'"); + + return pos({ + type: 'font-face', + declarations: decls + }); + } + + /** + * Parse import + */ + + var atimport = _compileAtrule('import'); + + /** + * Parse charset + */ + + var atcharset = _compileAtrule('charset'); + + /** + * Parse namespace + */ + + var atnamespace = _compileAtrule('namespace'); + + /** + * Parse non-block at-rules + */ + + + function _compileAtrule(name) { + var re = new RegExp('^@' + name + '\\s*([^;]+);'); + return function() { + var pos = position(); + var m = match(re); + if (!m) return; + var ret = { type: name }; + ret[name] = m[1].trim(); + return pos(ret); + } + } + + /** + * Parse at rule. + */ + + function atrule() { + if (css[0] != '@') return; + + return atkeyframes() + || atmedia() + || atcustommedia() + || atsupports() + || atimport() + || atcharset() + || atnamespace() + || atdocument() + || atpage() + || athost() + || atfontface(); + } + + /** + * Parse rule. + */ + + function rule() { + var pos = position(); + var sel = selector(); + + if (!sel) return error('selector missing'); + comments(); + + return pos({ + type: 'rule', + selectors: sel, + declarations: declarations() + }); + } + + return addParent(stylesheet()); +}; + +/** + * Trim `str`. + */ + +function trim(str) { + return str ? str.replace(/^\s+|\s+$/g, '') : ''; +} + +/** + * Adds non-enumerable parent node reference to each node. + */ + +function addParent(obj, parent) { + var isNode = obj && typeof obj.type === 'string'; + var childParent = isNode ? obj : parent; + + for (var k in obj) { + var value = obj[k]; + if (Array.isArray(value)) { + value.forEach(function(v) { addParent(v, childParent); }); + } else if (value && typeof value === 'object') { + addParent(value, childParent); + } + } + + if (isNode) { + Object.defineProperty(obj, 'parent', { + configurable: true, + writable: true, + enumerable: false, + value: parent || null + }); + } + + return obj; +} \ No newline at end of file diff --git a/src/utils.js b/src/utils.js index 2f07fb8e..57c70c62 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,5 +1,5 @@ import redent from 'redent' -import cssParse from 'css/lib/parse' +import cssParse from './css-parse' import isEqual from 'lodash/isEqual' class GenericTypeError extends Error {