whatwg.js 2.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
  1. import { utf8fromStringLoose } from '@exodus/bytes/utf8.js'
  2. import { createSinglebyteEncoder } from '@exodus/bytes/single-byte.js'
  3. import {
  4. isMultibyte,
  5. getMultibyteEncoder,
  6. normalizeEncoding,
  7. E_ENCODING,
  8. } from './fallback/encoding.js'
  9. import { percentEncoder } from './fallback/percent.js'
  10. import { encodeMap } from './fallback/single-byte.js'
  11. import { E_STRING } from './fallback/_utils.js'
  12. // https://url.spec.whatwg.org/#string-percent-encode-after-encoding
  13. // Codepoints below 0x20, 0x7F specifically, and above 0x7F (non-ASCII) are always encoded
  14. // > A C0 control is a code point in the range U+0000 NULL to U+001F INFORMATION SEPARATOR ONE, inclusive.
  15. // > The C0 control percent-encode set are the C0 controls and all code points greater than U+007E (~).
  16. export function percentEncodeAfterEncoding(encoding, input, percentEncodeSet, spaceAsPlus = false) {
  17. const enc = normalizeEncoding(encoding)
  18. // Ref: https://encoding.spec.whatwg.org/#get-an-encoder
  19. if (!enc || enc === 'replacement' || enc === 'utf-16le' || enc === 'utf-16be') {
  20. throw new RangeError(E_ENCODING)
  21. }
  22. const percent = percentEncoder(percentEncodeSet, spaceAsPlus)
  23. if (enc === 'utf-8') return percent(utf8fromStringLoose(input))
  24. const multi = isMultibyte(enc)
  25. const encoder = multi ? getMultibyteEncoder() : createSinglebyteEncoder
  26. const fatal = encoder(enc)
  27. try {
  28. return percent(fatal(input))
  29. } catch {}
  30. let res = ''
  31. let last = 0
  32. if (multi) {
  33. const rep = enc === 'gb18030' ? percent(fatal('\uFFFD')) : `%26%23${0xff_fd}%3B` // only gb18030 can encode it
  34. const escaping = encoder(enc, (cp, u, i) => {
  35. res += percent(u, last, i)
  36. res += cp >= 0xd8_00 && cp < 0xe0_00 ? rep : `%26%23${cp}%3B` // &#cp;
  37. last = i
  38. return 0 // no bytes emitted
  39. })
  40. const u = escaping(input) // has side effects on res
  41. res += percent(u, last)
  42. } else {
  43. if (typeof input !== 'string') throw new TypeError(E_STRING) // all other paths have their own validation
  44. const m = encodeMap(enc)
  45. const len = input.length
  46. const u = new Uint8Array(len)
  47. for (let i = 0; i < len; i++) {
  48. const x = input.charCodeAt(i)
  49. const b = m[x]
  50. if (!b && x) {
  51. let cp = x
  52. const i0 = i
  53. if (x >= 0xd8_00 && x < 0xe0_00) {
  54. cp = 0xff_fd
  55. if (x < 0xdc_00 && i + 1 < len) {
  56. const x1 = input.charCodeAt(i + 1)
  57. if (x1 >= 0xdc_00 && x1 < 0xe0_00) {
  58. cp = 0x1_00_00 + ((x1 & 0x3_ff) | ((x & 0x3_ff) << 10))
  59. i++
  60. }
  61. }
  62. }
  63. res += `${percent(u, last, i0)}%26%23${cp}%3B` // &#cp;
  64. last = i + 1 // skip current
  65. } else {
  66. u[i] = b
  67. }
  68. }
  69. res += percent(u, last)
  70. }
  71. return res
  72. }