generic.cjs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717
  1. 'use strict';
  2. const genericConst = require('./generic-const.cjs');
  3. const genericAnPlusB = require('./generic-an-plus-b.cjs');
  4. const genericUrange = require('./generic-urange.cjs');
  5. const charCodeDefinitions = require('../tokenizer/char-code-definitions.cjs');
  6. const types = require('../tokenizer/types.cjs');
  7. const utils = require('../tokenizer/utils.cjs');
  8. // CSS mathematical functions categorized by return type behavior
  9. // See: https://www.w3.org/TR/css-values-4/#math
  10. // Calculation functions that return different types depending on input
  11. const calcFunctionNames = [
  12. 'calc(',
  13. '-moz-calc(',
  14. '-webkit-calc('
  15. ];
  16. // Comparison functions that return different types depending on input
  17. const comparisonFunctionNames = [
  18. 'min(',
  19. 'max(',
  20. 'clamp('
  21. ];
  22. // Functions that return a stepped value, i.e. a value that is rounded to the nearest step
  23. const steppedValueFunctionNames = [
  24. 'round(',
  25. 'mod(',
  26. 'rem('
  27. ];
  28. // Trigonometrical functions that return a <number>
  29. const trigNumberFunctionNames = [
  30. 'sin(',
  31. 'cos(',
  32. 'tan('
  33. ];
  34. // Trigonometrical functions that return a <angle>
  35. const trigAngleFunctionNames = [
  36. 'asin(',
  37. 'acos(',
  38. 'atan(',
  39. 'atan2('
  40. ];
  41. // Other functions that return a <number>
  42. const otherNumberFunctionNames = [
  43. 'pow(',
  44. 'sqrt(',
  45. 'log(',
  46. 'exp(',
  47. 'sign('
  48. ];
  49. // Exponential functions that return a <number> or <dimension> or <percentage>
  50. const expNumberDimensionPercentageFunctionNames = [
  51. 'hypot('
  52. ];
  53. // Return the same type as the input
  54. const signFunctionNames = [
  55. 'abs('
  56. ];
  57. const numberFunctionNames = [
  58. ...calcFunctionNames,
  59. ...comparisonFunctionNames,
  60. ...steppedValueFunctionNames,
  61. ...trigNumberFunctionNames,
  62. ...otherNumberFunctionNames,
  63. ...expNumberDimensionPercentageFunctionNames,
  64. ...signFunctionNames
  65. ];
  66. const percentageFunctionNames = [
  67. ...calcFunctionNames,
  68. ...comparisonFunctionNames,
  69. ...steppedValueFunctionNames,
  70. ...expNumberDimensionPercentageFunctionNames,
  71. ...signFunctionNames
  72. ];
  73. const dimensionFunctionNames = [
  74. ...calcFunctionNames,
  75. ...comparisonFunctionNames,
  76. ...steppedValueFunctionNames,
  77. ...trigAngleFunctionNames,
  78. ...expNumberDimensionPercentageFunctionNames,
  79. ...signFunctionNames
  80. ];
  81. const balancePair = new Map([
  82. [types.Function, types.RightParenthesis],
  83. [types.LeftParenthesis, types.RightParenthesis],
  84. [types.LeftSquareBracket, types.RightSquareBracket],
  85. [types.LeftCurlyBracket, types.RightCurlyBracket]
  86. ]);
  87. // safe char code getter
  88. function charCodeAt(str, index) {
  89. return index < str.length ? str.charCodeAt(index) : 0;
  90. }
  91. function eqStr(actual, expected) {
  92. return utils.cmpStr(actual, 0, actual.length, expected);
  93. }
  94. function eqStrAny(actual, expected) {
  95. for (let i = 0; i < expected.length; i++) {
  96. if (eqStr(actual, expected[i])) {
  97. return true;
  98. }
  99. }
  100. return false;
  101. }
  102. // IE postfix hack, i.e. 123\0 or 123px\9
  103. function isPostfixIeHack(str, offset) {
  104. if (offset !== str.length - 2) {
  105. return false;
  106. }
  107. return (
  108. charCodeAt(str, offset) === 0x005C && // U+005C REVERSE SOLIDUS (\)
  109. charCodeDefinitions.isDigit(charCodeAt(str, offset + 1))
  110. );
  111. }
  112. function outOfRange(opts, value, numEnd) {
  113. if (opts && opts.type === 'Range') {
  114. const num = Number(
  115. numEnd !== undefined && numEnd !== value.length
  116. ? value.substr(0, numEnd)
  117. : value
  118. );
  119. if (isNaN(num)) {
  120. return true;
  121. }
  122. // FIXME: when opts.min is a string it's a dimension, skip a range validation
  123. // for now since it requires a type covertation which is not implmented yet
  124. if (opts.min !== null && num < opts.min && typeof opts.min !== 'string') {
  125. return true;
  126. }
  127. // FIXME: when opts.max is a string it's a dimension, skip a range validation
  128. // for now since it requires a type covertation which is not implmented yet
  129. if (opts.max !== null && num > opts.max && typeof opts.max !== 'string') {
  130. return true;
  131. }
  132. }
  133. return false;
  134. }
  135. function consumeFunction(token, getNextToken) {
  136. let balanceCloseType = 0;
  137. let balanceStash = [];
  138. let length = 0;
  139. // balanced token consuming
  140. scan:
  141. do {
  142. switch (token.type) {
  143. case types.RightCurlyBracket:
  144. case types.RightParenthesis:
  145. case types.RightSquareBracket:
  146. if (token.type !== balanceCloseType) {
  147. break scan;
  148. }
  149. balanceCloseType = balanceStash.pop();
  150. if (balanceStash.length === 0) {
  151. length++;
  152. break scan;
  153. }
  154. break;
  155. case types.Function:
  156. case types.LeftParenthesis:
  157. case types.LeftSquareBracket:
  158. case types.LeftCurlyBracket:
  159. balanceStash.push(balanceCloseType);
  160. balanceCloseType = balancePair.get(token.type);
  161. break;
  162. }
  163. length++;
  164. } while (token = getNextToken(length));
  165. return length;
  166. }
  167. // TODO: implement
  168. // can be used wherever <length>, <frequency>, <angle>, <time>, <percentage>, <number>, or <integer> values are allowed
  169. // https://drafts.csswg.org/css-values/#calc-notation
  170. function math(next, functionNames) {
  171. return function(token, getNextToken, opts) {
  172. if (token === null) {
  173. return 0;
  174. }
  175. if (token.type === types.Function && eqStrAny(token.value, functionNames)) {
  176. return consumeFunction(token, getNextToken);
  177. }
  178. return next(token, getNextToken, opts);
  179. };
  180. }
  181. function tokenType(expectedTokenType) {
  182. return function(token) {
  183. if (token === null || token.type !== expectedTokenType) {
  184. return 0;
  185. }
  186. return 1;
  187. };
  188. }
  189. // =========================
  190. // Complex types
  191. //
  192. // https://drafts.csswg.org/css-values-4/#custom-idents
  193. // 4.2. Author-defined Identifiers: the <custom-ident> type
  194. // Some properties accept arbitrary author-defined identifiers as a component value.
  195. // This generic data type is denoted by <custom-ident>, and represents any valid CSS identifier
  196. // that would not be misinterpreted as a pre-defined keyword in that property’s value definition.
  197. //
  198. // See also: https://developer.mozilla.org/en-US/docs/Web/CSS/custom-ident
  199. function customIdent(token) {
  200. if (token === null || token.type !== types.Ident) {
  201. return 0;
  202. }
  203. const name = token.value.toLowerCase();
  204. // The CSS-wide keywords are not valid <custom-ident>s
  205. if (eqStrAny(name, genericConst.cssWideKeywords)) {
  206. return 0;
  207. }
  208. // The default keyword is reserved and is also not a valid <custom-ident>
  209. if (eqStr(name, 'default')) {
  210. return 0;
  211. }
  212. // TODO: ignore property specific keywords (as described https://developer.mozilla.org/en-US/docs/Web/CSS/custom-ident)
  213. // Specifications using <custom-ident> must specify clearly what other keywords
  214. // are excluded from <custom-ident>, if any—for example by saying that any pre-defined keywords
  215. // in that property’s value definition are excluded. Excluded keywords are excluded
  216. // in all ASCII case permutations.
  217. return 1;
  218. }
  219. // https://drafts.csswg.org/css-values-4/#dashed-idents
  220. // The <dashed-ident> production is a <custom-ident>, with all the case-sensitivity that implies,
  221. // with the additional restriction that it must start with two dashes (U+002D HYPHEN-MINUS).
  222. function dashedIdent(token) {
  223. if (token === null || token.type !== types.Ident) {
  224. return 0;
  225. }
  226. // ... must start with two dashes (U+002D HYPHEN-MINUS)
  227. if (charCodeAt(token.value, 0) !== 0x002D || charCodeAt(token.value, 1) !== 0x002D) {
  228. return 0;
  229. }
  230. return 1;
  231. }
  232. // https://drafts.csswg.org/css-variables/#typedef-custom-property-name
  233. // A custom property is any property whose name starts with two dashes (U+002D HYPHEN-MINUS), like --foo.
  234. // The <custom-property-name> production corresponds to this: it’s defined as any <dashed-ident>
  235. // (a valid identifier that starts with two dashes), except -- itself, which is reserved for future use by CSS.
  236. function customPropertyName(token) {
  237. // ... it’s defined as any <dashed-ident>
  238. if (!dashedIdent(token)) {
  239. return 0;
  240. }
  241. // ... except -- itself, which is reserved for future use by CSS
  242. if (token.value === '--') {
  243. return 0;
  244. }
  245. return 1;
  246. }
  247. // https://drafts.csswg.org/css-color-4/#hex-notation
  248. // The syntax of a <hex-color> is a <hash-token> token whose value consists of 3, 4, 6, or 8 hexadecimal digits.
  249. // In other words, a hex color is written as a hash character, "#", followed by some number of digits 0-9 or
  250. // letters a-f (the case of the letters doesn’t matter - #00ff00 is identical to #00FF00).
  251. function hexColor(token) {
  252. if (token === null || token.type !== types.Hash) {
  253. return 0;
  254. }
  255. const length = token.value.length;
  256. // valid values (length): #rgb (4), #rgba (5), #rrggbb (7), #rrggbbaa (9)
  257. if (length !== 4 && length !== 5 && length !== 7 && length !== 9) {
  258. return 0;
  259. }
  260. for (let i = 1; i < length; i++) {
  261. if (!charCodeDefinitions.isHexDigit(charCodeAt(token.value, i))) {
  262. return 0;
  263. }
  264. }
  265. return 1;
  266. }
  267. function idSelector(token) {
  268. if (token === null || token.type !== types.Hash) {
  269. return 0;
  270. }
  271. if (!charCodeDefinitions.isIdentifierStart(charCodeAt(token.value, 1), charCodeAt(token.value, 2), charCodeAt(token.value, 3))) {
  272. return 0;
  273. }
  274. return 1;
  275. }
  276. // https://drafts.csswg.org/css-syntax/#any-value
  277. // It represents the entirety of what a valid declaration can have as its value.
  278. function declarationValue(token, getNextToken) {
  279. if (!token) {
  280. return 0;
  281. }
  282. let balanceCloseType = 0;
  283. let balanceStash = [];
  284. let length = 0;
  285. // The <declaration-value> production matches any sequence of one or more tokens,
  286. // so long as the sequence does not contain ...
  287. scan:
  288. do {
  289. switch (token.type) {
  290. // ... <bad-string-token>, <bad-url-token>,
  291. case types.BadString:
  292. case types.BadUrl:
  293. break scan;
  294. // ... unmatched <)-token>, <]-token>, or <}-token>,
  295. case types.RightCurlyBracket:
  296. case types.RightParenthesis:
  297. case types.RightSquareBracket:
  298. if (token.type !== balanceCloseType) {
  299. break scan;
  300. }
  301. balanceCloseType = balanceStash.pop();
  302. break;
  303. // ... or top-level <semicolon-token> tokens
  304. case types.Semicolon:
  305. if (balanceCloseType === 0) {
  306. break scan;
  307. }
  308. break;
  309. // ... or <delim-token> tokens with a value of "!"
  310. case types.Delim:
  311. if (balanceCloseType === 0 && token.value === '!') {
  312. break scan;
  313. }
  314. break;
  315. case types.Function:
  316. case types.LeftParenthesis:
  317. case types.LeftSquareBracket:
  318. case types.LeftCurlyBracket:
  319. balanceStash.push(balanceCloseType);
  320. balanceCloseType = balancePair.get(token.type);
  321. break;
  322. }
  323. length++;
  324. } while (token = getNextToken(length));
  325. return length;
  326. }
  327. // https://drafts.csswg.org/css-syntax/#any-value
  328. // The <any-value> production is identical to <declaration-value>, but also
  329. // allows top-level <semicolon-token> tokens and <delim-token> tokens
  330. // with a value of "!". It represents the entirety of what valid CSS can be in any context.
  331. function anyValue(token, getNextToken) {
  332. if (!token) {
  333. return 0;
  334. }
  335. let balanceCloseType = 0;
  336. let balanceStash = [];
  337. let length = 0;
  338. // The <any-value> production matches any sequence of one or more tokens,
  339. // so long as the sequence ...
  340. scan:
  341. do {
  342. switch (token.type) {
  343. // ... does not contain <bad-string-token>, <bad-url-token>,
  344. case types.BadString:
  345. case types.BadUrl:
  346. break scan;
  347. // ... unmatched <)-token>, <]-token>, or <}-token>,
  348. case types.RightCurlyBracket:
  349. case types.RightParenthesis:
  350. case types.RightSquareBracket:
  351. if (token.type !== balanceCloseType) {
  352. break scan;
  353. }
  354. balanceCloseType = balanceStash.pop();
  355. break;
  356. case types.Function:
  357. case types.LeftParenthesis:
  358. case types.LeftSquareBracket:
  359. case types.LeftCurlyBracket:
  360. balanceStash.push(balanceCloseType);
  361. balanceCloseType = balancePair.get(token.type);
  362. break;
  363. }
  364. length++;
  365. } while (token = getNextToken(length));
  366. return length;
  367. }
  368. // =========================
  369. // Dimensions
  370. //
  371. function dimension(type) {
  372. if (type) {
  373. type = new Set(type);
  374. }
  375. return function(token, getNextToken, opts) {
  376. if (token === null || token.type !== types.Dimension) {
  377. return 0;
  378. }
  379. const numberEnd = utils.consumeNumber(token.value, 0);
  380. // check unit
  381. if (type !== null) {
  382. // check for IE postfix hack, i.e. 123px\0 or 123px\9
  383. const reverseSolidusOffset = token.value.indexOf('\\', numberEnd);
  384. const unit = reverseSolidusOffset === -1 || !isPostfixIeHack(token.value, reverseSolidusOffset)
  385. ? token.value.substr(numberEnd)
  386. : token.value.substring(numberEnd, reverseSolidusOffset);
  387. if (type.has(unit.toLowerCase()) === false) {
  388. return 0;
  389. }
  390. }
  391. // check range if specified
  392. if (outOfRange(opts, token.value, numberEnd)) {
  393. return 0;
  394. }
  395. return 1;
  396. };
  397. }
  398. // =========================
  399. // Percentage
  400. //
  401. // §5.5. Percentages: the <percentage> type
  402. // https://drafts.csswg.org/css-values-4/#percentages
  403. function percentage(token, getNextToken, opts) {
  404. // ... corresponds to the <percentage-token> production
  405. if (token === null || token.type !== types.Percentage) {
  406. return 0;
  407. }
  408. // check range if specified
  409. if (outOfRange(opts, token.value, token.value.length - 1)) {
  410. return 0;
  411. }
  412. return 1;
  413. }
  414. // =========================
  415. // Numeric
  416. //
  417. // https://drafts.csswg.org/css-values-4/#numbers
  418. // The value <zero> represents a literal number with the value 0. Expressions that merely
  419. // evaluate to a <number> with the value 0 (for example, calc(0)) do not match <zero>;
  420. // only literal <number-token>s do.
  421. function zero(next) {
  422. if (typeof next !== 'function') {
  423. next = function() {
  424. return 0;
  425. };
  426. }
  427. return function(token, getNextToken, opts) {
  428. if (token !== null && token.type === types.Number) {
  429. if (Number(token.value) === 0) {
  430. return 1;
  431. }
  432. }
  433. return next(token, getNextToken, opts);
  434. };
  435. }
  436. // § 5.3. Real Numbers: the <number> type
  437. // https://drafts.csswg.org/css-values-4/#numbers
  438. // Number values are denoted by <number>, and represent real numbers, possibly with a fractional component.
  439. // ... It corresponds to the <number-token> production
  440. function number(token, getNextToken, opts) {
  441. if (token === null) {
  442. return 0;
  443. }
  444. const numberEnd = utils.consumeNumber(token.value, 0);
  445. const isNumber = numberEnd === token.value.length;
  446. if (!isNumber && !isPostfixIeHack(token.value, numberEnd)) {
  447. return 0;
  448. }
  449. // check range if specified
  450. if (outOfRange(opts, token.value, numberEnd)) {
  451. return 0;
  452. }
  453. return 1;
  454. }
  455. // §5.2. Integers: the <integer> type
  456. // https://drafts.csswg.org/css-values-4/#integers
  457. function integer(token, getNextToken, opts) {
  458. // ... corresponds to a subset of the <number-token> production
  459. if (token === null || token.type !== types.Number) {
  460. return 0;
  461. }
  462. // The first digit of an integer may be immediately preceded by `-` or `+` to indicate the integer’s sign.
  463. let i = charCodeAt(token.value, 0) === 0x002B || // U+002B PLUS SIGN (+)
  464. charCodeAt(token.value, 0) === 0x002D ? 1 : 0; // U+002D HYPHEN-MINUS (-)
  465. // When written literally, an integer is one or more decimal digits 0 through 9 ...
  466. for (; i < token.value.length; i++) {
  467. if (!charCodeDefinitions.isDigit(charCodeAt(token.value, i))) {
  468. return 0;
  469. }
  470. }
  471. // check range if specified
  472. if (outOfRange(opts, token.value, i)) {
  473. return 0;
  474. }
  475. return 1;
  476. }
  477. // token types
  478. const tokenTypes = {
  479. 'ident-token': tokenType(types.Ident),
  480. 'function-token': tokenType(types.Function),
  481. 'at-keyword-token': tokenType(types.AtKeyword),
  482. 'hash-token': tokenType(types.Hash),
  483. 'string-token': tokenType(types.String),
  484. 'bad-string-token': tokenType(types.BadString),
  485. 'url-token': tokenType(types.Url),
  486. 'bad-url-token': tokenType(types.BadUrl),
  487. 'delim-token': tokenType(types.Delim),
  488. 'number-token': tokenType(types.Number),
  489. 'percentage-token': tokenType(types.Percentage),
  490. 'dimension-token': tokenType(types.Dimension),
  491. 'whitespace-token': tokenType(types.WhiteSpace),
  492. 'CDO-token': tokenType(types.CDO),
  493. 'CDC-token': tokenType(types.CDC),
  494. 'colon-token': tokenType(types.Colon),
  495. 'semicolon-token': tokenType(types.Semicolon),
  496. 'comma-token': tokenType(types.Comma),
  497. '[-token': tokenType(types.LeftSquareBracket),
  498. ']-token': tokenType(types.RightSquareBracket),
  499. '(-token': tokenType(types.LeftParenthesis),
  500. ')-token': tokenType(types.RightParenthesis),
  501. '{-token': tokenType(types.LeftCurlyBracket),
  502. '}-token': tokenType(types.RightCurlyBracket)
  503. };
  504. // token production types
  505. const productionTypes = {
  506. // token type aliases
  507. 'string': tokenType(types.String),
  508. 'ident': tokenType(types.Ident),
  509. // percentage
  510. 'percentage': math(percentage, percentageFunctionNames),
  511. // numeric
  512. 'zero': zero(),
  513. 'number': math(number, numberFunctionNames),
  514. 'integer': math(integer, numberFunctionNames),
  515. // complex types
  516. 'custom-ident': customIdent,
  517. 'dashed-ident': dashedIdent,
  518. 'custom-property-name': customPropertyName,
  519. 'hex-color': hexColor,
  520. 'id-selector': idSelector, // element( <id-selector> )
  521. 'an-plus-b': genericAnPlusB,
  522. 'urange': genericUrange,
  523. 'declaration-value': declarationValue,
  524. 'any-value': anyValue
  525. };
  526. const unitGroups = [
  527. 'length',
  528. 'angle',
  529. 'time',
  530. 'frequency',
  531. 'resolution',
  532. 'flex',
  533. 'decibel',
  534. 'semitones'
  535. ];
  536. // dimensions types depend on units set
  537. function createDemensionTypes(units) {
  538. const {
  539. angle,
  540. decibel,
  541. frequency,
  542. flex,
  543. length,
  544. resolution,
  545. semitones,
  546. time
  547. } = units || {};
  548. return {
  549. 'dimension': math(dimension(null), dimensionFunctionNames),
  550. 'angle': math(dimension(angle), dimensionFunctionNames),
  551. 'decibel': math(dimension(decibel), dimensionFunctionNames),
  552. 'frequency': math(dimension(frequency), dimensionFunctionNames),
  553. 'flex': math(dimension(flex), dimensionFunctionNames),
  554. 'length': math(zero(dimension(length)), dimensionFunctionNames),
  555. 'resolution': math(dimension(resolution), dimensionFunctionNames),
  556. 'semitones': math(dimension(semitones), dimensionFunctionNames),
  557. 'time': math(dimension(time), dimensionFunctionNames)
  558. };
  559. }
  560. // The <attr-unit> production matches any identifier that is an ASCII case-insensitive
  561. // match for the name of a CSS dimension unit, such as px, or the <delim-token> %.
  562. function createAttrUnit(units) {
  563. const unitSet = new Set();
  564. for (const group of unitGroups) {
  565. if (Array.isArray(units[group])) {
  566. for (const unit of units[group]) {
  567. unitSet.add(unit.toLowerCase());
  568. }
  569. }
  570. }
  571. return function attrUnit(token) {
  572. if (token === null) {
  573. return 0;
  574. }
  575. if (token.type === types.Delim && token.value === '%') {
  576. return 1;
  577. }
  578. if (token.type === types.Ident && unitSet.has(token.value.toLowerCase())) {
  579. return 1;
  580. }
  581. return 0;
  582. };
  583. }
  584. function createGenericTypes(units) {
  585. return {
  586. ...tokenTypes,
  587. ...productionTypes,
  588. ...createDemensionTypes(units),
  589. 'attr-unit': createAttrUnit(units)
  590. };
  591. }
  592. exports.createDemensionTypes = createDemensionTypes;
  593. exports.createGenericTypes = createGenericTypes;
  594. exports.productionTypes = productionTypes;
  595. exports.tokenTypes = tokenTypes;
  596. exports.unitGroups = unitGroups;