CssParser.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const ModuleDependencyWarning = require("../ModuleDependencyWarning");
  7. const { CSS_MODULE_TYPE_AUTO } = require("../ModuleTypeConstants");
  8. const Parser = require("../Parser");
  9. const WebpackError = require("../WebpackError");
  10. const ConstDependency = require("../dependencies/ConstDependency");
  11. const CssExportDependency = require("../dependencies/CssExportDependency");
  12. const CssImportDependency = require("../dependencies/CssImportDependency");
  13. const CssLocalIdentifierDependency = require("../dependencies/CssLocalIdentifierDependency");
  14. const CssSelfLocalIdentifierDependency = require("../dependencies/CssSelfLocalIdentifierDependency");
  15. const CssUrlDependency = require("../dependencies/CssUrlDependency");
  16. const StaticExportsDependency = require("../dependencies/StaticExportsDependency");
  17. const { parseResource } = require("../util/identifier");
  18. const walkCssTokens = require("./walkCssTokens");
  19. /** @typedef {import("../Parser").ParserState} ParserState */
  20. /** @typedef {import("../Parser").PreparsedAst} PreparsedAst */
  21. /** @typedef {[number, number]} Range */
  22. const CC_LEFT_CURLY = "{".charCodeAt(0);
  23. const CC_RIGHT_CURLY = "}".charCodeAt(0);
  24. const CC_COLON = ":".charCodeAt(0);
  25. const CC_SLASH = "/".charCodeAt(0);
  26. const CC_SEMICOLON = ";".charCodeAt(0);
  27. // https://www.w3.org/TR/css-syntax-3/#newline
  28. // We don't have `preprocessing` stage, so we need specify all of them
  29. const STRING_MULTILINE = /\\[\n\r\f]/g;
  30. // https://www.w3.org/TR/css-syntax-3/#whitespace
  31. const TRIM_WHITE_SPACES = /(^[ \t\n\r\f]*|[ \t\n\r\f]*$)/g;
  32. const UNESCAPE = /\\([0-9a-fA-F]{1,6}[ \t\n\r\f]?|[\s\S])/g;
  33. const IMAGE_SET_FUNCTION = /^(-\w+-)?image-set$/i;
  34. const OPTIONALLY_VENDOR_PREFIXED_KEYFRAMES_AT_RULE = /^@(-\w+-)?keyframes$/;
  35. const OPTIONALLY_VENDOR_PREFIXED_ANIMATION_PROPERTY =
  36. /^(-\w+-)?animation(-name)?$/i;
  37. const IS_MODULES = /\.module(s)?\.[^.]+$/i;
  38. /**
  39. * @param {string} str url string
  40. * @param {boolean} isString is url wrapped in quotes
  41. * @returns {string} normalized url
  42. */
  43. const normalizeUrl = (str, isString) => {
  44. // Remove extra spaces and newlines:
  45. // `url("im\
  46. // g.png")`
  47. if (isString) {
  48. str = str.replace(STRING_MULTILINE, "");
  49. }
  50. str = str
  51. // Remove unnecessary spaces from `url(" img.png ")`
  52. .replace(TRIM_WHITE_SPACES, "")
  53. // Unescape
  54. .replace(UNESCAPE, match => {
  55. if (match.length > 2) {
  56. return String.fromCharCode(parseInt(match.slice(1).trim(), 16));
  57. } else {
  58. return match[1];
  59. }
  60. });
  61. if (/^data:/i.test(str)) {
  62. return str;
  63. }
  64. if (str.includes("%")) {
  65. // Convert `url('%2E/img.png')` -> `url('./img.png')`
  66. try {
  67. str = decodeURIComponent(str);
  68. } catch (error) {
  69. // Ignore
  70. }
  71. }
  72. return str;
  73. };
  74. class LocConverter {
  75. /**
  76. * @param {string} input input
  77. */
  78. constructor(input) {
  79. this._input = input;
  80. this.line = 1;
  81. this.column = 0;
  82. this.pos = 0;
  83. }
  84. /**
  85. * @param {number} pos position
  86. * @returns {LocConverter} location converter
  87. */
  88. get(pos) {
  89. if (this.pos !== pos) {
  90. if (this.pos < pos) {
  91. const str = this._input.slice(this.pos, pos);
  92. let i = str.lastIndexOf("\n");
  93. if (i === -1) {
  94. this.column += str.length;
  95. } else {
  96. this.column = str.length - i - 1;
  97. this.line++;
  98. while (i > 0 && (i = str.lastIndexOf("\n", i - 1)) !== -1)
  99. this.line++;
  100. }
  101. } else {
  102. let i = this._input.lastIndexOf("\n", this.pos);
  103. while (i >= pos) {
  104. this.line--;
  105. i = i > 0 ? this._input.lastIndexOf("\n", i - 1) : -1;
  106. }
  107. this.column = pos - i;
  108. }
  109. this.pos = pos;
  110. }
  111. return this;
  112. }
  113. }
  114. const CSS_MODE_TOP_LEVEL = 0;
  115. const CSS_MODE_IN_BLOCK = 1;
  116. const CSS_MODE_IN_AT_IMPORT = 2;
  117. const CSS_MODE_AT_IMPORT_INVALID = 3;
  118. const CSS_MODE_AT_NAMESPACE_INVALID = 4;
  119. class CssParser extends Parser {
  120. constructor({
  121. allowModeSwitch = true,
  122. defaultMode = "global",
  123. namedExports = true
  124. } = {}) {
  125. super();
  126. this.allowModeSwitch = allowModeSwitch;
  127. this.defaultMode = defaultMode;
  128. this.namedExports = namedExports;
  129. }
  130. /**
  131. * @param {ParserState} state parser state
  132. * @param {string} message warning message
  133. * @param {LocConverter} locConverter location converter
  134. * @param {number} start start offset
  135. * @param {number} end end offset
  136. */
  137. _emitWarning(state, message, locConverter, start, end) {
  138. const { line: sl, column: sc } = locConverter.get(start);
  139. const { line: el, column: ec } = locConverter.get(end);
  140. state.current.addWarning(
  141. new ModuleDependencyWarning(state.module, new WebpackError(message), {
  142. start: { line: sl, column: sc },
  143. end: { line: el, column: ec }
  144. })
  145. );
  146. }
  147. /**
  148. * @param {string | Buffer | PreparsedAst} source the source to parse
  149. * @param {ParserState} state the parser state
  150. * @returns {ParserState} the parser state
  151. */
  152. parse(source, state) {
  153. if (Buffer.isBuffer(source)) {
  154. source = source.toString("utf-8");
  155. } else if (typeof source === "object") {
  156. throw new Error("webpackAst is unexpected for the CssParser");
  157. }
  158. if (source[0] === "\ufeff") {
  159. source = source.slice(1);
  160. }
  161. const module = state.module;
  162. /** @type {string | undefined} */
  163. let oldDefaultMode;
  164. if (
  165. module.type === CSS_MODULE_TYPE_AUTO &&
  166. IS_MODULES.test(
  167. parseResource(module.matchResource || module.resource).path
  168. )
  169. ) {
  170. oldDefaultMode = this.defaultMode;
  171. this.defaultMode = "local";
  172. }
  173. const locConverter = new LocConverter(source);
  174. /** @type {Set<string>}*/
  175. const declaredCssVariables = new Set();
  176. /** @type {number} */
  177. let scope = CSS_MODE_TOP_LEVEL;
  178. /** @type {number} */
  179. let blockNestingLevel = 0;
  180. /** @type {boolean} */
  181. let allowImportAtRule = true;
  182. /** @type {"local" | "global" | undefined} */
  183. let modeData = undefined;
  184. /** @type {[number, number] | undefined} */
  185. let lastIdentifier = undefined;
  186. /** @type [string, number, number][] */
  187. let balanced = [];
  188. /** @type {undefined | { start: number, url?: string, urlStart?: number, urlEnd?: number, layer?: string, layerStart?: number, layerEnd?: number, supports?: string, supportsStart?: number, supportsEnd?: number, inSupports?:boolean, media?: string }} */
  189. let importData = undefined;
  190. /** @type {boolean} */
  191. let inAnimationProperty = false;
  192. /** @type {boolean} */
  193. let isNextRulePrelude = true;
  194. /**
  195. * @param {string} input input
  196. * @param {number} pos position
  197. * @returns {boolean} true, when next is nested syntax
  198. */
  199. const isNextNestedSyntax = (input, pos) => {
  200. pos = walkCssTokens.eatWhitespaceAndComments(input, pos);
  201. if (input[pos] === "}") {
  202. return false;
  203. }
  204. // According spec only identifier can be used as a property name
  205. const isIdentifier = walkCssTokens.isIdentStartCodePoint(
  206. input.charCodeAt(pos)
  207. );
  208. return !isIdentifier;
  209. };
  210. /**
  211. * @returns {boolean} true, when in local scope
  212. */
  213. const isLocalMode = () =>
  214. modeData === "local" ||
  215. (this.defaultMode === "local" && modeData === undefined);
  216. /**
  217. * @param {string} chars characters
  218. * @returns {(input: string, pos: number) => number} function to eat characters
  219. */
  220. const eatUntil = chars => {
  221. const charCodes = Array.from({ length: chars.length }, (_, i) =>
  222. chars.charCodeAt(i)
  223. );
  224. const arr = Array.from(
  225. { length: charCodes.reduce((a, b) => Math.max(a, b), 0) + 1 },
  226. () => false
  227. );
  228. charCodes.forEach(cc => (arr[cc] = true));
  229. return (input, pos) => {
  230. for (;;) {
  231. const cc = input.charCodeAt(pos);
  232. if (cc < arr.length && arr[cc]) {
  233. return pos;
  234. }
  235. pos++;
  236. if (pos === input.length) return pos;
  237. }
  238. };
  239. };
  240. /**
  241. * @param {string} input input
  242. * @param {number} pos start position
  243. * @param {(input: string, pos: number) => number} eater eater
  244. * @returns {[number,string]} new position and text
  245. */
  246. const eatText = (input, pos, eater) => {
  247. let text = "";
  248. for (;;) {
  249. if (input.charCodeAt(pos) === CC_SLASH) {
  250. const newPos = walkCssTokens.eatComments(input, pos);
  251. if (pos !== newPos) {
  252. pos = newPos;
  253. if (pos === input.length) break;
  254. } else {
  255. text += "/";
  256. pos++;
  257. if (pos === input.length) break;
  258. }
  259. }
  260. const newPos = eater(input, pos);
  261. if (pos !== newPos) {
  262. text += input.slice(pos, newPos);
  263. pos = newPos;
  264. } else {
  265. break;
  266. }
  267. if (pos === input.length) break;
  268. }
  269. return [pos, text.trimEnd()];
  270. };
  271. const eatExportName = eatUntil(":};/");
  272. const eatExportValue = eatUntil("};/");
  273. /**
  274. * @param {string} input input
  275. * @param {number} pos start position
  276. * @returns {number} position after parse
  277. */
  278. const parseExports = (input, pos) => {
  279. pos = walkCssTokens.eatWhitespaceAndComments(input, pos);
  280. const cc = input.charCodeAt(pos);
  281. if (cc !== CC_LEFT_CURLY) {
  282. this._emitWarning(
  283. state,
  284. `Unexpected '${input[pos]}' at ${pos} during parsing of ':export' (expected '{')`,
  285. locConverter,
  286. pos,
  287. pos
  288. );
  289. return pos;
  290. }
  291. pos++;
  292. pos = walkCssTokens.eatWhitespaceAndComments(input, pos);
  293. for (;;) {
  294. if (input.charCodeAt(pos) === CC_RIGHT_CURLY) break;
  295. pos = walkCssTokens.eatWhitespaceAndComments(input, pos);
  296. if (pos === input.length) return pos;
  297. let start = pos;
  298. let name;
  299. [pos, name] = eatText(input, pos, eatExportName);
  300. if (pos === input.length) return pos;
  301. if (input.charCodeAt(pos) !== CC_COLON) {
  302. this._emitWarning(
  303. state,
  304. `Unexpected '${input[pos]}' at ${pos} during parsing of export name in ':export' (expected ':')`,
  305. locConverter,
  306. start,
  307. pos
  308. );
  309. return pos;
  310. }
  311. pos++;
  312. if (pos === input.length) return pos;
  313. pos = walkCssTokens.eatWhitespaceAndComments(input, pos);
  314. if (pos === input.length) return pos;
  315. let value;
  316. [pos, value] = eatText(input, pos, eatExportValue);
  317. if (pos === input.length) return pos;
  318. const cc = input.charCodeAt(pos);
  319. if (cc === CC_SEMICOLON) {
  320. pos++;
  321. if (pos === input.length) return pos;
  322. pos = walkCssTokens.eatWhitespaceAndComments(input, pos);
  323. if (pos === input.length) return pos;
  324. } else if (cc !== CC_RIGHT_CURLY) {
  325. this._emitWarning(
  326. state,
  327. `Unexpected '${input[pos]}' at ${pos} during parsing of export value in ':export' (expected ';' or '}')`,
  328. locConverter,
  329. start,
  330. pos
  331. );
  332. return pos;
  333. }
  334. const dep = new CssExportDependency(name, value);
  335. const { line: sl, column: sc } = locConverter.get(start);
  336. const { line: el, column: ec } = locConverter.get(pos);
  337. dep.setLoc(sl, sc, el, ec);
  338. module.addDependency(dep);
  339. }
  340. pos++;
  341. if (pos === input.length) return pos;
  342. pos = walkCssTokens.eatWhiteLine(input, pos);
  343. return pos;
  344. };
  345. const eatPropertyName = eatUntil(":{};");
  346. /**
  347. * @param {string} input input
  348. * @param {number} pos name start position
  349. * @param {number} end name end position
  350. * @returns {number} position after handling
  351. */
  352. const processLocalDeclaration = (input, pos, end) => {
  353. modeData = undefined;
  354. pos = walkCssTokens.eatWhitespaceAndComments(input, pos);
  355. const propertyNameStart = pos;
  356. const [propertyNameEnd, propertyName] = eatText(
  357. input,
  358. pos,
  359. eatPropertyName
  360. );
  361. if (input.charCodeAt(propertyNameEnd) !== CC_COLON) return end;
  362. pos = propertyNameEnd + 1;
  363. if (propertyName.startsWith("--")) {
  364. // CSS Variable
  365. const { line: sl, column: sc } = locConverter.get(propertyNameStart);
  366. const { line: el, column: ec } = locConverter.get(propertyNameEnd);
  367. const name = propertyName.slice(2);
  368. const dep = new CssLocalIdentifierDependency(
  369. name,
  370. [propertyNameStart, propertyNameEnd],
  371. "--"
  372. );
  373. dep.setLoc(sl, sc, el, ec);
  374. module.addDependency(dep);
  375. declaredCssVariables.add(name);
  376. } else if (
  377. !propertyName.startsWith("--") &&
  378. OPTIONALLY_VENDOR_PREFIXED_ANIMATION_PROPERTY.test(propertyName)
  379. ) {
  380. inAnimationProperty = true;
  381. }
  382. return pos;
  383. };
  384. /**
  385. * @param {string} input input
  386. */
  387. const processDeclarationValueDone = input => {
  388. if (inAnimationProperty && lastIdentifier) {
  389. const { line: sl, column: sc } = locConverter.get(lastIdentifier[0]);
  390. const { line: el, column: ec } = locConverter.get(lastIdentifier[1]);
  391. const name = input.slice(lastIdentifier[0], lastIdentifier[1]);
  392. const dep = new CssSelfLocalIdentifierDependency(name, lastIdentifier);
  393. dep.setLoc(sl, sc, el, ec);
  394. module.addDependency(dep);
  395. lastIdentifier = undefined;
  396. }
  397. };
  398. const eatKeyframes = eatUntil("{};/");
  399. const eatNameInVar = eatUntil(",)};/");
  400. walkCssTokens(source, {
  401. isSelector: () => {
  402. return isNextRulePrelude;
  403. },
  404. url: (input, start, end, contentStart, contentEnd) => {
  405. let value = normalizeUrl(input.slice(contentStart, contentEnd), false);
  406. switch (scope) {
  407. case CSS_MODE_IN_AT_IMPORT: {
  408. // Do not parse URLs in `supports(...)`
  409. if (importData.inSupports) {
  410. break;
  411. }
  412. if (importData.url) {
  413. this._emitWarning(
  414. state,
  415. `Duplicate of 'url(...)' in '${input.slice(
  416. importData.start,
  417. end
  418. )}'`,
  419. locConverter,
  420. start,
  421. end
  422. );
  423. break;
  424. }
  425. importData.url = value;
  426. importData.urlStart = start;
  427. importData.urlEnd = end;
  428. break;
  429. }
  430. // Do not parse URLs in import between rules
  431. case CSS_MODE_AT_NAMESPACE_INVALID:
  432. case CSS_MODE_AT_IMPORT_INVALID: {
  433. break;
  434. }
  435. case CSS_MODE_IN_BLOCK: {
  436. // Ignore `url()`, `url('')` and `url("")`, they are valid by spec
  437. if (value.length === 0) {
  438. break;
  439. }
  440. const dep = new CssUrlDependency(value, [start, end], "url");
  441. const { line: sl, column: sc } = locConverter.get(start);
  442. const { line: el, column: ec } = locConverter.get(end);
  443. dep.setLoc(sl, sc, el, ec);
  444. module.addDependency(dep);
  445. module.addCodeGenerationDependency(dep);
  446. break;
  447. }
  448. }
  449. return end;
  450. },
  451. string: (input, start, end) => {
  452. switch (scope) {
  453. case CSS_MODE_IN_AT_IMPORT: {
  454. const insideURLFunction =
  455. balanced[balanced.length - 1] &&
  456. balanced[balanced.length - 1][0] === "url";
  457. // Do not parse URLs in `supports(...)` and other strings if we already have a URL
  458. if (
  459. importData.inSupports ||
  460. (!insideURLFunction && importData.url)
  461. ) {
  462. break;
  463. }
  464. if (insideURLFunction && importData.url) {
  465. this._emitWarning(
  466. state,
  467. `Duplicate of 'url(...)' in '${input.slice(
  468. importData.start,
  469. end
  470. )}'`,
  471. locConverter,
  472. start,
  473. end
  474. );
  475. break;
  476. }
  477. importData.url = normalizeUrl(
  478. input.slice(start + 1, end - 1),
  479. true
  480. );
  481. if (!insideURLFunction) {
  482. importData.urlStart = start;
  483. importData.urlEnd = end;
  484. }
  485. break;
  486. }
  487. case CSS_MODE_IN_BLOCK: {
  488. // TODO move escaped parsing to tokenizer
  489. const last = balanced[balanced.length - 1];
  490. if (
  491. last &&
  492. (last[0].replace(/\\/g, "").toLowerCase() === "url" ||
  493. IMAGE_SET_FUNCTION.test(last[0].replace(/\\/g, "")))
  494. ) {
  495. let value = normalizeUrl(input.slice(start + 1, end - 1), true);
  496. // Ignore `url()`, `url('')` and `url("")`, they are valid by spec
  497. if (value.length === 0) {
  498. break;
  499. }
  500. const isUrl = last[0].replace(/\\/g, "").toLowerCase() === "url";
  501. const dep = new CssUrlDependency(
  502. value,
  503. [start, end],
  504. isUrl ? "string" : "url"
  505. );
  506. const { line: sl, column: sc } = locConverter.get(start);
  507. const { line: el, column: ec } = locConverter.get(end);
  508. dep.setLoc(sl, sc, el, ec);
  509. module.addDependency(dep);
  510. module.addCodeGenerationDependency(dep);
  511. }
  512. }
  513. }
  514. return end;
  515. },
  516. atKeyword: (input, start, end) => {
  517. const name = input.slice(start, end).toLowerCase();
  518. if (name === "@namespace") {
  519. scope = CSS_MODE_AT_NAMESPACE_INVALID;
  520. this._emitWarning(
  521. state,
  522. "'@namespace' is not supported in bundled CSS",
  523. locConverter,
  524. start,
  525. end
  526. );
  527. return end;
  528. } else if (name === "@import") {
  529. if (!allowImportAtRule) {
  530. scope = CSS_MODE_AT_IMPORT_INVALID;
  531. this._emitWarning(
  532. state,
  533. "Any '@import' rules must precede all other rules",
  534. locConverter,
  535. start,
  536. end
  537. );
  538. return end;
  539. }
  540. scope = CSS_MODE_IN_AT_IMPORT;
  541. importData = { start };
  542. } else if (
  543. this.allowModeSwitch &&
  544. OPTIONALLY_VENDOR_PREFIXED_KEYFRAMES_AT_RULE.test(name)
  545. ) {
  546. let pos = end;
  547. pos = walkCssTokens.eatWhitespaceAndComments(input, pos);
  548. if (pos === input.length) return pos;
  549. const [newPos, name] = eatText(input, pos, eatKeyframes);
  550. if (newPos === input.length) return newPos;
  551. if (input.charCodeAt(newPos) !== CC_LEFT_CURLY) {
  552. this._emitWarning(
  553. state,
  554. `Unexpected '${input[newPos]}' at ${newPos} during parsing of @keyframes (expected '{')`,
  555. locConverter,
  556. start,
  557. end
  558. );
  559. return newPos;
  560. }
  561. const { line: sl, column: sc } = locConverter.get(pos);
  562. const { line: el, column: ec } = locConverter.get(newPos);
  563. const dep = new CssLocalIdentifierDependency(name, [pos, newPos]);
  564. dep.setLoc(sl, sc, el, ec);
  565. module.addDependency(dep);
  566. pos = newPos;
  567. return pos + 1;
  568. } else if (this.allowModeSwitch && name === "@property") {
  569. let pos = end;
  570. pos = walkCssTokens.eatWhitespaceAndComments(input, pos);
  571. if (pos === input.length) return pos;
  572. const propertyNameStart = pos;
  573. const [propertyNameEnd, propertyName] = eatText(
  574. input,
  575. pos,
  576. eatKeyframes
  577. );
  578. if (propertyNameEnd === input.length) return propertyNameEnd;
  579. if (!propertyName.startsWith("--")) return propertyNameEnd;
  580. if (input.charCodeAt(propertyNameEnd) !== CC_LEFT_CURLY) {
  581. this._emitWarning(
  582. state,
  583. `Unexpected '${input[propertyNameEnd]}' at ${propertyNameEnd} during parsing of @property (expected '{')`,
  584. locConverter,
  585. start,
  586. end
  587. );
  588. return propertyNameEnd;
  589. }
  590. const { line: sl, column: sc } = locConverter.get(pos);
  591. const { line: el, column: ec } = locConverter.get(propertyNameEnd);
  592. const name = propertyName.slice(2);
  593. const dep = new CssLocalIdentifierDependency(
  594. name,
  595. [propertyNameStart, propertyNameEnd],
  596. "--"
  597. );
  598. dep.setLoc(sl, sc, el, ec);
  599. module.addDependency(dep);
  600. declaredCssVariables.add(name);
  601. pos = propertyNameEnd;
  602. return pos + 1;
  603. } else if (
  604. name === "@media" ||
  605. name === "@supports" ||
  606. name === "@layer" ||
  607. name === "@container"
  608. ) {
  609. modeData = isLocalMode() ? "local" : "global";
  610. isNextRulePrelude = true;
  611. return end;
  612. } else if (this.allowModeSwitch) {
  613. modeData = "global";
  614. isNextRulePrelude = false;
  615. }
  616. return end;
  617. },
  618. semicolon: (input, start, end) => {
  619. switch (scope) {
  620. case CSS_MODE_IN_AT_IMPORT: {
  621. const { start } = importData;
  622. if (importData.url === undefined) {
  623. this._emitWarning(
  624. state,
  625. `Expected URL in '${input.slice(start, end)}'`,
  626. locConverter,
  627. start,
  628. end
  629. );
  630. importData = undefined;
  631. scope = CSS_MODE_TOP_LEVEL;
  632. return end;
  633. }
  634. if (
  635. importData.urlStart > importData.layerStart ||
  636. importData.urlStart > importData.supportsStart
  637. ) {
  638. this._emitWarning(
  639. state,
  640. `An URL in '${input.slice(
  641. start,
  642. end
  643. )}' should be before 'layer(...)' or 'supports(...)'`,
  644. locConverter,
  645. start,
  646. end
  647. );
  648. importData = undefined;
  649. scope = CSS_MODE_TOP_LEVEL;
  650. return end;
  651. }
  652. if (importData.layerStart > importData.supportsStart) {
  653. this._emitWarning(
  654. state,
  655. `The 'layer(...)' in '${input.slice(
  656. start,
  657. end
  658. )}' should be before 'supports(...)'`,
  659. locConverter,
  660. start,
  661. end
  662. );
  663. importData = undefined;
  664. scope = CSS_MODE_TOP_LEVEL;
  665. return end;
  666. }
  667. const semicolonPos = end;
  668. end = walkCssTokens.eatWhiteLine(input, end + 1);
  669. const { line: sl, column: sc } = locConverter.get(start);
  670. const { line: el, column: ec } = locConverter.get(end);
  671. const lastEnd =
  672. importData.supportsEnd ||
  673. importData.layerEnd ||
  674. importData.urlEnd ||
  675. start;
  676. const pos = walkCssTokens.eatWhitespaceAndComments(input, lastEnd);
  677. // Prevent to consider comments as a part of media query
  678. if (pos !== semicolonPos - 1) {
  679. importData.media = input.slice(lastEnd, semicolonPos - 1).trim();
  680. }
  681. const url = importData.url.trim();
  682. if (url.length === 0) {
  683. const dep = new ConstDependency("", [start, end]);
  684. module.addPresentationalDependency(dep);
  685. dep.setLoc(sl, sc, el, ec);
  686. } else {
  687. const dep = new CssImportDependency(
  688. url,
  689. [start, end],
  690. importData.layer,
  691. importData.supports,
  692. importData.media && importData.media.length > 0
  693. ? importData.media
  694. : undefined
  695. );
  696. dep.setLoc(sl, sc, el, ec);
  697. module.addDependency(dep);
  698. }
  699. importData = undefined;
  700. scope = CSS_MODE_TOP_LEVEL;
  701. break;
  702. }
  703. case CSS_MODE_AT_IMPORT_INVALID:
  704. case CSS_MODE_AT_NAMESPACE_INVALID: {
  705. scope = CSS_MODE_TOP_LEVEL;
  706. break;
  707. }
  708. case CSS_MODE_IN_BLOCK: {
  709. if (this.allowModeSwitch) {
  710. processDeclarationValueDone(input);
  711. inAnimationProperty = false;
  712. isNextRulePrelude = isNextNestedSyntax(input, end);
  713. }
  714. break;
  715. }
  716. }
  717. return end;
  718. },
  719. leftCurlyBracket: (input, start, end) => {
  720. switch (scope) {
  721. case CSS_MODE_TOP_LEVEL: {
  722. allowImportAtRule = false;
  723. scope = CSS_MODE_IN_BLOCK;
  724. blockNestingLevel = 1;
  725. if (this.allowModeSwitch) {
  726. isNextRulePrelude = isNextNestedSyntax(input, end);
  727. }
  728. break;
  729. }
  730. case CSS_MODE_IN_BLOCK: {
  731. blockNestingLevel++;
  732. if (this.allowModeSwitch) {
  733. isNextRulePrelude = isNextNestedSyntax(input, end);
  734. }
  735. break;
  736. }
  737. }
  738. return end;
  739. },
  740. rightCurlyBracket: (input, start, end) => {
  741. switch (scope) {
  742. case CSS_MODE_IN_BLOCK: {
  743. if (isLocalMode()) {
  744. processDeclarationValueDone(input);
  745. inAnimationProperty = false;
  746. }
  747. if (--blockNestingLevel === 0) {
  748. scope = CSS_MODE_TOP_LEVEL;
  749. if (this.allowModeSwitch) {
  750. isNextRulePrelude = true;
  751. modeData = undefined;
  752. }
  753. } else if (this.allowModeSwitch) {
  754. isNextRulePrelude = isNextNestedSyntax(input, end);
  755. }
  756. break;
  757. }
  758. }
  759. return end;
  760. },
  761. identifier: (input, start, end) => {
  762. switch (scope) {
  763. case CSS_MODE_IN_BLOCK: {
  764. if (isLocalMode()) {
  765. // Handle only top level values and not inside functions
  766. if (inAnimationProperty && balanced.length === 0) {
  767. lastIdentifier = [start, end];
  768. } else {
  769. return processLocalDeclaration(input, start, end);
  770. }
  771. }
  772. break;
  773. }
  774. case CSS_MODE_IN_AT_IMPORT: {
  775. if (input.slice(start, end).toLowerCase() === "layer") {
  776. importData.layer = "";
  777. importData.layerStart = start;
  778. importData.layerEnd = end;
  779. }
  780. break;
  781. }
  782. }
  783. return end;
  784. },
  785. class: (input, start, end) => {
  786. if (isLocalMode()) {
  787. const name = input.slice(start + 1, end);
  788. const dep = new CssLocalIdentifierDependency(name, [start + 1, end]);
  789. const { line: sl, column: sc } = locConverter.get(start);
  790. const { line: el, column: ec } = locConverter.get(end);
  791. dep.setLoc(sl, sc, el, ec);
  792. module.addDependency(dep);
  793. }
  794. return end;
  795. },
  796. id: (input, start, end) => {
  797. if (isLocalMode()) {
  798. const name = input.slice(start + 1, end);
  799. const dep = new CssLocalIdentifierDependency(name, [start + 1, end]);
  800. const { line: sl, column: sc } = locConverter.get(start);
  801. const { line: el, column: ec } = locConverter.get(end);
  802. dep.setLoc(sl, sc, el, ec);
  803. module.addDependency(dep);
  804. }
  805. return end;
  806. },
  807. function: (input, start, end) => {
  808. let name = input.slice(start, end - 1);
  809. balanced.push([name, start, end]);
  810. if (
  811. scope === CSS_MODE_IN_AT_IMPORT &&
  812. name.toLowerCase() === "supports"
  813. ) {
  814. importData.inSupports = true;
  815. }
  816. if (isLocalMode()) {
  817. name = name.toLowerCase();
  818. // Don't rename animation name when we have `var()` function
  819. if (inAnimationProperty && balanced.length === 1) {
  820. lastIdentifier = undefined;
  821. }
  822. if (name === "var") {
  823. let pos = walkCssTokens.eatWhitespaceAndComments(input, end);
  824. if (pos === input.length) return pos;
  825. const [newPos, name] = eatText(input, pos, eatNameInVar);
  826. if (!name.startsWith("--")) return end;
  827. const { line: sl, column: sc } = locConverter.get(pos);
  828. const { line: el, column: ec } = locConverter.get(newPos);
  829. const dep = new CssSelfLocalIdentifierDependency(
  830. name.slice(2),
  831. [pos, newPos],
  832. "--",
  833. declaredCssVariables
  834. );
  835. dep.setLoc(sl, sc, el, ec);
  836. module.addDependency(dep);
  837. return newPos;
  838. }
  839. }
  840. return end;
  841. },
  842. leftParenthesis: (input, start, end) => {
  843. balanced.push(["(", start, end]);
  844. return end;
  845. },
  846. rightParenthesis: (input, start, end) => {
  847. const last = balanced[balanced.length - 1];
  848. const popped = balanced.pop();
  849. if (
  850. this.allowModeSwitch &&
  851. popped &&
  852. (popped[0] === ":local" || popped[0] === ":global")
  853. ) {
  854. modeData = balanced[balanced.length - 1]
  855. ? /** @type {"local" | "global"} */
  856. (balanced[balanced.length - 1][0])
  857. : undefined;
  858. const dep = new ConstDependency("", [start, end]);
  859. module.addPresentationalDependency(dep);
  860. return end;
  861. }
  862. switch (scope) {
  863. case CSS_MODE_IN_AT_IMPORT: {
  864. if (last && last[0] === "url" && !importData.inSupports) {
  865. importData.urlStart = last[1];
  866. importData.urlEnd = end;
  867. } else if (
  868. last &&
  869. last[0].toLowerCase() === "layer" &&
  870. !importData.inSupports
  871. ) {
  872. importData.layer = input.slice(last[2], end - 1).trim();
  873. importData.layerStart = last[1];
  874. importData.layerEnd = end;
  875. } else if (last && last[0].toLowerCase() === "supports") {
  876. importData.supports = input.slice(last[2], end - 1).trim();
  877. importData.supportsStart = last[1];
  878. importData.supportsEnd = end;
  879. importData.inSupports = false;
  880. }
  881. break;
  882. }
  883. }
  884. return end;
  885. },
  886. pseudoClass: (input, start, end) => {
  887. if (this.allowModeSwitch) {
  888. const name = input.slice(start, end).toLowerCase();
  889. if (name === ":global") {
  890. modeData = "global";
  891. // Eat extra whitespace and comments
  892. end = walkCssTokens.eatWhitespace(input, end);
  893. const dep = new ConstDependency("", [start, end]);
  894. module.addPresentationalDependency(dep);
  895. return end;
  896. } else if (name === ":local") {
  897. modeData = "local";
  898. // Eat extra whitespace and comments
  899. end = walkCssTokens.eatWhitespace(input, end);
  900. const dep = new ConstDependency("", [start, end]);
  901. module.addPresentationalDependency(dep);
  902. return end;
  903. }
  904. switch (scope) {
  905. case CSS_MODE_TOP_LEVEL: {
  906. if (name === ":export") {
  907. const pos = parseExports(input, end);
  908. const dep = new ConstDependency("", [start, pos]);
  909. module.addPresentationalDependency(dep);
  910. return pos;
  911. }
  912. break;
  913. }
  914. }
  915. }
  916. return end;
  917. },
  918. pseudoFunction: (input, start, end) => {
  919. let name = input.slice(start, end - 1);
  920. balanced.push([name, start, end]);
  921. if (this.allowModeSwitch) {
  922. name = name.toLowerCase();
  923. if (name === ":global") {
  924. modeData = "global";
  925. const dep = new ConstDependency("", [start, end]);
  926. module.addPresentationalDependency(dep);
  927. } else if (name === ":local") {
  928. modeData = "local";
  929. const dep = new ConstDependency("", [start, end]);
  930. module.addPresentationalDependency(dep);
  931. }
  932. }
  933. return end;
  934. },
  935. comma: (input, start, end) => {
  936. if (this.allowModeSwitch) {
  937. // Reset stack for `:global .class :local .class-other` selector after
  938. modeData = undefined;
  939. switch (scope) {
  940. case CSS_MODE_IN_BLOCK: {
  941. if (isLocalMode()) {
  942. processDeclarationValueDone(input);
  943. }
  944. break;
  945. }
  946. }
  947. }
  948. return end;
  949. }
  950. });
  951. if (oldDefaultMode) {
  952. this.defaultMode = oldDefaultMode;
  953. }
  954. module.buildInfo.strict = true;
  955. module.buildMeta.exportsType = this.namedExports ? "namespace" : "default";
  956. module.addDependency(new StaticExportsDependency([], true));
  957. return state;
  958. }
  959. }
  960. module.exports = CssParser;