parse.cjs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596
  1. 'use strict';
  2. const scanner = require('./scanner.cjs');
  3. const TAB = 9;
  4. const N = 10;
  5. const F = 12;
  6. const R = 13;
  7. const SPACE = 32;
  8. const EXCLAMATIONMARK = 33; // !
  9. const NUMBERSIGN = 35; // #
  10. const AMPERSAND = 38; // &
  11. const APOSTROPHE = 39; // '
  12. const LEFTPARENTHESIS = 40; // (
  13. const RIGHTPARENTHESIS = 41; // )
  14. const ASTERISK = 42; // *
  15. const PLUSSIGN = 43; // +
  16. const COMMA = 44; // ,
  17. const HYPERMINUS = 45; // -
  18. const LESSTHANSIGN = 60; // <
  19. const GREATERTHANSIGN = 62; // >
  20. const QUESTIONMARK = 63; // ?
  21. const COMMERCIALAT = 64; // @
  22. const LEFTSQUAREBRACKET = 91; // [
  23. const RIGHTSQUAREBRACKET = 93; // ]
  24. const LEFTCURLYBRACKET = 123; // {
  25. const VERTICALLINE = 124; // |
  26. const RIGHTCURLYBRACKET = 125; // }
  27. const INFINITY = 8734; // ∞
  28. const COMBINATOR_PRECEDENCE = {
  29. ' ': 1,
  30. '&&': 2,
  31. '||': 3,
  32. '|': 4
  33. };
  34. function readMultiplierRange(scanner) {
  35. let min = null;
  36. let max = null;
  37. scanner.eat(LEFTCURLYBRACKET);
  38. scanner.skipWs();
  39. min = scanner.scanNumber(scanner);
  40. scanner.skipWs();
  41. if (scanner.charCode() === COMMA) {
  42. scanner.pos++;
  43. scanner.skipWs();
  44. if (scanner.charCode() !== RIGHTCURLYBRACKET) {
  45. max = scanner.scanNumber(scanner);
  46. scanner.skipWs();
  47. }
  48. } else {
  49. max = min;
  50. }
  51. scanner.eat(RIGHTCURLYBRACKET);
  52. return {
  53. min: Number(min),
  54. max: max ? Number(max) : 0
  55. };
  56. }
  57. function readMultiplier(scanner) {
  58. let range = null;
  59. let comma = false;
  60. switch (scanner.charCode()) {
  61. case ASTERISK:
  62. scanner.pos++;
  63. range = {
  64. min: 0,
  65. max: 0
  66. };
  67. break;
  68. case PLUSSIGN:
  69. scanner.pos++;
  70. range = {
  71. min: 1,
  72. max: 0
  73. };
  74. break;
  75. case QUESTIONMARK:
  76. scanner.pos++;
  77. range = {
  78. min: 0,
  79. max: 1
  80. };
  81. break;
  82. case NUMBERSIGN:
  83. scanner.pos++;
  84. comma = true;
  85. if (scanner.charCode() === LEFTCURLYBRACKET) {
  86. range = readMultiplierRange(scanner);
  87. } else if (scanner.charCode() === QUESTIONMARK) {
  88. // https://www.w3.org/TR/css-values-4/#component-multipliers
  89. // > the # and ? multipliers may be stacked as #?
  90. // In this case just treat "#?" as a single multiplier
  91. // { min: 0, max: 0, comma: true }
  92. scanner.pos++;
  93. range = {
  94. min: 0,
  95. max: 0
  96. };
  97. } else {
  98. range = {
  99. min: 1,
  100. max: 0
  101. };
  102. }
  103. break;
  104. case LEFTCURLYBRACKET:
  105. range = readMultiplierRange(scanner);
  106. break;
  107. default:
  108. return null;
  109. }
  110. return {
  111. type: 'Multiplier',
  112. comma,
  113. min: range.min,
  114. max: range.max,
  115. term: null
  116. };
  117. }
  118. function maybeMultiplied(scanner, node) {
  119. const multiplier = readMultiplier(scanner);
  120. if (multiplier !== null) {
  121. multiplier.term = node;
  122. // https://www.w3.org/TR/css-values-4/#component-multipliers
  123. // > The + and # multipliers may be stacked as +#;
  124. // Represent "+#" as nested multipliers:
  125. // { ...<multiplier #>,
  126. // term: {
  127. // ...<multipler +>,
  128. // term: node
  129. // }
  130. // }
  131. if (scanner.charCode() === NUMBERSIGN &&
  132. scanner.charCodeAt(scanner.pos - 1) === PLUSSIGN) {
  133. return maybeMultiplied(scanner, multiplier);
  134. }
  135. // https://www.w3.org/TR/css-values-4/#component-multipliers
  136. // > the # and ? multipliers, {A} and ? multipliers, and {A,B} and ? multipliers
  137. // > may be stacked as #?, {A}?, and {A,B}?, respectively
  138. // Represent "{}?" as nested multipliers as well as "+#".
  139. // The "#?" case is already handled above, in maybeMultiplied()
  140. if (scanner.charCode() === QUESTIONMARK &&
  141. scanner.charCodeAt(scanner.pos - 1) === RIGHTCURLYBRACKET) {
  142. return maybeMultiplied(scanner, multiplier);
  143. }
  144. return multiplier;
  145. }
  146. return node;
  147. }
  148. function maybeToken(scanner) {
  149. const ch = scanner.peek();
  150. if (ch === '') {
  151. return null;
  152. }
  153. return maybeMultiplied(scanner, {
  154. type: 'Token',
  155. value: ch
  156. });
  157. }
  158. function readProperty(scanner) {
  159. let name;
  160. scanner.eat(LESSTHANSIGN);
  161. scanner.eat(APOSTROPHE);
  162. name = scanner.scanWord();
  163. scanner.eat(APOSTROPHE);
  164. scanner.eat(GREATERTHANSIGN);
  165. return maybeMultiplied(scanner, {
  166. type: 'Property',
  167. name
  168. });
  169. }
  170. // https://drafts.csswg.org/css-values-3/#numeric-ranges
  171. // 4.1. Range Restrictions and Range Definition Notation
  172. //
  173. // Range restrictions can be annotated in the numeric type notation using CSS bracketed
  174. // range notation—[min,max]—within the angle brackets, after the identifying keyword,
  175. // indicating a closed range between (and including) min and max.
  176. // For example, <integer [0, 10]> indicates an integer between 0 and 10, inclusive.
  177. function readTypeRange(scanner) {
  178. // use null for Infinity to make AST format JSON serializable/deserializable
  179. let min = null; // -Infinity
  180. let max = null; // Infinity
  181. let sign = 1;
  182. scanner.eat(LEFTSQUAREBRACKET);
  183. if (scanner.charCode() === HYPERMINUS) {
  184. scanner.peek();
  185. sign = -1;
  186. }
  187. if (sign == -1 && scanner.charCode() === INFINITY) {
  188. scanner.peek();
  189. } else {
  190. min = sign * Number(scanner.scanNumber(scanner));
  191. if (scanner.isNameCharCode()) {
  192. min += scanner.scanWord();
  193. }
  194. }
  195. scanner.skipWs();
  196. scanner.eat(COMMA);
  197. scanner.skipWs();
  198. if (scanner.charCode() === INFINITY) {
  199. scanner.peek();
  200. } else {
  201. sign = 1;
  202. if (scanner.charCode() === HYPERMINUS) {
  203. scanner.peek();
  204. sign = -1;
  205. }
  206. max = sign * Number(scanner.scanNumber(scanner));
  207. if (scanner.isNameCharCode()) {
  208. max += scanner.scanWord();
  209. }
  210. }
  211. scanner.eat(RIGHTSQUAREBRACKET);
  212. return {
  213. type: 'Range',
  214. min,
  215. max
  216. };
  217. }
  218. function readType(scanner) {
  219. let name;
  220. let opts = null;
  221. scanner.eat(LESSTHANSIGN);
  222. name = scanner.scanWord();
  223. // https://drafts.csswg.org/css-values-5/#boolean
  224. if (name === 'boolean-expr') {
  225. scanner.eat(LEFTSQUAREBRACKET);
  226. const implicitGroup = readImplicitGroup(scanner, RIGHTSQUAREBRACKET);
  227. scanner.eat(RIGHTSQUAREBRACKET);
  228. scanner.eat(GREATERTHANSIGN);
  229. return maybeMultiplied(scanner, {
  230. type: 'Boolean',
  231. term: implicitGroup.terms.length === 1
  232. ? implicitGroup.terms[0]
  233. : implicitGroup
  234. });
  235. }
  236. if (scanner.charCode() === LEFTPARENTHESIS &&
  237. scanner.nextCharCode() === RIGHTPARENTHESIS) {
  238. scanner.pos += 2;
  239. name += '()';
  240. }
  241. if (scanner.charCodeAt(scanner.findWsEnd(scanner.pos)) === LEFTSQUAREBRACKET) {
  242. scanner.skipWs();
  243. opts = readTypeRange(scanner);
  244. }
  245. scanner.eat(GREATERTHANSIGN);
  246. return maybeMultiplied(scanner, {
  247. type: 'Type',
  248. name,
  249. opts
  250. });
  251. }
  252. function readKeywordOrFunction(scanner) {
  253. const name = scanner.scanWord();
  254. if (scanner.charCode() === LEFTPARENTHESIS) {
  255. scanner.pos++;
  256. return {
  257. type: 'Function',
  258. name
  259. };
  260. }
  261. return maybeMultiplied(scanner, {
  262. type: 'Keyword',
  263. name
  264. });
  265. }
  266. function regroupTerms(terms, combinators) {
  267. function createGroup(terms, combinator) {
  268. return {
  269. type: 'Group',
  270. terms,
  271. combinator,
  272. disallowEmpty: false,
  273. explicit: false
  274. };
  275. }
  276. let combinator;
  277. combinators = Object.keys(combinators)
  278. .sort((a, b) => COMBINATOR_PRECEDENCE[a] - COMBINATOR_PRECEDENCE[b]);
  279. while (combinators.length > 0) {
  280. combinator = combinators.shift();
  281. let i = 0;
  282. let subgroupStart = 0;
  283. for (; i < terms.length; i++) {
  284. const term = terms[i];
  285. if (term.type === 'Combinator') {
  286. if (term.value === combinator) {
  287. if (subgroupStart === -1) {
  288. subgroupStart = i - 1;
  289. }
  290. terms.splice(i, 1);
  291. i--;
  292. } else {
  293. if (subgroupStart !== -1 && i - subgroupStart > 1) {
  294. terms.splice(
  295. subgroupStart,
  296. i - subgroupStart,
  297. createGroup(terms.slice(subgroupStart, i), combinator)
  298. );
  299. i = subgroupStart + 1;
  300. }
  301. subgroupStart = -1;
  302. }
  303. }
  304. }
  305. if (subgroupStart !== -1 && combinators.length) {
  306. terms.splice(
  307. subgroupStart,
  308. i - subgroupStart,
  309. createGroup(terms.slice(subgroupStart, i), combinator)
  310. );
  311. }
  312. }
  313. return combinator;
  314. }
  315. function readImplicitGroup(scanner, stopCharCode = -1) {
  316. const combinators = Object.create(null);
  317. const terms = [];
  318. let prevToken = null;
  319. let prevTokenPos = scanner.pos;
  320. let prevTokenIsFunction = false;
  321. while (scanner.charCode() !== stopCharCode) {
  322. let token = prevTokenIsFunction
  323. ? readImplicitGroup(scanner, RIGHTPARENTHESIS)
  324. : peek(scanner);
  325. if (!token) {
  326. break;
  327. }
  328. if (token.type === 'Spaces') {
  329. continue;
  330. }
  331. if (prevTokenIsFunction) {
  332. if (token.terms.length === 0) {
  333. prevTokenIsFunction = false;
  334. continue;
  335. }
  336. if (token.combinator === ' ') {
  337. while (token.terms.length > 1) {
  338. combinators[' '] = true; // a b
  339. terms.push({
  340. type: 'Combinator',
  341. value: ' '
  342. }, token.terms.shift());
  343. }
  344. token = token.terms[0];
  345. }
  346. }
  347. if (token.type === 'Combinator') {
  348. // check for combinator in group beginning and double combinator sequence
  349. if (prevToken === null || prevToken.type === 'Combinator') {
  350. scanner.pos = prevTokenPos;
  351. scanner.error('Unexpected combinator');
  352. }
  353. combinators[token.value] = true;
  354. } else if (prevToken !== null && prevToken.type !== 'Combinator') {
  355. combinators[' '] = true; // a b
  356. terms.push({
  357. type: 'Combinator',
  358. value: ' '
  359. });
  360. }
  361. terms.push(token);
  362. prevToken = token;
  363. prevTokenPos = scanner.pos;
  364. prevTokenIsFunction = token.type === 'Function';
  365. }
  366. // check for combinator in group ending
  367. if (prevToken !== null && prevToken.type === 'Combinator') {
  368. scanner.pos -= prevTokenPos;
  369. scanner.error('Unexpected combinator');
  370. }
  371. return {
  372. type: 'Group',
  373. terms,
  374. combinator: regroupTerms(terms, combinators) || ' ',
  375. disallowEmpty: false,
  376. explicit: false
  377. };
  378. }
  379. function readGroup(scanner) {
  380. let result;
  381. scanner.eat(LEFTSQUAREBRACKET);
  382. result = readImplicitGroup(scanner, RIGHTSQUAREBRACKET);
  383. scanner.eat(RIGHTSQUAREBRACKET);
  384. result.explicit = true;
  385. if (scanner.charCode() === EXCLAMATIONMARK) {
  386. scanner.pos++;
  387. result.disallowEmpty = true;
  388. }
  389. return result;
  390. }
  391. function peek(scanner) {
  392. let code = scanner.charCode();
  393. switch (code) {
  394. case RIGHTSQUAREBRACKET:
  395. // don't eat, stop scan a group
  396. break;
  397. case LEFTSQUAREBRACKET:
  398. return maybeMultiplied(scanner, readGroup(scanner));
  399. case LESSTHANSIGN:
  400. return scanner.nextCharCode() === APOSTROPHE
  401. ? readProperty(scanner)
  402. : readType(scanner);
  403. case VERTICALLINE:
  404. return {
  405. type: 'Combinator',
  406. value: scanner.substringToPos(
  407. scanner.pos + (scanner.nextCharCode() === VERTICALLINE ? 2 : 1)
  408. )
  409. };
  410. case AMPERSAND:
  411. scanner.pos++;
  412. scanner.eat(AMPERSAND);
  413. return {
  414. type: 'Combinator',
  415. value: '&&'
  416. };
  417. case COMMA:
  418. scanner.pos++;
  419. return {
  420. type: 'Comma'
  421. };
  422. case APOSTROPHE:
  423. return maybeMultiplied(scanner, {
  424. type: 'String',
  425. value: scanner.scanString()
  426. });
  427. case SPACE:
  428. case TAB:
  429. case N:
  430. case R:
  431. case F:
  432. return {
  433. type: 'Spaces',
  434. value: scanner.scanSpaces()
  435. };
  436. case COMMERCIALAT:
  437. code = scanner.nextCharCode();
  438. if (scanner.isNameCharCode(code)) {
  439. scanner.pos++;
  440. return {
  441. type: 'AtKeyword',
  442. name: scanner.scanWord()
  443. };
  444. }
  445. return maybeToken(scanner);
  446. case ASTERISK:
  447. case PLUSSIGN:
  448. case QUESTIONMARK:
  449. case NUMBERSIGN:
  450. case EXCLAMATIONMARK:
  451. // prohibited tokens (used as a multiplier start)
  452. break;
  453. case LEFTCURLYBRACKET:
  454. // LEFTCURLYBRACKET is allowed since mdn/data uses it w/o quoting
  455. // check next char isn't a number, because it's likely a disjoined multiplier
  456. code = scanner.nextCharCode();
  457. if (code < 48 || code > 57) {
  458. return maybeToken(scanner);
  459. }
  460. break;
  461. default:
  462. if (scanner.isNameCharCode(code)) {
  463. return readKeywordOrFunction(scanner);
  464. }
  465. return maybeToken(scanner);
  466. }
  467. }
  468. function parse(source) {
  469. const scanner$1 = new scanner.Scanner(source);
  470. const result = readImplicitGroup(scanner$1);
  471. if (scanner$1.pos !== source.length) {
  472. scanner$1.error('Unexpected input');
  473. }
  474. // reduce redundant groups with single group term
  475. if (result.terms.length === 1 && result.terms[0].type === 'Group') {
  476. return result.terms[0];
  477. }
  478. return result;
  479. }
  480. exports.parse = parse;