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 }; if (!options.position) return positionNoop; return function(node){ node.position = { start: start, end: { line: lineno, column: column }, source: options.source }; whitespace(); return node; } } /** * Return `node`. */ function positionNoop(node) { whitespace(); return node; } /** * Error `msg`. */ function error(msg) { var err = new Error(msg + ' near line ' + lineno + ':' + column); err.filename = options.source; err.line = lineno; err.column = column; err.source = css; throw err; } /** * Parse stylesheet. */ function stylesheet() { return { type: 'stylesheet', stylesheet: { rules: rules() } }; } /** * 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.charAt(0) != '}' && (node = atrule() || rule())) { 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()) rules.push(c); return rules; } /** * Parse comment. */ function comment() { var pos = position(); if ('/' != css.charAt(0) || '*' != css.charAt(1)) return; var i = 2; while (null != css.charAt(i) && ('*' != css.charAt(i) || '/' != css.charAt(i + 1))) ++i; i += 2; 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; return trim(m[0]).split(/\s*,\s*/); } /** * 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(/^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^\)]*?\)|[^};])+)/); if (!val) return error('property missing value'); var ret = pos({ type: 'declaration', property: prop, value: trim(val[0]) }); // ; match(/^[;\s]*/); return ret; } /** * Parse declarations. */ function declarations() { var decls = []; if (!open()) return error("missing '{'"); comments(decls); // declarations var decl; while (decl = declaration()) { 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 */); 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 */); 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 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 import */ function atimport() { return _atrule('import'); } /** * Parse charset */ function atcharset() { return _atrule('charset'); } /** * Parse namespace */ function atnamespace() { return _atrule('namespace') } /** * Parse non-block at-rules */ function _atrule(name) { var pos = position(); var m = match(new RegExp('^@' + name + ' *([^;\\n]+);')); if (!m) return; var ret = { type: name }; ret[name] = trim(m[1]); return pos(ret); } /** * Parse at rule. */ function atrule() { if (css[0] != '@') return; return atkeyframes() || atmedia() || atsupports() || atimport() || atcharset() || atnamespace() || atdocument() || atpage() || athost(); } /** * Parse rule. */ function rule() { var pos = position(); var sel = selector(); if (!sel) return; comments(); return pos({ type: 'rule', selectors: sel, declarations: declarations() }); } return stylesheet(); }; /** * Trim `str`. */ function trim(str) { return str ? str.replace(/^\s+|\s+$/g, '') : ''; }