CssParser.js 18 KB

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