generic.js 20 KB

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