UNPKG

18.5 kBJavaScriptView Raw
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3 Author Tobias Koppers @sokra
4*/
5
6"use strict";
7
8const Parser = require("../Parser");
9const ConstDependency = require("../dependencies/ConstDependency");
10const CssExportDependency = require("../dependencies/CssExportDependency");
11const CssImportDependency = require("../dependencies/CssImportDependency");
12const CssLocalIdentifierDependency = require("../dependencies/CssLocalIdentifierDependency");
13const CssSelfLocalIdentifierDependency = require("../dependencies/CssSelfLocalIdentifierDependency");
14const CssUrlDependency = require("../dependencies/CssUrlDependency");
15const StaticExportsDependency = require("../dependencies/StaticExportsDependency");
16const walkCssTokens = require("./walkCssTokens");
17
18/** @typedef {import("../Parser").ParserState} ParserState */
19/** @typedef {import("../Parser").PreparsedAst} PreparsedAst */
20
21const CC_LEFT_CURLY = "{".charCodeAt(0);
22const CC_RIGHT_CURLY = "}".charCodeAt(0);
23const CC_COLON = ":".charCodeAt(0);
24const CC_SLASH = "/".charCodeAt(0);
25const CC_SEMICOLON = ";".charCodeAt(0);
26
27const cssUnescape = str => {
28 return str.replace(/\\([0-9a-fA-F]{1,6}[ \t\n\r\f]?|[\s\S])/g, match => {
29 if (match.length > 2) {
30 return String.fromCharCode(parseInt(match.slice(1).trim(), 16));
31 } else {
32 return match[1];
33 }
34 });
35};
36
37class LocConverter {
38 constructor(input) {
39 this._input = input;
40 this.line = 1;
41 this.column = 0;
42 this.pos = 0;
43 }
44
45 get(pos) {
46 if (this.pos !== pos) {
47 if (this.pos < pos) {
48 const str = this._input.slice(this.pos, pos);
49 let i = str.lastIndexOf("\n");
50 if (i === -1) {
51 this.column += str.length;
52 } else {
53 this.column = str.length - i - 1;
54 this.line++;
55 while (i > 0 && (i = str.lastIndexOf("\n", i - 1)) !== -1)
56 this.line++;
57 }
58 } else {
59 let i = this._input.lastIndexOf("\n", this.pos);
60 while (i >= pos) {
61 this.line--;
62 i = i > 0 ? this._input.lastIndexOf("\n", i - 1) : -1;
63 }
64 this.column = pos - i;
65 }
66 this.pos = pos;
67 }
68 return this;
69 }
70}
71
72const CSS_MODE_TOP_LEVEL = 0;
73const CSS_MODE_IN_RULE = 1;
74const CSS_MODE_IN_LOCAL_RULE = 2;
75const CSS_MODE_AT_IMPORT_EXPECT_URL = 3;
76// TODO implement layer and supports for @import
77const CSS_MODE_AT_IMPORT_EXPECT_SUPPORTS = 4;
78const CSS_MODE_AT_IMPORT_EXPECT_MEDIA = 5;
79const CSS_MODE_AT_OTHER = 6;
80
81const explainMode = mode => {
82 switch (mode) {
83 case CSS_MODE_TOP_LEVEL:
84 return "parsing top level css";
85 case CSS_MODE_IN_RULE:
86 return "parsing css rule content (global)";
87 case CSS_MODE_IN_LOCAL_RULE:
88 return "parsing css rule content (local)";
89 case CSS_MODE_AT_IMPORT_EXPECT_URL:
90 return "parsing @import (expecting url)";
91 case CSS_MODE_AT_IMPORT_EXPECT_SUPPORTS:
92 return "parsing @import (expecting optionally supports or media query)";
93 case CSS_MODE_AT_IMPORT_EXPECT_MEDIA:
94 return "parsing @import (expecting optionally media query)";
95 case CSS_MODE_AT_OTHER:
96 return "parsing at-rule";
97 default:
98 return mode;
99 }
100};
101
102class CssParser extends Parser {
103 constructor({
104 allowPseudoBlocks = true,
105 allowModeSwitch = true,
106 defaultMode = "global"
107 } = {}) {
108 super();
109 this.allowPseudoBlocks = allowPseudoBlocks;
110 this.allowModeSwitch = allowModeSwitch;
111 this.defaultMode = defaultMode;
112 }
113
114 /**
115 * @param {string | Buffer | PreparsedAst} source the source to parse
116 * @param {ParserState} state the parser state
117 * @returns {ParserState} the parser state
118 */
119 parse(source, state) {
120 if (Buffer.isBuffer(source)) {
121 source = source.toString("utf-8");
122 } else if (typeof source === "object") {
123 throw new Error("webpackAst is unexpected for the CssParser");
124 }
125 if (source[0] === "\ufeff") {
126 source = source.slice(1);
127 }
128
129 const module = state.module;
130
131 const declaredCssVariables = new Set();
132
133 const locConverter = new LocConverter(source);
134 let mode = CSS_MODE_TOP_LEVEL;
135 let modePos = 0;
136 let modeNestingLevel = 0;
137 let modeData = undefined;
138 let singleClassSelector = undefined;
139 let lastIdentifier = undefined;
140 const modeStack = [];
141 const isTopLevelLocal = () =>
142 modeData === "local" ||
143 (this.defaultMode === "local" && modeData === undefined);
144 const eatWhiteLine = (input, pos) => {
145 for (;;) {
146 const cc = input.charCodeAt(pos);
147 if (cc === 32 || cc === 9) {
148 pos++;
149 continue;
150 }
151 if (cc === 10) pos++;
152 break;
153 }
154 return pos;
155 };
156 const eatUntil = chars => {
157 const charCodes = Array.from({ length: chars.length }, (_, i) =>
158 chars.charCodeAt(i)
159 );
160 const arr = Array.from(
161 { length: charCodes.reduce((a, b) => Math.max(a, b), 0) + 1 },
162 () => false
163 );
164 charCodes.forEach(cc => (arr[cc] = true));
165 return (input, pos) => {
166 for (;;) {
167 const cc = input.charCodeAt(pos);
168 if (cc < arr.length && arr[cc]) {
169 return pos;
170 }
171 pos++;
172 if (pos === input.length) return pos;
173 }
174 };
175 };
176 const eatText = (input, pos, eater) => {
177 let text = "";
178 for (;;) {
179 if (input.charCodeAt(pos) === CC_SLASH) {
180 const newPos = walkCssTokens.eatComments(input, pos);
181 if (pos !== newPos) {
182 pos = newPos;
183 if (pos === input.length) break;
184 } else {
185 text += "/";
186 pos++;
187 if (pos === input.length) break;
188 }
189 }
190 const newPos = eater(input, pos);
191 if (pos !== newPos) {
192 text += input.slice(pos, newPos);
193 pos = newPos;
194 } else {
195 break;
196 }
197 if (pos === input.length) break;
198 }
199 return [pos, text.trimRight()];
200 };
201 const eatExportName = eatUntil(":};/");
202 const eatExportValue = eatUntil("};/");
203 const parseExports = (input, pos) => {
204 pos = walkCssTokens.eatWhitespaceAndComments(input, pos);
205 const cc = input.charCodeAt(pos);
206 if (cc !== CC_LEFT_CURLY)
207 throw new Error(
208 `Unexpected ${input[pos]} at ${pos} during parsing of ':export' (expected '{')`
209 );
210 pos++;
211 pos = walkCssTokens.eatWhitespaceAndComments(input, pos);
212 for (;;) {
213 if (input.charCodeAt(pos) === CC_RIGHT_CURLY) break;
214 pos = walkCssTokens.eatWhitespaceAndComments(input, pos);
215 if (pos === input.length) return pos;
216 let start = pos;
217 let name;
218 [pos, name] = eatText(input, pos, eatExportName);
219 if (pos === input.length) return pos;
220 if (input.charCodeAt(pos) !== CC_COLON) {
221 throw new Error(
222 `Unexpected ${input[pos]} at ${pos} during parsing of export name in ':export' (expected ':')`
223 );
224 }
225 pos++;
226 if (pos === input.length) return pos;
227 pos = walkCssTokens.eatWhitespaceAndComments(input, pos);
228 if (pos === input.length) return pos;
229 let value;
230 [pos, value] = eatText(input, pos, eatExportValue);
231 if (pos === input.length) return pos;
232 const cc = input.charCodeAt(pos);
233 if (cc === CC_SEMICOLON) {
234 pos++;
235 if (pos === input.length) return pos;
236 pos = walkCssTokens.eatWhitespaceAndComments(input, pos);
237 if (pos === input.length) return pos;
238 } else if (cc !== CC_RIGHT_CURLY) {
239 throw new Error(
240 `Unexpected ${input[pos]} at ${pos} during parsing of export value in ':export' (expected ';' or '}')`
241 );
242 }
243 const dep = new CssExportDependency(name, value);
244 const { line: sl, column: sc } = locConverter.get(start);
245 const { line: el, column: ec } = locConverter.get(pos);
246 dep.setLoc(sl, sc, el, ec);
247 module.addDependency(dep);
248 }
249 pos++;
250 if (pos === input.length) return pos;
251 pos = eatWhiteLine(input, pos);
252 return pos;
253 };
254 const eatPropertyName = eatUntil(":{};");
255 const processLocalDeclaration = (input, pos) => {
256 modeData = undefined;
257 const start = pos;
258 pos = walkCssTokens.eatWhitespaceAndComments(input, pos);
259 const propertyNameStart = pos;
260 const [propertyNameEnd, propertyName] = eatText(
261 input,
262 pos,
263 eatPropertyName
264 );
265 if (input.charCodeAt(propertyNameEnd) !== CC_COLON) return start;
266 pos = propertyNameEnd + 1;
267 if (propertyName.startsWith("--")) {
268 // CSS Variable
269 const { line: sl, column: sc } = locConverter.get(propertyNameStart);
270 const { line: el, column: ec } = locConverter.get(propertyNameEnd);
271 const name = propertyName.slice(2);
272 const dep = new CssLocalIdentifierDependency(
273 name,
274 [propertyNameStart, propertyNameEnd],
275 "--"
276 );
277 dep.setLoc(sl, sc, el, ec);
278 module.addDependency(dep);
279 declaredCssVariables.add(name);
280 } else if (
281 propertyName === "animation-name" ||
282 propertyName === "animation"
283 ) {
284 modeData = "animation";
285 lastIdentifier = undefined;
286 }
287 return pos;
288 };
289 const processDeclarationValueDone = (input, pos) => {
290 if (modeData === "animation" && lastIdentifier) {
291 const { line: sl, column: sc } = locConverter.get(lastIdentifier[0]);
292 const { line: el, column: ec } = locConverter.get(lastIdentifier[1]);
293 const name = input.slice(lastIdentifier[0], lastIdentifier[1]);
294 const dep = new CssSelfLocalIdentifierDependency(name, lastIdentifier);
295 dep.setLoc(sl, sc, el, ec);
296 module.addDependency(dep);
297 }
298 };
299 const eatKeyframes = eatUntil("{};/");
300 const eatNameInVar = eatUntil(",)};/");
301 walkCssTokens(source, {
302 isSelector: () => {
303 return mode !== CSS_MODE_IN_RULE && mode !== CSS_MODE_IN_LOCAL_RULE;
304 },
305 url: (input, start, end, contentStart, contentEnd) => {
306 const value = cssUnescape(input.slice(contentStart, contentEnd));
307 switch (mode) {
308 case CSS_MODE_AT_IMPORT_EXPECT_URL: {
309 modeData.url = value;
310 mode = CSS_MODE_AT_IMPORT_EXPECT_SUPPORTS;
311 break;
312 }
313 case CSS_MODE_AT_IMPORT_EXPECT_SUPPORTS:
314 case CSS_MODE_AT_IMPORT_EXPECT_MEDIA:
315 throw new Error(
316 `Unexpected ${input.slice(
317 start,
318 end
319 )} at ${start} during ${explainMode(mode)}`
320 );
321 default: {
322 const dep = new CssUrlDependency(value, [start, end], "url");
323 const { line: sl, column: sc } = locConverter.get(start);
324 const { line: el, column: ec } = locConverter.get(end);
325 dep.setLoc(sl, sc, el, ec);
326 module.addDependency(dep);
327 module.addCodeGenerationDependency(dep);
328 break;
329 }
330 }
331 return end;
332 },
333 string: (input, start, end) => {
334 switch (mode) {
335 case CSS_MODE_AT_IMPORT_EXPECT_URL: {
336 modeData.url = cssUnescape(input.slice(start + 1, end - 1));
337 mode = CSS_MODE_AT_IMPORT_EXPECT_SUPPORTS;
338 break;
339 }
340 }
341 return end;
342 },
343 atKeyword: (input, start, end) => {
344 const name = input.slice(start, end);
345 if (name === "@namespace") {
346 throw new Error("@namespace is not supported in bundled CSS");
347 }
348 if (name === "@import") {
349 if (mode !== CSS_MODE_TOP_LEVEL) {
350 throw new Error(
351 `Unexpected @import at ${start} during ${explainMode(mode)}`
352 );
353 }
354 mode = CSS_MODE_AT_IMPORT_EXPECT_URL;
355 modePos = end;
356 modeData = {
357 start: start,
358 url: undefined,
359 supports: undefined
360 };
361 }
362 if (name === "@keyframes") {
363 let pos = end;
364 pos = walkCssTokens.eatWhitespaceAndComments(input, pos);
365 if (pos === input.length) return pos;
366 const [newPos, name] = eatText(input, pos, eatKeyframes);
367 const { line: sl, column: sc } = locConverter.get(pos);
368 const { line: el, column: ec } = locConverter.get(newPos);
369 const dep = new CssLocalIdentifierDependency(name, [pos, newPos]);
370 dep.setLoc(sl, sc, el, ec);
371 module.addDependency(dep);
372 pos = newPos;
373 if (pos === input.length) return pos;
374 if (input.charCodeAt(pos) !== CC_LEFT_CURLY) {
375 throw new Error(
376 `Unexpected ${input[pos]} at ${pos} during parsing of @keyframes (expected '{')`
377 );
378 }
379 mode = CSS_MODE_IN_LOCAL_RULE;
380 modeNestingLevel = 1;
381 return pos + 1;
382 }
383 return end;
384 },
385 semicolon: (input, start, end) => {
386 switch (mode) {
387 case CSS_MODE_AT_IMPORT_EXPECT_URL:
388 throw new Error(`Expected URL for @import at ${start}`);
389 case CSS_MODE_AT_IMPORT_EXPECT_MEDIA:
390 case CSS_MODE_AT_IMPORT_EXPECT_SUPPORTS: {
391 const { line: sl, column: sc } = locConverter.get(modeData.start);
392 const { line: el, column: ec } = locConverter.get(end);
393 end = eatWhiteLine(input, end);
394 const media = input.slice(modePos, start).trim();
395 const dep = new CssImportDependency(
396 modeData.url,
397 [modeData.start, end],
398 modeData.supports,
399 media
400 );
401 dep.setLoc(sl, sc, el, ec);
402 module.addDependency(dep);
403 break;
404 }
405 case CSS_MODE_IN_LOCAL_RULE: {
406 processDeclarationValueDone(input, start);
407 return processLocalDeclaration(input, end);
408 }
409 case CSS_MODE_IN_RULE: {
410 return end;
411 }
412 }
413 mode = CSS_MODE_TOP_LEVEL;
414 modeData = undefined;
415 singleClassSelector = undefined;
416 return end;
417 },
418 leftCurlyBracket: (input, start, end) => {
419 switch (mode) {
420 case CSS_MODE_TOP_LEVEL:
421 mode = isTopLevelLocal()
422 ? CSS_MODE_IN_LOCAL_RULE
423 : CSS_MODE_IN_RULE;
424 modeNestingLevel = 1;
425 if (mode === CSS_MODE_IN_LOCAL_RULE)
426 return processLocalDeclaration(input, end);
427 break;
428 case CSS_MODE_IN_RULE:
429 case CSS_MODE_IN_LOCAL_RULE:
430 modeNestingLevel++;
431 break;
432 }
433 return end;
434 },
435 rightCurlyBracket: (input, start, end) => {
436 switch (mode) {
437 case CSS_MODE_IN_LOCAL_RULE:
438 processDeclarationValueDone(input, start);
439 /* falls through */
440 case CSS_MODE_IN_RULE:
441 if (--modeNestingLevel === 0) {
442 mode = CSS_MODE_TOP_LEVEL;
443 modeData = undefined;
444 singleClassSelector = undefined;
445 }
446 break;
447 }
448 return end;
449 },
450 id: (input, start, end) => {
451 singleClassSelector = false;
452 switch (mode) {
453 case CSS_MODE_TOP_LEVEL:
454 if (isTopLevelLocal()) {
455 const name = input.slice(start + 1, end);
456 const dep = new CssLocalIdentifierDependency(name, [
457 start + 1,
458 end
459 ]);
460 const { line: sl, column: sc } = locConverter.get(start);
461 const { line: el, column: ec } = locConverter.get(end);
462 dep.setLoc(sl, sc, el, ec);
463 module.addDependency(dep);
464 }
465 break;
466 }
467 return end;
468 },
469 identifier: (input, start, end) => {
470 singleClassSelector = false;
471 switch (mode) {
472 case CSS_MODE_IN_LOCAL_RULE:
473 if (modeData === "animation") {
474 lastIdentifier = [start, end];
475 }
476 break;
477 }
478 return end;
479 },
480 class: (input, start, end) => {
481 switch (mode) {
482 case CSS_MODE_TOP_LEVEL: {
483 if (isTopLevelLocal()) {
484 const name = input.slice(start + 1, end);
485 const dep = new CssLocalIdentifierDependency(name, [
486 start + 1,
487 end
488 ]);
489 const { line: sl, column: sc } = locConverter.get(start);
490 const { line: el, column: ec } = locConverter.get(end);
491 dep.setLoc(sl, sc, el, ec);
492 module.addDependency(dep);
493 if (singleClassSelector === undefined) singleClassSelector = name;
494 } else {
495 singleClassSelector = false;
496 }
497 break;
498 }
499 }
500 return end;
501 },
502 leftParenthesis: (input, start, end) => {
503 switch (mode) {
504 case CSS_MODE_TOP_LEVEL: {
505 modeStack.push(false);
506 break;
507 }
508 }
509 return end;
510 },
511 rightParenthesis: (input, start, end) => {
512 switch (mode) {
513 case CSS_MODE_TOP_LEVEL: {
514 const newModeData = modeStack.pop();
515 if (newModeData !== false) {
516 modeData = newModeData;
517 const dep = new ConstDependency("", [start, end]);
518 module.addPresentationalDependency(dep);
519 }
520 break;
521 }
522 }
523 return end;
524 },
525 pseudoClass: (input, start, end) => {
526 singleClassSelector = false;
527 switch (mode) {
528 case CSS_MODE_TOP_LEVEL: {
529 const name = input.slice(start, end);
530 if (this.allowModeSwitch && name === ":global") {
531 modeData = "global";
532 const dep = new ConstDependency("", [start, end]);
533 module.addPresentationalDependency(dep);
534 } else if (this.allowModeSwitch && name === ":local") {
535 modeData = "local";
536 const dep = new ConstDependency("", [start, end]);
537 module.addPresentationalDependency(dep);
538 } else if (this.allowPseudoBlocks && name === ":export") {
539 const pos = parseExports(input, end);
540 const dep = new ConstDependency("", [start, pos]);
541 module.addPresentationalDependency(dep);
542 return pos;
543 }
544 break;
545 }
546 }
547 return end;
548 },
549 pseudoFunction: (input, start, end) => {
550 switch (mode) {
551 case CSS_MODE_TOP_LEVEL: {
552 const name = input.slice(start, end - 1);
553 if (this.allowModeSwitch && name === ":global") {
554 modeStack.push(modeData);
555 modeData = "global";
556 const dep = new ConstDependency("", [start, end]);
557 module.addPresentationalDependency(dep);
558 } else if (this.allowModeSwitch && name === ":local") {
559 modeStack.push(modeData);
560 modeData = "local";
561 const dep = new ConstDependency("", [start, end]);
562 module.addPresentationalDependency(dep);
563 } else {
564 modeStack.push(false);
565 }
566 break;
567 }
568 }
569 return end;
570 },
571 function: (input, start, end) => {
572 switch (mode) {
573 case CSS_MODE_IN_LOCAL_RULE: {
574 const name = input.slice(start, end - 1);
575 if (name === "var") {
576 let pos = walkCssTokens.eatWhitespaceAndComments(input, end);
577 if (pos === input.length) return pos;
578 const [newPos, name] = eatText(input, pos, eatNameInVar);
579 if (!name.startsWith("--")) return end;
580 const { line: sl, column: sc } = locConverter.get(pos);
581 const { line: el, column: ec } = locConverter.get(newPos);
582 const dep = new CssSelfLocalIdentifierDependency(
583 name.slice(2),
584 [pos, newPos],
585 "--",
586 declaredCssVariables
587 );
588 dep.setLoc(sl, sc, el, ec);
589 module.addDependency(dep);
590 return newPos;
591 }
592 break;
593 }
594 }
595 return end;
596 },
597 comma: (input, start, end) => {
598 switch (mode) {
599 case CSS_MODE_TOP_LEVEL:
600 modeData = undefined;
601 modeStack.length = 0;
602 break;
603 case CSS_MODE_IN_LOCAL_RULE:
604 processDeclarationValueDone(input, start);
605 break;
606 }
607 return end;
608 }
609 });
610
611 module.buildInfo.strict = true;
612 module.buildMeta.exportsType = "namespace";
613 module.addDependency(new StaticExportsDependency([], true));
614 return state;
615 }
616}
617
618module.exports = CssParser;