parse.cjs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  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. return multiplier;
  136. }
  137. return node;
  138. }
  139. function maybeToken(scanner) {
  140. const ch = scanner.peek();
  141. if (ch === '') {
  142. return null;
  143. }
  144. return maybeMultiplied(scanner, {
  145. type: 'Token',
  146. value: ch
  147. });
  148. }
  149. function readProperty(scanner) {
  150. let name;
  151. scanner.eat(LESSTHANSIGN);
  152. scanner.eat(APOSTROPHE);
  153. name = scanner.scanWord();
  154. scanner.eat(APOSTROPHE);
  155. scanner.eat(GREATERTHANSIGN);
  156. return maybeMultiplied(scanner, {
  157. type: 'Property',
  158. name
  159. });
  160. }
  161. // https://drafts.csswg.org/css-values-3/#numeric-ranges
  162. // 4.1. Range Restrictions and Range Definition Notation
  163. //
  164. // Range restrictions can be annotated in the numeric type notation using CSS bracketed
  165. // range notation—[min,max]—within the angle brackets, after the identifying keyword,
  166. // indicating a closed range between (and including) min and max.
  167. // For example, <integer [0, 10]> indicates an integer between 0 and 10, inclusive.
  168. function readTypeRange(scanner) {
  169. // use null for Infinity to make AST format JSON serializable/deserializable
  170. let min = null; // -Infinity
  171. let max = null; // Infinity
  172. let sign = 1;
  173. scanner.eat(LEFTSQUAREBRACKET);
  174. if (scanner.charCode() === HYPERMINUS) {
  175. scanner.peek();
  176. sign = -1;
  177. }
  178. if (sign == -1 && scanner.charCode() === INFINITY) {
  179. scanner.peek();
  180. } else {
  181. min = sign * Number(scanner.scanNumber(scanner));
  182. if (scanner.isNameCharCode()) {
  183. min += scanner.scanWord();
  184. }
  185. }
  186. scanner.skipWs();
  187. scanner.eat(COMMA);
  188. scanner.skipWs();
  189. if (scanner.charCode() === INFINITY) {
  190. scanner.peek();
  191. } else {
  192. sign = 1;
  193. if (scanner.charCode() === HYPERMINUS) {
  194. scanner.peek();
  195. sign = -1;
  196. }
  197. max = sign * Number(scanner.scanNumber(scanner));
  198. if (scanner.isNameCharCode()) {
  199. max += scanner.scanWord();
  200. }
  201. }
  202. scanner.eat(RIGHTSQUAREBRACKET);
  203. return {
  204. type: 'Range',
  205. min,
  206. max
  207. };
  208. }
  209. function readType(scanner) {
  210. let name;
  211. let opts = null;
  212. scanner.eat(LESSTHANSIGN);
  213. name = scanner.scanWord();
  214. // https://drafts.csswg.org/css-values-5/#boolean
  215. if (name === 'boolean-expr') {
  216. scanner.eat(LEFTSQUAREBRACKET);
  217. const implicitGroup = readImplicitGroup(scanner, RIGHTSQUAREBRACKET);
  218. scanner.eat(RIGHTSQUAREBRACKET);
  219. scanner.eat(GREATERTHANSIGN);
  220. return maybeMultiplied(scanner, {
  221. type: 'Boolean',
  222. term: implicitGroup.terms.length === 1
  223. ? implicitGroup.terms[0]
  224. : implicitGroup
  225. });
  226. }
  227. if (scanner.charCode() === LEFTPARENTHESIS &&
  228. scanner.nextCharCode() === RIGHTPARENTHESIS) {
  229. scanner.pos += 2;
  230. name += '()';
  231. }
  232. if (scanner.charCodeAt(scanner.findWsEnd(scanner.pos)) === LEFTSQUAREBRACKET) {
  233. scanner.skipWs();
  234. opts = readTypeRange(scanner);
  235. }
  236. scanner.eat(GREATERTHANSIGN);
  237. return maybeMultiplied(scanner, {
  238. type: 'Type',
  239. name,
  240. opts
  241. });
  242. }
  243. function readKeywordOrFunction(scanner) {
  244. const name = scanner.scanWord();
  245. if (scanner.charCode() === LEFTPARENTHESIS) {
  246. scanner.pos++;
  247. return {
  248. type: 'Function',
  249. name
  250. };
  251. }
  252. return maybeMultiplied(scanner, {
  253. type: 'Keyword',
  254. name
  255. });
  256. }
  257. function regroupTerms(terms, combinators) {
  258. function createGroup(terms, combinator) {
  259. return {
  260. type: 'Group',
  261. terms,
  262. combinator,
  263. disallowEmpty: false,
  264. explicit: false
  265. };
  266. }
  267. let combinator;
  268. combinators = Object.keys(combinators)
  269. .sort((a, b) => COMBINATOR_PRECEDENCE[a] - COMBINATOR_PRECEDENCE[b]);
  270. while (combinators.length > 0) {
  271. combinator = combinators.shift();
  272. let i = 0;
  273. let subgroupStart = 0;
  274. for (; i < terms.length; i++) {
  275. const term = terms[i];
  276. if (term.type === 'Combinator') {
  277. if (term.value === combinator) {
  278. if (subgroupStart === -1) {
  279. subgroupStart = i - 1;
  280. }
  281. terms.splice(i, 1);
  282. i--;
  283. } else {
  284. if (subgroupStart !== -1 && i - subgroupStart > 1) {
  285. terms.splice(
  286. subgroupStart,
  287. i - subgroupStart,
  288. createGroup(terms.slice(subgroupStart, i), combinator)
  289. );
  290. i = subgroupStart + 1;
  291. }
  292. subgroupStart = -1;
  293. }
  294. }
  295. }
  296. if (subgroupStart !== -1 && combinators.length) {
  297. terms.splice(
  298. subgroupStart,
  299. i - subgroupStart,
  300. createGroup(terms.slice(subgroupStart, i), combinator)
  301. );
  302. }
  303. }
  304. return combinator;
  305. }
  306. function readImplicitGroup(scanner, stopCharCode) {
  307. const combinators = Object.create(null);
  308. const terms = [];
  309. let token;
  310. let prevToken = null;
  311. let prevTokenPos = scanner.pos;
  312. while (scanner.charCode() !== stopCharCode && (token = peek(scanner, stopCharCode))) {
  313. if (token.type !== 'Spaces') {
  314. if (token.type === 'Combinator') {
  315. // check for combinator in group beginning and double combinator sequence
  316. if (prevToken === null || prevToken.type === 'Combinator') {
  317. scanner.pos = prevTokenPos;
  318. scanner.error('Unexpected combinator');
  319. }
  320. combinators[token.value] = true;
  321. } else if (prevToken !== null && prevToken.type !== 'Combinator') {
  322. combinators[' '] = true; // a b
  323. terms.push({
  324. type: 'Combinator',
  325. value: ' '
  326. });
  327. }
  328. terms.push(token);
  329. prevToken = token;
  330. prevTokenPos = scanner.pos;
  331. }
  332. }
  333. // check for combinator in group ending
  334. if (prevToken !== null && prevToken.type === 'Combinator') {
  335. scanner.pos -= prevTokenPos;
  336. scanner.error('Unexpected combinator');
  337. }
  338. return {
  339. type: 'Group',
  340. terms,
  341. combinator: regroupTerms(terms, combinators) || ' ',
  342. disallowEmpty: false,
  343. explicit: false
  344. };
  345. }
  346. function readGroup(scanner, stopCharCode) {
  347. let result;
  348. scanner.eat(LEFTSQUAREBRACKET);
  349. result = readImplicitGroup(scanner, stopCharCode);
  350. scanner.eat(RIGHTSQUAREBRACKET);
  351. result.explicit = true;
  352. if (scanner.charCode() === EXCLAMATIONMARK) {
  353. scanner.pos++;
  354. result.disallowEmpty = true;
  355. }
  356. return result;
  357. }
  358. function peek(scanner, stopCharCode) {
  359. let code = scanner.charCode();
  360. switch (code) {
  361. case RIGHTSQUAREBRACKET:
  362. // don't eat, stop scan a group
  363. break;
  364. case LEFTSQUAREBRACKET:
  365. return maybeMultiplied(scanner, readGroup(scanner, stopCharCode));
  366. case LESSTHANSIGN:
  367. return scanner.nextCharCode() === APOSTROPHE
  368. ? readProperty(scanner)
  369. : readType(scanner);
  370. case VERTICALLINE:
  371. return {
  372. type: 'Combinator',
  373. value: scanner.substringToPos(
  374. scanner.pos + (scanner.nextCharCode() === VERTICALLINE ? 2 : 1)
  375. )
  376. };
  377. case AMPERSAND:
  378. scanner.pos++;
  379. scanner.eat(AMPERSAND);
  380. return {
  381. type: 'Combinator',
  382. value: '&&'
  383. };
  384. case COMMA:
  385. scanner.pos++;
  386. return {
  387. type: 'Comma'
  388. };
  389. case APOSTROPHE:
  390. return maybeMultiplied(scanner, {
  391. type: 'String',
  392. value: scanner.scanString()
  393. });
  394. case SPACE:
  395. case TAB:
  396. case N:
  397. case R:
  398. case F:
  399. return {
  400. type: 'Spaces',
  401. value: scanner.scanSpaces()
  402. };
  403. case COMMERCIALAT:
  404. code = scanner.nextCharCode();
  405. if (scanner.isNameCharCode(code)) {
  406. scanner.pos++;
  407. return {
  408. type: 'AtKeyword',
  409. name: scanner.scanWord()
  410. };
  411. }
  412. return maybeToken(scanner);
  413. case ASTERISK:
  414. case PLUSSIGN:
  415. case QUESTIONMARK:
  416. case NUMBERSIGN:
  417. case EXCLAMATIONMARK:
  418. // prohibited tokens (used as a multiplier start)
  419. break;
  420. case LEFTCURLYBRACKET:
  421. // LEFTCURLYBRACKET is allowed since mdn/data uses it w/o quoting
  422. // check next char isn't a number, because it's likely a disjoined multiplier
  423. code = scanner.nextCharCode();
  424. if (code < 48 || code > 57) {
  425. return maybeToken(scanner);
  426. }
  427. break;
  428. default:
  429. if (scanner.isNameCharCode(code)) {
  430. return readKeywordOrFunction(scanner);
  431. }
  432. return maybeToken(scanner);
  433. }
  434. }
  435. function parse(source) {
  436. const scanner$1 = new scanner.Scanner(source);
  437. const result = readImplicitGroup(scanner$1);
  438. if (scanner$1.pos !== source.length) {
  439. scanner$1.error('Unexpected input');
  440. }
  441. // reduce redundant groups with single group term
  442. if (result.terms.length === 1 && result.terms[0].type === 'Group') {
  443. return result.terms[0];
  444. }
  445. return result;
  446. }
  447. exports.parse = parse;