123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618 |
- /*
- MIT License http://www.opensource.org/licenses/mit-license.php
- Author Tobias Koppers @sokra
- */
- "use strict";
- const Parser = require("../Parser");
- const ConstDependency = require("../dependencies/ConstDependency");
- const CssExportDependency = require("../dependencies/CssExportDependency");
- const CssImportDependency = require("../dependencies/CssImportDependency");
- const CssLocalIdentifierDependency = require("../dependencies/CssLocalIdentifierDependency");
- const CssSelfLocalIdentifierDependency = require("../dependencies/CssSelfLocalIdentifierDependency");
- const CssUrlDependency = require("../dependencies/CssUrlDependency");
- const StaticExportsDependency = require("../dependencies/StaticExportsDependency");
- const walkCssTokens = require("./walkCssTokens");
- /** @typedef {import("../Parser").ParserState} ParserState */
- /** @typedef {import("../Parser").PreparsedAst} PreparsedAst */
- const CC_LEFT_CURLY = "{".charCodeAt(0);
- const CC_RIGHT_CURLY = "}".charCodeAt(0);
- const CC_COLON = ":".charCodeAt(0);
- const CC_SLASH = "/".charCodeAt(0);
- const CC_SEMICOLON = ";".charCodeAt(0);
- const cssUnescape = str => {
- return str.replace(/\\([0-9a-fA-F]{1,6}[ \t\n\r\f]?|[\s\S])/g, match => {
- if (match.length > 2) {
- return String.fromCharCode(parseInt(match.slice(1).trim(), 16));
- } else {
- return match[1];
- }
- });
- };
- class LocConverter {
- constructor(input) {
- this._input = input;
- this.line = 1;
- this.column = 0;
- this.pos = 0;
- }
- get(pos) {
- if (this.pos !== pos) {
- if (this.pos < pos) {
- const str = this._input.slice(this.pos, pos);
- let i = str.lastIndexOf("\n");
- if (i === -1) {
- this.column += str.length;
- } else {
- this.column = str.length - i - 1;
- this.line++;
- while (i > 0 && (i = str.lastIndexOf("\n", i - 1)) !== -1)
- this.line++;
- }
- } else {
- let i = this._input.lastIndexOf("\n", this.pos);
- while (i >= pos) {
- this.line--;
- i = i > 0 ? this._input.lastIndexOf("\n", i - 1) : -1;
- }
- this.column = pos - i;
- }
- this.pos = pos;
- }
- return this;
- }
- }
- const CSS_MODE_TOP_LEVEL = 0;
- const CSS_MODE_IN_RULE = 1;
- const CSS_MODE_IN_LOCAL_RULE = 2;
- const CSS_MODE_AT_IMPORT_EXPECT_URL = 3;
- // TODO implement layer and supports for @import
- const CSS_MODE_AT_IMPORT_EXPECT_SUPPORTS = 4;
- const CSS_MODE_AT_IMPORT_EXPECT_MEDIA = 5;
- const CSS_MODE_AT_OTHER = 6;
- const explainMode = mode => {
- switch (mode) {
- case CSS_MODE_TOP_LEVEL:
- return "parsing top level css";
- case CSS_MODE_IN_RULE:
- return "parsing css rule content (global)";
- case CSS_MODE_IN_LOCAL_RULE:
- return "parsing css rule content (local)";
- case CSS_MODE_AT_IMPORT_EXPECT_URL:
- return "parsing @import (expecting url)";
- case CSS_MODE_AT_IMPORT_EXPECT_SUPPORTS:
- return "parsing @import (expecting optionally supports or media query)";
- case CSS_MODE_AT_IMPORT_EXPECT_MEDIA:
- return "parsing @import (expecting optionally media query)";
- case CSS_MODE_AT_OTHER:
- return "parsing at-rule";
- default:
- return mode;
- }
- };
- class CssParser extends Parser {
- constructor({
- allowPseudoBlocks = true,
- allowModeSwitch = true,
- defaultMode = "global"
- } = {}) {
- super();
- this.allowPseudoBlocks = allowPseudoBlocks;
- this.allowModeSwitch = allowModeSwitch;
- this.defaultMode = defaultMode;
- }
- /**
- * @param {string | Buffer | PreparsedAst} source the source to parse
- * @param {ParserState} state the parser state
- * @returns {ParserState} the parser state
- */
- parse(source, state) {
- if (Buffer.isBuffer(source)) {
- source = source.toString("utf-8");
- } else if (typeof source === "object") {
- throw new Error("webpackAst is unexpected for the CssParser");
- }
- if (source[0] === "\ufeff") {
- source = source.slice(1);
- }
- const module = state.module;
- const declaredCssVariables = new Set();
- const locConverter = new LocConverter(source);
- let mode = CSS_MODE_TOP_LEVEL;
- let modePos = 0;
- let modeNestingLevel = 0;
- let modeData = undefined;
- let singleClassSelector = undefined;
- let lastIdentifier = undefined;
- const modeStack = [];
- const isTopLevelLocal = () =>
- modeData === "local" ||
- (this.defaultMode === "local" && modeData === undefined);
- const eatWhiteLine = (input, pos) => {
- for (;;) {
- const cc = input.charCodeAt(pos);
- if (cc === 32 || cc === 9) {
- pos++;
- continue;
- }
- if (cc === 10) pos++;
- break;
- }
- return pos;
- };
- const eatUntil = chars => {
- const charCodes = Array.from({ length: chars.length }, (_, i) =>
- chars.charCodeAt(i)
- );
- const arr = Array.from(
- { length: charCodes.reduce((a, b) => Math.max(a, b), 0) + 1 },
- () => false
- );
- charCodes.forEach(cc => (arr[cc] = true));
- return (input, pos) => {
- for (;;) {
- const cc = input.charCodeAt(pos);
- if (cc < arr.length && arr[cc]) {
- return pos;
- }
- pos++;
- if (pos === input.length) return pos;
- }
- };
- };
- const eatText = (input, pos, eater) => {
- let text = "";
- for (;;) {
- if (input.charCodeAt(pos) === CC_SLASH) {
- const newPos = walkCssTokens.eatComments(input, pos);
- if (pos !== newPos) {
- pos = newPos;
- if (pos === input.length) break;
- } else {
- text += "/";
- pos++;
- if (pos === input.length) break;
- }
- }
- const newPos = eater(input, pos);
- if (pos !== newPos) {
- text += input.slice(pos, newPos);
- pos = newPos;
- } else {
- break;
- }
- if (pos === input.length) break;
- }
- return [pos, text.trimEnd()];
- };
- const eatExportName = eatUntil(":};/");
- const eatExportValue = eatUntil("};/");
- const parseExports = (input, pos) => {
- pos = walkCssTokens.eatWhitespaceAndComments(input, pos);
- const cc = input.charCodeAt(pos);
- if (cc !== CC_LEFT_CURLY)
- throw new Error(
- `Unexpected ${input[pos]} at ${pos} during parsing of ':export' (expected '{')`
- );
- pos++;
- pos = walkCssTokens.eatWhitespaceAndComments(input, pos);
- for (;;) {
- if (input.charCodeAt(pos) === CC_RIGHT_CURLY) break;
- pos = walkCssTokens.eatWhitespaceAndComments(input, pos);
- if (pos === input.length) return pos;
- let start = pos;
- let name;
- [pos, name] = eatText(input, pos, eatExportName);
- if (pos === input.length) return pos;
- if (input.charCodeAt(pos) !== CC_COLON) {
- throw new Error(
- `Unexpected ${input[pos]} at ${pos} during parsing of export name in ':export' (expected ':')`
- );
- }
- pos++;
- if (pos === input.length) return pos;
- pos = walkCssTokens.eatWhitespaceAndComments(input, pos);
- if (pos === input.length) return pos;
- let value;
- [pos, value] = eatText(input, pos, eatExportValue);
- if (pos === input.length) return pos;
- const cc = input.charCodeAt(pos);
- if (cc === CC_SEMICOLON) {
- pos++;
- if (pos === input.length) return pos;
- pos = walkCssTokens.eatWhitespaceAndComments(input, pos);
- if (pos === input.length) return pos;
- } else if (cc !== CC_RIGHT_CURLY) {
- throw new Error(
- `Unexpected ${input[pos]} at ${pos} during parsing of export value in ':export' (expected ';' or '}')`
- );
- }
- const dep = new CssExportDependency(name, value);
- const { line: sl, column: sc } = locConverter.get(start);
- const { line: el, column: ec } = locConverter.get(pos);
- dep.setLoc(sl, sc, el, ec);
- module.addDependency(dep);
- }
- pos++;
- if (pos === input.length) return pos;
- pos = eatWhiteLine(input, pos);
- return pos;
- };
- const eatPropertyName = eatUntil(":{};");
- const processLocalDeclaration = (input, pos) => {
- modeData = undefined;
- const start = pos;
- pos = walkCssTokens.eatWhitespaceAndComments(input, pos);
- const propertyNameStart = pos;
- const [propertyNameEnd, propertyName] = eatText(
- input,
- pos,
- eatPropertyName
- );
- if (input.charCodeAt(propertyNameEnd) !== CC_COLON) return start;
- pos = propertyNameEnd + 1;
- if (propertyName.startsWith("--")) {
- // CSS Variable
- const { line: sl, column: sc } = locConverter.get(propertyNameStart);
- const { line: el, column: ec } = locConverter.get(propertyNameEnd);
- const name = propertyName.slice(2);
- const dep = new CssLocalIdentifierDependency(
- name,
- [propertyNameStart, propertyNameEnd],
- "--"
- );
- dep.setLoc(sl, sc, el, ec);
- module.addDependency(dep);
- declaredCssVariables.add(name);
- } else if (
- propertyName === "animation-name" ||
- propertyName === "animation"
- ) {
- modeData = "animation";
- lastIdentifier = undefined;
- }
- return pos;
- };
- const processDeclarationValueDone = (input, pos) => {
- if (modeData === "animation" && lastIdentifier) {
- const { line: sl, column: sc } = locConverter.get(lastIdentifier[0]);
- const { line: el, column: ec } = locConverter.get(lastIdentifier[1]);
- const name = input.slice(lastIdentifier[0], lastIdentifier[1]);
- const dep = new CssSelfLocalIdentifierDependency(name, lastIdentifier);
- dep.setLoc(sl, sc, el, ec);
- module.addDependency(dep);
- }
- };
- const eatKeyframes = eatUntil("{};/");
- const eatNameInVar = eatUntil(",)};/");
- walkCssTokens(source, {
- isSelector: () => {
- return mode !== CSS_MODE_IN_RULE && mode !== CSS_MODE_IN_LOCAL_RULE;
- },
- url: (input, start, end, contentStart, contentEnd) => {
- const value = cssUnescape(input.slice(contentStart, contentEnd));
- switch (mode) {
- case CSS_MODE_AT_IMPORT_EXPECT_URL: {
- modeData.url = value;
- mode = CSS_MODE_AT_IMPORT_EXPECT_SUPPORTS;
- break;
- }
- case CSS_MODE_AT_IMPORT_EXPECT_SUPPORTS:
- case CSS_MODE_AT_IMPORT_EXPECT_MEDIA:
- throw new Error(
- `Unexpected ${input.slice(
- start,
- end
- )} at ${start} during ${explainMode(mode)}`
- );
- default: {
- const dep = new CssUrlDependency(value, [start, end], "url");
- const { line: sl, column: sc } = locConverter.get(start);
- const { line: el, column: ec } = locConverter.get(end);
- dep.setLoc(sl, sc, el, ec);
- module.addDependency(dep);
- module.addCodeGenerationDependency(dep);
- break;
- }
- }
- return end;
- },
- string: (input, start, end) => {
- switch (mode) {
- case CSS_MODE_AT_IMPORT_EXPECT_URL: {
- modeData.url = cssUnescape(input.slice(start + 1, end - 1));
- mode = CSS_MODE_AT_IMPORT_EXPECT_SUPPORTS;
- break;
- }
- }
- return end;
- },
- atKeyword: (input, start, end) => {
- const name = input.slice(start, end);
- if (name === "@namespace") {
- throw new Error("@namespace is not supported in bundled CSS");
- }
- if (name === "@import") {
- if (mode !== CSS_MODE_TOP_LEVEL) {
- throw new Error(
- `Unexpected @import at ${start} during ${explainMode(mode)}`
- );
- }
- mode = CSS_MODE_AT_IMPORT_EXPECT_URL;
- modePos = end;
- modeData = {
- start: start,
- url: undefined,
- supports: undefined
- };
- }
- if (name === "@keyframes") {
- let pos = end;
- pos = walkCssTokens.eatWhitespaceAndComments(input, pos);
- if (pos === input.length) return pos;
- const [newPos, name] = eatText(input, pos, eatKeyframes);
- const { line: sl, column: sc } = locConverter.get(pos);
- const { line: el, column: ec } = locConverter.get(newPos);
- const dep = new CssLocalIdentifierDependency(name, [pos, newPos]);
- dep.setLoc(sl, sc, el, ec);
- module.addDependency(dep);
- pos = newPos;
- if (pos === input.length) return pos;
- if (input.charCodeAt(pos) !== CC_LEFT_CURLY) {
- throw new Error(
- `Unexpected ${input[pos]} at ${pos} during parsing of @keyframes (expected '{')`
- );
- }
- mode = CSS_MODE_IN_LOCAL_RULE;
- modeNestingLevel = 1;
- return pos + 1;
- }
- return end;
- },
- semicolon: (input, start, end) => {
- switch (mode) {
- case CSS_MODE_AT_IMPORT_EXPECT_URL:
- throw new Error(`Expected URL for @import at ${start}`);
- case CSS_MODE_AT_IMPORT_EXPECT_MEDIA:
- case CSS_MODE_AT_IMPORT_EXPECT_SUPPORTS: {
- const { line: sl, column: sc } = locConverter.get(modeData.start);
- const { line: el, column: ec } = locConverter.get(end);
- end = eatWhiteLine(input, end);
- const media = input.slice(modePos, start).trim();
- const dep = new CssImportDependency(
- modeData.url,
- [modeData.start, end],
- modeData.supports,
- media
- );
- dep.setLoc(sl, sc, el, ec);
- module.addDependency(dep);
- break;
- }
- case CSS_MODE_IN_LOCAL_RULE: {
- processDeclarationValueDone(input, start);
- return processLocalDeclaration(input, end);
- }
- case CSS_MODE_IN_RULE: {
- return end;
- }
- }
- mode = CSS_MODE_TOP_LEVEL;
- modeData = undefined;
- singleClassSelector = undefined;
- return end;
- },
- leftCurlyBracket: (input, start, end) => {
- switch (mode) {
- case CSS_MODE_TOP_LEVEL:
- mode = isTopLevelLocal()
- ? CSS_MODE_IN_LOCAL_RULE
- : CSS_MODE_IN_RULE;
- modeNestingLevel = 1;
- if (mode === CSS_MODE_IN_LOCAL_RULE)
- return processLocalDeclaration(input, end);
- break;
- case CSS_MODE_IN_RULE:
- case CSS_MODE_IN_LOCAL_RULE:
- modeNestingLevel++;
- break;
- }
- return end;
- },
- rightCurlyBracket: (input, start, end) => {
- switch (mode) {
- case CSS_MODE_IN_LOCAL_RULE:
- processDeclarationValueDone(input, start);
- /* falls through */
- case CSS_MODE_IN_RULE:
- if (--modeNestingLevel === 0) {
- mode = CSS_MODE_TOP_LEVEL;
- modeData = undefined;
- singleClassSelector = undefined;
- }
- break;
- }
- return end;
- },
- id: (input, start, end) => {
- singleClassSelector = false;
- switch (mode) {
- case CSS_MODE_TOP_LEVEL:
- if (isTopLevelLocal()) {
- const name = input.slice(start + 1, end);
- const dep = new CssLocalIdentifierDependency(name, [
- start + 1,
- end
- ]);
- const { line: sl, column: sc } = locConverter.get(start);
- const { line: el, column: ec } = locConverter.get(end);
- dep.setLoc(sl, sc, el, ec);
- module.addDependency(dep);
- }
- break;
- }
- return end;
- },
- identifier: (input, start, end) => {
- singleClassSelector = false;
- switch (mode) {
- case CSS_MODE_IN_LOCAL_RULE:
- if (modeData === "animation") {
- lastIdentifier = [start, end];
- }
- break;
- }
- return end;
- },
- class: (input, start, end) => {
- switch (mode) {
- case CSS_MODE_TOP_LEVEL: {
- if (isTopLevelLocal()) {
- const name = input.slice(start + 1, end);
- const dep = new CssLocalIdentifierDependency(name, [
- start + 1,
- end
- ]);
- const { line: sl, column: sc } = locConverter.get(start);
- const { line: el, column: ec } = locConverter.get(end);
- dep.setLoc(sl, sc, el, ec);
- module.addDependency(dep);
- if (singleClassSelector === undefined) singleClassSelector = name;
- } else {
- singleClassSelector = false;
- }
- break;
- }
- }
- return end;
- },
- leftParenthesis: (input, start, end) => {
- switch (mode) {
- case CSS_MODE_TOP_LEVEL: {
- modeStack.push(false);
- break;
- }
- }
- return end;
- },
- rightParenthesis: (input, start, end) => {
- switch (mode) {
- case CSS_MODE_TOP_LEVEL: {
- const newModeData = modeStack.pop();
- if (newModeData !== false) {
- modeData = newModeData;
- const dep = new ConstDependency("", [start, end]);
- module.addPresentationalDependency(dep);
- }
- break;
- }
- }
- return end;
- },
- pseudoClass: (input, start, end) => {
- singleClassSelector = false;
- switch (mode) {
- case CSS_MODE_TOP_LEVEL: {
- const name = input.slice(start, end);
- if (this.allowModeSwitch && name === ":global") {
- modeData = "global";
- const dep = new ConstDependency("", [start, end]);
- module.addPresentationalDependency(dep);
- } else if (this.allowModeSwitch && name === ":local") {
- modeData = "local";
- const dep = new ConstDependency("", [start, end]);
- module.addPresentationalDependency(dep);
- } else if (this.allowPseudoBlocks && name === ":export") {
- const pos = parseExports(input, end);
- const dep = new ConstDependency("", [start, pos]);
- module.addPresentationalDependency(dep);
- return pos;
- }
- break;
- }
- }
- return end;
- },
- pseudoFunction: (input, start, end) => {
- switch (mode) {
- case CSS_MODE_TOP_LEVEL: {
- const name = input.slice(start, end - 1);
- if (this.allowModeSwitch && name === ":global") {
- modeStack.push(modeData);
- modeData = "global";
- const dep = new ConstDependency("", [start, end]);
- module.addPresentationalDependency(dep);
- } else if (this.allowModeSwitch && name === ":local") {
- modeStack.push(modeData);
- modeData = "local";
- const dep = new ConstDependency("", [start, end]);
- module.addPresentationalDependency(dep);
- } else {
- modeStack.push(false);
- }
- break;
- }
- }
- return end;
- },
- function: (input, start, end) => {
- switch (mode) {
- case CSS_MODE_IN_LOCAL_RULE: {
- const name = input.slice(start, end - 1);
- if (name === "var") {
- let pos = walkCssTokens.eatWhitespaceAndComments(input, end);
- if (pos === input.length) return pos;
- const [newPos, name] = eatText(input, pos, eatNameInVar);
- if (!name.startsWith("--")) return end;
- const { line: sl, column: sc } = locConverter.get(pos);
- const { line: el, column: ec } = locConverter.get(newPos);
- const dep = new CssSelfLocalIdentifierDependency(
- name.slice(2),
- [pos, newPos],
- "--",
- declaredCssVariables
- );
- dep.setLoc(sl, sc, el, ec);
- module.addDependency(dep);
- return newPos;
- }
- break;
- }
- }
- return end;
- },
- comma: (input, start, end) => {
- switch (mode) {
- case CSS_MODE_TOP_LEVEL:
- modeData = undefined;
- modeStack.length = 0;
- break;
- case CSS_MODE_IN_LOCAL_RULE:
- processDeclarationValueDone(input, start);
- break;
- }
- return end;
- }
- });
- module.buildInfo.strict = true;
- module.buildMeta.exportsType = "namespace";
- module.addDependency(new StaticExportsDependency([], true));
- return state;
- }
- }
- module.exports = CssParser;
|