core.js 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. /**
  2. * @typedef CoreOptions
  3. * @property {ReadonlyArray<string>} [subset=[]]
  4. * Whether to only escape the given subset of characters.
  5. * @property {boolean} [escapeOnly=false]
  6. * Whether to only escape possibly dangerous characters.
  7. * Those characters are `"`, `&`, `'`, `<`, `>`, and `` ` ``.
  8. *
  9. * @typedef FormatOptions
  10. * @property {(code: number, next: number, options: CoreWithFormatOptions) => string} format
  11. * Format strategy.
  12. *
  13. * @typedef {CoreOptions & FormatOptions & import('./util/format-smart.js').FormatSmartOptions} CoreWithFormatOptions
  14. */
  15. const defaultSubsetRegex = /["&'<>`]/g
  16. const surrogatePairsRegex = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g
  17. const controlCharactersRegex =
  18. // eslint-disable-next-line no-control-regex, unicorn/no-hex-escape
  19. /[\x01-\t\v\f\x0E-\x1F\x7F\x81\x8D\x8F\x90\x9D\xA0-\uFFFF]/g
  20. const regexEscapeRegex = /[|\\{}()[\]^$+*?.]/g
  21. /** @type {WeakMap<ReadonlyArray<string>, RegExp>} */
  22. const subsetToRegexCache = new WeakMap()
  23. /**
  24. * Encode certain characters in `value`.
  25. *
  26. * @param {string} value
  27. * @param {CoreWithFormatOptions} options
  28. * @returns {string}
  29. */
  30. export function core(value, options) {
  31. value = value.replace(
  32. options.subset
  33. ? charactersToExpressionCached(options.subset)
  34. : defaultSubsetRegex,
  35. basic
  36. )
  37. if (options.subset || options.escapeOnly) {
  38. return value
  39. }
  40. return (
  41. value
  42. // Surrogate pairs.
  43. .replace(surrogatePairsRegex, surrogate)
  44. // BMP control characters (C0 except for LF, CR, SP; DEL; and some more
  45. // non-ASCII ones).
  46. .replace(controlCharactersRegex, basic)
  47. )
  48. /**
  49. * @param {string} pair
  50. * @param {number} index
  51. * @param {string} all
  52. */
  53. function surrogate(pair, index, all) {
  54. return options.format(
  55. (pair.charCodeAt(0) - 0xd800) * 0x400 +
  56. pair.charCodeAt(1) -
  57. 0xdc00 +
  58. 0x10000,
  59. all.charCodeAt(index + 2),
  60. options
  61. )
  62. }
  63. /**
  64. * @param {string} character
  65. * @param {number} index
  66. * @param {string} all
  67. */
  68. function basic(character, index, all) {
  69. return options.format(
  70. character.charCodeAt(0),
  71. all.charCodeAt(index + 1),
  72. options
  73. )
  74. }
  75. }
  76. /**
  77. * A wrapper function that caches the result of `charactersToExpression` with a WeakMap.
  78. * This can improve performance when tooling calls `charactersToExpression` repeatedly
  79. * with the same subset.
  80. *
  81. * @param {ReadonlyArray<string>} subset
  82. * @returns {RegExp}
  83. */
  84. function charactersToExpressionCached(subset) {
  85. let cached = subsetToRegexCache.get(subset)
  86. if (!cached) {
  87. cached = charactersToExpression(subset)
  88. subsetToRegexCache.set(subset, cached)
  89. }
  90. return cached
  91. }
  92. /**
  93. * @param {ReadonlyArray<string>} subset
  94. * @returns {RegExp}
  95. */
  96. function charactersToExpression(subset) {
  97. /** @type {Array<string>} */
  98. const groups = []
  99. let index = -1
  100. while (++index < subset.length) {
  101. groups.push(subset[index].replace(regexEscapeRegex, '\\$&'))
  102. }
  103. return new RegExp('(?:' + groups.join('|') + ')', 'g')
  104. }