CssParser.js 29 KB

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