parse.js 13 KB

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