UNPKG

5.09 kBJavaScriptView Raw
1// http://www.w3.org/TR/CSS21/grammar.html
2// https://github.com/visionmedia/css-parse/pull/49#issuecomment-30088027
3var COMMENT_REGEX = /\/\*[^*]*\*+([^/*][^*]*\*+)*\//g;
4
5var NEWLINE_REGEX = /\n/g;
6var WHITESPACE_REGEX = /^\s*/;
7
8// declaration
9var PROPERTY_REGEX = /^(\*?[-#/*\\\w]+(\[[0-9a-z_-]+\])?)\s*/;
10var COLON_REGEX = /^:\s*/;
11var VALUE_REGEX = /^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^)]*?\)|[^};])+)/;
12var SEMICOLON_REGEX = /^[;\s]*/;
13
14// https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/Trim#Polyfill
15var TRIM_REGEX = /^\s+|\s+$/g;
16
17// strings
18var NEWLINE = '\n';
19var FORWARD_SLASH = '/';
20var ASTERISK = '*';
21var EMPTY_STRING = '';
22
23// types
24var TYPE_COMMENT = 'comment';
25var TYPE_DECLARATION = 'declaration';
26
27/**
28 * @param {String} style
29 * @param {Object} [options]
30 * @return {Object[]}
31 * @throws {TypeError}
32 * @throws {Error}
33 */
34module.exports = function (style, options) {
35 if (typeof style !== 'string') {
36 throw new TypeError('First argument must be a string');
37 }
38
39 if (!style) return [];
40
41 options = options || {};
42
43 /**
44 * Positional.
45 */
46 var lineno = 1;
47 var column = 1;
48
49 /**
50 * Update lineno and column based on `str`.
51 *
52 * @param {String} str
53 */
54 function updatePosition(str) {
55 var lines = str.match(NEWLINE_REGEX);
56 if (lines) lineno += lines.length;
57 var i = str.lastIndexOf(NEWLINE);
58 column = ~i ? str.length - i : column + str.length;
59 }
60
61 /**
62 * Mark position and patch `node.position`.
63 *
64 * @return {Function}
65 */
66 function position() {
67 var start = { line: lineno, column: column };
68 return function (node) {
69 node.position = new Position(start);
70 whitespace();
71 return node;
72 };
73 }
74
75 /**
76 * Store position information for a node.
77 *
78 * @constructor
79 * @property {Object} start
80 * @property {Object} end
81 * @property {undefined|String} source
82 */
83 function Position(start) {
84 this.start = start;
85 this.end = { line: lineno, column: column };
86 this.source = options.source;
87 }
88
89 /**
90 * Non-enumerable source string.
91 */
92 Position.prototype.content = style;
93
94 var errorsList = [];
95
96 /**
97 * Error `msg`.
98 *
99 * @param {String} msg
100 * @throws {Error}
101 */
102 function error(msg) {
103 var err = new Error(
104 options.source + ':' + lineno + ':' + column + ': ' + msg
105 );
106 err.reason = msg;
107 err.filename = options.source;
108 err.line = lineno;
109 err.column = column;
110 err.source = style;
111
112 if (options.silent) {
113 errorsList.push(err);
114 } else {
115 throw err;
116 }
117 }
118
119 /**
120 * Match `re` and return captures.
121 *
122 * @param {RegExp} re
123 * @return {undefined|Array}
124 */
125 function match(re) {
126 var m = re.exec(style);
127 if (!m) return;
128 var str = m[0];
129 updatePosition(str);
130 style = style.slice(str.length);
131 return m;
132 }
133
134 /**
135 * Parse whitespace.
136 */
137 function whitespace() {
138 match(WHITESPACE_REGEX);
139 }
140
141 /**
142 * Parse comments.
143 *
144 * @param {Object[]} [rules]
145 * @return {Object[]}
146 */
147 function comments(rules) {
148 var c;
149 rules = rules || [];
150 while ((c = comment())) {
151 if (c !== false) {
152 rules.push(c);
153 }
154 }
155 return rules;
156 }
157
158 /**
159 * Parse comment.
160 *
161 * @return {Object}
162 * @throws {Error}
163 */
164 function comment() {
165 var pos = position();
166 if (FORWARD_SLASH != style.charAt(0) || ASTERISK != style.charAt(1)) return;
167
168 var i = 2;
169 while (
170 EMPTY_STRING != style.charAt(i) &&
171 (ASTERISK != style.charAt(i) || FORWARD_SLASH != style.charAt(i + 1))
172 ) {
173 ++i;
174 }
175 i += 2;
176
177 if (EMPTY_STRING === style.charAt(i - 1)) {
178 return error('End of comment missing');
179 }
180
181 var str = style.slice(2, i - 2);
182 column += 2;
183 updatePosition(str);
184 style = style.slice(i);
185 column += 2;
186
187 return pos({
188 type: TYPE_COMMENT,
189 comment: str
190 });
191 }
192
193 /**
194 * Parse declaration.
195 *
196 * @return {Object}
197 * @throws {Error}
198 */
199 function declaration() {
200 var pos = position();
201
202 // prop
203 var prop = match(PROPERTY_REGEX);
204 if (!prop) return;
205 comment();
206
207 // :
208 if (!match(COLON_REGEX)) return error("property missing ':'");
209
210 // val
211 var val = match(VALUE_REGEX);
212
213 var ret = pos({
214 type: TYPE_DECLARATION,
215 property: trim(prop[0].replace(COMMENT_REGEX, EMPTY_STRING)),
216 value: val
217 ? trim(val[0].replace(COMMENT_REGEX, EMPTY_STRING))
218 : EMPTY_STRING
219 });
220
221 // ;
222 match(SEMICOLON_REGEX);
223
224 return ret;
225 }
226
227 /**
228 * Parse declarations.
229 *
230 * @return {Object[]}
231 */
232 function declarations() {
233 var decls = [];
234
235 comments(decls);
236
237 // declarations
238 var decl;
239 while ((decl = declaration())) {
240 if (decl !== false) {
241 decls.push(decl);
242 comments(decls);
243 }
244 }
245
246 return decls;
247 }
248
249 whitespace();
250 return declarations();
251};
252
253/**
254 * Trim `str`.
255 *
256 * @param {String} str
257 * @return {String}
258 */
259function trim(str) {
260 return str ? str.replace(TRIM_REGEX, EMPTY_STRING) : EMPTY_STRING;
261}