You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

722 lines
19 KiB

  1. /*********************************************************************
  2. * These are commonly used parsers for CSS Values they take a string *
  3. * to parse and return a string after it's been converted, if needed *
  4. ********************************************************************/
  5. 'use strict';
  6. const namedColors = require('./named_colors.json');
  7. const { hslToRgb } = require('./utils/colorSpace');
  8. exports.TYPES = {
  9. INTEGER: 1,
  10. NUMBER: 2,
  11. LENGTH: 3,
  12. PERCENT: 4,
  13. URL: 5,
  14. COLOR: 6,
  15. STRING: 7,
  16. ANGLE: 8,
  17. KEYWORD: 9,
  18. NULL_OR_EMPTY_STR: 10,
  19. CALC: 11,
  20. };
  21. // rough regular expressions
  22. var integerRegEx = /^[-+]?[0-9]+$/;
  23. var numberRegEx = /^[-+]?[0-9]*\.?[0-9]+$/;
  24. var lengthRegEx = /^(0|[-+]?[0-9]*\.?[0-9]+(in|cm|em|mm|pt|pc|px|ex|rem|vh|vw|ch))$/;
  25. var percentRegEx = /^[-+]?[0-9]*\.?[0-9]+%$/;
  26. var urlRegEx = /^url\(\s*([^)]*)\s*\)$/;
  27. var stringRegEx = /^("[^"]*"|'[^']*')$/;
  28. var colorRegEx1 = /^#([0-9a-fA-F]{3,4}){1,2}$/;
  29. var colorRegEx2 = /^rgb\(([^)]*)\)$/;
  30. var colorRegEx3 = /^rgba\(([^)]*)\)$/;
  31. var calcRegEx = /^calc\(([^)]*)\)$/;
  32. var colorRegEx4 = /^hsla?\(\s*(-?\d+|-?\d*.\d+)\s*,\s*(-?\d+|-?\d*.\d+)%\s*,\s*(-?\d+|-?\d*.\d+)%\s*(,\s*(-?\d+|-?\d*.\d+)\s*)?\)/;
  33. var angleRegEx = /^([-+]?[0-9]*\.?[0-9]+)(deg|grad|rad)$/;
  34. // This will return one of the above types based on the passed in string
  35. exports.valueType = function valueType(val) {
  36. if (val === '' || val === null) {
  37. return exports.TYPES.NULL_OR_EMPTY_STR;
  38. }
  39. if (typeof val === 'number') {
  40. val = val.toString();
  41. }
  42. if (typeof val !== 'string') {
  43. return undefined;
  44. }
  45. if (integerRegEx.test(val)) {
  46. return exports.TYPES.INTEGER;
  47. }
  48. if (numberRegEx.test(val)) {
  49. return exports.TYPES.NUMBER;
  50. }
  51. if (lengthRegEx.test(val)) {
  52. return exports.TYPES.LENGTH;
  53. }
  54. if (percentRegEx.test(val)) {
  55. return exports.TYPES.PERCENT;
  56. }
  57. if (urlRegEx.test(val)) {
  58. return exports.TYPES.URL;
  59. }
  60. if (calcRegEx.test(val)) {
  61. return exports.TYPES.CALC;
  62. }
  63. if (stringRegEx.test(val)) {
  64. return exports.TYPES.STRING;
  65. }
  66. if (angleRegEx.test(val)) {
  67. return exports.TYPES.ANGLE;
  68. }
  69. if (colorRegEx1.test(val)) {
  70. return exports.TYPES.COLOR;
  71. }
  72. var res = colorRegEx2.exec(val);
  73. var parts;
  74. if (res !== null) {
  75. parts = res[1].split(/\s*,\s*/);
  76. if (parts.length !== 3) {
  77. return undefined;
  78. }
  79. if (
  80. parts.every(percentRegEx.test.bind(percentRegEx)) ||
  81. parts.every(integerRegEx.test.bind(integerRegEx))
  82. ) {
  83. return exports.TYPES.COLOR;
  84. }
  85. return undefined;
  86. }
  87. res = colorRegEx3.exec(val);
  88. if (res !== null) {
  89. parts = res[1].split(/\s*,\s*/);
  90. if (parts.length !== 4) {
  91. return undefined;
  92. }
  93. if (
  94. parts.slice(0, 3).every(percentRegEx.test.bind(percentRegEx)) ||
  95. parts.slice(0, 3).every(integerRegEx.test.bind(integerRegEx))
  96. ) {
  97. if (numberRegEx.test(parts[3])) {
  98. return exports.TYPES.COLOR;
  99. }
  100. }
  101. return undefined;
  102. }
  103. if (colorRegEx4.test(val)) {
  104. return exports.TYPES.COLOR;
  105. }
  106. // could still be a color, one of the standard keyword colors
  107. val = val.toLowerCase();
  108. if (namedColors.includes(val)) {
  109. return exports.TYPES.COLOR;
  110. }
  111. switch (val) {
  112. // the following are deprecated in CSS3
  113. case 'activeborder':
  114. case 'activecaption':
  115. case 'appworkspace':
  116. case 'background':
  117. case 'buttonface':
  118. case 'buttonhighlight':
  119. case 'buttonshadow':
  120. case 'buttontext':
  121. case 'captiontext':
  122. case 'graytext':
  123. case 'highlight':
  124. case 'highlighttext':
  125. case 'inactiveborder':
  126. case 'inactivecaption':
  127. case 'inactivecaptiontext':
  128. case 'infobackground':
  129. case 'infotext':
  130. case 'menu':
  131. case 'menutext':
  132. case 'scrollbar':
  133. case 'threeddarkshadow':
  134. case 'threedface':
  135. case 'threedhighlight':
  136. case 'threedlightshadow':
  137. case 'threedshadow':
  138. case 'window':
  139. case 'windowframe':
  140. case 'windowtext':
  141. return exports.TYPES.COLOR;
  142. default:
  143. return exports.TYPES.KEYWORD;
  144. }
  145. };
  146. exports.parseInteger = function parseInteger(val) {
  147. var type = exports.valueType(val);
  148. if (type === exports.TYPES.NULL_OR_EMPTY_STR) {
  149. return val;
  150. }
  151. if (type !== exports.TYPES.INTEGER) {
  152. return undefined;
  153. }
  154. return String(parseInt(val, 10));
  155. };
  156. exports.parseNumber = function parseNumber(val) {
  157. var type = exports.valueType(val);
  158. if (type === exports.TYPES.NULL_OR_EMPTY_STR) {
  159. return val;
  160. }
  161. if (type !== exports.TYPES.NUMBER && type !== exports.TYPES.INTEGER) {
  162. return undefined;
  163. }
  164. return String(parseFloat(val));
  165. };
  166. exports.parseLength = function parseLength(val) {
  167. if (val === 0 || val === '0') {
  168. return '0px';
  169. }
  170. var type = exports.valueType(val);
  171. if (type === exports.TYPES.NULL_OR_EMPTY_STR) {
  172. return val;
  173. }
  174. if (type !== exports.TYPES.LENGTH) {
  175. return undefined;
  176. }
  177. return val;
  178. };
  179. exports.parsePercent = function parsePercent(val) {
  180. if (val === 0 || val === '0') {
  181. return '0%';
  182. }
  183. var type = exports.valueType(val);
  184. if (type === exports.TYPES.NULL_OR_EMPTY_STR) {
  185. return val;
  186. }
  187. if (type !== exports.TYPES.PERCENT) {
  188. return undefined;
  189. }
  190. return val;
  191. };
  192. // either a length or a percent
  193. exports.parseMeasurement = function parseMeasurement(val) {
  194. var type = exports.valueType(val);
  195. if (type === exports.TYPES.CALC) {
  196. return val;
  197. }
  198. var length = exports.parseLength(val);
  199. if (length !== undefined) {
  200. return length;
  201. }
  202. return exports.parsePercent(val);
  203. };
  204. exports.parseUrl = function parseUrl(val) {
  205. var type = exports.valueType(val);
  206. if (type === exports.TYPES.NULL_OR_EMPTY_STR) {
  207. return val;
  208. }
  209. var res = urlRegEx.exec(val);
  210. // does it match the regex?
  211. if (!res) {
  212. return undefined;
  213. }
  214. var str = res[1];
  215. // if it starts with single or double quotes, does it end with the same?
  216. if ((str[0] === '"' || str[0] === "'") && str[0] !== str[str.length - 1]) {
  217. return undefined;
  218. }
  219. if (str[0] === '"' || str[0] === "'") {
  220. str = str.substr(1, str.length - 2);
  221. }
  222. var i;
  223. for (i = 0; i < str.length; i++) {
  224. switch (str[i]) {
  225. case '(':
  226. case ')':
  227. case ' ':
  228. case '\t':
  229. case '\n':
  230. case "'":
  231. case '"':
  232. return undefined;
  233. case '\\':
  234. i++;
  235. break;
  236. }
  237. }
  238. return 'url(' + str + ')';
  239. };
  240. exports.parseString = function parseString(val) {
  241. var type = exports.valueType(val);
  242. if (type === exports.TYPES.NULL_OR_EMPTY_STR) {
  243. return val;
  244. }
  245. if (type !== exports.TYPES.STRING) {
  246. return undefined;
  247. }
  248. var i;
  249. for (i = 1; i < val.length - 1; i++) {
  250. switch (val[i]) {
  251. case val[0]:
  252. return undefined;
  253. case '\\':
  254. i++;
  255. while (i < val.length - 1 && /[0-9A-Fa-f]/.test(val[i])) {
  256. i++;
  257. }
  258. break;
  259. }
  260. }
  261. if (i >= val.length) {
  262. return undefined;
  263. }
  264. return val;
  265. };
  266. exports.parseColor = function parseColor(val) {
  267. var type = exports.valueType(val);
  268. if (type === exports.TYPES.NULL_OR_EMPTY_STR) {
  269. return val;
  270. }
  271. var red,
  272. green,
  273. blue,
  274. hue,
  275. saturation,
  276. lightness,
  277. alpha = 1;
  278. var parts;
  279. var res = colorRegEx1.exec(val);
  280. // is it #aaa, #ababab, #aaaa, #abababaa
  281. if (res) {
  282. var defaultHex = val.substr(1);
  283. var hex = val.substr(1);
  284. if (hex.length === 3 || hex.length === 4) {
  285. hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
  286. if (defaultHex.length === 4) {
  287. hex = hex + defaultHex[3] + defaultHex[3];
  288. }
  289. }
  290. red = parseInt(hex.substr(0, 2), 16);
  291. green = parseInt(hex.substr(2, 2), 16);
  292. blue = parseInt(hex.substr(4, 2), 16);
  293. if (hex.length === 8) {
  294. var hexAlpha = hex.substr(6, 2);
  295. var hexAlphaToRgbaAlpha = Number((parseInt(hexAlpha, 16) / 255).toFixed(3));
  296. return 'rgba(' + red + ', ' + green + ', ' + blue + ', ' + hexAlphaToRgbaAlpha + ')';
  297. }
  298. return 'rgb(' + red + ', ' + green + ', ' + blue + ')';
  299. }
  300. res = colorRegEx2.exec(val);
  301. if (res) {
  302. parts = res[1].split(/\s*,\s*/);
  303. if (parts.length !== 3) {
  304. return undefined;
  305. }
  306. if (parts.every(percentRegEx.test.bind(percentRegEx))) {
  307. red = Math.floor((parseFloat(parts[0].slice(0, -1)) * 255) / 100);
  308. green = Math.floor((parseFloat(parts[1].slice(0, -1)) * 255) / 100);
  309. blue = Math.floor((parseFloat(parts[2].slice(0, -1)) * 255) / 100);
  310. } else if (parts.every(integerRegEx.test.bind(integerRegEx))) {
  311. red = parseInt(parts[0], 10);
  312. green = parseInt(parts[1], 10);
  313. blue = parseInt(parts[2], 10);
  314. } else {
  315. return undefined;
  316. }
  317. red = Math.min(255, Math.max(0, red));
  318. green = Math.min(255, Math.max(0, green));
  319. blue = Math.min(255, Math.max(0, blue));
  320. return 'rgb(' + red + ', ' + green + ', ' + blue + ')';
  321. }
  322. res = colorRegEx3.exec(val);
  323. if (res) {
  324. parts = res[1].split(/\s*,\s*/);
  325. if (parts.length !== 4) {
  326. return undefined;
  327. }
  328. if (parts.slice(0, 3).every(percentRegEx.test.bind(percentRegEx))) {
  329. red = Math.floor((parseFloat(parts[0].slice(0, -1)) * 255) / 100);
  330. green = Math.floor((parseFloat(parts[1].slice(0, -1)) * 255) / 100);
  331. blue = Math.floor((parseFloat(parts[2].slice(0, -1)) * 255) / 100);
  332. alpha = parseFloat(parts[3]);
  333. } else if (parts.slice(0, 3).every(integerRegEx.test.bind(integerRegEx))) {
  334. red = parseInt(parts[0], 10);
  335. green = parseInt(parts[1], 10);
  336. blue = parseInt(parts[2], 10);
  337. alpha = parseFloat(parts[3]);
  338. } else {
  339. return undefined;
  340. }
  341. if (isNaN(alpha)) {
  342. alpha = 1;
  343. }
  344. red = Math.min(255, Math.max(0, red));
  345. green = Math.min(255, Math.max(0, green));
  346. blue = Math.min(255, Math.max(0, blue));
  347. alpha = Math.min(1, Math.max(0, alpha));
  348. if (alpha === 1) {
  349. return 'rgb(' + red + ', ' + green + ', ' + blue + ')';
  350. }
  351. return 'rgba(' + red + ', ' + green + ', ' + blue + ', ' + alpha + ')';
  352. }
  353. res = colorRegEx4.exec(val);
  354. if (res) {
  355. const [, _hue, _saturation, _lightness, _alphaString = ''] = res;
  356. const _alpha = parseFloat(_alphaString.replace(',', '').trim());
  357. if (!_hue || !_saturation || !_lightness) {
  358. return undefined;
  359. }
  360. hue = parseFloat(_hue);
  361. saturation = parseInt(_saturation, 10);
  362. lightness = parseInt(_lightness, 10);
  363. if (_alpha && numberRegEx.test(_alpha)) {
  364. alpha = parseFloat(_alpha);
  365. }
  366. const [r, g, b] = hslToRgb(hue, saturation / 100, lightness / 100);
  367. if (!_alphaString || alpha === 1) {
  368. return 'rgb(' + r + ', ' + g + ', ' + b + ')';
  369. }
  370. return 'rgba(' + r + ', ' + g + ', ' + b + ', ' + alpha + ')';
  371. }
  372. if (type === exports.TYPES.COLOR) {
  373. return val;
  374. }
  375. return undefined;
  376. };
  377. exports.parseAngle = function parseAngle(val) {
  378. var type = exports.valueType(val);
  379. if (type === exports.TYPES.NULL_OR_EMPTY_STR) {
  380. return val;
  381. }
  382. if (type !== exports.TYPES.ANGLE) {
  383. return undefined;
  384. }
  385. var res = angleRegEx.exec(val);
  386. var flt = parseFloat(res[1]);
  387. if (res[2] === 'rad') {
  388. flt *= 180 / Math.PI;
  389. } else if (res[2] === 'grad') {
  390. flt *= 360 / 400;
  391. }
  392. while (flt < 0) {
  393. flt += 360;
  394. }
  395. while (flt > 360) {
  396. flt -= 360;
  397. }
  398. return flt + 'deg';
  399. };
  400. exports.parseKeyword = function parseKeyword(val, valid_keywords) {
  401. var type = exports.valueType(val);
  402. if (type === exports.TYPES.NULL_OR_EMPTY_STR) {
  403. return val;
  404. }
  405. if (type !== exports.TYPES.KEYWORD) {
  406. return undefined;
  407. }
  408. val = val.toString().toLowerCase();
  409. var i;
  410. for (i = 0; i < valid_keywords.length; i++) {
  411. if (valid_keywords[i].toLowerCase() === val) {
  412. return valid_keywords[i];
  413. }
  414. }
  415. return undefined;
  416. };
  417. // utility to translate from border-width to borderWidth
  418. var dashedToCamelCase = function(dashed) {
  419. var i;
  420. var camel = '';
  421. var nextCap = false;
  422. for (i = 0; i < dashed.length; i++) {
  423. if (dashed[i] !== '-') {
  424. camel += nextCap ? dashed[i].toUpperCase() : dashed[i];
  425. nextCap = false;
  426. } else {
  427. nextCap = true;
  428. }
  429. }
  430. return camel;
  431. };
  432. exports.dashedToCamelCase = dashedToCamelCase;
  433. var is_space = /\s/;
  434. var opening_deliminators = ['"', "'", '('];
  435. var closing_deliminators = ['"', "'", ')'];
  436. // this splits on whitespace, but keeps quoted and parened parts together
  437. var getParts = function(str) {
  438. var deliminator_stack = [];
  439. var length = str.length;
  440. var i;
  441. var parts = [];
  442. var current_part = '';
  443. var opening_index;
  444. var closing_index;
  445. for (i = 0; i < length; i++) {
  446. opening_index = opening_deliminators.indexOf(str[i]);
  447. closing_index = closing_deliminators.indexOf(str[i]);
  448. if (is_space.test(str[i])) {
  449. if (deliminator_stack.length === 0) {
  450. if (current_part !== '') {
  451. parts.push(current_part);
  452. }
  453. current_part = '';
  454. } else {
  455. current_part += str[i];
  456. }
  457. } else {
  458. if (str[i] === '\\') {
  459. i++;
  460. current_part += str[i];
  461. } else {
  462. current_part += str[i];
  463. if (
  464. closing_index !== -1 &&
  465. closing_index === deliminator_stack[deliminator_stack.length - 1]
  466. ) {
  467. deliminator_stack.pop();
  468. } else if (opening_index !== -1) {
  469. deliminator_stack.push(opening_index);
  470. }
  471. }
  472. }
  473. }
  474. if (current_part !== '') {
  475. parts.push(current_part);
  476. }
  477. return parts;
  478. };
  479. /*
  480. * this either returns undefined meaning that it isn't valid
  481. * or returns an object where the keys are dashed short
  482. * hand properties and the values are the values to set
  483. * on them
  484. */
  485. exports.shorthandParser = function parse(v, shorthand_for) {
  486. var obj = {};
  487. var type = exports.valueType(v);
  488. if (type === exports.TYPES.NULL_OR_EMPTY_STR) {
  489. Object.keys(shorthand_for).forEach(function(property) {
  490. obj[property] = '';
  491. });
  492. return obj;
  493. }
  494. if (typeof v === 'number') {
  495. v = v.toString();
  496. }
  497. if (typeof v !== 'string') {
  498. return undefined;
  499. }
  500. if (v.toLowerCase() === 'inherit') {
  501. return {};
  502. }
  503. var parts = getParts(v);
  504. var valid = true;
  505. parts.forEach(function(part, i) {
  506. var part_valid = false;
  507. Object.keys(shorthand_for).forEach(function(property) {
  508. if (shorthand_for[property].isValid(part, i)) {
  509. part_valid = true;
  510. obj[property] = part;
  511. }
  512. });
  513. valid = valid && part_valid;
  514. });
  515. if (!valid) {
  516. return undefined;
  517. }
  518. return obj;
  519. };
  520. exports.shorthandSetter = function(property, shorthand_for) {
  521. return function(v) {
  522. var obj = exports.shorthandParser(v, shorthand_for);
  523. if (obj === undefined) {
  524. return;
  525. }
  526. //console.log('shorthandSetter for:', property, 'obj:', obj);
  527. Object.keys(obj).forEach(function(subprop) {
  528. // in case subprop is an implicit property, this will clear
  529. // *its* subpropertiesX
  530. var camel = dashedToCamelCase(subprop);
  531. this[camel] = obj[subprop];
  532. // in case it gets translated into something else (0 -> 0px)
  533. obj[subprop] = this[camel];
  534. this.removeProperty(subprop);
  535. // don't add in empty properties
  536. if (obj[subprop] !== '') {
  537. this._values[subprop] = obj[subprop];
  538. }
  539. }, this);
  540. Object.keys(shorthand_for).forEach(function(subprop) {
  541. if (!obj.hasOwnProperty(subprop)) {
  542. this.removeProperty(subprop);
  543. delete this._values[subprop];
  544. }
  545. }, this);
  546. // in case the value is something like 'none' that removes all values,
  547. // check that the generated one is not empty, first remove the property
  548. // if it already exists, then call the shorthandGetter, if it's an empty
  549. // string, don't set the property
  550. this.removeProperty(property);
  551. var calculated = exports.shorthandGetter(property, shorthand_for).call(this);
  552. if (calculated !== '') {
  553. this._setProperty(property, calculated);
  554. }
  555. };
  556. };
  557. exports.shorthandGetter = function(property, shorthand_for) {
  558. return function() {
  559. if (this._values[property] !== undefined) {
  560. return this.getPropertyValue(property);
  561. }
  562. return Object.keys(shorthand_for)
  563. .map(function(subprop) {
  564. return this.getPropertyValue(subprop);
  565. }, this)
  566. .filter(function(value) {
  567. return value !== '';
  568. })
  569. .join(' ');
  570. };
  571. };
  572. // isValid(){1,4} | inherit
  573. // if one, it applies to all
  574. // if two, the first applies to the top and bottom, and the second to left and right
  575. // if three, the first applies to the top, the second to left and right, the third bottom
  576. // if four, top, right, bottom, left
  577. exports.implicitSetter = function(property_before, property_after, isValid, parser) {
  578. property_after = property_after || '';
  579. if (property_after !== '') {
  580. property_after = '-' + property_after;
  581. }
  582. var part_names = ['top', 'right', 'bottom', 'left'];
  583. return function(v) {
  584. if (typeof v === 'number') {
  585. v = v.toString();
  586. }
  587. if (typeof v !== 'string') {
  588. return undefined;
  589. }
  590. var parts;
  591. if (v.toLowerCase() === 'inherit' || v === '') {
  592. parts = [v];
  593. } else {
  594. parts = getParts(v);
  595. }
  596. if (parts.length < 1 || parts.length > 4) {
  597. return undefined;
  598. }
  599. if (!parts.every(isValid)) {
  600. return undefined;
  601. }
  602. parts = parts.map(function(part) {
  603. return parser(part);
  604. });
  605. this._setProperty(property_before + property_after, parts.join(' '));
  606. if (parts.length === 1) {
  607. parts[1] = parts[0];
  608. }
  609. if (parts.length === 2) {
  610. parts[2] = parts[0];
  611. }
  612. if (parts.length === 3) {
  613. parts[3] = parts[1];
  614. }
  615. for (var i = 0; i < 4; i++) {
  616. var property = property_before + '-' + part_names[i] + property_after;
  617. this.removeProperty(property);
  618. if (parts[i] !== '') {
  619. this._values[property] = parts[i];
  620. }
  621. }
  622. return v;
  623. };
  624. };
  625. //
  626. // Companion to implicitSetter, but for the individual parts.
  627. // This sets the individual value, and checks to see if all four
  628. // sub-parts are set. If so, it sets the shorthand version and removes
  629. // the individual parts from the cssText.
  630. //
  631. exports.subImplicitSetter = function(prefix, part, isValid, parser) {
  632. var property = prefix + '-' + part;
  633. var subparts = [prefix + '-top', prefix + '-right', prefix + '-bottom', prefix + '-left'];
  634. return function(v) {
  635. if (typeof v === 'number') {
  636. v = v.toString();
  637. }
  638. if (typeof v !== 'string') {
  639. return undefined;
  640. }
  641. if (!isValid(v)) {
  642. return undefined;
  643. }
  644. v = parser(v);
  645. this._setProperty(property, v);
  646. var parts = [];
  647. for (var i = 0; i < 4; i++) {
  648. if (this._values[subparts[i]] == null || this._values[subparts[i]] === '') {
  649. break;
  650. }
  651. parts.push(this._values[subparts[i]]);
  652. }
  653. if (parts.length === 4) {
  654. for (i = 0; i < 4; i++) {
  655. this.removeProperty(subparts[i]);
  656. this._values[subparts[i]] = parts[i];
  657. }
  658. this._setProperty(prefix, parts.join(' '));
  659. }
  660. return v;
  661. };
  662. };
  663. var camel_to_dashed = /[A-Z]/g;
  664. var first_segment = /^\([^-]\)-/;
  665. var vendor_prefixes = ['o', 'moz', 'ms', 'webkit'];
  666. exports.camelToDashed = function(camel_case) {
  667. var match;
  668. var dashed = camel_case.replace(camel_to_dashed, '-$&').toLowerCase();
  669. match = dashed.match(first_segment);
  670. if (match && vendor_prefixes.indexOf(match[1]) !== -1) {
  671. dashed = '-' + dashed;
  672. }
  673. return dashed;
  674. };