parse.js 15 KB

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