diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..9bcdb46 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,6 @@ +{ + "extends": [ + "eslint-config-egg/typescript", + "eslint-config-egg/lib/rules/enforce-node-prefix" + ] +} diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml new file mode 100644 index 0000000..0374854 --- /dev/null +++ b/.github/workflows/nodejs.yml @@ -0,0 +1,16 @@ +name: CI + +on: + push: + branches: [ master ] + + pull_request: + branches: [ master ] + +jobs: + Job: + name: Node.js + uses: node-modules/github-actions/.github/workflows/node-test.yml@master + with: + os: 'ubuntu-latest' + version: '16, 18, 20, 21' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1c6cbb1 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,13 @@ +name: Release + +on: + push: + branches: [ master ] + +jobs: + release: + name: Node.js + uses: node-modules/github-actions/.github/workflows/node-release.yml@master + secrets: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + GIT_TOKEN: ${{ secrets.GIT_TOKEN }} diff --git a/.gitignore b/.gitignore index f124bd2..fa7b903 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ results node_modules npm-debug.log +.tshy* +dist diff --git a/.jshintignore b/.jshintignore deleted file mode 100644 index 63c6333..0000000 --- a/.jshintignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules/ -coverage/ -.tmp/ -.git/ diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index da51ea3..0000000 --- a/.jshintrc +++ /dev/null @@ -1,95 +0,0 @@ -{ - // JSHint Default Configuration File (as on JSHint website) - // See http://jshint.com/docs/ for more details - - "maxerr" : 50, // {int} Maximum error before stopping - - // Enforcing - "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.) - "camelcase" : false, // true: Identifiers must be in camelCase - "curly" : true, // true: Require {} for every new block or scope - "eqeqeq" : true, // true: Require triple equals (===) for comparison - "forin" : false, // true: Require filtering for..in loops with obj.hasOwnProperty() - "immed" : false, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` - "indent" : false, // {int} Number of spaces to use for indentation - "latedef" : false, // true: Require variables/functions to be defined before being used - "newcap" : false, // true: Require capitalization of all constructor functions e.g. `new F()` - "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee` - "noempty" : true, // true: Prohibit use of empty blocks - "nonew" : false, // true: Prohibit use of constructors for side-effects (without assignment) - "plusplus" : false, // true: Prohibit use of `++` & `--` - "quotmark" : false, // Quotation mark consistency: - // false : do nothing (default) - // true : ensure whatever is used is consistent - // "single" : require single quotes - // "double" : require double quotes - "undef" : true, // true: Require all non-global variables to be declared (prevents global leaks) - "unused" : false, // true: Require all defined variables be used - "strict" : true, // true: Requires all functions run in ES5 Strict Mode - "trailing" : false, // true: Prohibit trailing whitespaces - "maxparams" : false, // {int} Max number of formal params allowed per function - "maxdepth" : false, // {int} Max depth of nested blocks (within functions) - "maxstatements" : false, // {int} Max number statements per function - "maxcomplexity" : false, // {int} Max cyclomatic complexity per function - "maxlen" : false, // {int} Max number of characters per line - - // Relaxing - "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons) - "boss" : true, // true: Tolerate assignments where comparisons would be expected - "debug" : false, // true: Allow debugger statements e.g. browser breakpoints. - "eqnull" : false, // true: Tolerate use of `== null` - "es5" : false, // true: Allow ES5 syntax (ex: getters and setters) - "esnext" : true, // true: Allow ES.next (ES6) syntax (ex: `const`) - "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features) - // (ex: `for each`, multiple try/catch, function expression…) - "evil" : false, // true: Tolerate use of `eval` and `new Function()` - "expr" : true, // true: Tolerate `ExpressionStatement` as Programs - "funcscope" : false, // true: Tolerate defining variables inside control statements" - "globalstrict" : false, // true: Allow global "use strict" (also enables 'strict') - "iterator" : false, // true: Tolerate using the `__iterator__` property - "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block - "laxbreak" : true, // true: Tolerate possibly unsafe line breakings - "laxcomma" : false, // true: Tolerate comma-first style coding - "loopfunc" : false, // true: Tolerate functions being defined in loops - "multistr" : true, // true: Tolerate multi-line strings - "proto" : false, // true: Tolerate using the `__proto__` property - "scripturl" : false, // true: Tolerate script-targeted URLs - "smarttabs" : false, // true: Tolerate mixed tabs/spaces when used for alignment - "shadow" : true, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` - "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation - "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` - "validthis" : true, // true: Tolerate using this in a non-constructor function - - // Environments - "browser" : true, // Web Browser (window, document, etc) - "couch" : false, // CouchDB - "devel" : true, // Development/debugging (alert, confirm, etc) - "dojo" : false, // Dojo Toolkit - "jquery" : false, // jQuery - "mootools" : false, // MooTools - "node" : true, // Node.js - "nonstandard" : false, // Widely adopted globals (escape, unescape, etc) - "prototypejs" : false, // Prototype and Scriptaculous - "rhino" : false, // Rhino - "worker" : false, // Web Workers - "wsh" : false, // Windows Scripting Host - "yui" : false, // Yahoo User Interface - "noyield" : true, // allow generators without a yield - - // Legacy - "nomen" : false, // true: Prohibit dangling `_` in variables - "onevar" : false, // true: Allow only one `var` statement per function - "passfail" : false, // true: Stop on first error - "white" : false, // true: Check against strict whitespace and indentation rules - - // Custom Globals - "globals" : { // additional predefined global variables - // mocha - "describe": true, - "it": true, - "before": true, - "afterEach": true, - "beforeEach": true, - "after": true - } -} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index fbe1a93..0000000 --- a/.travis.yml +++ /dev/null @@ -1,10 +0,0 @@ -sudo: false -language: node_js -node_js: - - '3' - - '2' - - '1' - - '0.12' - - '0.10' -script: "npm run test-travis" -after_script: "npm install coveralls@2 && cat ./coverage/lcov.info | coveralls" diff --git a/AUTHORS b/AUTHORS deleted file mode 100644 index d5f2def..0000000 --- a/AUTHORS +++ /dev/null @@ -1,10 +0,0 @@ -# Ordered by date of first contribution. -# Auto-generated by 'contributors' on Fri, 25 Apr 2014 13:09:46 GMT. -# https://github.com/xingrz/node-contributors - -fengmk2 (https://github.com/fengmk2) -aleafs (https://github.com/aleafs) -azbykov (https://github.com/azbykov) -alsotang (https://github.com/alsotang) -twang (https://github.com/twang3) - diff --git a/History.md b/CHANGELOG.md similarity index 100% rename from History.md rename to CHANGELOG.md diff --git a/README.md b/README.md index e82a16b..880b4f2 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,175 @@ -urlencode [![Build Status](https://secure.travis-ci.org/node-modules/urlencode.png)](http://travis-ci.org/node-modules/urlencode) [![Coverage Status](https://coveralls.io/repos/node-modules/urlencode/badge.png)](https://coveralls.io/r/node-modules/urlencode) -======= - -[![NPM](https://nodei.co/npm/urlencode.png?downloads=true&stars=true)](https://nodei.co/npm/urlencode/) +# urlencode + +[![NPM version][npm-image]][npm-url] +[![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) +[![Test coverage][codecov-image]][codecov-url] +[![Known Vulnerabilities][snyk-image]][snyk-url] +[![npm download][download-image]][download-url] + +[npm-image]: https://img.shields.io/npm/v/urlencode.svg?style=flat-square +[npm-url]: https://npmjs.org/package/urlencode +[codecov-image]: https://codecov.io/gh/node-modules/urlencode/branch/master/graph/badge.svg +[codecov-url]: https://codecov.io/gh/node-modules/urlencode +[snyk-image]: https://snyk.io/test/npm/urlencode/badge.svg?style=flat-square +[snyk-url]: https://snyk.io/test/npm/urlencode +[download-image]: https://img.shields.io/npm/dm/urlencode.svg?style=flat-square +[download-url]: https://npmjs.org/package/urlencode encodeURIComponent with charset, e.g.: `gbk` ## Install ```bash -$ npm install urlencode +npm install urlencode ``` ## Usage -```js -var urlencode = require('urlencode'); +```ts +import { encode, decode, parse, stringify } from 'urlencode'; -console.log(urlencode('苏千')); // default is utf8 -console.log(urlencode('苏千', 'gbk')); // '%CB%D5%C7%A7' +console.log(encode('苏千')); // default is utf8 +console.log(encode('苏千', 'gbk')); // '%CB%D5%C7%A7' // decode gbk -urlencode.decode('%CB%D5%C7%A7', 'gbk'); // '苏千' +decode('%CB%D5%C7%A7', 'gbk'); // '苏千' // parse gbk querystring -urlencode.parse('nick=%CB%D5%C7%A7', {charset: 'gbk'}); // {nick: '苏千'} +parse('nick=%CB%D5%C7%A7', { charset: 'gbk' }); // {nick: '苏千'} // stringify obj with gbk encoding -var str = 'x[y][0][v][w]=' + urlencode('雾空', 'gbk'); // x[y][0][v][w]=%CE%ED%BF%D5 +var str = 'x[y][0][v][w]=' + encode('雾空', 'gbk'); // x[y][0][v][w]=%CE%ED%BF%D5 var obj = {'x' : {'y' : [{'v' : {'w' : '雾空'}}]}}; -urlencode.stringify(obj, {charset: 'gbk'}).should.equal(str); - +assert.equal(urlencode.stringify(obj, { charset: 'gbk' }, str); ``` ## Benchmark -### urlencode(str, encoding) +### encode(str, encoding) ```bash -$ node benchmark/urlencode.js +$ node benchmark/urlencode.cjs -node version: v0.10.26 -urlencode(str) x 11,980 ops/sec ±1.13% (100 runs sampled) -urlencode(str, "gbk") x 8,575 ops/sec ±1.58% (94 runs sampled) -encodeURIComponent(str) x 11,677 ops/sec ±2.32% (93 runs sampled) -Fastest is urlencode(str) -``` +node version: v21.1.0 +"苏千测试\n, 哈哈, haha" -### urlencode.decode(str, encoding) + urlencode Benchmark + node version: v21.1.0, date: Sat Oct 28 2023 21:01:00 GMT+0800 (中国标准时间) + Starting... + 4 tests completed. + + urlencode(str) x 4,617,242 ops/sec ±2.60% (95 runs sampled) + urlencode(str, "gbk") x 1,122,430 ops/sec ±2.20% (95 runs sampled) + encodeURIComponent(str) x 4,608,523 ops/sec ±2.94% (93 runs sampled) + encodeUTF8(str) x 833,170 ops/sec ±1.37% (96 runs sampled) + +node version: v20.9.0 +"苏千测试\n, 哈哈, haha" + + urlencode Benchmark + node version: v20.9.0, date: Sat Oct 28 2023 21:01:37 GMT+0800 (中国标准时间) + Starting... + 4 tests completed. + + urlencode(str) x 4,304,468 ops/sec ±2.83% (89 runs sampled) + urlencode(str, "gbk") x 1,005,759 ops/sec ±2.10% (90 runs sampled) + encodeURIComponent(str) x 4,289,880 ops/sec ±2.99% (92 runs sampled) + encodeUTF8(str) x 827,841 ops/sec ±1.06% (96 runs sampled) + +node version: v18.18.0 +"苏千测试\n, 哈哈, haha" + + urlencode Benchmark + node version: v18.18.0, date: Sat Oct 28 2023 19:34:06 GMT+0800 (中国标准时间) + Starting... + 4 tests completed. + + urlencode(str) x 4,597,865 ops/sec ±0.22% (96 runs sampled) + urlencode(str, "gbk") x 633,620 ops/sec ±15.31% (71 runs sampled) + encodeURIComponent(str) x 3,902,229 ops/sec ±2.49% (87 runs sampled) + encodeUTF8(str) x 510,456 ops/sec ±26.76% (88 runs sampled) + +node version: v16.20.2 +"苏千测试\n, 哈哈, haha" + + urlencode Benchmark + node version: v16.20.2, date: Sat Oct 28 2023 21:02:11 GMT+0800 (中国标准时间) + Starting... + 4 tests completed. + + urlencode(str) x 4,438,372 ops/sec ±1.80% (93 runs sampled) + urlencode(str, "gbk") x 1,175,761 ops/sec ±0.68% (95 runs sampled) + encodeURIComponent(str) x 4,374,525 ops/sec ±1.96% (97 runs sampled) + encodeUTF8(str) x 751,616 ops/sec ±2.49% (86 runs sampled) -```bash -$ node benchmark/urlencode.decode.js - -node version: v0.10.26 -urlencode.decode(str) x 26,027 ops/sec ±7.51% (73 runs sampled) -urlencode.decode(str, "gbk") x 14,409 ops/sec ±1.72% (98 runs sampled) -decodeURIComponent(str) x 36,052 ops/sec ±0.90% (96 runs sampled) -urlencode.parse(qs, {charset: "gbk"}) x 16,401 ops/sec ±1.09% (98 runs sampled) -urlencode.parse(qs, {charset: "utf8"}) x 23,381 ops/sec ±2.22% (93 runs sampled) -Fastest is decodeURIComponent(str) ``` -## TODO +### decode(str, encoding) + +```bash +$ node benchmark/urlencode.decode.cjs + +node version: v21.1.0, date: "2023-10-28T12:51:20.191Z" + + urlencode.decode Benchmark + node version: v21.1.0, date: Sat Oct 28 2023 20:51:20 GMT+0800 (中国标准时间) + Starting... + 7 tests completed. + + urlencode.decode(str) x 515,410 ops/sec ±1.95% (91 runs sampled) + urlencode.decode(str, "gbk") x 54,018 ops/sec ±3.17% (78 runs sampled) + decodeURIComponent(str) x 313,204 ops/sec ±2.93% (78 runs sampled) + urlencode.parse(qs, {charset: "gbk"}) x 311,613 ops/sec ±1.26% (95 runs sampled) + urlencode.stringify(data, {charset: "gbk"}) x 316,558 ops/sec ±1.55% (93 runs sampled) + urlencode.parse(qs, {charset: "utf8"}) x 490,744 ops/sec ±1.25% (94 runs sampled) + urlencode.stringify(data, {charset: "utf8"}) x 357,206 ops/sec ±0.46% (97 runs sampled) + +node version: v20.9.0, date: "2023-10-28T12:49:57.236Z" + + urlencode.decode Benchmark + node version: v20.9.0, date: Sat Oct 28 2023 20:49:57 GMT+0800 (中国标准时间) + Starting... + 7 tests completed. + + urlencode.decode(str) x 573,899 ops/sec ±0.62% (95 runs sampled) + urlencode.decode(str, "gbk") x 83,184 ops/sec ±0.13% (100 runs sampled) + decodeURIComponent(str) x 573,371 ops/sec ±1.67% (93 runs sampled) + urlencode.parse(qs, {charset: "gbk"}) x 303,202 ops/sec ±0.70% (100 runs sampled) + urlencode.stringify(data, {charset: "gbk"}) x 319,546 ops/sec ±0.29% (99 runs sampled) + urlencode.parse(qs, {charset: "utf8"}) x 462,578 ops/sec ±0.25% (98 runs sampled) + urlencode.stringify(data, {charset: "utf8"}) x 343,487 ops/sec ±0.17% (100 runs sampled) + +node version: v18.18.0, date: "2023-10-28T12:44:56.355Z" + + urlencode.decode Benchmark + node version: v18.18.0, date: Sat Oct 28 2023 20:44:56 GMT+0800 (中国标准时间) + Starting... + 7 tests completed. + + urlencode.decode(str) x 550,451 ops/sec ±1.74% (98 runs sampled) + urlencode.decode(str, "gbk") x 67,311 ops/sec ±1.16% (96 runs sampled) + decodeURIComponent(str) x 569,461 ops/sec ±0.30% (93 runs sampled) + urlencode.parse(qs, {charset: "gbk"}) x 293,407 ops/sec ±0.90% (97 runs sampled) + urlencode.stringify(data, {charset: "gbk"}) x 234,162 ops/sec ±4.55% (75 runs sampled) + urlencode.parse(qs, {charset: "utf8"}) x 316,697 ops/sec ±4.37% (78 runs sampled) + urlencode.stringify(data, {charset: "utf8"}) x 192,787 ops/sec ±4.58% (80 runs sampled) + +node version: v16.20.2, date: "2023-10-28T12:47:38.431Z" + + urlencode.decode Benchmark + node version: v16.20.2, date: Sat Oct 28 2023 20:47:38 GMT+0800 (中国标准时间) + Starting... + 7 tests completed. + + urlencode.decode(str) x 537,995 ops/sec ±2.07% (96 runs sampled) + urlencode.decode(str, "gbk") x 78,073 ops/sec ±0.17% (99 runs sampled) + decodeURIComponent(str) x 558,509 ops/sec ±0.48% (96 runs sampled) + urlencode.parse(qs, {charset: "gbk"}) x 252,590 ops/sec ±2.87% (90 runs sampled) + urlencode.stringify(data, {charset: "gbk"}) x 287,978 ops/sec ±2.47% (92 runs sampled) + urlencode.parse(qs, {charset: "utf8"}) x 416,600 ops/sec ±0.72% (93 runs sampled) + urlencode.stringify(data, {charset: "utf8"}) x 281,319 ops/sec ±2.43% (85 runs sampled) -* [x] stringify() +``` ## License diff --git a/benchmark/urlencode.js b/benchmark/urlencode.cjs similarity index 60% rename from benchmark/urlencode.js rename to benchmark/urlencode.cjs index 7cb7710..5a1bd09 100644 --- a/benchmark/urlencode.js +++ b/benchmark/urlencode.cjs @@ -1,26 +1,14 @@ -/*! - * urlencode - benchmark/urlencode.js - * Copyright(c) 2012 - 2014 fengmk2 - * MIT Licensed - */ - -"use strict"; - -/** - * Module dependencies. - */ - -var Benchmark = require('benchmark'); -var benchmarks = require('beautify-benchmark'); -var urlencode = require('../'); +const Benchmark = require('benchmark'); +const benchmarks = require('beautify-benchmark'); +const { encode } = require('../'); console.log('node version: %s', process.version); function encodeUTF8(str) { - var encodeStr = ''; - var buf = new Buffer(str); - var ch = ''; - for (var i = 0; i < buf.length; i++) { + let encodeStr = ''; + const buf = Buffer.from(str); + let ch = ''; + for (let i = 0; i < buf.length; i++) { ch = buf[i].toString('16'); if (ch.length === 1) { ch = '0' + ch; @@ -32,20 +20,20 @@ function encodeUTF8(str) { console.log('%j', decodeURIComponent(encodeUTF8('苏千测试\n, 哈哈, haha'))); -var suite = new Benchmark.Suite(); +const suite = new Benchmark.Suite(); suite .add('urlencode(str)', function () { // urlencode('苏千'); - urlencode('苏千写的\nurlencode,应该有用'); + encode('苏千写的\nurlencode,应该有用'); // urlencode('suqian want to sleep early tonight.'); // urlencode('你让同一个项目中写两份一样代码的人情何以堪呢,你让同一个项目中写两份一样代码的人情何以堪呢,你让同一个项目中写两份一样代码的人情何以堪呢,你让同一个项目中写两份一样代码的人情何以堪呢'); }) .add('urlencode(str, "gbk")', function () { // urlencode('苏千', 'gbk'); - urlencode('苏千写的\nurlencode,应该有用', 'gbk'); + encode('苏千写的\nurlencode,应该有用', 'gbk'); // urlencode('suqian want to sleep early tonight.', 'gbk'); // urlencode('你让同一个项目中写两份一样代码的人情何以堪呢,你让同一个项目中写两份一样代码的人情何以堪呢,你让同一个项目中写两份一样代码的人情何以堪呢,你让同一个项目中写两份一样代码的人情何以堪呢', 'gbk'); }) @@ -75,32 +63,3 @@ suite benchmarks.log(); }) .run({ 'async': false }); - -// $ node benchmark/urlencode.js -// -// node version: v0.11.12 -// -// urlencode Benchmark -// node version: v0.11.12, date: Sat Aug 23 2014 23:15:27 GMT+0800 (CST) -// Starting... -// 4 tests completed. -// -// urlencode(str) x 427,364 ops/sec ±3.46% (93 runs sampled) -// urlencode(str, "gbk") x 80,073 ops/sec ±1.92% (91 runs sampled) -// encodeURIComponent(str) x 453,495 ops/sec ±0.78% (97 runs sampled) -// encodeUTF8(str) x 117,744 ops/sec ±0.83% (100 runs sampled) - -// $ node benchmark/urlencode.js -// -// node version: v0.11.14 -// 苏千测试, 哈哈, haha -// -// urlencode Benchmark -// node version: v0.11.14, date: Fri Sep 26 2014 10:50:01 GMT+0800 (CST) -// Starting... -// 4 tests completed. -// -// urlencode(str) x 378,559 ops/sec ±4.10% (85 runs sampled) -// urlencode(str, "gbk") x 104,806 ops/sec ±3.64% (85 runs sampled) -// encodeURIComponent(str) x 328,671 ops/sec ±5.99% (76 runs sampled) -// encodeUTF8(str) x 75,077 ops/sec ±6.34% (77 runs sampled) diff --git a/benchmark/urlencode.decode.cjs b/benchmark/urlencode.decode.cjs new file mode 100644 index 0000000..3eab0e7 --- /dev/null +++ b/benchmark/urlencode.decode.cjs @@ -0,0 +1,80 @@ +const Benchmark = require('benchmark'); +const benchmarks = require('beautify-benchmark'); +const { parse, encode, decode, stringify } = require('../'); + +console.log('node version: %s, date: %j', process.version, new Date()); + +const suite = new Benchmark.Suite(); + +const utf8DecodeItems = [ + encode('苏千'), + encode('苏千写的urlencode,应该有用'), + encode('suqian want to sleep early tonight.'), + encode('你让同一个项目中写两份一样代码的人情何以堪呢,你让同一个项目中写两份一样代码的人情何以堪呢,你让同一个项目中写两份一样代码的人情何以堪呢,你让同一个项目中写两份一样代码的人情何以堪呢'), +]; + +const gbkDecodeItems = [ + encode('苏千', 'gbk'), + encode('苏千写的urlencode,应该有用', 'gbk'), + encode('suqian want to sleep early tonight.', 'gbk'), + encode('你让同一个项目中写两份一样代码的人情何以堪呢,你让同一个项目中写两份一样代码的人情何以堪呢,你让同一个项目中写两份一样代码的人情何以堪呢,你让同一个项目中写两份一样代码的人情何以堪呢', 'gbk'), +]; + +// console.log(decode(gbkDecodeItems[3], 'gbk')) + +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'; +const data = parse(gbkEncodeString, {charset: 'gbk'}); + +// console.log(stringify(data, {charset: 'gbk'}) === gbkEncodeString); + +suite + +.add('urlencode.decode(str)', function () { + decode(utf8DecodeItems[0]); + decode(utf8DecodeItems[1]); + decode(utf8DecodeItems[2]); + decode(utf8DecodeItems[3]); +}) + +.add('urlencode.decode(str, "gbk")', function () { + decode(gbkDecodeItems[0], 'gbk'); + decode(gbkDecodeItems[1], 'gbk'); + decode(gbkDecodeItems[2], 'gbk'); + decode(gbkDecodeItems[3], 'gbk'); +}) + +.add('decodeURIComponent(str)', function () { + decodeURIComponent(utf8DecodeItems[0]); + decodeURIComponent(utf8DecodeItems[1]); + decodeURIComponent(utf8DecodeItems[2]); + decodeURIComponent(utf8DecodeItems[3]); +}) + +.add('urlencode.parse(qs, {charset: "gbk"})', function () { + parse(gbkEncodeString, {charset: 'gbk'}); +}) + +.add('urlencode.stringify(data, {charset: "gbk"})', function () { + stringify(data, {charset: 'gbk'}); +}) + +.add('urlencode.parse(qs, {charset: "utf8"})', function () { + 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', + {charset: 'utf8'}); +}) + +.add('urlencode.stringify(data, {charset: "utf8"})', function () { + stringify(data, {charset: 'utf8'}); +}) + +.on('cycle', function(event) { + benchmarks.add(event.target); +}) +.on('start', function(event) { + console.log('\n urlencode.decode Benchmark\n node version: %s, date: %s\n Starting...', + process.version, Date()); +}) +.on('complete', function done() { + benchmarks.log(); +}) +.run({ 'async': false }); diff --git a/benchmark/urlencode.decode.js b/benchmark/urlencode.decode.js deleted file mode 100644 index d318710..0000000 --- a/benchmark/urlencode.decode.js +++ /dev/null @@ -1,91 +0,0 @@ -/**! - * urlencode - benchmark/urlencode.decode.js - * - * MIT Licensed - * - * Authors: - * fengmk2 (http://fengmk2.com) - */ - -"use strict"; - -/** - * Module dependencies. - */ - -var Benchmark = require('benchmark'); -var urlencode = require('../'); - -console.log('node version: %s, date: %j', process.version, new Date()); - -var suite = new Benchmark.Suite(); - -var utf8DecodeItems = [ - urlencode('苏千'), - urlencode('苏千写的urlencode,应该有用'), - urlencode('suqian want to sleep early tonight.'), - urlencode('你让同一个项目中写两份一样代码的人情何以堪呢,你让同一个项目中写两份一样代码的人情何以堪呢,你让同一个项目中写两份一样代码的人情何以堪呢,你让同一个项目中写两份一样代码的人情何以堪呢'), -]; - -var gbkDecodeItems = [ - urlencode('苏千', 'gbk'), - urlencode('苏千写的urlencode,应该有用', 'gbk'), - urlencode('suqian want to sleep early tonight.', 'gbk'), - urlencode('你让同一个项目中写两份一样代码的人情何以堪呢,你让同一个项目中写两份一样代码的人情何以堪呢,你让同一个项目中写两份一样代码的人情何以堪呢,你让同一个项目中写两份一样代码的人情何以堪呢', 'gbk'), -]; - -// console.log(urlencode.decode(gbkDecodeItems[3], 'gbk')) - -var 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'; -var data = urlencode.parse(gbkEncodeString, {charset: 'gbk'}); - -// console.log(urlencode.stringify(data, {charset: 'gbk'}) === gbkEncodeString); - -suite - -.add('urlencode.decode(str)', function () { - urlencode.decode(utf8DecodeItems[0]); - urlencode.decode(utf8DecodeItems[1]); - urlencode.decode(utf8DecodeItems[2]); - urlencode.decode(utf8DecodeItems[3]); -}) - -.add('urlencode.decode(str, "gbk")', function () { - urlencode.decode(gbkDecodeItems[0], 'gbk'); - urlencode.decode(gbkDecodeItems[1], 'gbk'); - urlencode.decode(gbkDecodeItems[2], 'gbk'); - urlencode.decode(gbkDecodeItems[3], 'gbk'); -}) - -.add('decodeURIComponent(str)', function () { - decodeURIComponent(utf8DecodeItems[0]); - decodeURIComponent(utf8DecodeItems[1]); - decodeURIComponent(utf8DecodeItems[2]); - decodeURIComponent(utf8DecodeItems[3]); -}) - -.add('urlencode.parse(qs, {charset: "gbk"})', function () { - urlencode.parse(gbkEncodeString, {charset: 'gbk'}); -}) - -.add('urlencode.stringify(data, {charset: "gbk"})', function () { - urlencode.stringify(data, {charset: 'gbk'}); -}) - -.add('urlencode.parse(qs, {charset: "utf8"})', function () { - urlencode.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', - {charset: 'utf8'}); -}) - -.add('urlencode.stringify(data, {charset: "utf8"})', function () { - urlencode.stringify(data, {charset: 'utf8'}); -}) - -// add listeners -.on('cycle', function (event) { - console.log(String(event.target)); -}) -.on('complete', function () { - console.log('Fastest is ' + this.filter('fastest').pluck('name')); -}) -.run(); diff --git a/lib/urlencode.js b/lib/urlencode.js deleted file mode 100644 index 79ab7f5..0000000 --- a/lib/urlencode.js +++ /dev/null @@ -1,227 +0,0 @@ -/**! - * urlencode - lib/urlencode.js - * - * MIT Licensed - * - * Authors: - * fengmk2 (http://fengmk2.com) - */ - -"use strict"; - -/** - * Module dependencies. - */ - -var iconv = require('iconv-lite'); - -function isUTF8(charset) { - if (!charset) { - return true; - } - charset = charset.toLowerCase(); - return charset === 'utf8' || charset === 'utf-8'; -} - -function encode(str, charset) { - if (isUTF8(charset)) { - return encodeURIComponent(str); - } - - var buf = iconv.encode(str, charset); - var encodeStr = ''; - var ch = ''; - for (var i = 0; i < buf.length; i++) { - ch = buf[i].toString('16'); - if (ch.length === 1) { - ch = '0' + ch; - } - encodeStr += '%' + ch; - } - encodeStr = encodeStr.toUpperCase(); - return encodeStr; -} - -function decode(str, charset) { - if (isUTF8(charset)) { - return decodeURIComponent(str); - } - - var bytes = []; - for (var i = 0; i < str.length; ) { - if (str[i] === '%') { - i++; - bytes.push(parseInt(str.substring(i, i + 2), 16)); - i += 2; - } else { - bytes.push(str.charCodeAt(i)); - i++; - } - } - var buf = new Buffer(bytes); - return iconv.decode(buf, charset); -} - -function parse(qs, sep, eq, options) { - if (typeof sep === 'object') { - // parse(qs, options) - options = sep; - sep = null; - } - - sep = sep || '&'; - eq = eq || '='; - var obj = {}; - - if (typeof qs !== 'string' || qs.length === 0) { - return obj; - } - - var regexp = /\+/g; - qs = qs.split(sep); - - var maxKeys = 1000; - var charset = null; - if (options) { - if (typeof options.maxKeys === 'number') { - maxKeys = options.maxKeys; - } - if (typeof options.charset === 'string') { - charset = options.charset; - } - } - - var len = qs.length; - // maxKeys <= 0 means that we should not limit keys count - if (maxKeys > 0 && len > maxKeys) { - len = maxKeys; - } - - for (var i = 0; i < len; ++i) { - var x = qs[i].replace(regexp, '%20'); - var idx = x.indexOf(eq); - var kstr, vstr, k, v; - - if (idx >= 0) { - kstr = x.substr(0, idx); - vstr = x.substr(idx + 1); - } else { - kstr = x; - vstr = ''; - } - - if (kstr && kstr.indexOf('%') >= 0) { - try { - k = decode(kstr, charset); - } catch (e) { - k = kstr; - } - } else { - k = kstr; - } - - if (vstr && vstr.indexOf('%') >= 0) { - try { - v = decode(vstr, charset); - } catch (e) { - v = vstr; - } - } else { - v = vstr; - } - - if (!has(obj, k)) { - obj[k] = v; - } else if (Array.isArray(obj[k])) { - obj[k].push(v); - } else { - obj[k] = [obj[k], v]; - } - } - - return obj; -} - -function has(obj, prop) { - return Object.prototype.hasOwnProperty.call(obj, prop); -} - -function isASCII(str) { - return (/^[\x00-\x7F]*$/).test(str); -} - -function encodeComponent(item, charset) { - item = String(item); - if (isASCII(item)) { - item = encodeURIComponent(item); - } else { - item = encode(item, charset); - } - return item; -} - -var stringify = function(obj, prefix, options) { - if (typeof prefix !== 'string') { - options = prefix || {}; - prefix = null; - } - var charset = options.charset || 'utf-8'; - if (Array.isArray(obj)) { - return stringifyArray(obj, prefix, options); - } else if ('[object Object]' === {}.toString.call(obj)) { - return stringifyObject(obj, prefix, options); - } else if ('string' === typeof obj) { - return stringifyString(obj, prefix, options); - } else { - return prefix + '=' + encodeComponent(String(obj), charset); - } -}; - -function stringifyString(str, prefix, options) { - if (!prefix) { - throw new TypeError('stringify expects an object'); - } - var charset = options.charset; - return prefix + '=' + encodeComponent(str, charset); -} - -function stringifyArray(arr, prefix, options) { - var ret = []; - if (!prefix) { - throw new TypeError('stringify expects an object'); - } - for (var i = 0; i < arr.length; i++) { - ret.push(stringify(arr[i], prefix + '[' + i + ']', options)); - } - return ret.join('&'); -} - -function stringifyObject(obj, prefix, options) { - var ret = []; - var keys = Object.keys(obj); - var key; - - var charset = options.charset; - for (var i = 0, len = keys.length; i < len; ++i) { - key = keys[i]; - if ('' === key) { - continue; - } - if (null === obj[key]) { - ret.push(encode(key, charset) + '='); - } else { - ret.push(stringify( - obj[key], - prefix ? prefix + '[' + encodeComponent(key, charset) + ']': encodeComponent(key, charset), - options)); - } - } - - return ret.join('&'); -} - -module.exports = encode; -module.exports.encode = encode; -module.exports.decode = decode; -module.exports.parse = parse; -module.exports.stringify = stringify; diff --git a/package.json b/package.json index d1a738f..8c02a0d 100644 --- a/package.json +++ b/package.json @@ -2,32 +2,29 @@ "name": "urlencode", "version": "1.1.0", "description": "encodeURIComponent with charset", - "main": "lib/urlencode.js", - "files": [ - "lib" - ], "scripts": { - "test": "mocha -R spec -t 20000 -r should test/*.test.js", - "test-cov": "istanbul cover node_modules/.bin/_mocha -- -t 20000 -r should test/*.test.js", - "test-travis": "istanbul cover node_modules/.bin/_mocha --report lcovonly -- -t 20000 -r should test/*.test.js", - "jshint": "jshint .", - "autod": "autod -w --prefix '~' -t test -e examples", - "cnpm": "npm install --registry=https://registry.npm.taobao.org", - "benchmark": "node benchmark/urlencode.js && node benchmark/urlencode.decode.js" + "test": "egg-bin test", + "ci": "npm run lint && egg-bin cov && npm run prepublishOnly && npm run benchmark", + "lint": "eslint . --ext ts", + "benchmark": "node benchmark/urlencode.cjs && node benchmark/urlencode.decode.cjs", + "prepublishOnly": "tshy && tshy-after" }, "dependencies": { - "iconv-lite": "~0.4.11" + "iconv-lite": "~0.6.3" }, "devDependencies": { - "autod": "*", - "beautify-benchmark": "*", - "benchmark": "*", - "blanket": "*", - "contributors": "*", - "istanbul": "~0.3.17", - "jshint": "*", - "mocha": "3", - "should": "7" + "@eggjs/tsconfig": "^1.3.3", + "@types/mocha": "^10.0.3", + "@types/node": "^20.8.7", + "beautify-benchmark": "^0.2.4", + "benchmark": "^2.1.4", + "egg-bin": "^6.5.2", + "eslint": "^8.51.0", + "eslint-config-egg": "^13.0.0", + "git-contributor": "^2.1.5", + "tshy": "^1.5.0", + "tshy-after": "^1.0.0", + "typescript": "^5.2.2" }, "homepage": "https://github.com/node-modules/urlencode", "repository": { @@ -43,5 +40,31 @@ "parse" ], "author": "fengmk2 ", - "license": "MIT" + "license": "MIT", + "files": [ + "dist", + "src" + ], + "tshy": { + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts" + } + }, + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/commonjs/index.d.ts", + "default": "./dist/commonjs/index.js" + } + } + }, + "main": "./dist/commonjs/index.js", + "types": "./dist/commonjs/index.d.ts", + "type": "module" } diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..4c41ed4 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,220 @@ +import iconv from 'iconv-lite'; + +export type SupportEncodeValue = string | number | boolean | undefined | null; +export type SupportEncodeObject = Record; +export interface Options { + charset?: string; + maxKeys?: number; +} + +function isUTF8(charset?: string) { + if (!charset) { + return true; + } + charset = charset.toLowerCase(); + return charset === 'utf8' || charset === 'utf-8'; +} + +export function encode(str: string, charset?: string | null) { + if (!charset || isUTF8(charset)) { + return encodeURIComponent(str); + } + + const buf = iconv.encode(str, charset); + let encodeStr = ''; + let ch = ''; + for (let i = 0; i < buf.length; i++) { + ch = buf[i].toString(16); + if (ch.length === 1) { + ch = '0' + ch; + } + encodeStr += '%' + ch; + } + encodeStr = encodeStr.toUpperCase(); + return encodeStr; +} + +export default encode; + +export function decode(str: string, charset?: string | null) { + if (!charset || isUTF8(charset)) { + return decodeURIComponent(str); + } + + const bytes = []; + for (let i = 0; i < str.length;) { + if (str[i] === '%') { + i++; + bytes.push(parseInt(str.substring(i, i + 2), 16)); + i += 2; + } else { + bytes.push(str.charCodeAt(i)); + i++; + } + } + const buf = Buffer.from(bytes); + return iconv.decode(buf, charset); +} + +export function parse(qs: string, options?: Options): SupportEncodeObject; +export function parse(qs: string, sep?: string, eq?: string, options?: Options): SupportEncodeObject; +export function parse(qs: string, sepOrOptions?: string | Options, eq?: string, options?: Options): SupportEncodeObject { + let sep: string | undefined; + if (typeof sepOrOptions === 'object') { + // parse(qs, options) + options = sepOrOptions; + } else { + // parse(qs, sep, eq, options) + sep = sepOrOptions; + } + + sep = sep || '&'; + eq = eq || '='; + const obj: SupportEncodeObject = {}; + + if (typeof qs !== 'string' || qs.length === 0) { + return obj; + } + + const regexp = /\+/g; + const splits = qs.split(sep); + + let maxKeys = 1000; + let charset = ''; + if (options) { + if (typeof options.maxKeys === 'number') { + maxKeys = options.maxKeys; + } + if (typeof options.charset === 'string') { + charset = options.charset; + } + } + + let len = splits.length; + // maxKeys <= 0 means that we should not limit keys count + if (maxKeys > 0 && len > maxKeys) { + len = maxKeys; + } + + for (let i = 0; i < len; ++i) { + const x = splits[i].replace(regexp, '%20'); + const idx = x.indexOf(eq); + let keyString: string; + let valueString: string; + let k: string; + let v: string; + + if (idx >= 0) { + keyString = x.substring(0, idx); + valueString = x.substring(idx + 1); + } else { + keyString = x; + valueString = ''; + } + + if (keyString && keyString.includes('%')) { + try { + k = decode(keyString, charset); + } catch (e) { + k = keyString; + } + } else { + k = keyString; + } + + if (valueString && valueString.includes('%')) { + try { + v = decode(valueString, charset); + } catch (e) { + v = valueString; + } + } else { + v = valueString; + } + + if (!has(obj, k)) { + obj[k] = v; + } else if (Array.isArray(obj[k])) { + (obj[k] as any).push(v); + } else { + obj[k] = [ obj[k], v ]; + } + } + + return obj; +} + +function has(obj: object, prop: string) { + return Object.prototype.hasOwnProperty.call(obj, prop); +} + +function isASCII(str: string) { + // eslint-disable-next-line no-control-regex + return /^[\x00-\x7F]*$/.test(str); +} + +function encodeComponent(item: string, charset?: string) { + item = String(item); + if (isASCII(item)) { + item = encodeURIComponent(item); + } else { + item = encode(item, charset); + } + return item; +} + +function stringifyArray(values: (SupportEncodeValue | SupportEncodeObject)[], prefix: string, options: Options) { + const items = []; + for (const [ index, value ] of values.entries()) { + items.push(stringify(value, `${prefix}[${index}]`, options)); + } + return items.join('&'); +} + +function stringifyObject(obj: SupportEncodeObject, prefix: string, options: Options) { + const items = []; + const charset = options.charset; + for (const key in obj) { + if (key === '') { + continue; + } + const value = obj[key]; + if (value === null || value === undefined) { + items.push(encode(key, charset) + '='); + } else { + const keyPrefix = prefix ? prefix + '[' + encodeComponent(key, charset) + ']' : encodeComponent(key, charset); + items.push(stringify(value, keyPrefix, options)); + } + } + return items.join('&'); +} + +export function stringify(obj: object | SupportEncodeValue, prefix?: string): string; +export function stringify(obj: object | SupportEncodeValue, options?: Options): string; +export function stringify(obj: object | SupportEncodeValue, prefix?: string, options?: Options): string; +export function stringify(obj: object | SupportEncodeValue, prefixOrOptions?: string | Options, options?: Options): string { + let prefix: string | undefined; + if (typeof prefixOrOptions !== 'string') { + options = prefixOrOptions || {}; + } else { + prefix = prefixOrOptions; + } + options = options ?? {}; + if (Array.isArray(obj)) { + if (!prefix) { + throw new TypeError('stringify expects an object'); + } + return stringifyArray(obj, prefix, options); + } + + const objValue = String(obj); + if (obj && typeof obj === 'object' && objValue === '[object Object]') { + return stringifyObject(obj as SupportEncodeObject, prefix ?? '', options); + } + + if (!prefix) { + throw new TypeError('stringify expects an object'); + } + const charset = options?.charset ?? 'utf-8'; + return `${prefix}=${encodeComponent(objValue, charset)}`; +} diff --git a/test/urlencode.test.js b/test/urlencode.test.js deleted file mode 100644 index bf7f96d..0000000 --- a/test/urlencode.test.js +++ /dev/null @@ -1,199 +0,0 @@ -/*! - * urlencode - test/urlencode.test.js - * - * Copyright(c) 2012 - 2014 fengmk2 - * MIT Licensed - */ - -"use strict"; - -/** - * Module dependencies. - */ - -var urlencode = require('../'); -var should = require('should'); - -describe('urlencode.test.js', function () { - describe('encode() and decode()', function () { - var items = [ - [ '苏千', null, encodeURIComponent('苏千') ], - [ '苏千', undefined, encodeURIComponent('苏千') ], - [ '苏千', '', encodeURIComponent('苏千') ], - [ '苏千', 'utf8', encodeURIComponent('苏千') ], - [ '苏千', 'utf-8', encodeURIComponent('苏千') ], - [ 'nodeJS', 'gbk', '%6E%6F%64%65%4A%53' ], - [ '苏千', 'gbk', '%CB%D5%C7%A7' ], - [ '苏千,nodejs。!@#¥%……&**(&*)&)}{|~~!@+——?、》《。,“‘:;|、】【}{~·中文', 'gbk', - '%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' ], - [ '\\诚%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'], - [ '\n\r\n', 'gbk', '%0A%0D%0A' ] - ]; - - items.forEach(function (item) { - var str = item[0]; - var charset = item[1]; - var expect = item[2]; - it('should enocde ' + str.substring(0, 20) + ' with ' + charset + ' to ' + expect.substring(0, 30), function () { - urlencode(str, charset).should.equal(expect); - }); - }); - - var decodeItems = [ - [ '%CB%D5%C7%A7a', 'gbk', '苏千a' ], - [ '%CB%D5%C7%A7a%0A%C7%A7a%a', 'gbk', '苏千a\n千a\n' ], - [ - '%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', - 'gbk', - '苏千,nodejs。!@#¥%……&**(&*)&)}{|~~!@+——?、》《。,“‘:;|、】【}{~·中文', - ], - ]; - - decodeItems.forEach(function (item) { - var str = item[0]; - var charset = item[1]; - var expect = item[2]; - it('should decode ' + str.substring(0, 20) + ' with ' + charset + ' to ' + expect.substring(0, 30), function () { - urlencode.decode(str, charset).should.equal(expect); - }); - }); - - items.forEach(function (item) { - var str = item[2]; - var charset = item[1]; - var expect = item[0]; - it('should decode ' + str.substring(0, 20) + ' with ' + charset + ' to ' + expect.substring(0, 30), function () { - urlencode.decode(str, charset).should.equal(expect); - }); - }); - }); - - describe('parse()', function () { - it('should work with gbk encoding', function () { - var 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'; - var obj = urlencode.parse(qs, {charset: 'gbk'}); - obj.should.eql({ - umidtoken: 'Tc230acc03a564530aee31d22701e9b95', - usertag4: '0', - usertag3: '512', - usertag2: '0', - status: '0', - userid: '665377421', - out_user: 'suqian.yf@taobao.com', - promotedtype: '0', - account_no: '20885028063394350156', - loginstatus: 'true', - usertag: '0', - nick: '苏千', - tairlastupdatetime: '1319008872', - strid: 'a68f6ee38f44d2b89ca508444c1ccaf9' - }); - }); - - // TODO - // var qs = 'x[y][0][v][w]=%CE%ED%BF%D5'; - // var obj = {'x' : {'y' : [{'v' : {'w' : '雾空'}}]}}; - // urlencode.parse(qs, {charset: 'gbk'}) - // .should.eql(obj); - }); - - describe('stringify()', function () { - it('should work with gbk encoding', function () { - var obj = {xm: '苏千', xb: 1, xh: 1111}; - urlencode.stringify(obj, {charset: 'gbk'}) - .should.equal('xm=%CB%D5%C7%A7&xb=1&xh=1111'); - - - // `qs` and `obj` is copy from `describe('parse()', ->)` - var 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'; - obj = { - umidtoken: 'Tc230acc03a564530aee31d22701e9b95', - usertag4: '0', - usertag3: '512', - usertag2: '0', - status: '0', - userid: '665377421', - out_user: 'suqian.yf@taobao.com', - promotedtype: '0', - account_no: '20885028063394350156', - loginstatus: 'true', - usertag: '0', - nick: '苏千', - tairlastupdatetime: '1319008872', - strid: 'a68f6ee38f44d2b89ca508444c1ccaf9' - }; - urlencode.stringify(obj, {charset: 'gbk'}) - .should.equal(qs); - - - // str: x[y][0][v][w]=%CE%ED%BF%D5 - var str = 'x[y][0][v][w]=' + urlencode('雾空', 'gbk'); - var obj = {'x' : {'y' : [{'v' : {'w' : '雾空'}}]}}; - urlencode.stringify(obj, {charset: 'gbk'}).should.equal(str); - - - // str : xh=23123&%CE%ED%BF%D5=%CE%ED%BF%D5 - // 这里是 chrome 在 gbk 编码网页的行为 - var str = 'xh=13241234' + - '&xb=1' + - '&' + urlencode('雾空', 'gbk') + '=' + urlencode('雾空', 'gbk'); - var obj = {xh: 13241234, xb: 1, '雾空': '雾空'}; - urlencode.stringify(obj, {charset: 'gbk'}).should.equal(str); - }); - - it('should work with utf-8 encoding', function () { - var obj = {h: 1, j: 2, k: '3'}; - - urlencode.stringify(obj, {charset: 'utf-8'}) - .should.equal('h=1&j=2&k=3'); - - urlencode.stringify(obj) - .should.equal('h=1&j=2&k=3'); - - var str = 'x[y][0][v][w]=1'; - var obj = {'x' : {'y' : [{'v' : {'w' : '1'}}]}}; - urlencode.stringify(obj).should.equal(str); - - var str = 'x[y][0][v][w]=' + encodeURIComponent('雾空'); - var obj = {'x' : {'y' : [{'v' : {'w' : '雾空'}}]}}; - urlencode.stringify(obj).should.equal(str); - - var str = 'x[y][0][v][w]=' + encodeURIComponent('雾空'); - var obj = {'x' : {'y' : [{'v' : {'w' : '雾空'}}]}}; - urlencode.stringify(obj, {charset: 'utf-8'}).should.equal(str); - }); - - it('should work with big5 encoding', function () { - var str = 'x[y][0][v][w]=' + urlencode('雾空', 'big5'); - var obj = {'x' : {'y' : [{'v' : {'w' : '雾空'}}]}}; - urlencode.stringify(obj, {charset: 'big5'}).should.equal(str); - }); - - it('should support nest obj and array', function () { - var encoding = 'gbk'; - var obj = { - edp: { - name: ['阿里', '巴巴', '数据产品'], - hello: 100, - nihao: '100', - }, - good: '好' - }; - // qs : edp[name][0]=%B0%A2%C0%EF - // &edp[name][1]=%B0%CD%B0%CD - // &edp[name][2]=%CA%FD%BE%DD%B2%FA%C6%B7 - // &edp[hello]=100 - // &edp[nihao]=100 - // &good=%BA%C3 - var qs = 'edp[name][0]=' + urlencode('阿里', encoding) + - '&edp[name][1]=' + urlencode('巴巴', encoding) + - '&edp[name][2]=' + urlencode('数据产品', encoding) + - '&edp[hello]=100' + - '&edp[nihao]=100' + - '&good=' + urlencode('好', encoding); - urlencode.stringify(obj, {charset: 'gbk'}) - .should.equal(qs); - }); - }); - -}); diff --git a/test/urlencode.test.ts b/test/urlencode.test.ts new file mode 100644 index 0000000..b4bfbb1 --- /dev/null +++ b/test/urlencode.test.ts @@ -0,0 +1,184 @@ +import { strict as assert } from 'node:assert'; +import { encode, decode, stringify, parse } from '../src/index.js'; +import urlencode from '../src/index.js'; +import type { SupportEncodeObject } from '../src/index.js'; + +describe('urlencode.test.js', () => { + describe('encode() and decode()', () => { + const items = [ + [ '苏千', null, encodeURIComponent('苏千') ], + [ '苏千', undefined, encodeURIComponent('苏千') ], + [ '苏千', '', encodeURIComponent('苏千') ], + [ '苏千', 'utf8', encodeURIComponent('苏千') ], + [ '苏千', 'utf-8', encodeURIComponent('苏千') ], + [ 'nodeJS', 'gbk', '%6E%6F%64%65%4A%53' ], + [ '苏千', 'gbk', '%CB%D5%C7%A7' ], + [ '苏千,nodejs。!@#¥%……&**(&*)&)}{|~~!@+——?、》《。,“‘:;|、】【}{~·中文', 'gbk', + '%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' ], + [ '\\诚%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' ], + [ '\n\r\n', 'gbk', '%0A%0D%0A' ], + ]; + + items.forEach(item => { + const str = item[0] as string; + const charset = item[1]; + const expect = item[2] as string; + it('should encode ' + str.substring(0, 20) + ' with ' + charset + ' to ' + expect.substring(0, 30), () => { + assert.equal(encode(str, charset), expect); + assert.equal(urlencode(str, charset), expect); + }); + }); + + const decodeItems = [ + [ '%CB%D5%C7%A7a', 'gbk', '苏千a' ], + [ '%CB%D5%C7%A7a%0A%C7%A7a%a', 'gbk', '苏千a\n千a\n' ], + [ + '%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', + 'gbk', + '苏千,nodejs。!@#¥%……&**(&*)&)}{|~~!@+——?、》《。,“‘:;|、】【}{~·中文', + ], + ]; + + decodeItems.forEach(item => { + const str = item[0]; + const charset = item[1]; + const expect = item[2]; + it('should decode ' + str.substring(0, 20) + ' with ' + charset + ' to ' + expect.substring(0, 30), () => { + assert.equal(decode(str, charset), expect); + }); + }); + + items.forEach(item => { + const str = item[2] as string; + const charset = item[1]; + const expect = item[0] as string; + it('should decode ' + str.substring(0, 20) + ' with ' + charset + ' to ' + expect.substring(0, 30), () => { + assert.equal(decode(str, charset), expect); + }); + }); + }); + + describe('parse()', () => { + it('should work with gbk encoding', () => { + 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'; + const obj = parse(qs, { charset: 'gbk' }); + assert.deepEqual(obj, { + umidtoken: 'Tc230acc03a564530aee31d22701e9b95', + usertag4: '0', + usertag3: '512', + usertag2: '0', + status: '0', + userid: '665377421', + out_user: 'suqian.yf@taobao.com', + promotedtype: '0', + account_no: '20885028063394350156', + loginstatus: 'true', + usertag: '0', + nick: '苏千', + tairlastupdatetime: '1319008872', + strid: 'a68f6ee38f44d2b89ca508444c1ccaf9', + }); + }); + + // TODO + // var qs = 'x[y][0][v][w]=%CE%ED%BF%D5'; + // var obj = {'x' : {'y' : [{'v' : {'w' : '雾空'}}]}}; + // urlencode.parse(qs, {charset: 'gbk'}) + // .should.eql(obj); + }); + + describe('stringify()', () => { + it('should work with gbk encoding', () => { + let obj: SupportEncodeObject = { xm: '苏千', xb: 1, xh: 1111 }; + assert.equal(stringify(obj, { charset: 'gbk' }), 'xm=%CB%D5%C7%A7&xb=1&xh=1111'); + + + // `qs` and `obj` is copy from `describe('parse()', ->)` + 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'; + obj = { + umidtoken: 'Tc230acc03a564530aee31d22701e9b95', + usertag4: '0', + usertag3: '512', + usertag2: '0', + status: '0', + userid: '665377421', + out_user: 'suqian.yf@taobao.com', + promotedtype: '0', + account_no: '20885028063394350156', + loginstatus: 'true', + usertag: '0', + nick: '苏千', + tairlastupdatetime: '1319008872', + strid: 'a68f6ee38f44d2b89ca508444c1ccaf9', + }; + assert.equal(stringify(obj, { charset: 'gbk' }), qs); + + + // str: x[y][0][v][w]=%CE%ED%BF%D5 + let str = 'x[y][0][v][w]=' + encode('雾空', 'gbk'); + obj = { x: { y: [{ v: { w: '雾空' } }] } }; + assert.equal(stringify(obj, { charset: 'gbk' }), str); + + + // str : xh=23123&%CE%ED%BF%D5=%CE%ED%BF%D5 + // 这里是 chrome 在 gbk 编码网页的行为 + str = 'xh=13241234' + + '&xb=1' + + '&' + encode('雾空', 'gbk') + '=' + encode('雾空', 'gbk'); + obj = { xh: 13241234, xb: 1, 雾空: '雾空' }; + assert.equal(stringify(obj, { charset: 'gbk' }), str); + }); + + it('should work with utf-8 encoding', () => { + let obj: SupportEncodeObject = { h: 1, j: 2, k: '3' }; + + assert.equal(stringify(obj, { charset: 'utf-8' }), 'h=1&j=2&k=3'); + + assert.equal(stringify(obj), 'h=1&j=2&k=3'); + + let str = 'x[y][0][v][w]=1'; + obj = { x: { y: [{ v: { w: '1' } }] } }; + assert.equal(stringify(obj), str); + + str = 'x[y][0][v][w]=' + encodeURIComponent('雾空'); + obj = { x: { y: [{ v: { w: '雾空' } }] } }; + assert.equal(stringify(obj), str); + + str = 'x[y][0][v][w]=' + encodeURIComponent('雾空'); + obj = { x: { y: [{ v: { w: '雾空' } }] } }; + assert.equal(stringify(obj, { charset: 'utf-8' }), str); + }); + + it('should work with big5 encoding', () => { + const str = 'x[y][0][v][w]=' + encode('雾空', 'big5'); + const obj = { x: { y: [{ v: { w: '雾空' } }] } }; + assert.equal(stringify(obj, { charset: 'big5' }), str); + }); + + it('should support nest obj and array', () => { + const encoding = 'gbk'; + const obj: SupportEncodeObject = { + edp: { + name: [ '阿里', '巴巴', '数据产品' ], + hello: 100, + nihao: '100', + }, + good: '好', + }; + // qs : edp[name][0]=%B0%A2%C0%EF + // &edp[name][1]=%B0%CD%B0%CD + // &edp[name][2]=%CA%FD%BE%DD%B2%FA%C6%B7 + // &edp[hello]=100 + // &edp[nihao]=100 + // &good=%BA%C3 + const qs = 'edp[name][0]=' + encode('阿里', encoding) + + '&edp[name][1]=' + encode('巴巴', encoding) + + '&edp[name][2]=' + encode('数据产品', encoding) + + '&edp[hello]=100' + + '&edp[nihao]=100' + + '&good=' + encode('好', encoding); + assert.equal(stringify(obj, { charset: 'gbk' }), qs); + }); + }); + +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ff41b73 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@eggjs/tsconfig", + "compilerOptions": { + "strict": true, + "noImplicitAny": true, + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext" + } +}