index.js 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. /**
  2. * Module dependencies
  3. */
  4. var balanced = require("balanced-match")
  5. var reduceFunctionCall = require("reduce-function-call")
  6. var mexp = require("math-expression-evaluator")
  7. /**
  8. * Constantes
  9. */
  10. var MAX_STACK = 100 // should be enough for a single calc()...
  11. var NESTED_CALC_RE = /(\+|\-|\*|\\|[^a-z]|)(\s*)(\()/g
  12. /**
  13. * Global variables
  14. */
  15. var stack
  16. /**
  17. * Expose reduceCSSCalc plugin
  18. *
  19. * @type {Function}
  20. */
  21. module.exports = reduceCSSCalc
  22. /**
  23. * Reduce CSS calc() in a string, whenever it's possible
  24. *
  25. * @param {String} value css input
  26. */
  27. function reduceCSSCalc(value, decimalPrecision) {
  28. stack = 0
  29. decimalPrecision = Math.pow(10, decimalPrecision === undefined ? 5 : decimalPrecision)
  30. // Allow calc() on multiple lines
  31. value = value.replace(/\n+/g, " ")
  32. /**
  33. * Evaluates an expression
  34. *
  35. * @param {String} expression
  36. * @returns {String}
  37. */
  38. function evaluateExpression (expression, functionIdentifier, call) {
  39. if (stack++ > MAX_STACK) {
  40. stack = 0
  41. throw new Error("Call stack overflow for " + call)
  42. }
  43. if (expression === "") {
  44. throw new Error(functionIdentifier + "(): '" + call + "' must contain a non-whitespace string")
  45. }
  46. expression = evaluateNestedExpression(expression, call)
  47. var units = getUnitsInExpression(expression)
  48. // If the expression contains multiple units or CSS variables,
  49. // then let the expression be (i.e. browser calc())
  50. if (units.length > 1 || expression.indexOf("var(") > -1) {
  51. return functionIdentifier + "(" + expression + ")"
  52. }
  53. var unit = units[0] || ""
  54. if (unit === "%") {
  55. // Convert percentages to numbers, to handle expressions like: 50% * 50% (will become: 25%):
  56. // console.log(expression)
  57. expression = expression.replace(/\b[0-9\.]+%/g, function(percent) {
  58. return parseFloat(percent.slice(0, -1)) * 0.01
  59. })
  60. }
  61. // Remove units in expression:
  62. var toEvaluate = expression.replace(new RegExp(unit, "gi"), "")
  63. var result
  64. try {
  65. result = mexp.eval(toEvaluate)
  66. }
  67. catch (e) {
  68. return functionIdentifier + "(" + expression + ")"
  69. }
  70. // Transform back to a percentage result:
  71. if (unit === "%") {
  72. result *= 100
  73. }
  74. // adjust rounding shit
  75. // (0.1 * 0.2 === 0.020000000000000004)
  76. if (functionIdentifier.length || unit === "%") {
  77. result = Math.round(result * decimalPrecision) / decimalPrecision
  78. }
  79. // Add unit
  80. result += unit
  81. return result
  82. }
  83. /**
  84. * Evaluates nested expressions
  85. *
  86. * @param {String} expression
  87. * @returns {String}
  88. */
  89. function evaluateNestedExpression(expression, call) {
  90. // Remove the calc part from nested expressions to ensure
  91. // better browser compatibility
  92. expression = expression.replace(/((?:\-[a-z]+\-)?calc)/g, "")
  93. var evaluatedPart = ""
  94. var nonEvaluatedPart = expression
  95. var matches
  96. while ((matches = NESTED_CALC_RE.exec(nonEvaluatedPart))) {
  97. if (matches[0].index > 0) {
  98. evaluatedPart += nonEvaluatedPart.substring(0, matches[0].index)
  99. }
  100. var balancedExpr = balanced("(", ")", nonEvaluatedPart.substring([0].index))
  101. if (balancedExpr.body === "") {
  102. throw new Error("'" + expression + "' must contain a non-whitespace string")
  103. }
  104. var evaluated = evaluateExpression(balancedExpr.body, "", call)
  105. evaluatedPart += balancedExpr.pre + evaluated
  106. nonEvaluatedPart = balancedExpr.post
  107. }
  108. return evaluatedPart + nonEvaluatedPart
  109. }
  110. return reduceFunctionCall(value, /((?:\-[a-z]+\-)?calc)\(/, evaluateExpression)
  111. }
  112. /**
  113. * Checks what units are used in an expression
  114. *
  115. * @param {String} expression
  116. * @returns {Array}
  117. */
  118. function getUnitsInExpression(expression) {
  119. var uniqueUnits = []
  120. var uniqueLowerCaseUnits = []
  121. var unitRegEx = /[\.0-9]([%a-z]+)/gi
  122. var matches = unitRegEx.exec(expression)
  123. while (matches) {
  124. if (!matches || !matches[1]) {
  125. continue
  126. }
  127. if (uniqueLowerCaseUnits.indexOf(matches[1].toLowerCase()) === -1) {
  128. uniqueUnits.push(matches[1])
  129. uniqueLowerCaseUnits.push(matches[1].toLowerCase())
  130. }
  131. matches = unitRegEx.exec(expression)
  132. }
  133. return uniqueUnits
  134. }