├── .eslintrc ├── .github └── workflows │ ├── nodejs.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── benchmark ├── urlencode.cjs └── urlencode.decode.cjs ├── package.json ├── src └── index.ts ├── test └── urlencode.test.ts └── tsconfig.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint-config-egg/typescript", 4 | "eslint-config-egg/lib/rules/enforce-node-prefix" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | pull_request: 8 | branches: [ master ] 9 | 10 | jobs: 11 | Job: 12 | name: Node.js 13 | uses: node-modules/github-actions/.github/workflows/node-test.yml@master 14 | with: 15 | os: 'ubuntu-latest' 16 | version: '16, 18, 20, 21' 17 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | release: 9 | name: Node.js 10 | uses: node-modules/github-actions/.github/workflows/node-release.yml@master 11 | secrets: 12 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 13 | GIT_TOKEN: ${{ secrets.GIT_TOKEN }} 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | coverage/ 3 | *.seed 4 | *.log 5 | *.csv 6 | *.dat 7 | *.out 8 | *.pid 9 | *.gz 10 | 11 | pids 12 | logs 13 | results 14 | 15 | node_modules 16 | npm-debug.log 17 | .tshy* 18 | dist 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [2.0.0](https://github.com/node-modules/urlencode/compare/v1.1.0...v2.0.0) (2023-10-28) 4 | 5 | 6 | ### ⚠ BREAKING CHANGES 7 | 8 | * Drop Node.js < 16 support 9 | 10 | closes https://github.com/node-modules/urlencode/issues/22 11 | 12 | ### Features 13 | 14 | * support esm and cjs both ([#23](https://github.com/node-modules/urlencode/issues/23)) ([8f9308f](https://github.com/node-modules/urlencode/commit/8f9308fc830a2ba380343c8bce154aae3f3551f0)) 15 | 16 | 1.1.0 / 2015-08-14 17 | ================== 18 | 19 | * fix typo 20 | * feat: Support IE8 21 | 22 | 1.0.1 / 2015-07-06 23 | ================== 24 | 25 | * refactor: add \n to benchmark 26 | * fix '\n' encoding 27 | 28 | 1.0.0 / 2015-04-04 29 | ================== 30 | 31 | * deps: upgrade iconv-lite to 0.4.7 32 | 33 | 0.2.0 / 2014-04-25 34 | ================== 35 | 36 | * urlencode.stringify done (@alsotang) 37 | 38 | 0.1.2 / 2014-04-09 39 | ================== 40 | 41 | * remove unused variable QueryString (@azbykov) 42 | 43 | 0.1.1 / 2014-02-25 44 | ================== 45 | 46 | * improve parse() performance 10x 47 | 48 | 0.1.0 / 2014-02-24 49 | ================== 50 | 51 | * decode with charset 52 | * add npm image 53 | * remove 0.6 for travis 54 | * update to support coveralls 55 | * use jscover instead of jscoverage 56 | * update gitignore 57 | * Merge pull request #1 from aleafs/master 58 | * Add http entries test case 59 | 60 | 0.0.1 / 2012-10-31 61 | ================== 62 | 63 | * encode() done. add benchmark and tests 64 | * Initial commit 65 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | This software is licensed under the MIT License. 2 | 3 | Copyright (C) 2012 - 2014 fengmk2 4 | Copyright (C) 2015 node-modules 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # urlencode 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![Node.js CI](https://github.com/node-modules/urlencode/actions/workflows/nodejs.yml/badge.svg)](https://github.com/node-modules/urllib/actions/workflows/nodejs.yml) 5 | [![Test coverage][codecov-image]][codecov-url] 6 | [![Known Vulnerabilities][snyk-image]][snyk-url] 7 | [![npm download][download-image]][download-url] 8 | 9 | [npm-image]: https://img.shields.io/npm/v/urlencode.svg?style=flat-square 10 | [npm-url]: https://npmjs.org/package/urlencode 11 | [codecov-image]: https://codecov.io/gh/node-modules/urlencode/branch/master/graph/badge.svg 12 | [codecov-url]: https://codecov.io/gh/node-modules/urlencode 13 | [snyk-image]: https://snyk.io/test/npm/urlencode/badge.svg?style=flat-square 14 | [snyk-url]: https://snyk.io/test/npm/urlencode 15 | [download-image]: https://img.shields.io/npm/dm/urlencode.svg?style=flat-square 16 | [download-url]: https://npmjs.org/package/urlencode 17 | 18 | encodeURIComponent with charset, e.g.: `gbk` 19 | 20 | ## Install 21 | 22 | ```bash 23 | npm install urlencode 24 | ``` 25 | 26 | ## Usage 27 | 28 | ```ts 29 | import { encode, decode, parse, stringify } from 'urlencode'; 30 | 31 | console.log(encode('苏千')); // default is utf8 32 | console.log(encode('苏千', 'gbk')); // '%CB%D5%C7%A7' 33 | 34 | // decode gbk 35 | decode('%CB%D5%C7%A7', 'gbk'); // '苏千' 36 | 37 | // parse gbk querystring 38 | parse('nick=%CB%D5%C7%A7', { charset: 'gbk' }); // {nick: '苏千'} 39 | 40 | // stringify obj with gbk encoding 41 | var str = 'x[y][0][v][w]=' + encode('雾空', 'gbk'); // x[y][0][v][w]=%CE%ED%BF%D5 42 | var obj = {'x' : {'y' : [{'v' : {'w' : '雾空'}}]}}; 43 | assert.equal(urlencode.stringify(obj, { charset: 'gbk' }, str); 44 | ``` 45 | 46 | ## Benchmark 47 | 48 | ### encode(str, encoding) 49 | 50 | ```bash 51 | $ node benchmark/urlencode.cjs 52 | 53 | node version: v21.1.0 54 | "苏千测试\n, 哈哈, haha" 55 | 56 | urlencode Benchmark 57 | node version: v21.1.0, date: Sat Oct 28 2023 21:01:00 GMT+0800 (中国标准时间) 58 | Starting... 59 | 4 tests completed. 60 | 61 | urlencode(str) x 4,617,242 ops/sec ±2.60% (95 runs sampled) 62 | urlencode(str, "gbk") x 1,122,430 ops/sec ±2.20% (95 runs sampled) 63 | encodeURIComponent(str) x 4,608,523 ops/sec ±2.94% (93 runs sampled) 64 | encodeUTF8(str) x 833,170 ops/sec ±1.37% (96 runs sampled) 65 | 66 | node version: v20.9.0 67 | "苏千测试\n, 哈哈, haha" 68 | 69 | urlencode Benchmark 70 | node version: v20.9.0, date: Sat Oct 28 2023 21:01:37 GMT+0800 (中国标准时间) 71 | Starting... 72 | 4 tests completed. 73 | 74 | urlencode(str) x 4,304,468 ops/sec ±2.83% (89 runs sampled) 75 | urlencode(str, "gbk") x 1,005,759 ops/sec ±2.10% (90 runs sampled) 76 | encodeURIComponent(str) x 4,289,880 ops/sec ±2.99% (92 runs sampled) 77 | encodeUTF8(str) x 827,841 ops/sec ±1.06% (96 runs sampled) 78 | 79 | node version: v18.18.0 80 | "苏千测试\n, 哈哈, haha" 81 | 82 | urlencode Benchmark 83 | node version: v18.18.0, date: Sat Oct 28 2023 19:34:06 GMT+0800 (中国标准时间) 84 | Starting... 85 | 4 tests completed. 86 | 87 | urlencode(str) x 4,597,865 ops/sec ±0.22% (96 runs sampled) 88 | urlencode(str, "gbk") x 633,620 ops/sec ±15.31% (71 runs sampled) 89 | encodeURIComponent(str) x 3,902,229 ops/sec ±2.49% (87 runs sampled) 90 | encodeUTF8(str) x 510,456 ops/sec ±26.76% (88 runs sampled) 91 | 92 | node version: v16.20.2 93 | "苏千测试\n, 哈哈, haha" 94 | 95 | urlencode Benchmark 96 | node version: v16.20.2, date: Sat Oct 28 2023 21:02:11 GMT+0800 (中国标准时间) 97 | Starting... 98 | 4 tests completed. 99 | 100 | urlencode(str) x 4,438,372 ops/sec ±1.80% (93 runs sampled) 101 | urlencode(str, "gbk") x 1,175,761 ops/sec ±0.68% (95 runs sampled) 102 | encodeURIComponent(str) x 4,374,525 ops/sec ±1.96% (97 runs sampled) 103 | encodeUTF8(str) x 751,616 ops/sec ±2.49% (86 runs sampled) 104 | 105 | ``` 106 | 107 | ### decode(str, encoding) 108 | 109 | ```bash 110 | $ node benchmark/urlencode.decode.cjs 111 | 112 | node version: v21.1.0, date: "2023-10-28T12:51:20.191Z" 113 | 114 | urlencode.decode Benchmark 115 | node version: v21.1.0, date: Sat Oct 28 2023 20:51:20 GMT+0800 (中国标准时间) 116 | Starting... 117 | 7 tests completed. 118 | 119 | urlencode.decode(str) x 515,410 ops/sec ±1.95% (91 runs sampled) 120 | urlencode.decode(str, "gbk") x 54,018 ops/sec ±3.17% (78 runs sampled) 121 | decodeURIComponent(str) x 313,204 ops/sec ±2.93% (78 runs sampled) 122 | urlencode.parse(qs, {charset: "gbk"}) x 311,613 ops/sec ±1.26% (95 runs sampled) 123 | urlencode.stringify(data, {charset: "gbk"}) x 316,558 ops/sec ±1.55% (93 runs sampled) 124 | urlencode.parse(qs, {charset: "utf8"}) x 490,744 ops/sec ±1.25% (94 runs sampled) 125 | urlencode.stringify(data, {charset: "utf8"}) x 357,206 ops/sec ±0.46% (97 runs sampled) 126 | 127 | node version: v20.9.0, date: "2023-10-28T12:49:57.236Z" 128 | 129 | urlencode.decode Benchmark 130 | node version: v20.9.0, date: Sat Oct 28 2023 20:49:57 GMT+0800 (中国标准时间) 131 | Starting... 132 | 7 tests completed. 133 | 134 | urlencode.decode(str) x 573,899 ops/sec ±0.62% (95 runs sampled) 135 | urlencode.decode(str, "gbk") x 83,184 ops/sec ±0.13% (100 runs sampled) 136 | decodeURIComponent(str) x 573,371 ops/sec ±1.67% (93 runs sampled) 137 | urlencode.parse(qs, {charset: "gbk"}) x 303,202 ops/sec ±0.70% (100 runs sampled) 138 | urlencode.stringify(data, {charset: "gbk"}) x 319,546 ops/sec ±0.29% (99 runs sampled) 139 | urlencode.parse(qs, {charset: "utf8"}) x 462,578 ops/sec ±0.25% (98 runs sampled) 140 | urlencode.stringify(data, {charset: "utf8"}) x 343,487 ops/sec ±0.17% (100 runs sampled) 141 | 142 | node version: v18.18.0, date: "2023-10-28T12:44:56.355Z" 143 | 144 | urlencode.decode Benchmark 145 | node version: v18.18.0, date: Sat Oct 28 2023 20:44:56 GMT+0800 (中国标准时间) 146 | Starting... 147 | 7 tests completed. 148 | 149 | urlencode.decode(str) x 550,451 ops/sec ±1.74% (98 runs sampled) 150 | urlencode.decode(str, "gbk") x 67,311 ops/sec ±1.16% (96 runs sampled) 151 | decodeURIComponent(str) x 569,461 ops/sec ±0.30% (93 runs sampled) 152 | urlencode.parse(qs, {charset: "gbk"}) x 293,407 ops/sec ±0.90% (97 runs sampled) 153 | urlencode.stringify(data, {charset: "gbk"}) x 234,162 ops/sec ±4.55% (75 runs sampled) 154 | urlencode.parse(qs, {charset: "utf8"}) x 316,697 ops/sec ±4.37% (78 runs sampled) 155 | urlencode.stringify(data, {charset: "utf8"}) x 192,787 ops/sec ±4.58% (80 runs sampled) 156 | 157 | node version: v16.20.2, date: "2023-10-28T12:47:38.431Z" 158 | 159 | urlencode.decode Benchmark 160 | node version: v16.20.2, date: Sat Oct 28 2023 20:47:38 GMT+0800 (中国标准时间) 161 | Starting... 162 | 7 tests completed. 163 | 164 | urlencode.decode(str) x 537,995 ops/sec ±2.07% (96 runs sampled) 165 | urlencode.decode(str, "gbk") x 78,073 ops/sec ±0.17% (99 runs sampled) 166 | decodeURIComponent(str) x 558,509 ops/sec ±0.48% (96 runs sampled) 167 | urlencode.parse(qs, {charset: "gbk"}) x 252,590 ops/sec ±2.87% (90 runs sampled) 168 | urlencode.stringify(data, {charset: "gbk"}) x 287,978 ops/sec ±2.47% (92 runs sampled) 169 | urlencode.parse(qs, {charset: "utf8"}) x 416,600 ops/sec ±0.72% (93 runs sampled) 170 | urlencode.stringify(data, {charset: "utf8"}) x 281,319 ops/sec ±2.43% (85 runs sampled) 171 | 172 | ``` 173 | 174 | ## License 175 | 176 | [MIT](LICENSE.txt) 177 | -------------------------------------------------------------------------------- /benchmark/urlencode.cjs: -------------------------------------------------------------------------------- 1 | const Benchmark = require('benchmark'); 2 | const benchmarks = require('beautify-benchmark'); 3 | const { encode } = require('../'); 4 | 5 | console.log('node version: %s', process.version); 6 | 7 | function encodeUTF8(str) { 8 | let encodeStr = ''; 9 | const buf = Buffer.from(str); 10 | let ch = ''; 11 | for (let i = 0; i < buf.length; i++) { 12 | ch = buf[i].toString('16'); 13 | if (ch.length === 1) { 14 | ch = '0' + ch; 15 | } 16 | encodeStr += '%' + ch; 17 | } 18 | return encodeStr.toUpperCase(); 19 | } 20 | 21 | console.log('%j', decodeURIComponent(encodeUTF8('苏千测试\n, 哈哈, haha'))); 22 | 23 | const suite = new Benchmark.Suite(); 24 | 25 | suite 26 | 27 | .add('urlencode(str)', function () { 28 | // urlencode('苏千'); 29 | encode('苏千写的\nurlencode,应该有用'); 30 | // urlencode('suqian want to sleep early tonight.'); 31 | // urlencode('你让同一个项目中写两份一样代码的人情何以堪呢,你让同一个项目中写两份一样代码的人情何以堪呢,你让同一个项目中写两份一样代码的人情何以堪呢,你让同一个项目中写两份一样代码的人情何以堪呢'); 32 | }) 33 | 34 | .add('urlencode(str, "gbk")', function () { 35 | // urlencode('苏千', 'gbk'); 36 | encode('苏千写的\nurlencode,应该有用', 'gbk'); 37 | // urlencode('suqian want to sleep early tonight.', 'gbk'); 38 | // urlencode('你让同一个项目中写两份一样代码的人情何以堪呢,你让同一个项目中写两份一样代码的人情何以堪呢,你让同一个项目中写两份一样代码的人情何以堪呢,你让同一个项目中写两份一样代码的人情何以堪呢', 'gbk'); 39 | }) 40 | 41 | .add('encodeURIComponent(str)', function () { 42 | // encodeURIComponent('苏千'); 43 | encodeURIComponent('苏千写的\nurlencode,应该有用'); 44 | // encodeURIComponent('suqian want to sleep early tonight.'); 45 | // encodeURIComponent('你让同一个项目中写两份一样代码的人情何以堪呢,你让同一个项目中写两份一样代码的人情何以堪呢,你让同一个项目中写两份一样代码的人情何以堪呢,你让同一个项目中写两份一样代码的人情何以堪呢'); 46 | }) 47 | 48 | .add('encodeUTF8(str)', function () { 49 | // encodeUTF8('苏千'); 50 | encodeUTF8('苏千写的\nurlencode,应该有用'); 51 | // encodeUTF8('suqian want to sleep early tonight.'); 52 | // encodeUTF8('你让同一个项目中写两份一样代码的人情何以堪呢,你让同一个项目中写两份一样代码的人情何以堪呢,你让同一个项目中写两份一样代码的人情何以堪呢,你让同一个项目中写两份一样代码的人情何以堪呢'); 53 | }) 54 | 55 | .on('cycle', function(event) { 56 | benchmarks.add(event.target); 57 | }) 58 | .on('start', function(event) { 59 | console.log('\n urlencode Benchmark\n node version: %s, date: %s\n Starting...', 60 | process.version, Date()); 61 | }) 62 | .on('complete', function done() { 63 | benchmarks.log(); 64 | }) 65 | .run({ 'async': false }); 66 | -------------------------------------------------------------------------------- /benchmark/urlencode.decode.cjs: -------------------------------------------------------------------------------- 1 | const Benchmark = require('benchmark'); 2 | const benchmarks = require('beautify-benchmark'); 3 | const { parse, encode, decode, stringify } = require('../'); 4 | 5 | console.log('node version: %s, date: %j', process.version, new Date()); 6 | 7 | const suite = new Benchmark.Suite(); 8 | 9 | const utf8DecodeItems = [ 10 | encode('苏千'), 11 | encode('苏千写的urlencode,应该有用'), 12 | encode('suqian want to sleep early tonight.'), 13 | encode('你让同一个项目中写两份一样代码的人情何以堪呢,你让同一个项目中写两份一样代码的人情何以堪呢,你让同一个项目中写两份一样代码的人情何以堪呢,你让同一个项目中写两份一样代码的人情何以堪呢'), 14 | ]; 15 | 16 | const gbkDecodeItems = [ 17 | encode('苏千', 'gbk'), 18 | encode('苏千写的urlencode,应该有用', 'gbk'), 19 | encode('suqian want to sleep early tonight.', 'gbk'), 20 | encode('你让同一个项目中写两份一样代码的人情何以堪呢,你让同一个项目中写两份一样代码的人情何以堪呢,你让同一个项目中写两份一样代码的人情何以堪呢,你让同一个项目中写两份一样代码的人情何以堪呢', 'gbk'), 21 | ]; 22 | 23 | // console.log(decode(gbkDecodeItems[3], 'gbk')) 24 | 25 | const gbkEncodeString = 'umidtoken=Tc230acc03a564530aee31d22701e9b95&usertag4=0&usertag3=512&usertag2=0&status=0&userid=665377421&out_user=suqian.yf%40taobao.com&promotedtype=0&account_no=20885028063394350156&loginstatus=true&usertag=0&nick=%CB%D5%C7%A7&tairlastupdatetime=1319008872&strid=a68f6ee38f44d2b89ca508444c1ccaf9'; 26 | const data = parse(gbkEncodeString, {charset: 'gbk'}); 27 | 28 | // console.log(stringify(data, {charset: 'gbk'}) === gbkEncodeString); 29 | 30 | suite 31 | 32 | .add('urlencode.decode(str)', function () { 33 | decode(utf8DecodeItems[0]); 34 | decode(utf8DecodeItems[1]); 35 | decode(utf8DecodeItems[2]); 36 | decode(utf8DecodeItems[3]); 37 | }) 38 | 39 | .add('urlencode.decode(str, "gbk")', function () { 40 | decode(gbkDecodeItems[0], 'gbk'); 41 | decode(gbkDecodeItems[1], 'gbk'); 42 | decode(gbkDecodeItems[2], 'gbk'); 43 | decode(gbkDecodeItems[3], 'gbk'); 44 | }) 45 | 46 | .add('decodeURIComponent(str)', function () { 47 | decodeURIComponent(utf8DecodeItems[0]); 48 | decodeURIComponent(utf8DecodeItems[1]); 49 | decodeURIComponent(utf8DecodeItems[2]); 50 | decodeURIComponent(utf8DecodeItems[3]); 51 | }) 52 | 53 | .add('urlencode.parse(qs, {charset: "gbk"})', function () { 54 | parse(gbkEncodeString, {charset: 'gbk'}); 55 | }) 56 | 57 | .add('urlencode.stringify(data, {charset: "gbk"})', function () { 58 | stringify(data, {charset: 'gbk'}); 59 | }) 60 | 61 | .add('urlencode.parse(qs, {charset: "utf8"})', function () { 62 | parse('umidtoken=Tc230acc03a564530aee31d22701e9b95&usertag4=0&usertag3=512&usertag2=0&status=0&userid=665377421&out_user=suqian.yf%40taobao.com&promotedtype=0&account_no=20885028063394350156&loginstatus=true&usertag=0&nick=%E8%8B%8F%E5%8D%83&tairlastupdatetime=1319008872&strid=a68f6ee38f44d2b89ca508444c1ccaf9', 63 | {charset: 'utf8'}); 64 | }) 65 | 66 | .add('urlencode.stringify(data, {charset: "utf8"})', function () { 67 | stringify(data, {charset: 'utf8'}); 68 | }) 69 | 70 | .on('cycle', function(event) { 71 | benchmarks.add(event.target); 72 | }) 73 | .on('start', function(event) { 74 | console.log('\n urlencode.decode Benchmark\n node version: %s, date: %s\n Starting...', 75 | process.version, Date()); 76 | }) 77 | .on('complete', function done() { 78 | benchmarks.log(); 79 | }) 80 | .run({ 'async': false }); 81 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "urlencode", 3 | "version": "2.0.0", 4 | "description": "encodeURIComponent with charset", 5 | "scripts": { 6 | "test": "egg-bin test", 7 | "ci": "npm run lint && egg-bin cov && npm run prepublishOnly && npm run benchmark", 8 | "lint": "eslint . --ext ts", 9 | "benchmark": "node benchmark/urlencode.cjs && node benchmark/urlencode.decode.cjs", 10 | "prepublishOnly": "tshy && tshy-after" 11 | }, 12 | "dependencies": { 13 | "iconv-lite": "~0.6.3" 14 | }, 15 | "devDependencies": { 16 | "@eggjs/tsconfig": "^1.3.3", 17 | "@types/mocha": "^10.0.3", 18 | "@types/node": "^20.8.7", 19 | "beautify-benchmark": "^0.2.4", 20 | "benchmark": "^2.1.4", 21 | "egg-bin": "^6.5.2", 22 | "eslint": "^8.51.0", 23 | "eslint-config-egg": "^13.0.0", 24 | "git-contributor": "^2.1.5", 25 | "tshy": "^1.5.0", 26 | "tshy-after": "^1.0.0", 27 | "typescript": "^5.2.2" 28 | }, 29 | "homepage": "https://github.com/node-modules/urlencode", 30 | "repository": { 31 | "type": "git", 32 | "url": "git://github.com/node-modules/urlencode.git" 33 | }, 34 | "keywords": [ 35 | "urlencode", 36 | "urldecode", 37 | "encodeURIComponent", 38 | "decodeURIComponent", 39 | "querystring", 40 | "parse" 41 | ], 42 | "author": "fengmk2 ", 43 | "license": "MIT", 44 | "files": [ 45 | "dist", 46 | "src" 47 | ], 48 | "tshy": { 49 | "exports": { 50 | "./package.json": "./package.json", 51 | ".": "./src/index.ts" 52 | } 53 | }, 54 | "exports": { 55 | "./package.json": "./package.json", 56 | ".": { 57 | "import": { 58 | "types": "./dist/esm/index.d.ts", 59 | "default": "./dist/esm/index.js" 60 | }, 61 | "require": { 62 | "types": "./dist/commonjs/index.d.ts", 63 | "default": "./dist/commonjs/index.js" 64 | } 65 | } 66 | }, 67 | "main": "./dist/commonjs/index.js", 68 | "types": "./dist/commonjs/index.d.ts", 69 | "type": "module" 70 | } 71 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import iconv from 'iconv-lite'; 2 | 3 | export type SupportEncodeValue = string | number | boolean | undefined | null; 4 | export type SupportEncodeObject = Record; 5 | export interface Options { 6 | charset?: string; 7 | maxKeys?: number; 8 | } 9 | 10 | function isUTF8(charset?: string) { 11 | if (!charset) { 12 | return true; 13 | } 14 | charset = charset.toLowerCase(); 15 | return charset === 'utf8' || charset === 'utf-8'; 16 | } 17 | 18 | export function encode(str: string, charset?: string | null) { 19 | if (!charset || isUTF8(charset)) { 20 | return encodeURIComponent(str); 21 | } 22 | 23 | const buf = iconv.encode(str, charset); 24 | let encodeStr = ''; 25 | let ch = ''; 26 | for (let i = 0; i < buf.length; i++) { 27 | ch = buf[i].toString(16); 28 | if (ch.length === 1) { 29 | ch = '0' + ch; 30 | } 31 | encodeStr += '%' + ch; 32 | } 33 | encodeStr = encodeStr.toUpperCase(); 34 | return encodeStr; 35 | } 36 | 37 | export default encode; 38 | 39 | export function decode(str: string, charset?: string | null) { 40 | if (!charset || isUTF8(charset)) { 41 | return decodeURIComponent(str); 42 | } 43 | 44 | const bytes = []; 45 | for (let i = 0; i < str.length;) { 46 | if (str[i] === '%') { 47 | i++; 48 | bytes.push(parseInt(str.substring(i, i + 2), 16)); 49 | i += 2; 50 | } else { 51 | bytes.push(str.charCodeAt(i)); 52 | i++; 53 | } 54 | } 55 | const buf = Buffer.from(bytes); 56 | return iconv.decode(buf, charset); 57 | } 58 | 59 | export function parse(qs: string, options?: Options): SupportEncodeObject; 60 | export function parse(qs: string, sep?: string, eq?: string, options?: Options): SupportEncodeObject; 61 | export function parse(qs: string, sepOrOptions?: string | Options, eq?: string, options?: Options): SupportEncodeObject { 62 | let sep: string | undefined; 63 | if (typeof sepOrOptions === 'object') { 64 | // parse(qs, options) 65 | options = sepOrOptions; 66 | } else { 67 | // parse(qs, sep, eq, options) 68 | sep = sepOrOptions; 69 | } 70 | 71 | sep = sep || '&'; 72 | eq = eq || '='; 73 | const obj: SupportEncodeObject = {}; 74 | 75 | if (typeof qs !== 'string' || qs.length === 0) { 76 | return obj; 77 | } 78 | 79 | const regexp = /\+/g; 80 | const splits = qs.split(sep); 81 | 82 | let maxKeys = 1000; 83 | let charset = ''; 84 | if (options) { 85 | if (typeof options.maxKeys === 'number') { 86 | maxKeys = options.maxKeys; 87 | } 88 | if (typeof options.charset === 'string') { 89 | charset = options.charset; 90 | } 91 | } 92 | 93 | let len = splits.length; 94 | // maxKeys <= 0 means that we should not limit keys count 95 | if (maxKeys > 0 && len > maxKeys) { 96 | len = maxKeys; 97 | } 98 | 99 | for (let i = 0; i < len; ++i) { 100 | const x = splits[i].replace(regexp, '%20'); 101 | const idx = x.indexOf(eq); 102 | let keyString: string; 103 | let valueString: string; 104 | let k: string; 105 | let v: string; 106 | 107 | if (idx >= 0) { 108 | keyString = x.substring(0, idx); 109 | valueString = x.substring(idx + 1); 110 | } else { 111 | keyString = x; 112 | valueString = ''; 113 | } 114 | 115 | if (keyString && keyString.includes('%')) { 116 | try { 117 | k = decode(keyString, charset); 118 | } catch (e) { 119 | k = keyString; 120 | } 121 | } else { 122 | k = keyString; 123 | } 124 | 125 | if (valueString && valueString.includes('%')) { 126 | try { 127 | v = decode(valueString, charset); 128 | } catch (e) { 129 | v = valueString; 130 | } 131 | } else { 132 | v = valueString; 133 | } 134 | 135 | if (!has(obj, k)) { 136 | obj[k] = v; 137 | } else if (Array.isArray(obj[k])) { 138 | (obj[k] as any).push(v); 139 | } else { 140 | obj[k] = [ obj[k], v ]; 141 | } 142 | } 143 | 144 | return obj; 145 | } 146 | 147 | function has(obj: object, prop: string) { 148 | return Object.prototype.hasOwnProperty.call(obj, prop); 149 | } 150 | 151 | function isASCII(str: string) { 152 | // eslint-disable-next-line no-control-regex 153 | return /^[\x00-\x7F]*$/.test(str); 154 | } 155 | 156 | function encodeComponent(item: string, charset?: string) { 157 | item = String(item); 158 | if (isASCII(item)) { 159 | item = encodeURIComponent(item); 160 | } else { 161 | item = encode(item, charset); 162 | } 163 | return item; 164 | } 165 | 166 | function stringifyArray(values: (SupportEncodeValue | SupportEncodeObject)[], prefix: string, options: Options) { 167 | const items = []; 168 | for (const [ index, value ] of values.entries()) { 169 | items.push(stringify(value, `${prefix}[${index}]`, options)); 170 | } 171 | return items.join('&'); 172 | } 173 | 174 | function stringifyObject(obj: SupportEncodeObject, prefix: string, options: Options) { 175 | const items = []; 176 | const charset = options.charset; 177 | for (const key in obj) { 178 | if (key === '') { 179 | continue; 180 | } 181 | const value = obj[key]; 182 | if (value === null || value === undefined) { 183 | items.push(encode(key, charset) + '='); 184 | } else { 185 | const keyPrefix = prefix ? prefix + '[' + encodeComponent(key, charset) + ']' : encodeComponent(key, charset); 186 | items.push(stringify(value, keyPrefix, options)); 187 | } 188 | } 189 | return items.join('&'); 190 | } 191 | 192 | export function stringify(obj: object | SupportEncodeValue, prefix?: string): string; 193 | export function stringify(obj: object | SupportEncodeValue, options?: Options): string; 194 | export function stringify(obj: object | SupportEncodeValue, prefix?: string, options?: Options): string; 195 | export function stringify(obj: object | SupportEncodeValue, prefixOrOptions?: string | Options, options?: Options): string { 196 | let prefix: string | undefined; 197 | if (typeof prefixOrOptions !== 'string') { 198 | options = prefixOrOptions || {}; 199 | } else { 200 | prefix = prefixOrOptions; 201 | } 202 | options = options ?? {}; 203 | if (Array.isArray(obj)) { 204 | if (!prefix) { 205 | throw new TypeError('stringify expects an object'); 206 | } 207 | return stringifyArray(obj, prefix, options); 208 | } 209 | 210 | const objValue = String(obj); 211 | if (obj && typeof obj === 'object' && objValue === '[object Object]') { 212 | return stringifyObject(obj as SupportEncodeObject, prefix ?? '', options); 213 | } 214 | 215 | if (!prefix) { 216 | throw new TypeError('stringify expects an object'); 217 | } 218 | const charset = options?.charset ?? 'utf-8'; 219 | return `${prefix}=${encodeComponent(objValue, charset)}`; 220 | } 221 | -------------------------------------------------------------------------------- /test/urlencode.test.ts: -------------------------------------------------------------------------------- 1 | import { strict as assert } from 'node:assert'; 2 | import { encode, decode, stringify, parse } from '../src/index.js'; 3 | import urlencode from '../src/index.js'; 4 | import type { SupportEncodeObject } from '../src/index.js'; 5 | 6 | describe('urlencode.test.js', () => { 7 | describe('encode() and decode()', () => { 8 | const items = [ 9 | [ '苏千', null, encodeURIComponent('苏千') ], 10 | [ '苏千', undefined, encodeURIComponent('苏千') ], 11 | [ '苏千', '', encodeURIComponent('苏千') ], 12 | [ '苏千', 'utf8', encodeURIComponent('苏千') ], 13 | [ '苏千', 'utf-8', encodeURIComponent('苏千') ], 14 | [ 'nodeJS', 'gbk', '%6E%6F%64%65%4A%53' ], 15 | [ '苏千', 'gbk', '%CB%D5%C7%A7' ], 16 | [ '苏千,nodejs。!@#¥%……&**(&*)&)}{|~~!@+——?、》《。,“‘:;|、】【}{~·中文', 'gbk', 17 | '%CB%D5%C7%A7%A3%AC%6E%6F%64%65%6A%73%A1%A3%A3%A1%40%23%A3%A4%25%A1%AD%A1%AD%26%2A%2A%A3%A8%26%2A%A3%A9%26%A3%A9%7D%7B%7C%7E%7E%A3%A1%40%2B%A1%AA%A1%AA%A3%BF%A1%A2%A1%B7%A1%B6%A1%A3%A3%AC%A1%B0%A1%AE%A3%BA%A3%BB%7C%A1%A2%A1%BF%A1%BE%7D%7B%7E%A1%A4%D6%D0%CE%C4' ], 18 | [ '\\诚%http://github.com/aleafs?a=b&c[1]= &c2#', 'gbk', '%5C%B3%CF%25%68%74%74%70%3A%2F%2F%67%69%74%68%75%62%2E%63%6F%6D%2F%61%6C%65%61%66%73%3F%61%3D%62%26%63%5B%31%5D%3D%20%26%63%32%23' ], 19 | [ '\n\r\n', 'gbk', '%0A%0D%0A' ], 20 | ]; 21 | 22 | items.forEach(item => { 23 | const str = item[0] as string; 24 | const charset = item[1]; 25 | const expect = item[2] as string; 26 | it('should encode ' + str.substring(0, 20) + ' with ' + charset + ' to ' + expect.substring(0, 30), () => { 27 | assert.equal(encode(str, charset), expect); 28 | assert.equal(urlencode(str, charset), expect); 29 | }); 30 | }); 31 | 32 | const decodeItems = [ 33 | [ '%CB%D5%C7%A7a', 'gbk', '苏千a' ], 34 | [ '%CB%D5%C7%A7a%0A%C7%A7a%a', 'gbk', '苏千a\n千a\n' ], 35 | [ 36 | '%CB%D5%C7%A7%A3%ACnodejs%A1%A3%A3%A1%40%23%A3%A4%25%A1%AD%A1%AD%26**%A3%A8%26*%A3%A9%26%A3%A9%7D%7B%7C%7E%7E%A3%A1%40%2B%A1%AA%A1%AA%A3%BF%A1%A2%A1%B7%A1%B6%A1%A3%A3%AC%A1%B0%A1%AE%A3%BA%A3%BB%7C%A1%A2%A1%BF%A1%BE%7D%7B%7E%A1%A4%D6%D0%CE%C4', 37 | 'gbk', 38 | '苏千,nodejs。!@#¥%……&**(&*)&)}{|~~!@+——?、》《。,“‘:;|、】【}{~·中文', 39 | ], 40 | ]; 41 | 42 | decodeItems.forEach(item => { 43 | const str = item[0]; 44 | const charset = item[1]; 45 | const expect = item[2]; 46 | it('should decode ' + str.substring(0, 20) + ' with ' + charset + ' to ' + expect.substring(0, 30), () => { 47 | assert.equal(decode(str, charset), expect); 48 | }); 49 | }); 50 | 51 | items.forEach(item => { 52 | const str = item[2] as string; 53 | const charset = item[1]; 54 | const expect = item[0] as string; 55 | it('should decode ' + str.substring(0, 20) + ' with ' + charset + ' to ' + expect.substring(0, 30), () => { 56 | assert.equal(decode(str, charset), expect); 57 | }); 58 | }); 59 | }); 60 | 61 | describe('parse()', () => { 62 | it('should work with gbk encoding', () => { 63 | const qs = 'umidtoken=Tc230acc03a564530aee31d22701e9b95&usertag4=0&usertag3=512&usertag2=0&status=0&userid=665377421&out_user=suqian.yf%40taobao.com&promotedtype=0&account_no=20885028063394350156&loginstatus=true&usertag=0&nick=%CB%D5%C7%A7&tairlastupdatetime=1319008872&strid=a68f6ee38f44d2b89ca508444c1ccaf9'; 64 | const obj = parse(qs, { charset: 'gbk' }); 65 | assert.deepEqual(obj, { 66 | umidtoken: 'Tc230acc03a564530aee31d22701e9b95', 67 | usertag4: '0', 68 | usertag3: '512', 69 | usertag2: '0', 70 | status: '0', 71 | userid: '665377421', 72 | out_user: 'suqian.yf@taobao.com', 73 | promotedtype: '0', 74 | account_no: '20885028063394350156', 75 | loginstatus: 'true', 76 | usertag: '0', 77 | nick: '苏千', 78 | tairlastupdatetime: '1319008872', 79 | strid: 'a68f6ee38f44d2b89ca508444c1ccaf9', 80 | }); 81 | }); 82 | 83 | // TODO 84 | // var qs = 'x[y][0][v][w]=%CE%ED%BF%D5'; 85 | // var obj = {'x' : {'y' : [{'v' : {'w' : '雾空'}}]}}; 86 | // urlencode.parse(qs, {charset: 'gbk'}) 87 | // .should.eql(obj); 88 | }); 89 | 90 | describe('stringify()', () => { 91 | it('should work with gbk encoding', () => { 92 | let obj: SupportEncodeObject = { xm: '苏千', xb: 1, xh: 1111 }; 93 | assert.equal(stringify(obj, { charset: 'gbk' }), 'xm=%CB%D5%C7%A7&xb=1&xh=1111'); 94 | 95 | 96 | // `qs` and `obj` is copy from `describe('parse()', ->)` 97 | const qs = 'umidtoken=Tc230acc03a564530aee31d22701e9b95&usertag4=0&usertag3=512&usertag2=0&status=0&userid=665377421&out_user=suqian.yf%40taobao.com&promotedtype=0&account_no=20885028063394350156&loginstatus=true&usertag=0&nick=%CB%D5%C7%A7&tairlastupdatetime=1319008872&strid=a68f6ee38f44d2b89ca508444c1ccaf9'; 98 | obj = { 99 | umidtoken: 'Tc230acc03a564530aee31d22701e9b95', 100 | usertag4: '0', 101 | usertag3: '512', 102 | usertag2: '0', 103 | status: '0', 104 | userid: '665377421', 105 | out_user: 'suqian.yf@taobao.com', 106 | promotedtype: '0', 107 | account_no: '20885028063394350156', 108 | loginstatus: 'true', 109 | usertag: '0', 110 | nick: '苏千', 111 | tairlastupdatetime: '1319008872', 112 | strid: 'a68f6ee38f44d2b89ca508444c1ccaf9', 113 | }; 114 | assert.equal(stringify(obj, { charset: 'gbk' }), qs); 115 | 116 | 117 | // str: x[y][0][v][w]=%CE%ED%BF%D5 118 | let str = 'x[y][0][v][w]=' + encode('雾空', 'gbk'); 119 | obj = { x: { y: [{ v: { w: '雾空' } }] } }; 120 | assert.equal(stringify(obj, { charset: 'gbk' }), str); 121 | 122 | 123 | // str : xh=23123&%CE%ED%BF%D5=%CE%ED%BF%D5 124 | // 这里是 chrome 在 gbk 编码网页的行为 125 | str = 'xh=13241234' + 126 | '&xb=1' + 127 | '&' + encode('雾空', 'gbk') + '=' + encode('雾空', 'gbk'); 128 | obj = { xh: 13241234, xb: 1, 雾空: '雾空' }; 129 | assert.equal(stringify(obj, { charset: 'gbk' }), str); 130 | }); 131 | 132 | it('should work with utf-8 encoding', () => { 133 | let obj: SupportEncodeObject = { h: 1, j: 2, k: '3' }; 134 | 135 | assert.equal(stringify(obj, { charset: 'utf-8' }), 'h=1&j=2&k=3'); 136 | 137 | assert.equal(stringify(obj), 'h=1&j=2&k=3'); 138 | 139 | let str = 'x[y][0][v][w]=1'; 140 | obj = { x: { y: [{ v: { w: '1' } }] } }; 141 | assert.equal(stringify(obj), str); 142 | 143 | str = 'x[y][0][v][w]=' + encodeURIComponent('雾空'); 144 | obj = { x: { y: [{ v: { w: '雾空' } }] } }; 145 | assert.equal(stringify(obj), str); 146 | 147 | str = 'x[y][0][v][w]=' + encodeURIComponent('雾空'); 148 | obj = { x: { y: [{ v: { w: '雾空' } }] } }; 149 | assert.equal(stringify(obj, { charset: 'utf-8' }), str); 150 | }); 151 | 152 | it('should work with big5 encoding', () => { 153 | const str = 'x[y][0][v][w]=' + encode('雾空', 'big5'); 154 | const obj = { x: { y: [{ v: { w: '雾空' } }] } }; 155 | assert.equal(stringify(obj, { charset: 'big5' }), str); 156 | }); 157 | 158 | it('should support nest obj and array', () => { 159 | const encoding = 'gbk'; 160 | const obj: SupportEncodeObject = { 161 | edp: { 162 | name: [ '阿里', '巴巴', '数据产品' ], 163 | hello: 100, 164 | nihao: '100', 165 | }, 166 | good: '好', 167 | }; 168 | // qs : edp[name][0]=%B0%A2%C0%EF 169 | // &edp[name][1]=%B0%CD%B0%CD 170 | // &edp[name][2]=%CA%FD%BE%DD%B2%FA%C6%B7 171 | // &edp[hello]=100 172 | // &edp[nihao]=100 173 | // &good=%BA%C3 174 | const qs = 'edp[name][0]=' + encode('阿里', encoding) + 175 | '&edp[name][1]=' + encode('巴巴', encoding) + 176 | '&edp[name][2]=' + encode('数据产品', encoding) + 177 | '&edp[hello]=100' + 178 | '&edp[nihao]=100' + 179 | '&good=' + encode('好', encoding); 180 | assert.equal(stringify(obj, { charset: 'gbk' }), qs); 181 | }); 182 | }); 183 | 184 | }); 185 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@eggjs/tsconfig", 3 | "compilerOptions": { 4 | "strict": true, 5 | "noImplicitAny": true, 6 | "target": "ES2022", 7 | "module": "NodeNext", 8 | "moduleResolution": "NodeNext" 9 | } 10 | } 11 | --------------------------------------------------------------------------------