parsers.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. /**
  2. * These are commonly used parsers for CSS Values they take a string to parse
  3. * and return a string after it's been converted, if needed
  4. */
  5. "use strict";
  6. const { resolve: resolveColor, utils } = require("@asamuzakjp/css-color");
  7. const { asciiLowercase } = require("./utils/strings");
  8. const { cssCalc, isColor, isGradient, splitValue } = utils;
  9. // CSS global values
  10. // @see https://drafts.csswg.org/css-cascade-5/#defaulting-keywords
  11. const GLOBAL_VALUE = Object.freeze(["initial", "inherit", "unset", "revert", "revert-layer"]);
  12. // Numeric data types
  13. const NUM_TYPE = Object.freeze({
  14. UNDEFINED: 0,
  15. VAR: 1,
  16. NUMBER: 2,
  17. PERCENT: 4,
  18. LENGTH: 8,
  19. ANGLE: 0x10,
  20. CALC: 0x20
  21. });
  22. // System colors
  23. // @see https://drafts.csswg.org/css-color/#css-system-colors
  24. // @see https://drafts.csswg.org/css-color/#deprecated-system-colors
  25. const SYS_COLOR = Object.freeze([
  26. "accentcolor",
  27. "accentcolortext",
  28. "activeborder",
  29. "activecaption",
  30. "activetext",
  31. "appworkspace",
  32. "background",
  33. "buttonborder",
  34. "buttonface",
  35. "buttonhighlight",
  36. "buttonshadow",
  37. "buttontext",
  38. "canvas",
  39. "canvastext",
  40. "captiontext",
  41. "field",
  42. "fieldtext",
  43. "graytext",
  44. "highlight",
  45. "highlighttext",
  46. "inactiveborder",
  47. "inactivecaption",
  48. "inactivecaptiontext",
  49. "infobackground",
  50. "infotext",
  51. "linktext",
  52. "mark",
  53. "marktext",
  54. "menu",
  55. "menutext",
  56. "scrollbar",
  57. "selecteditem",
  58. "selecteditemtext",
  59. "threeddarkshadow",
  60. "threedface",
  61. "threedhighlight",
  62. "threedlightshadow",
  63. "threedshadow",
  64. "visitedtext",
  65. "window",
  66. "windowframe",
  67. "windowtext"
  68. ]);
  69. // Regular expressions
  70. const DIGIT = "(?:0|[1-9]\\d*)";
  71. const NUMBER = `[+-]?(?:${DIGIT}(?:\\.\\d*)?|\\.\\d+)(?:e-?${DIGIT})?`;
  72. const unitRegEx = new RegExp(`^(${NUMBER})([a-z]+|%)?$`, "i");
  73. const urlRegEx = /^url\(\s*((?:[^)]|\\\))*)\s*\)$/;
  74. const keywordRegEx = /^[a-z]+(?:-[a-z]+)*$/i;
  75. const stringRegEx = /^("[^"]*"|'[^']*')$/;
  76. const varRegEx = /^var\(/;
  77. const varContainedRegEx = /(?<=[*/\s(])var\(/;
  78. const calcRegEx =
  79. /^(?:a?(?:cos|sin|tan)|abs|atan2|calc|clamp|exp|hypot|log|max|min|mod|pow|rem|round|sign|sqrt)\(/;
  80. const functionRegEx = /^([a-z][a-z\d]*(?:-[a-z\d]+)*)\(/i;
  81. const getNumericType = function getNumericType(val) {
  82. if (varRegEx.test(val)) {
  83. return NUM_TYPE.VAR;
  84. }
  85. if (calcRegEx.test(val)) {
  86. return NUM_TYPE.CALC;
  87. }
  88. if (unitRegEx.test(val)) {
  89. const [, , unit] = unitRegEx.exec(val);
  90. if (!unit) {
  91. return NUM_TYPE.NUMBER;
  92. }
  93. if (unit === "%") {
  94. return NUM_TYPE.PERCENT;
  95. }
  96. if (/^(?:[cm]m|[dls]?v(?:[bhiw]|max|min)|in|p[ctx]|q|r?(?:[cl]h|cap|e[mx]|ic))$/i.test(unit)) {
  97. return NUM_TYPE.LENGTH;
  98. }
  99. if (/^(?:deg|g?rad|turn)$/i.test(unit)) {
  100. return NUM_TYPE.ANGLE;
  101. }
  102. }
  103. return NUM_TYPE.UNDEFINED;
  104. };
  105. // Prepare stringified value.
  106. exports.prepareValue = function prepareValue(value, globalObject = globalThis) {
  107. // `null` is converted to an empty string.
  108. // @see https://webidl.spec.whatwg.org/#LegacyNullToEmptyString
  109. if (value === null) {
  110. return "";
  111. }
  112. const type = typeof value;
  113. switch (type) {
  114. case "string":
  115. return value.trim();
  116. case "number":
  117. return value.toString();
  118. case "undefined":
  119. return "undefined";
  120. case "symbol":
  121. throw new globalObject.TypeError("Can not convert symbol to string.");
  122. default: {
  123. const str = value.toString();
  124. if (typeof str === "string") {
  125. return str;
  126. }
  127. throw new globalObject.TypeError(`Can not convert ${type} to string.`);
  128. }
  129. }
  130. };
  131. exports.hasVarFunc = function hasVarFunc(val) {
  132. return varRegEx.test(val) || varContainedRegEx.test(val);
  133. };
  134. exports.parseNumber = function parseNumber(val, restrictToPositive = false) {
  135. if (val === "") {
  136. return "";
  137. }
  138. const type = getNumericType(val);
  139. switch (type) {
  140. case NUM_TYPE.VAR:
  141. return val;
  142. case NUM_TYPE.CALC:
  143. return cssCalc(val, {
  144. format: "specifiedValue"
  145. });
  146. case NUM_TYPE.NUMBER: {
  147. const num = parseFloat(val);
  148. if (restrictToPositive && num < 0) {
  149. return;
  150. }
  151. return `${num}`;
  152. }
  153. default:
  154. if (varContainedRegEx.test(val)) {
  155. return val;
  156. }
  157. }
  158. };
  159. exports.parseLength = function parseLength(val, restrictToPositive = false) {
  160. if (val === "") {
  161. return "";
  162. }
  163. const type = getNumericType(val);
  164. switch (type) {
  165. case NUM_TYPE.VAR:
  166. return val;
  167. case NUM_TYPE.CALC:
  168. return cssCalc(val, {
  169. format: "specifiedValue"
  170. });
  171. case NUM_TYPE.NUMBER:
  172. if (parseFloat(val) === 0) {
  173. return "0px";
  174. }
  175. return;
  176. case NUM_TYPE.LENGTH: {
  177. const [, numVal, unit] = unitRegEx.exec(val);
  178. const num = parseFloat(numVal);
  179. if (restrictToPositive && num < 0) {
  180. return;
  181. }
  182. return `${num}${asciiLowercase(unit)}`;
  183. }
  184. default:
  185. if (varContainedRegEx.test(val)) {
  186. return val;
  187. }
  188. }
  189. };
  190. exports.parsePercent = function parsePercent(val, restrictToPositive = false) {
  191. if (val === "") {
  192. return "";
  193. }
  194. const type = getNumericType(val);
  195. switch (type) {
  196. case NUM_TYPE.VAR:
  197. return val;
  198. case NUM_TYPE.CALC:
  199. return cssCalc(val, {
  200. format: "specifiedValue"
  201. });
  202. case NUM_TYPE.NUMBER:
  203. if (parseFloat(val) === 0) {
  204. return "0%";
  205. }
  206. return;
  207. case NUM_TYPE.PERCENT: {
  208. const [, numVal, unit] = unitRegEx.exec(val);
  209. const num = parseFloat(numVal);
  210. if (restrictToPositive && num < 0) {
  211. return;
  212. }
  213. return `${num}${asciiLowercase(unit)}`;
  214. }
  215. default:
  216. if (varContainedRegEx.test(val)) {
  217. return val;
  218. }
  219. }
  220. };
  221. // Either a length or a percent.
  222. exports.parseMeasurement = function parseMeasurement(val, restrictToPositive = false) {
  223. if (val === "") {
  224. return "";
  225. }
  226. const type = getNumericType(val);
  227. switch (type) {
  228. case NUM_TYPE.VAR:
  229. return val;
  230. case NUM_TYPE.CALC:
  231. return cssCalc(val, {
  232. format: "specifiedValue"
  233. });
  234. case NUM_TYPE.NUMBER:
  235. if (parseFloat(val) === 0) {
  236. return "0px";
  237. }
  238. return;
  239. case NUM_TYPE.LENGTH:
  240. case NUM_TYPE.PERCENT: {
  241. const [, numVal, unit] = unitRegEx.exec(val);
  242. const num = parseFloat(numVal);
  243. if (restrictToPositive && num < 0) {
  244. return;
  245. }
  246. return `${num}${asciiLowercase(unit)}`;
  247. }
  248. default:
  249. if (varContainedRegEx.test(val)) {
  250. return val;
  251. }
  252. }
  253. };
  254. exports.parseAngle = function parseAngle(val, normalizeDeg = false) {
  255. if (val === "") {
  256. return "";
  257. }
  258. const type = getNumericType(val);
  259. switch (type) {
  260. case NUM_TYPE.VAR:
  261. return val;
  262. case NUM_TYPE.CALC:
  263. return cssCalc(val, {
  264. format: "specifiedValue"
  265. });
  266. case NUM_TYPE.NUMBER:
  267. if (parseFloat(val) === 0) {
  268. return "0deg";
  269. }
  270. return;
  271. case NUM_TYPE.ANGLE: {
  272. let [, numVal, unit] = unitRegEx.exec(val);
  273. numVal = parseFloat(numVal);
  274. unit = asciiLowercase(unit);
  275. if (unit === "deg") {
  276. if (normalizeDeg && numVal < 0) {
  277. while (numVal < 0) {
  278. numVal += 360;
  279. }
  280. }
  281. numVal %= 360;
  282. }
  283. return `${numVal}${unit}`;
  284. }
  285. default:
  286. if (varContainedRegEx.test(val)) {
  287. return val;
  288. }
  289. }
  290. };
  291. exports.parseUrl = function parseUrl(val) {
  292. if (val === "") {
  293. return val;
  294. }
  295. const res = urlRegEx.exec(val);
  296. if (!res) {
  297. return;
  298. }
  299. let str = res[1];
  300. // If it starts with single or double quotes, does it end with the same?
  301. if ((str[0] === '"' || str[0] === "'") && str[0] !== str[str.length - 1]) {
  302. return;
  303. }
  304. if (str[0] === '"' || str[0] === "'") {
  305. str = str.substr(1, str.length - 2);
  306. }
  307. let urlstr = "";
  308. let escaped = false;
  309. for (let i = 0; i < str.length; i++) {
  310. switch (str[i]) {
  311. case "\\":
  312. if (escaped) {
  313. urlstr += "\\\\";
  314. escaped = false;
  315. } else {
  316. escaped = true;
  317. }
  318. break;
  319. case "(":
  320. case ")":
  321. case " ":
  322. case "\t":
  323. case "\n":
  324. case "'":
  325. if (!escaped) {
  326. return;
  327. }
  328. urlstr += str[i];
  329. escaped = false;
  330. break;
  331. case '"':
  332. if (!escaped) {
  333. return;
  334. }
  335. urlstr += '\\"';
  336. escaped = false;
  337. break;
  338. default:
  339. urlstr += str[i];
  340. escaped = false;
  341. }
  342. }
  343. return `url("${urlstr}")`;
  344. };
  345. exports.parseString = function parseString(val) {
  346. if (val === "") {
  347. return "";
  348. }
  349. if (!stringRegEx.test(val)) {
  350. return;
  351. }
  352. val = val.substr(1, val.length - 2);
  353. let str = "";
  354. let escaped = false;
  355. for (let i = 0; i < val.length; i++) {
  356. switch (val[i]) {
  357. case "\\":
  358. if (escaped) {
  359. str += "\\\\";
  360. escaped = false;
  361. } else {
  362. escaped = true;
  363. }
  364. break;
  365. case '"':
  366. str += '\\"';
  367. escaped = false;
  368. break;
  369. default:
  370. str += val[i];
  371. escaped = false;
  372. }
  373. }
  374. return `"${str}"`;
  375. };
  376. exports.parseKeyword = function parseKeyword(val, validKeywords = []) {
  377. if (val === "") {
  378. return "";
  379. }
  380. if (varRegEx.test(val)) {
  381. return val;
  382. }
  383. val = asciiLowercase(val.toString());
  384. if (validKeywords.includes(val) || GLOBAL_VALUE.includes(val)) {
  385. return val;
  386. }
  387. };
  388. exports.parseColor = function parseColor(val) {
  389. if (val === "") {
  390. return "";
  391. }
  392. if (varRegEx.test(val)) {
  393. return val;
  394. }
  395. if (/^[a-z]+$/i.test(val)) {
  396. const v = asciiLowercase(val);
  397. if (SYS_COLOR.includes(v)) {
  398. return v;
  399. }
  400. }
  401. const res = resolveColor(val, {
  402. format: "specifiedValue"
  403. });
  404. if (res) {
  405. return res;
  406. }
  407. return exports.parseKeyword(val);
  408. };
  409. exports.parseImage = function parseImage(val) {
  410. if (val === "") {
  411. return "";
  412. }
  413. if (varRegEx.test(val)) {
  414. return val;
  415. }
  416. if (keywordRegEx.test(val)) {
  417. return exports.parseKeyword(val, ["none"]);
  418. }
  419. const values = splitValue(val, {
  420. delimiter: ",",
  421. preserveComment: varContainedRegEx.test(val)
  422. });
  423. let isImage = Boolean(values.length);
  424. for (let i = 0; i < values.length; i++) {
  425. const image = values[i];
  426. if (image === "") {
  427. return "";
  428. }
  429. if (isGradient(image) || /^(?:none|inherit)$/i.test(image)) {
  430. continue;
  431. }
  432. const imageUrl = exports.parseUrl(image);
  433. if (imageUrl) {
  434. values[i] = imageUrl;
  435. } else {
  436. isImage = false;
  437. break;
  438. }
  439. }
  440. if (isImage) {
  441. return values.join(", ");
  442. }
  443. };
  444. exports.parseFunction = function parseFunction(val) {
  445. if (val === "") {
  446. return {
  447. name: null,
  448. value: ""
  449. };
  450. }
  451. if (functionRegEx.test(val) && val.endsWith(")")) {
  452. if (varRegEx.test(val) || varContainedRegEx.test(val)) {
  453. return {
  454. name: "var",
  455. value: val
  456. };
  457. }
  458. const [, name] = functionRegEx.exec(val);
  459. const value = val
  460. .replace(new RegExp(`^${name}\\(`), "")
  461. .replace(/\)$/, "")
  462. .trim();
  463. return {
  464. name,
  465. value
  466. };
  467. }
  468. };
  469. exports.parseShorthand = function parseShorthand(val, shorthandFor, preserve = false) {
  470. const obj = {};
  471. if (val === "" || exports.hasVarFunc(val)) {
  472. for (const [property] of shorthandFor) {
  473. obj[property] = "";
  474. }
  475. return obj;
  476. }
  477. const key = exports.parseKeyword(val);
  478. if (key) {
  479. if (key === "inherit") {
  480. return obj;
  481. }
  482. return;
  483. }
  484. const parts = splitValue(val);
  485. const shorthandArr = [...shorthandFor];
  486. for (const part of parts) {
  487. let partValid = false;
  488. for (let i = 0; i < shorthandArr.length; i++) {
  489. const [property, value] = shorthandArr[i];
  490. if (value.isValid(part)) {
  491. partValid = true;
  492. obj[property] = value.parse(part);
  493. if (!preserve) {
  494. shorthandArr.splice(i, 1);
  495. break;
  496. }
  497. }
  498. }
  499. if (!partValid) {
  500. return;
  501. }
  502. }
  503. return obj;
  504. };
  505. // Returns `false` for global values, e.g. "inherit".
  506. exports.isValidColor = function isValidColor(val) {
  507. if (SYS_COLOR.includes(asciiLowercase(val))) {
  508. return true;
  509. }
  510. return isColor(val);
  511. };
  512. // Splits value into an array.
  513. // @see https://github.com/asamuzaK/cssColor/blob/main/src/js/util.ts
  514. exports.splitValue = splitValue;