UNPKG

10.7 kBJavaScriptView Raw
1// http://www.w3.org/TR/CSS21/grammar.html
2// https://github.com/visionmedia/css-parse/pull/49#issuecomment-30088027
3var commentre = /\/\*[^*]*\*+([^/*][^*]*\*+)*\//g
4
5module.exports = function(css, options){
6 options = options || {};
7
8 /**
9 * Positional.
10 */
11
12 var lineno = 1;
13 var column = 1;
14
15 /**
16 * Update lineno and column based on `str`.
17 */
18
19 function updatePosition(str) {
20 var lines = str.match(/\n/g);
21 if (lines) lineno += lines.length;
22 var i = str.lastIndexOf('\n');
23 column = ~i ? str.length - i : column + str.length;
24 }
25
26 /**
27 * Mark position and patch `node.position`.
28 */
29
30 function position() {
31 var start = { line: lineno, column: column };
32 return function(node){
33 node.position = new Position(start);
34 whitespace();
35 return node;
36 };
37 }
38
39 /**
40 * Store position information for a node
41 */
42
43 function Position(start) {
44 this.start = start;
45 this.end = { line: lineno, column: column };
46 this.source = options.source;
47 }
48
49 /**
50 * Non-enumerable source string
51 */
52
53 Position.prototype.content = css;
54
55 /**
56 * Error `msg`.
57 */
58
59 function error(msg) {
60 if (options.silent === true) {
61 return false;
62 }
63
64 var err = new Error(msg + ' near line ' + lineno + ':' + column);
65 err.filename = options.source;
66 err.line = lineno;
67 err.column = column;
68 err.source = css;
69 throw err;
70 }
71
72 /**
73 * Parse stylesheet.
74 */
75
76 function stylesheet() {
77 return {
78 type: 'stylesheet',
79 stylesheet: {
80 rules: rules()
81 }
82 };
83 }
84
85 /**
86 * Opening brace.
87 */
88
89 function open() {
90 return match(/^{\s*/);
91 }
92
93 /**
94 * Closing brace.
95 */
96
97 function close() {
98 return match(/^}/);
99 }
100
101 /**
102 * Parse ruleset.
103 */
104
105 function rules() {
106 var node;
107 var rules = [];
108 whitespace();
109 comments(rules);
110 while (css.length && css.charAt(0) != '}' && (node = atrule() || rule())) {
111 if (node !== false) {
112 rules.push(node);
113 comments(rules);
114 }
115 }
116 return rules;
117 }
118
119 /**
120 * Match `re` and return captures.
121 */
122
123 function match(re) {
124 var m = re.exec(css);
125 if (!m) return;
126 var str = m[0];
127 updatePosition(str);
128 css = css.slice(str.length);
129 return m;
130 }
131
132 /**
133 * Parse whitespace.
134 */
135
136 function whitespace() {
137 match(/^\s*/);
138 }
139
140 /**
141 * Parse comments;
142 */
143
144 function comments(rules) {
145 var c;
146 rules = rules || [];
147 while (c = comment()) {
148 if (c !== false) {
149 rules.push(c);
150 }
151 }
152 return rules;
153 }
154
155 /**
156 * Parse comment.
157 */
158
159 function comment() {
160 var pos = position();
161 if ('/' != css.charAt(0) || '*' != css.charAt(1)) return;
162
163 var i = 2;
164 while ("" != css.charAt(i) && ('*' != css.charAt(i) || '/' != css.charAt(i + 1))) ++i;
165 i += 2;
166
167 if ("" === css.charAt(i-1)) {
168 return error('End of comment missing');
169 }
170
171 var str = css.slice(2, i - 2);
172 column += 2;
173 updatePosition(str);
174 css = css.slice(i);
175 column += 2;
176
177 return pos({
178 type: 'comment',
179 comment: str
180 });
181 }
182
183 /**
184 * Parse selector.
185 */
186
187 function selector() {
188 var m = match(/^([^{]+)/);
189 if (!m) return;
190 /* @fix Remove all comments from selectors
191 * http://ostermiller.org/findcomment.html */
192 return trim(m[0])
193 .replace(/\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*\/+/g, '')
194 .replace(/(?:"[^"]*"|'[^']*')/g, function(m) {
195 return m.replace(/,/g, '\u200C');
196 })
197 .split(/\s*(?![^(]*\)),\s*/)
198 .map(function(s) {
199 return s.replace(/\u200C/g, ',');
200 });
201 }
202
203 /**
204 * Parse declaration.
205 */
206
207 function declaration() {
208 var pos = position();
209
210 // prop
211 var prop = match(/^(\*?[-#\/\*\\\w]+(\[[0-9a-z_-]+\])?)\s*/);
212 if (!prop) return;
213 prop = trim(prop[0]);
214
215 // :
216 if (!match(/^:\s*/)) return error("property missing ':'");
217
218 // val
219 var val = match(/^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^\)]*?\)|[^};])+)/);
220
221 var ret = pos({
222 type: 'declaration',
223 property: prop.replace(commentre, ''),
224 value: val ? trim(val[0]).replace(commentre, '') : ''
225 });
226
227 // ;
228 match(/^[;\s]*/);
229
230 return ret;
231 }
232
233 /**
234 * Parse declarations.
235 */
236
237 function declarations() {
238 var decls = [];
239
240 if (!open()) return error("missing '{'");
241 comments(decls);
242
243 // declarations
244 var decl;
245 while (decl = declaration()) {
246 if (decl !== false) {
247 decls.push(decl);
248 comments(decls);
249 }
250 }
251
252 if (!close()) return error("missing '}'");
253 return decls;
254 }
255
256 /**
257 * Parse keyframe.
258 */
259
260 function keyframe() {
261 var m;
262 var vals = [];
263 var pos = position();
264
265 while (m = match(/^((\d+\.\d+|\.\d+|\d+)%?|[a-z]+)\s*/)) {
266 vals.push(m[1]);
267 match(/^,\s*/);
268 }
269
270 if (!vals.length) return;
271
272 return pos({
273 type: 'keyframe',
274 values: vals,
275 declarations: declarations()
276 });
277 }
278
279 /**
280 * Parse keyframes.
281 */
282
283 function atkeyframes() {
284 var pos = position();
285 var m = match(/^@([-\w]+)?keyframes */);
286
287 if (!m) return;
288 var vendor = m[1];
289
290 // identifier
291 var m = match(/^([-\w]+)\s*/);
292 if (!m) return error("@keyframes missing name");
293 var name = m[1];
294
295 if (!open()) return error("@keyframes missing '{'");
296
297 var frame;
298 var frames = comments();
299 while (frame = keyframe()) {
300 frames.push(frame);
301 frames = frames.concat(comments());
302 }
303
304 if (!close()) return error("@keyframes missing '}'");
305
306 return pos({
307 type: 'keyframes',
308 name: name,
309 vendor: vendor,
310 keyframes: frames
311 });
312 }
313
314 /**
315 * Parse supports.
316 */
317
318 function atsupports() {
319 var pos = position();
320 var m = match(/^@supports *([^{]+)/);
321
322 if (!m) return;
323 var supports = trim(m[1]);
324
325 if (!open()) return error("@supports missing '{'");
326
327 var style = comments().concat(rules());
328
329 if (!close()) return error("@supports missing '}'");
330
331 return pos({
332 type: 'supports',
333 supports: supports,
334 rules: style
335 });
336 }
337
338 /**
339 * Parse host.
340 */
341
342 function athost() {
343 var pos = position();
344 var m = match(/^@host */);
345
346 if (!m) return;
347
348 if (!open()) return error("@host missing '{'");
349
350 var style = comments().concat(rules());
351
352 if (!close()) return error("@host missing '}'");
353
354 return pos({
355 type: 'host',
356 rules: style
357 });
358 }
359
360 /**
361 * Parse media.
362 */
363
364 function atmedia() {
365 var pos = position();
366 var m = match(/^@media *([^{]+)/);
367
368 if (!m) return;
369 var media = trim(m[1]);
370
371 if (!open()) return error("@media missing '{'");
372
373 var style = comments().concat(rules());
374
375 if (!close()) return error("@media missing '}'");
376
377 return pos({
378 type: 'media',
379 media: media,
380 rules: style
381 });
382 }
383
384
385 /**
386 * Parse custom-media.
387 */
388
389 function atcustommedia() {
390 var pos = position();
391 var m = match(/^@custom-media (--[^\s]+) *([^{;]+);/);
392 if (!m) return;
393
394 return pos({
395 type: 'custom-media',
396 name: trim(m[1]),
397 media: trim(m[2])
398 });
399 }
400
401 /**
402 * Parse paged media.
403 */
404
405 function atpage() {
406 var pos = position();
407 var m = match(/^@page */);
408 if (!m) return;
409
410 var sel = selector() || [];
411
412 if (!open()) return error("@page missing '{'");
413 var decls = comments();
414
415 // declarations
416 var decl;
417 while (decl = declaration()) {
418 decls.push(decl);
419 decls = decls.concat(comments());
420 }
421
422 if (!close()) return error("@page missing '}'");
423
424 return pos({
425 type: 'page',
426 selectors: sel,
427 declarations: decls
428 });
429 }
430
431 /**
432 * Parse document.
433 */
434
435 function atdocument() {
436 var pos = position();
437 var m = match(/^@([-\w]+)?document *([^{]+)/);
438 if (!m) return;
439
440 var vendor = trim(m[1]);
441 var doc = trim(m[2]);
442
443 if (!open()) return error("@document missing '{'");
444
445 var style = comments().concat(rules());
446
447 if (!close()) return error("@document missing '}'");
448
449 return pos({
450 type: 'document',
451 document: doc,
452 vendor: vendor,
453 rules: style
454 });
455 }
456
457 /**
458 * Parse font-face.
459 */
460
461 function atfontface() {
462 var pos = position();
463 var m = match(/^@font-face */);
464 if (!m) return;
465
466 if (!open()) return error("@font-face missing '{'");
467 var decls = comments();
468
469 // declarations
470 var decl;
471 while (decl = declaration()) {
472 decls.push(decl);
473 decls = decls.concat(comments());
474 }
475
476 if (!close()) return error("@font-face missing '}'");
477
478 return pos({
479 type: 'font-face',
480 declarations: decls
481 });
482 }
483
484 /**
485 * Parse import
486 */
487
488 var atimport = _compileAtrule('import');
489
490 /**
491 * Parse charset
492 */
493
494 var atcharset = _compileAtrule('charset');
495
496 /**
497 * Parse namespace
498 */
499
500 var atnamespace = _compileAtrule('namespace');
501
502 /**
503 * Parse non-block at-rules
504 */
505
506
507 function _compileAtrule(name) {
508 var re = new RegExp('^@' + name + ' *([^;\\n]+);');
509 return function() {
510 var pos = position();
511 var m = match(re);
512 if (!m) return;
513 var ret = { type: name };
514 ret[name] = m[1].trim();
515 return pos(ret);
516 }
517 }
518
519 /**
520 * Parse at rule.
521 */
522
523 function atrule() {
524 if (css[0] != '@') return;
525
526 return atkeyframes()
527 || atmedia()
528 || atcustommedia()
529 || atsupports()
530 || atimport()
531 || atcharset()
532 || atnamespace()
533 || atdocument()
534 || atpage()
535 || athost()
536 || atfontface();
537 }
538
539 /**
540 * Parse rule.
541 */
542
543 function rule() {
544 var pos = position();
545 var sel = selector();
546
547 if (!sel) return error('selector missing');
548 comments();
549
550 return pos({
551 type: 'rule',
552 selectors: sel,
553 declarations: declarations()
554 });
555 }
556
557 return addParent(stylesheet());
558};
559
560/**
561 * Trim `str`.
562 */
563
564function trim(str) {
565 return str ? str.replace(/^\s+|\s+$/g, '') : '';
566}
567
568/**
569 * Adds non-enumerable parent node reference to each node.
570 */
571
572function addParent(obj, parent) {
573 var isNode = obj && typeof obj.type === 'string';
574 var childParent = isNode ? obj : parent;
575
576 for (var k in obj) {
577 var value = obj[k];
578 if (Array.isArray(value)) {
579 value.forEach(function(v) { addParent(v, childParent); });
580 } else if (value && typeof value === 'object') {
581 addParent(value, childParent);
582 }
583 }
584
585 if (isNode) {
586 Object.defineProperty(obj, 'parent', {
587 configurable: true,
588 writable: true,
589 enumerable: false,
590 value: parent || null
591 });
592 }
593
594 return obj;
595}