CssParser.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047
  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. if (isLocalMode()) {
  562. const { line: sl, column: sc } = locConverter.get(pos);
  563. const { line: el, column: ec } = locConverter.get(newPos);
  564. const dep = new CssLocalIdentifierDependency(name, [pos, newPos]);
  565. dep.setLoc(sl, sc, el, ec);
  566. module.addDependency(dep);
  567. }
  568. pos = newPos;
  569. return pos + 1;
  570. } else if (this.allowModeSwitch && name === "@property") {
  571. let pos = end;
  572. pos = walkCssTokens.eatWhitespaceAndComments(input, pos);
  573. if (pos === input.length) return pos;
  574. const propertyNameStart = pos;
  575. const [propertyNameEnd, propertyName] = eatText(
  576. input,
  577. pos,
  578. eatKeyframes
  579. );
  580. if (propertyNameEnd === input.length) return propertyNameEnd;
  581. if (!propertyName.startsWith("--")) return propertyNameEnd;
  582. if (input.charCodeAt(propertyNameEnd) !== CC_LEFT_CURLY) {
  583. this._emitWarning(
  584. state,
  585. `Unexpected '${input[propertyNameEnd]}' at ${propertyNameEnd} during parsing of @property (expected '{')`,
  586. locConverter,
  587. start,
  588. end
  589. );
  590. return propertyNameEnd;
  591. }
  592. const name = propertyName.slice(2);
  593. declaredCssVariables.add(name);
  594. if (isLocalMode()) {
  595. const { line: sl, column: sc } = locConverter.get(pos);
  596. const { line: el, column: ec } = locConverter.get(propertyNameEnd);
  597. const dep = new CssLocalIdentifierDependency(
  598. name,
  599. [propertyNameStart, propertyNameEnd],
  600. "--"
  601. );
  602. dep.setLoc(sl, sc, el, ec);
  603. module.addDependency(dep);
  604. }
  605. pos = propertyNameEnd;
  606. return pos + 1;
  607. } else if (
  608. name === "@media" ||
  609. name === "@supports" ||
  610. name === "@layer" ||
  611. name === "@container"
  612. ) {
  613. modeData = isLocalMode() ? "local" : "global";
  614. isNextRulePrelude = true;
  615. return end;
  616. } else if (this.allowModeSwitch) {
  617. modeData = "global";
  618. isNextRulePrelude = false;
  619. }
  620. return end;
  621. },
  622. semicolon: (input, start, end) => {
  623. switch (scope) {
  624. case CSS_MODE_IN_AT_IMPORT: {
  625. const { start } = importData;
  626. if (importData.url === undefined) {
  627. this._emitWarning(
  628. state,
  629. `Expected URL in '${input.slice(start, end)}'`,
  630. locConverter,
  631. start,
  632. end
  633. );
  634. importData = undefined;
  635. scope = CSS_MODE_TOP_LEVEL;
  636. return end;
  637. }
  638. if (
  639. importData.urlStart > importData.layerStart ||
  640. importData.urlStart > importData.supportsStart
  641. ) {
  642. this._emitWarning(
  643. state,
  644. `An URL in '${input.slice(
  645. start,
  646. end
  647. )}' should be before 'layer(...)' or 'supports(...)'`,
  648. locConverter,
  649. start,
  650. end
  651. );
  652. importData = undefined;
  653. scope = CSS_MODE_TOP_LEVEL;
  654. return end;
  655. }
  656. if (importData.layerStart > importData.supportsStart) {
  657. this._emitWarning(
  658. state,
  659. `The 'layer(...)' in '${input.slice(
  660. start,
  661. end
  662. )}' should be before 'supports(...)'`,
  663. locConverter,
  664. start,
  665. end
  666. );
  667. importData = undefined;
  668. scope = CSS_MODE_TOP_LEVEL;
  669. return end;
  670. }
  671. const semicolonPos = end;
  672. end = walkCssTokens.eatWhiteLine(input, end);
  673. const { line: sl, column: sc } = locConverter.get(start);
  674. const { line: el, column: ec } = locConverter.get(end);
  675. const lastEnd =
  676. importData.supportsEnd ||
  677. importData.layerEnd ||
  678. importData.urlEnd ||
  679. start;
  680. const pos = walkCssTokens.eatWhitespaceAndComments(input, lastEnd);
  681. // Prevent to consider comments as a part of media query
  682. if (pos !== semicolonPos - 1) {
  683. importData.media = input.slice(lastEnd, semicolonPos - 1).trim();
  684. }
  685. const url = importData.url.trim();
  686. if (url.length === 0) {
  687. const dep = new ConstDependency("", [start, end]);
  688. module.addPresentationalDependency(dep);
  689. dep.setLoc(sl, sc, el, ec);
  690. } else {
  691. const dep = new CssImportDependency(
  692. url,
  693. [start, end],
  694. importData.layer,
  695. importData.supports,
  696. importData.media && importData.media.length > 0
  697. ? importData.media
  698. : undefined
  699. );
  700. dep.setLoc(sl, sc, el, ec);
  701. module.addDependency(dep);
  702. }
  703. importData = undefined;
  704. scope = CSS_MODE_TOP_LEVEL;
  705. break;
  706. }
  707. case CSS_MODE_AT_IMPORT_INVALID:
  708. case CSS_MODE_AT_NAMESPACE_INVALID: {
  709. scope = CSS_MODE_TOP_LEVEL;
  710. break;
  711. }
  712. case CSS_MODE_IN_BLOCK: {
  713. if (this.allowModeSwitch) {
  714. processDeclarationValueDone(input);
  715. inAnimationProperty = false;
  716. isNextRulePrelude = isNextNestedSyntax(input, end);
  717. }
  718. break;
  719. }
  720. }
  721. return end;
  722. },
  723. leftCurlyBracket: (input, start, end) => {
  724. switch (scope) {
  725. case CSS_MODE_TOP_LEVEL: {
  726. allowImportAtRule = false;
  727. scope = CSS_MODE_IN_BLOCK;
  728. blockNestingLevel = 1;
  729. if (this.allowModeSwitch) {
  730. isNextRulePrelude = isNextNestedSyntax(input, end);
  731. }
  732. break;
  733. }
  734. case CSS_MODE_IN_BLOCK: {
  735. blockNestingLevel++;
  736. if (this.allowModeSwitch) {
  737. isNextRulePrelude = isNextNestedSyntax(input, end);
  738. }
  739. break;
  740. }
  741. }
  742. return end;
  743. },
  744. rightCurlyBracket: (input, start, end) => {
  745. switch (scope) {
  746. case CSS_MODE_IN_BLOCK: {
  747. if (isLocalMode()) {
  748. processDeclarationValueDone(input);
  749. inAnimationProperty = false;
  750. }
  751. if (--blockNestingLevel === 0) {
  752. scope = CSS_MODE_TOP_LEVEL;
  753. if (this.allowModeSwitch) {
  754. isNextRulePrelude = true;
  755. modeData = undefined;
  756. }
  757. } else if (this.allowModeSwitch) {
  758. isNextRulePrelude = isNextNestedSyntax(input, end);
  759. }
  760. break;
  761. }
  762. }
  763. return end;
  764. },
  765. identifier: (input, start, end) => {
  766. switch (scope) {
  767. case CSS_MODE_IN_BLOCK: {
  768. if (isLocalMode()) {
  769. // Handle only top level values and not inside functions
  770. if (inAnimationProperty && balanced.length === 0) {
  771. lastIdentifier = [start, end];
  772. } else {
  773. return processLocalDeclaration(input, start, end);
  774. }
  775. }
  776. break;
  777. }
  778. case CSS_MODE_IN_AT_IMPORT: {
  779. if (input.slice(start, end).toLowerCase() === "layer") {
  780. importData.layer = "";
  781. importData.layerStart = start;
  782. importData.layerEnd = end;
  783. }
  784. break;
  785. }
  786. }
  787. return end;
  788. },
  789. class: (input, start, end) => {
  790. if (isLocalMode()) {
  791. const name = input.slice(start + 1, end);
  792. const dep = new CssLocalIdentifierDependency(name, [start + 1, end]);
  793. const { line: sl, column: sc } = locConverter.get(start);
  794. const { line: el, column: ec } = locConverter.get(end);
  795. dep.setLoc(sl, sc, el, ec);
  796. module.addDependency(dep);
  797. }
  798. return end;
  799. },
  800. id: (input, start, end) => {
  801. if (isLocalMode()) {
  802. const name = input.slice(start + 1, end);
  803. const dep = new CssLocalIdentifierDependency(name, [start + 1, end]);
  804. const { line: sl, column: sc } = locConverter.get(start);
  805. const { line: el, column: ec } = locConverter.get(end);
  806. dep.setLoc(sl, sc, el, ec);
  807. module.addDependency(dep);
  808. }
  809. return end;
  810. },
  811. function: (input, start, end) => {
  812. let name = input.slice(start, end - 1);
  813. balanced.push([name, start, end]);
  814. if (
  815. scope === CSS_MODE_IN_AT_IMPORT &&
  816. name.toLowerCase() === "supports"
  817. ) {
  818. importData.inSupports = true;
  819. }
  820. if (isLocalMode()) {
  821. name = name.toLowerCase();
  822. // Don't rename animation name when we have `var()` function
  823. if (inAnimationProperty && balanced.length === 1) {
  824. lastIdentifier = undefined;
  825. }
  826. if (name === "var") {
  827. let pos = walkCssTokens.eatWhitespaceAndComments(input, end);
  828. if (pos === input.length) return pos;
  829. const [newPos, name] = eatText(input, pos, eatNameInVar);
  830. if (!name.startsWith("--")) return end;
  831. const { line: sl, column: sc } = locConverter.get(pos);
  832. const { line: el, column: ec } = locConverter.get(newPos);
  833. const dep = new CssSelfLocalIdentifierDependency(
  834. name.slice(2),
  835. [pos, newPos],
  836. "--",
  837. declaredCssVariables
  838. );
  839. dep.setLoc(sl, sc, el, ec);
  840. module.addDependency(dep);
  841. return newPos;
  842. }
  843. }
  844. return end;
  845. },
  846. leftParenthesis: (input, start, end) => {
  847. balanced.push(["(", start, end]);
  848. return end;
  849. },
  850. rightParenthesis: (input, start, end) => {
  851. const last = balanced[balanced.length - 1];
  852. const popped = balanced.pop();
  853. if (
  854. this.allowModeSwitch &&
  855. popped &&
  856. (popped[0] === ":local" || popped[0] === ":global")
  857. ) {
  858. modeData = balanced[balanced.length - 1]
  859. ? /** @type {"local" | "global"} */
  860. (balanced[balanced.length - 1][0])
  861. : undefined;
  862. const dep = new ConstDependency("", [start, end]);
  863. module.addPresentationalDependency(dep);
  864. return end;
  865. }
  866. switch (scope) {
  867. case CSS_MODE_IN_AT_IMPORT: {
  868. if (last && last[0] === "url" && !importData.inSupports) {
  869. importData.urlStart = last[1];
  870. importData.urlEnd = end;
  871. } else if (
  872. last &&
  873. last[0].toLowerCase() === "layer" &&
  874. !importData.inSupports
  875. ) {
  876. importData.layer = input.slice(last[2], end - 1).trim();
  877. importData.layerStart = last[1];
  878. importData.layerEnd = end;
  879. } else if (last && last[0].toLowerCase() === "supports") {
  880. importData.supports = input.slice(last[2], end - 1).trim();
  881. importData.supportsStart = last[1];
  882. importData.supportsEnd = end;
  883. importData.inSupports = false;
  884. }
  885. break;
  886. }
  887. }
  888. return end;
  889. },
  890. pseudoClass: (input, start, end) => {
  891. if (this.allowModeSwitch) {
  892. const name = input.slice(start, end).toLowerCase();
  893. if (name === ":global") {
  894. modeData = "global";
  895. // Eat extra whitespace and comments
  896. end = walkCssTokens.eatWhitespace(input, end);
  897. const dep = new ConstDependency("", [start, end]);
  898. module.addPresentationalDependency(dep);
  899. return end;
  900. } else if (name === ":local") {
  901. modeData = "local";
  902. // Eat extra whitespace and comments
  903. end = walkCssTokens.eatWhitespace(input, end);
  904. const dep = new ConstDependency("", [start, end]);
  905. module.addPresentationalDependency(dep);
  906. return end;
  907. }
  908. switch (scope) {
  909. case CSS_MODE_TOP_LEVEL: {
  910. if (name === ":export") {
  911. const pos = parseExports(input, end);
  912. const dep = new ConstDependency("", [start, pos]);
  913. module.addPresentationalDependency(dep);
  914. return pos;
  915. }
  916. break;
  917. }
  918. }
  919. }
  920. return end;
  921. },
  922. pseudoFunction: (input, start, end) => {
  923. let name = input.slice(start, end - 1);
  924. balanced.push([name, start, end]);
  925. if (this.allowModeSwitch) {
  926. name = name.toLowerCase();
  927. if (name === ":global") {
  928. modeData = "global";
  929. const dep = new ConstDependency("", [start, end]);
  930. module.addPresentationalDependency(dep);
  931. } else if (name === ":local") {
  932. modeData = "local";
  933. const dep = new ConstDependency("", [start, end]);
  934. module.addPresentationalDependency(dep);
  935. }
  936. }
  937. return end;
  938. },
  939. comma: (input, start, end) => {
  940. if (this.allowModeSwitch) {
  941. // Reset stack for `:global .class :local .class-other` selector after
  942. modeData = undefined;
  943. switch (scope) {
  944. case CSS_MODE_IN_BLOCK: {
  945. if (isLocalMode()) {
  946. processDeclarationValueDone(input);
  947. }
  948. break;
  949. }
  950. }
  951. }
  952. return end;
  953. }
  954. });
  955. if (oldDefaultMode) {
  956. this.defaultMode = oldDefaultMode;
  957. }
  958. module.buildInfo.strict = true;
  959. module.buildMeta.exportsType = this.namedExports ? "namespace" : "default";
  960. if (!this.namedExports) {
  961. module.buildMeta.defaultObject = "redirect";
  962. }
  963. module.addDependency(new StaticExportsDependency([], true));
  964. return state;
  965. }
  966. }
  967. module.exports = CssParser;