index.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports["default"] = findOperators;
  6. exports.isInsideFunctionCall = isInsideFunctionCall;
  7. exports.mathOperatorCharType = mathOperatorCharType;
  8. /**
  9. * Processes a string and finds Sass operators in it
  10. *
  11. * @param {Object} args - Named arguments object
  12. * @param {String} args.string - the input string
  13. * @param {Number} args.globalIndex - the position of args.string from the start of the line
  14. * @param {Boolean} args.isAfterColon - pass "true" if the string is
  15. * a variable value, a mixin/function parameter default.
  16. * In such cases + and / tend to be operations more often
  17. * @param {Function} args.callback - will be called on every instance of
  18. * an operator. Accepts parameters:
  19. * • string - the default source string
  20. * • globalIndex - the string's position in the outer input
  21. * • startIndex - index in string, where the operator starts
  22. * • endIndex - index in string, where the operator ends (for `==`, etc.)
  23. *
  24. * @return {Array} array of { symbol, globalIndex, startIndex, endIndex }
  25. * for each operator found within a string
  26. */
  27. function findOperators(_ref) {
  28. var string = _ref.string,
  29. globalIndex = _ref.globalIndex,
  30. isAfterColon = _ref.isAfterColon,
  31. callback = _ref.callback;
  32. var mathOperators = ["+", "/", "-", "*", "%"];
  33. // A stack of modes activated for the current char: string, interpolation
  34. // Calculations inside strings are not processed, so spaces are not linted
  35. var modesEntered = [{
  36. mode: "normal",
  37. isCalculationEnabled: true,
  38. character: null
  39. }];
  40. var result = [];
  41. var lastModeIndex = 0;
  42. for (var i = 0; i < string.length; i++) {
  43. var character = string[i];
  44. var substringStartingWithIndex = string.substring(i);
  45. var lastMode = modesEntered[lastModeIndex];
  46. // If entering/exiting a string
  47. if (character === '"' || character === "'") {
  48. if (lastMode && lastMode.isCalculationEnabled === true) {
  49. modesEntered.push({
  50. mode: "string",
  51. isCalculationEnabled: false,
  52. character: character
  53. });
  54. lastModeIndex++;
  55. } else if (lastMode && lastMode.mode === "string" && lastMode.character === character && string[i - 1] !== "\\") {
  56. modesEntered.pop();
  57. lastModeIndex--;
  58. }
  59. }
  60. // If entering/exiting interpolation (may be inside a string)
  61. // Comparing with length-2 because `#{` at the very end doesnt matter
  62. if (character === "#" && i + 1 < string.length - 2 && string[i + 1] === "{") {
  63. modesEntered.push({
  64. mode: "interpolation",
  65. isCalculationEnabled: true
  66. });
  67. lastModeIndex++;
  68. } else if (character === "}") {
  69. modesEntered.pop();
  70. lastModeIndex--;
  71. }
  72. // Don't lint if inside a string
  73. if (lastMode && lastMode.isCalculationEnabled === false) {
  74. continue;
  75. }
  76. // If it's a math operator
  77. if (mathOperators.includes(character) && mathOperatorCharType(string, i, isAfterColon) === "op" ||
  78. // or is "<" or ">"
  79. substringStartingWithIndex.search(/^[<>]([^=]|$)/) !== -1) {
  80. result.push({
  81. symbol: string[i],
  82. globalIndex: globalIndex,
  83. startIndex: i,
  84. endIndex: i
  85. });
  86. if (callback) {
  87. callback(string, globalIndex, i, i);
  88. }
  89. }
  90. // "<=", ">=", "!=", "=="
  91. if (substringStartingWithIndex.search(/^[><=!]=/) !== -1) {
  92. result.push({
  93. symbol: string[i],
  94. globalIndex: globalIndex,
  95. startIndex: i,
  96. endIndex: i + 1
  97. });
  98. if (callback) {
  99. callback(string, globalIndex, i, i + 1);
  100. }
  101. }
  102. }
  103. // result.length > 0 && console.log(string, result)
  104. return result;
  105. }
  106. /**
  107. * Checks if a character is an operator, a sign (+ or -), or part of a string
  108. *
  109. * @param {String} string - the source string
  110. * @param {Number} index - the index of the character in string to check
  111. * @param {Boolean} isAfterColon - if the value string a variable
  112. * value, a mixin/function parameter default. In such cases + and / tend
  113. * to be operations more often
  114. * @return {String|false}
  115. * • "op", if the character is a operator in a math/string operation
  116. * • "sign" if it is a + or - before a numeric,
  117. * • "char" if it is a part of a string,
  118. * • false - if it is none from above (most likely an error)
  119. */
  120. function mathOperatorCharType(string, index, isAfterColon) {
  121. // !Checking here to prevent unnecessary calculations and deep recursion
  122. // when calling isPrecedingOperator()
  123. if (!["+", "/", "-", "*", "%"].includes(string[index])) {
  124. return "char";
  125. }
  126. var character = string[index];
  127. var prevCharacter = string[index - 1];
  128. if (prevCharacter !== "\\") {
  129. // ---- Processing + characters
  130. if (character === "+") {
  131. return checkPlus(string, index, isAfterColon);
  132. }
  133. // ---- Processing - characters
  134. if (character === "-") {
  135. return checkMinus(string, index);
  136. }
  137. // ---- Processing * character
  138. if (character === "*") {
  139. return checkMultiplication(string, index);
  140. }
  141. // ---- Processing % character
  142. if (character === "%") {
  143. return checkPercent(string, index);
  144. }
  145. // ---- Processing / character
  146. // https://sass-lang.com/documentation/operators/numeric#slash-separated-values
  147. if (character === "/") {
  148. return checkSlash(string, index, isAfterColon);
  149. }
  150. }
  151. return "char";
  152. }
  153. // --------------------------------------------------------------------------
  154. // Functions for checking particular characters (+, -, /)
  155. // --------------------------------------------------------------------------
  156. /**
  157. * Checks the specified `*` char type: operator, sign (*), part of string
  158. *
  159. * @param {String} string - the source string
  160. * @param {Number} index - the index of the character in string to check
  161. * @return {String|false}
  162. * • "op", if the character is a operator in a math/string operation
  163. * • "sign" if it is a sign before a positive number,
  164. * • "char" if it is a part of a string or identifier,
  165. * • false - if it is none from above (most likely an error)
  166. */
  167. function checkMultiplication(string, index) {
  168. var insideFn = isInsideFunctionCall(string, index);
  169. if (insideFn.is && insideFn.fn) {
  170. var fnArgsReg = new RegExp(insideFn.fn + "\\(([^)]+)\\)");
  171. var fnArgs = string.match(fnArgsReg);
  172. var isSingleMultiplicationChar = Array.isArray(fnArgs) && fnArgs[1] === "*";
  173. // e.g. selector(:has(*))
  174. if (isSingleMultiplicationChar) {
  175. return "char";
  176. }
  177. }
  178. return "op";
  179. }
  180. /**
  181. * Checks the specified `+` char type: operator, sign (+ or -), part of string
  182. *
  183. * @param {String} string - the source string
  184. * @param {Number} index - the index of the character in string to check
  185. * @param {Boolean} isAftercolon - if the value string a variable
  186. * value, a mixin/function parameter default. In such cases + is always an
  187. * operator if surrounded by numbers/values with units
  188. * @return {String|false}
  189. * • "op", if the character is a operator in a math/string operation
  190. * • "sign" if it is a sign before a positive number,
  191. * • false - if it is none from above (most likely an error)
  192. */
  193. function checkPlus(string, index, isAftercolon) {
  194. var before = string.substring(0, index);
  195. var after = string.substring(index + 1);
  196. // If the character is at the beginning of the input
  197. var isAtStart_ = isAtStart(string, index);
  198. // If the character is at the end of the input
  199. var isAtEnd_ = isAtEnd(string, index);
  200. var isWhitespaceBefore = before.search(/\s$/) !== -1;
  201. var isWhitespaceAfter = after.search(/^\s/) !== -1;
  202. var isValueWithUnitAfter_ = isValueWithUnitAfter(after);
  203. var isNumberAfter_ = isNumberAfter(after);
  204. var isInterpolationAfter_ = isInterpolationAfter(after);
  205. // The early check above helps prevent deep recursion here
  206. var isPrecedingOperator_ = isPrecedingOperator(string, index);
  207. if (isAtStart_) {
  208. // console.log("+, `+<sth>` or `+ <sth>`")
  209. return "sign";
  210. }
  211. // E.g. `1+1`, `string+#fff`
  212. if (!isAtStart_ && !isWhitespaceBefore && !isAtEnd_ && !isWhitespaceAfter) {
  213. // E.g. `1-+1`
  214. if (isPrecedingOperator_) {
  215. // console.log('1+1')
  216. return "sign";
  217. }
  218. // console.log("+, no spaces")
  219. return "op";
  220. }
  221. // e.g. `something +something`
  222. if (!isAtEnd_ && !isWhitespaceAfter) {
  223. // e.g. `+something`, ` ... , +something`, etc.
  224. if (isNoOperandBefore(string, index)) {
  225. // console.log("+, nothing before")
  226. return "sign";
  227. }
  228. // e.g. `sth +10px`, `sth +1`
  229. if (isValueWithUnitAfter_.is && !isValueWithUnitAfter_.opsBetween || isNumberAfter_.is && !isNumberAfter_.opsBetween) {
  230. if (isAftercolon === true) {
  231. // console.log(": 10px +1")
  232. return "op";
  233. }
  234. // e.g. `(sth +10px)`, `fun(sth +1)`
  235. if (isInsideParens(string, index) || isInsideFunctionCall(string, index).is) {
  236. // console.log("+10px or +1, inside function or parens")
  237. return "op";
  238. }
  239. // e.g. `#{10px +1}`
  240. if (isInsideInterpolation(string, index)) {
  241. // console.log('+, #{10px +1}')
  242. return "op";
  243. }
  244. // console.log('+, default')
  245. return "sign";
  246. }
  247. // e.g. `sth +#fff`, `sth +string`, `sth +#{...}`, `sth +$var`
  248. if (isStringAfter(after) || isHexColorAfter(after) || after[0] === "$" || isInterpolationAfter_.is && !isInterpolationAfter_.opsBefore) {
  249. // e.g. `sth+ +string`
  250. if (isPrecedingOperator_) {
  251. // console.log("+10px or +1, before is an operator")
  252. return "sign";
  253. }
  254. // console.log("+#000, +string, +#{sth}, +$var")
  255. return "op";
  256. }
  257. // console.log('sth +sth, default')
  258. return "op";
  259. }
  260. // If the + is after a value, e.g. `$var+`
  261. if (!isAtStart_ && !isWhitespaceBefore) {
  262. // It is always an operator. Prior to Sass 4, `#{...}+` was different,
  263. // but that's not logical and had been fixed.
  264. // console.log('1+ sth')
  265. return "op";
  266. }
  267. // If it has whitespaces on both sides
  268. // console.log('sth + sth')
  269. return "op";
  270. }
  271. /**
  272. * Checks the specified `-` character: operator, sign (+ or -), part of string
  273. *
  274. * @param {String} string - the source string
  275. * @param {Number} index - the index of the character in string to check
  276. * @return {String|false}
  277. * • "op", if the character is a operator in a math/string operation
  278. * • "sign" if it is a sign before a negative number,
  279. * • "char" if it is a part of a string or identifier,
  280. * • false - if it is none from above (most likely an error)
  281. */
  282. function checkMinus(string, index) {
  283. var before = string.substring(0, index);
  284. var after = string.substring(index + 1);
  285. // If the character is at the beginning of the input
  286. var isAtStart_ = isAtStart(string, index);
  287. // If the character is at the end of the input
  288. var isAtEnd_ = isAtEnd(string, index);
  289. var isWhitespaceBefore = before.search(/\s$/) !== -1;
  290. var isWhitespaceAfter = after.search(/^\s/) !== -1;
  291. var isValueWithUnitAfter_ = isValueWithUnitAfter(after);
  292. var isValueWithUnitBefore_ = isValueWithUnitBefore(before);
  293. var isNumberAfter_ = isNumberAfter(after);
  294. var isNumberBefore_ = isNumberBefore(before);
  295. var isInterpolationAfter_ = isInterpolationAfter(after);
  296. var isParensAfter_ = isParensAfter(after);
  297. var isParensBefore_ = isParensBefore(before);
  298. // The early check above helps prevent deep recursion here
  299. var isPrecedingOperator_ = isPrecedingOperator(string, index);
  300. var isInsideFunctionCall_ = isInsideFunctionCall(string, index);
  301. if (isAtStart_) {
  302. // console.log("-, -<sth> or - <sth>")
  303. return "sign";
  304. }
  305. // `10 - 11`
  306. if (!isAtEnd_ && !isAtStart_ && isWhitespaceBefore && isWhitespaceAfter) {
  307. // console.log("-, Op: 10px - 10px")
  308. return "op";
  309. }
  310. // e.g. `something -10px`
  311. if (!isAtEnd_ && !isAtStart_ && isWhitespaceBefore && !isWhitespaceAfter) {
  312. if (isParensAfter_.is && !isParensAfter_.opsBefore) {
  313. // console.log("-, Op: <sth> -(...)")
  314. return "op";
  315. }
  316. // e.g. `#{10px -1}`, `#{math.acos(-0.5)}`
  317. if (isInsideInterpolation(string, index)) {
  318. // e.g. `url(https://my-url.com/image-#{$i -2}-dark.svg)`
  319. if (isInsideFunctionCall_.fn === "url") {
  320. return "op";
  321. }
  322. if (isInsideFunctionCall_.is && (isValueWithUnitAfter_.is && !isValueWithUnitAfter_.opsBetween || isNumberAfter_.is && !isNumberAfter_.opsBetween)) {
  323. return "sign";
  324. }
  325. // e.g. `#{$i * -10}px`
  326. if (isWhitespaceBefore && isNumberAfter_.is && isPrecedingOperator_) {
  327. return "sign";
  328. }
  329. return "op";
  330. }
  331. // e.g. `sth -1px`, `sth -1`.
  332. // Always a sign, even inside parens/function args
  333. if (isValueWithUnitAfter_.is && !isValueWithUnitAfter_.opsBetween || isNumberAfter_.is && !isNumberAfter_.opsBetween) {
  334. // console.log("-, sign: -1px or -1")
  335. return "sign";
  336. }
  337. // e.g. `sth --1`, `sth +-2px`
  338. if (isValueWithUnitAfter_.is && isValueWithUnitAfter_.opsBetween || isNumberAfter_.is && isNumberAfter_.opsBetween) {
  339. // console.log("-, op: --1px or --1")
  340. return "op";
  341. }
  342. // `<sth> -string`, `<sth> -#{...}`
  343. if (isStringAfter(after) || isInterpolationAfter_.is && !isInterpolationAfter_.opsBefore) {
  344. // console.log("-, char: -#{...}")
  345. return "char";
  346. }
  347. // e.g. `#0af -#f0a`, and edge-cases can take a hike
  348. if (isHexColorAfter(after) && isHexColorBefore(before.trim())) {
  349. // console.log("-, op: #fff-, -#fff")
  350. return "op";
  351. }
  352. // If the - is before a variable, than it's most likely an operator
  353. if (after[0] === "$") {
  354. if (isPrecedingOperator_) {
  355. // console.log("-, sign: -$var, another operator before")
  356. return "sign";
  357. }
  358. // console.log("-, op: -$var, NO other operator before")
  359. return "op";
  360. }
  361. // By default let's make it an sign for now
  362. // console.log('-, sign: default in <sth> -<sth>')
  363. return "sign";
  364. }
  365. // No whitespace before,
  366. // e.g. `10x- something`
  367. if (!isAtEnd_ && !isAtStart_ && !isWhitespaceBefore && isWhitespaceAfter) {
  368. if (isParensBefore_) {
  369. // console.log('-, op: `(...)- <sth>`')
  370. return "op";
  371. }
  372. // e.g. `#{10px- 1}`
  373. if (isInsideInterpolation(string, index)) {
  374. return "op";
  375. }
  376. if (isNumberBefore(before) || isHexColorBefore(before)) {
  377. // console.log('`-, op: 10- <sth>, #aff- <sth>`')
  378. return "op";
  379. }
  380. // console.log('-, char: default in <sth>- <sth>')
  381. return "char";
  382. }
  383. // NO Whitespace,
  384. // e.g. `10px-1`
  385. if (!isAtEnd_ && !isAtStart_ && !isWhitespaceBefore && !isWhitespaceAfter) {
  386. // console.log('no spaces')
  387. // `<something>-1`, `<something>-10px`
  388. if (isValueWithUnitAfter_.is && !isValueWithUnitAfter_.opsBetween || isNumberAfter_.is && !isNumberAfter_.opsBetween) {
  389. // `10px-1`, `1-10px`, `1-1`, `1x-1x`
  390. if (isValueWithUnitBefore_ || isNumberBefore_) {
  391. // console.log("-, op: 1-10px")
  392. return "op";
  393. }
  394. // The - could be a "sign" here, but for now "char" does the job
  395. }
  396. // `1-$var`
  397. if (isNumberBefore_ && after[0] === "$") {
  398. // console.log("-, op: 1-$var")
  399. return "op";
  400. }
  401. // `fn()-10px`
  402. if (isFunctionBefore(before) && (isNumberAfter_.is && !isNumberAfter_.opsBetween || isValueWithUnitAfter_.is && !isValueWithUnitAfter_.opsBetween)) {
  403. // console.log("-, op: fn()-10px")
  404. return "op";
  405. }
  406. }
  407. // And in all the other cases it's a character inside a string
  408. // console.log("-, default: char")
  409. return "char";
  410. }
  411. /**
  412. * Checks the specified `/` character: operator, sign (+ or -), part of string
  413. *
  414. * @param {String} string - the source string
  415. * @param {Number} index - the index of the character in string to check
  416. * @param {Boolean} isAfterColon - if the value string a variable
  417. * value, a mixin/function parameter default. In such cases / is always an
  418. * operator if surrounded by numbers/values with units
  419. * @return {String|false}
  420. * • "op", if the character is a operator in a math/string operation
  421. * • "char" if it gets compiled as-is, e.g. `font: 10px/1.2;`,
  422. * • false - if it is none from above (most likely an error)
  423. */
  424. function checkSlash(string, index, isAfterColon) {
  425. // Trimming these, as spaces before/after a slash don't matter
  426. var before = string.substring(0, index).trim();
  427. var after = string.substring(index + 1).trim();
  428. var isValueWithUnitAfter_ = isValueWithUnitAfter(after);
  429. var isValueWithUnitBefore_ = isValueWithUnitBefore(before);
  430. var isNumberAfter_ = isNumberAfter(after);
  431. var isNumberBefore_ = isNumberBefore(before);
  432. var isParensAfter_ = isParensAfter(after);
  433. var isParensBefore_ = isParensBefore(before);
  434. // FIRST OFF. Interpolation on any of the sides is a NO-GO for division op
  435. if (isInterpolationBefore(before).is || isInterpolationAfter(after).is) {
  436. // console.log("/, interpolation")
  437. return "char";
  438. }
  439. // having a dot before probably means a relative path.
  440. // e.g. url(../../image.png)
  441. if (isDotBefore(before)) {
  442. return "char";
  443. }
  444. // e.g. `(1px/1)`, `fn(7 / 15)`, but not `url(8/11)`
  445. var isInsideFn = isInsideFunctionCall(string, index);
  446. if (isInsideFn.is && isInsideFn.fn === "url") {
  447. // e.g. `url(https://my-url.com/image-#{$i /2}-dark.svg)`
  448. if (isInsideInterpolation(string, index)) {
  449. return "op";
  450. }
  451. return "char";
  452. }
  453. // e.g. `10px/normal`
  454. if (isStringBefore(before).is || isStringAfter(after)) {
  455. // console.log("/, string")
  456. return "char";
  457. }
  458. // For all other value options (numbers, value+unit, hex color)
  459. // `$var/1`, `#fff/-$var`
  460. // Here we don't care if there is a sign before the var
  461. if (isVariableBefore(before) || isVariableAfter(after).is) {
  462. // console.log("/, variable")
  463. return "op";
  464. }
  465. if (isFunctionBefore(before) || isFunctionAfter(after).is) {
  466. // console.log("/, function as operand")
  467. return "op";
  468. }
  469. if (isParensBefore_ || isParensAfter_.is) {
  470. // console.log("/, function as operand")
  471. return "op";
  472. }
  473. // `$var: 10px/2; // 5px`
  474. if (isAfterColon === true && (isValueWithUnitAfter_.is || isNumberAfter_.is) && (isValueWithUnitBefore_ || isNumberBefore_)) {
  475. return "op";
  476. }
  477. // Quick check of the following operator symbol - if it is a math operator
  478. if (
  479. // +, *, % count as operators unless after interpolation or at the start
  480. before.search(/[^{,(}\s]\s*[+*%][^(){},]+$/) !== -1 ||
  481. // We consider minus as op only if surrounded by whitespaces (` - `);
  482. before.search(/[^{,(}\s]\s+-\s[^(){},]+$/) !== -1 ||
  483. // `10/2 * 3`, `10/2 % 3`, with or without spaces
  484. after.search(/^[^(){},]+[*%]/) !== -1 ||
  485. // `10px/2px+1`, `10px/2px+ 1`
  486. after.search(/^[^(){},\s]+\+/) !== -1 ||
  487. // Anything but `10px/2px +1`, `10px/2px +1px`
  488. after.search(/^[^(){},\s]+\s+(\+\D)/) !== -1 ||
  489. // Following ` -`: only if `$var` after (`10/10 -$var`)
  490. after.search(/^[^(){},\s]+\s+-(\$|\s)/) !== -1 ||
  491. // Following `-`: only if number after (`10s/10s-10`, `10s/10s-.1`)
  492. after.search(/^[^(){},\s]+-(\.)?\d/) !== -1 ||
  493. // Or if there is a number before anything but string after (not `10s/1-str`,)
  494. after.search(/^(\d*\.)?\d+-\s*[^#a-zA-Z_\s]/) !== -1) {
  495. // console.log("/, math op around")
  496. return "op";
  497. }
  498. if (isInsideParens(string, index) || isInsideFn.is && isInsideFn.fn !== "url") {
  499. // console.log("/, parens or function arg")
  500. return "op";
  501. }
  502. // console.log("/, default")
  503. return "char";
  504. }
  505. /**
  506. * Checks the specified `%` character: operator or part of value
  507. *
  508. * @param {String} string - the source string
  509. * @param {Number} index - the index of the character in string to check
  510. * @return {String|false}
  511. * • "op", if the character is a operator in a math/string operation
  512. * • "char" if it gets compiled as-is, e.g. `width: 10%`,
  513. * • false - if it is none from above (most likely an error)
  514. */
  515. function checkPercent(string, index) {
  516. // Trimming these, as spaces before/after a slash don't matter
  517. var before = string.substring(0, index);
  518. var after = string.substring(index + 1);
  519. // If the character is at the beginning of the input
  520. var isAtStart_ = isAtStart(string, index);
  521. // If the character is at the end of the input
  522. var isAtEnd_ = isAtEnd(string, index);
  523. var isWhitespaceBefore = before.search(/\s$/) !== -1;
  524. var isWhitespaceAfter = after.search(/^\s/) !== -1;
  525. var isParensBefore_ = isParensBefore(before);
  526. // FIRST OFF. Interpolation on any of the sides is a NO-GO
  527. if (isInterpolationBefore(before.trim()).is || isInterpolationAfter(after.trim()).is) {
  528. // console.log("%, interpolation")
  529. return "char";
  530. }
  531. if (isAtStart_ || isAtEnd_) {
  532. // console.log("%, start/end")
  533. return "char";
  534. }
  535. // In `<sth> %<sth>` it's most likely an operator (except for interpolation
  536. // checked above)
  537. if (isWhitespaceBefore && !isWhitespaceAfter) {
  538. // console.log("%, `<sth> %<sth>`")
  539. return "op";
  540. }
  541. // `$var% 1`, `$var%1`, `$var%-1`
  542. if (isVariableBefore(before) || isParensBefore_) {
  543. // console.log("%, after a variable, function or parens")
  544. return "op";
  545. }
  546. // in all other cases in `<sth>% <sth>` it is most likely a unit
  547. if (!isWhitespaceBefore && isWhitespaceAfter) {
  548. // console.log("%, `<sth>% <sth>`")
  549. return "char";
  550. }
  551. // console.log("%, default")
  552. return "char";
  553. }
  554. // --------------------------------------------------------------------------
  555. // Lots of elementary helpers
  556. // --------------------------------------------------------------------------
  557. function isAtStart(string, index) {
  558. var before = string.substring(0, index).trim();
  559. return before.length === 0 || before.search(/[({,]$/) !== -1;
  560. }
  561. function isAtEnd(string, index) {
  562. var after = string.substring(index + 1).trim();
  563. return after.length === 0 || after.search(/^[,)}]/) !== -1;
  564. }
  565. function isInsideParens(string, index) {
  566. var before = string.substring(0, index).trim();
  567. var after = string.substring(index + 1).trim();
  568. return before.search(/(?:^|[,{\s])\([^(){},]+$/) !== -1 && after.search(/^[^(){},\s]+\s*\)/) !== -1;
  569. }
  570. function isInsideInterpolation(string, index) {
  571. var before = string.substring(0, index).trim();
  572. return before.search(/#{[^}]*$/) !== -1;
  573. }
  574. /**
  575. * Checks if the character is inside a function arguments
  576. *
  577. * @param {String} string - the input string
  578. * @param {Number} index - current character index
  579. * @return {Object} return
  580. * {Boolean} return.is - if inside a function arguments
  581. * {String} return.fn - function name
  582. */
  583. function isInsideFunctionCall(string, index) {
  584. var result = {
  585. is: false,
  586. fn: null
  587. };
  588. var before = string.substring(0, index).trim();
  589. var after = string.substring(index + 1).trim();
  590. var beforeMatch = before.match(/(?:[a-zA-Z_-][\w-]*\()?(:?[a-zA-Z_-][\w-]*)\(/);
  591. if (beforeMatch && beforeMatch[0] && after.search(/^[^(,]+\)/) !== -1) {
  592. result.is = true;
  593. result.fn = beforeMatch[1];
  594. }
  595. return result;
  596. }
  597. /**
  598. * Checks if there is a string before the character.
  599. * Also checks if there is a math operator in between
  600. *
  601. * @param {String} before - the input string that preceses the character
  602. * @return {Object} return
  603. * {Boolean} return.is - if there is a string
  604. * {String} return.opsBetween - if there are operators in between
  605. */
  606. function isStringBefore(before) {
  607. var result = {
  608. is: false,
  609. opsBetween: false
  610. };
  611. var stringOpsClipped = before.replace(/(\s*[+/*%]|\s+-)+$/, "");
  612. if (stringOpsClipped !== before) {
  613. result.opsBetween = true;
  614. }
  615. // If it is quoted
  616. if (stringOpsClipped[stringOpsClipped.length - 1] === '"' || stringOpsClipped[stringOpsClipped.length - 1] === "'") {
  617. result.is = true;
  618. } else if (stringOpsClipped.search(/(?:^|[/(){},: ])([a-zA-Z_][\w-]*|-+[a-zA-Z_][\w-]*)$/) !== -1) {
  619. // First pattern: a1, a1a, a-1,
  620. result.is = true;
  621. }
  622. return result;
  623. }
  624. function isStringAfter(after) {
  625. var stringTrimmed = after.trim();
  626. // If it is quoted
  627. if (stringTrimmed[0] === '"' || stringTrimmed[0] === "'") return true;
  628. // e.g. `a1`, `a1a`, `a-1`, and even `--s323`
  629. return stringTrimmed.search(/^([a-zA-Z_][\w-]*|-+[a-zA-Z_][\w-]*)(?:$|[)}, ])/) !== -1;
  630. }
  631. function isInterpolationAfter(after) {
  632. var result = {
  633. is: false,
  634. opsBetween: false
  635. };
  636. var matches = after.match(/^\s*([+/*%-]\s*)*#{/);
  637. if (matches) {
  638. if (matches[0]) {
  639. result.is = true;
  640. }
  641. if (matches[1]) {
  642. result.opsBetween = true;
  643. }
  644. }
  645. return result;
  646. }
  647. function isParensAfter(after) {
  648. var result = {
  649. is: false,
  650. opsBetween: false
  651. };
  652. var matches = after.match(/^\s*([+/*%-]\s*)*\(/);
  653. if (matches) {
  654. if (matches[0]) {
  655. result.is = true;
  656. }
  657. if (matches[1]) {
  658. result.opsBetween = true;
  659. }
  660. }
  661. return result;
  662. }
  663. function isParensBefore(before) {
  664. return before.search(/\)\s*$/) !== -1;
  665. }
  666. /**
  667. * Checks if there is an interpolation before the character.
  668. * Also checks if there is a math operator in between
  669. *
  670. * @param {String} before - the input string that preceses the character
  671. * @return {Object} return
  672. * {Boolean} return.is - if there is an interpolation
  673. * {String} return.opsBetween - if there are operators in between
  674. */
  675. function isInterpolationBefore(before) {
  676. var result = {
  677. is: false,
  678. opsBetween: false
  679. };
  680. // Removing preceding operators if any
  681. var beforeOpsClipped = before.replace(/(\s*[+/*%-])+$/, "");
  682. if (beforeOpsClipped !== before) {
  683. result.opsBetween = true;
  684. }
  685. if (beforeOpsClipped[beforeOpsClipped.length - 1] === "}") {
  686. result.is = true;
  687. }
  688. return result;
  689. }
  690. function isValueWithUnitBefore(before) {
  691. // 1px, 0.1p-x, .2p-, 11.2pdf-df1df_
  692. // Surprisingly, ` d.10px` - .10px is separated from a sequence
  693. // and is considered a value with a unit
  694. return before.trim().search(/(^|[/(, .])\d[\w-]+$/) !== -1;
  695. }
  696. function isValueWithUnitAfter(after) {
  697. var result = {
  698. is: false,
  699. opsBetween: false
  700. };
  701. // 1px, 0.1p-x, .2p-, 11.2pdf-dfd1f_
  702. // Again, ` d.10px` - .10px is separated from a sequence
  703. // and is considered a value with a unit
  704. var matches = after.match(/^\s*([+/*%-]\s*)*(\d+(\.\d+)?|\.\d+)[\w-%]+(?:$|[)}, ])/);
  705. if (matches) {
  706. if (matches[0]) {
  707. result.is = true;
  708. }
  709. if (matches[1]) {
  710. result.opsBetween = true;
  711. }
  712. }
  713. return result;
  714. }
  715. function isNumberAfter(after) {
  716. var result = {
  717. is: false,
  718. opsBetween: false
  719. };
  720. var matches = after.match(/^\s*([+/*%-]\s*)*(\d+(\.\d+)?|\.\d+)(?:$|[)}, ])/);
  721. if (matches) {
  722. if (matches[0]) {
  723. result.is = true;
  724. }
  725. if (matches[1]) {
  726. result.opsBetween = true;
  727. }
  728. }
  729. return result;
  730. }
  731. function isNumberBefore(before) {
  732. return before.trim().search(/(?:^|[/(){},\s])(\d+(\.\d+)?|\.\d+)$/) !== -1;
  733. }
  734. function isVariableBefore(before) {
  735. return before.trim().search(/\$[\w-]+$/) !== -1;
  736. }
  737. function isVariableAfter(after) {
  738. var result = {
  739. is: false,
  740. opsBetween: false
  741. };
  742. var matches = after.match(/^\s*([+/*%-]\s*)*\$/);
  743. if (matches) {
  744. if (matches[0]) {
  745. result.is = true;
  746. }
  747. if (matches[1]) {
  748. result.opsBetween = true;
  749. }
  750. }
  751. return result;
  752. }
  753. function isDotBefore(before) {
  754. return before.slice(-1) === ".";
  755. }
  756. function isFunctionBefore(before) {
  757. return before.trim().search(/[\w-]\(.*?\)\s*$/) !== -1;
  758. }
  759. function isFunctionAfter(after) {
  760. var result = {
  761. is: false,
  762. opsBetween: false
  763. };
  764. // `-fn()` is a valid function name, so if a - should be a sign/operator,
  765. // it must have a space after
  766. var matches = after.match(/^\s*(-\s+|[+/*%]\s*)*[a-zA-Z_-][\w-]*\(/);
  767. if (matches) {
  768. if (matches[0]) {
  769. result.is = true;
  770. }
  771. if (matches[1]) {
  772. result.opsBetween = true;
  773. }
  774. }
  775. return result;
  776. }
  777. /**
  778. * Checks if the input string is a hex color value
  779. *
  780. * @param {String} string - the input
  781. * @return {Boolean} true, if the input is a hex color
  782. */
  783. function isHexColor(string) {
  784. return string.trim().search(/^#([\da-fA-F]{3}|[\da-fA-F]{6})$/) !== -1;
  785. }
  786. function isHexColorAfter(after) {
  787. var afterTrimmed = after.match(/(.*?)(?:[)},+/*%\-\s]|$)/)[1].trim();
  788. return isHexColor(afterTrimmed);
  789. }
  790. function isHexColorBefore(before) {
  791. return before.search(/(?:[/(){},+*%-\s]|^)#([\da-fA-F]{3}|[\da-fA-F]{6})$/) !== -1;
  792. }
  793. /**
  794. * Checks if there is no operand before the current char
  795. * In other words, the current char is at the start of a possible operation,
  796. * e.g. at the string start, after the opening paren or after a comma
  797. *
  798. * @param {String} string - the input string
  799. * @param {Number} index - current char's position in string
  800. * @return {Boolean}
  801. */
  802. function isNoOperandBefore(string, index) {
  803. var before = string.substring(0, index).trim();
  804. return before.length === 0 || before.search(/[({,]&/) !== -1;
  805. }
  806. function isPrecedingOperator(string, index) {
  807. var prevCharIndex = -1;
  808. for (var i = index - 1; i >= 0; i--) {
  809. if (string[i].search(/\s/) === -1) {
  810. prevCharIndex = i;
  811. break;
  812. }
  813. }
  814. if (prevCharIndex === -1) {
  815. return false;
  816. }
  817. if (mathOperatorCharType(string, prevCharIndex) === "op") {
  818. return true;
  819. }
  820. return false;
  821. }