escape-svg.js 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. //
  2. // Helper Functions
  3. //
  4. function lookupVariable(context, variableName) {
  5. const { frames, importantScope } = context
  6. return tree.Variable.prototype.find(frames, frame => {
  7. const { value, important } = frame.variable(variableName) || {}
  8. if (value === undefined)
  9. return
  10. if (important && importantScope[importantScope.length - 1])
  11. importantScope[importantScope.length - 1].important = important
  12. return value.eval(context)
  13. })
  14. }
  15. // @TODO: [@calvinjuarez] unify this function between files, maybe even canonize it as a
  16. // `Ruleset`/`DetachedRuleset` method at some point.
  17. function rulesetToMap(context, { ruleset: { rules } } = { ruleset: { rules: [] } }) {
  18. const map = {}
  19. rules.forEach(rule => {
  20. // Not exactly sure how to handle other types (or if they should be handled at all).
  21. if (! (rule instanceof tree.Declaration))
  22. return
  23. const { name: key, value } = rule.eval(context)
  24. map[key] = value
  25. })
  26. return map
  27. }
  28. //
  29. // Less Functions
  30. //
  31. // Requires the use of quotes around data URIs.
  32. functions.add('escape-svg', function (value = {}) {
  33. let escapedStr = value.toCSS ? value.toCSS() : ''
  34. if (! escapedStr.includes('data:image/svg+xml'))
  35. return value
  36. const escapeCharsVar = lookupVariable(this.context, '@escaped-characters')
  37. let escapeCharsMap = {}
  38. // Currently Less treats the `@escaped-characters` variable as a string instead of a ruleset, due
  39. // to its unconventional use of special characters like `<` as property names. In case this is
  40. // fixed in the future, we’ll handle both possible values here (string and ruleset).
  41. if (escapeCharsVar instanceof tree.Quoted) {
  42. // Remove leading `{` and trailing `}` as well as the last instance of a `;`, then split by
  43. // `;`.
  44. const escapeCharsKeyValueArr = escapeCharsVar.value.replace(/[{}\s]+|;(?=\n})/g, '').split(';')
  45. escapeCharsKeyValueArr.forEach(escapeCharKeyValueStr => {
  46. const [key, value] = escapeCharKeyValueStr.split(':')
  47. escapeCharsMap[key] = value
  48. })
  49. } else
  50. escapeCharsMap = rulesetToMap(this.context, lookupVariable(this.context, '@escaped-characters'))
  51. let escapeCharsMapKeys = []
  52. try {
  53. escapeCharsMapKeys = Object.keys(escapeCharsMap)
  54. } catch (err) {
  55. // Do nothing.
  56. }
  57. let shouldAddURLWrapper = false
  58. // Strip the `url()` wrapping the string, if present (to prevent it from being escaped).
  59. if (escapedStr.startsWith('url(')) {
  60. escapedStr = escapedStr.replace(/^url\(|\)$/g, '')
  61. shouldAddURLWrapper = true
  62. }
  63. escapeCharsMapKeys.forEach(key => {
  64. const value = escapeCharsMap[key]
  65. if (key === ')' || key === '(')
  66. key = `\\${key}`
  67. escapedStr = escapedStr.replace(new RegExp(key, 'g'), value)
  68. })
  69. // Re-add the `url()` wrapper if necessary.
  70. if (shouldAddURLWrapper)
  71. escapedStr = `url(${escapedStr})`
  72. return new tree.Quoted('', escapedStr)
  73. })