├── .gitattributes
├── .gitignore
├── .travis.yml
├── Gruntfile.js
├── LICENSE-MIT.txt
├── README.md
├── bin
└── q
├── bower.json
├── component.json
├── man
└── q.1
├── package.json
├── q.js
├── scripts
└── export-data.js
├── src
└── q.js
└── tests
├── index.html
└── tests.js
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Automatically normalize line endings for all text-based files
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Coverage report
2 | coverage
3 |
4 | # Installed npm modules
5 | node_modules
6 |
7 | # Folder view configuration files
8 | .DS_Store
9 | Desktop.ini
10 |
11 | # Thumbnail cache files
12 | ._*
13 | Thumbs.db
14 |
15 | # Files that might appear on external disks
16 | .Spotlight-V100
17 | .Trashes
18 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "0.10"
4 | - "0.12"
5 | - "iojs"
6 | before_script:
7 | - "npm install -g grunt-cli"
8 | # Narwhal uses a hardcoded path to openjdk v6, so use that version
9 | - "sudo apt-get update -qq"
10 | - "sudo apt-get install -qq openjdk-6-jre"
11 | - "PACKAGE=rhino1_7R5; wget https://github.com/mozilla/rhino/releases/download/Rhino1_7R5_RELEASE/$PACKAGE.zip && sudo unzip $PACKAGE -d /opt/ && rm $PACKAGE.zip"
12 | - "PACKAGE=rhino1_7R5; echo -e '#!/bin/sh\\njava -jar /opt/'$PACKAGE'/js.jar $@' | sudo tee /usr/local/bin/rhino && sudo chmod +x /usr/local/bin/rhino"
13 | - "PACKAGE=ringojs-0.9; wget http://ringojs.org/downloads/$PACKAGE.zip && sudo unzip $PACKAGE -d /opt/ && rm $PACKAGE.zip"
14 | - "PACKAGE=ringojs-0.9; sudo ln -s /opt/$PACKAGE/bin/ringo /usr/local/bin/ringo && sudo chmod +x /usr/local/bin/ringo"
15 | - "PACKAGE=v0.3.2; wget https://github.com/280north/narwhal/archive/$PACKAGE.zip && sudo unzip $PACKAGE -d /opt/ && rm $PACKAGE.zip"
16 | - "PACKAGE=narwhal-0.3.2; sudo ln -s /opt/$PACKAGE/bin/narwhal /usr/local/bin/narwhal && sudo chmod +x /usr/local/bin/narwhal"
17 | # If the enviroment stores rt.jar in a different directory, find it and symlink the directory
18 | - "PREFIX=/usr/lib/jvm; if [ ! -d $PREFIX/java-6-openjdk ]; then for d in $PREFIX/java-6-openjdk-*; do if [ -e $d/jre/lib/rt.jar ]; then sudo ln -s $d $PREFIX/java-6-openjdk; break; fi; done; fi"
19 | script:
20 | "grunt ci"
21 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | module.exports = function(grunt) {
2 |
3 | grunt.initConfig({
4 | 'shell': {
5 | 'options': {
6 | 'stdout': true,
7 | 'stderr': true,
8 | 'failOnError': true
9 | },
10 | 'cover': {
11 | 'command': 'istanbul cover --report "html" --verbose --dir "coverage" "tests/tests.js"'
12 | },
13 | 'test-narwhal': {
14 | 'command': 'echo "Testing in Narwhal..."; export NARWHAL_OPTIMIZATION=-1; narwhal "tests/tests.js"'
15 | },
16 | 'test-phantomjs': {
17 | 'command': 'echo "Testing in PhantomJS..."; phantomjs "tests/tests.js"'
18 | },
19 | 'test-rhino': {
20 | 'command': 'echo "Testing in Rhino..."; rhino -opt -1 "tests.js"',
21 | 'options': {
22 | 'execOptions': {
23 | 'cwd': 'tests'
24 | }
25 | }
26 | },
27 | 'test-ringo': {
28 | 'command': 'echo "Testing in Ringo..."; ringo -o -1 "tests/tests.js"'
29 | },
30 | 'test-node': {
31 | 'command': 'echo "Testing in Node..."; node "tests/tests.js"'
32 | },
33 | 'test-browser': {
34 | 'command': 'echo "Testing in a browser..."; open "tests/index.html"'
35 | }
36 | },
37 | 'template': {
38 | 'build': {
39 | 'options': {
40 | 'data': function() {
41 | return require('./scripts/export-data.js');
42 | }
43 | },
44 | 'files': {
45 | 'q.js': ['src/q.js']
46 | }
47 | }
48 | }
49 | });
50 |
51 | grunt.loadNpmTasks('grunt-shell');
52 | grunt.loadNpmTasks('grunt-template');
53 |
54 | grunt.registerTask('cover', 'shell:cover');
55 | grunt.registerTask('ci', [
56 | 'template',
57 | 'shell:test-narwhal',
58 | 'shell:test-phantomjs',
59 | 'shell:test-rhino',
60 | 'shell:test-ringo',
61 | 'shell:test-node',
62 | ]);
63 | grunt.registerTask('test', [
64 | 'ci',
65 | 'shell:test-browser'
66 | ]);
67 |
68 | grunt.registerTask('default', [
69 | 'template',
70 | 'shell:test-node'
71 | ]);
72 |
73 | };
74 |
--------------------------------------------------------------------------------
/LICENSE-MIT.txt:
--------------------------------------------------------------------------------
1 | Copyright Mathias Bynens
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # q-encoding [](https://travis-ci.org/mathiasbynens/q-encoding) [](https://gemnasium.com/mathiasbynens/q-encoding)
2 |
3 | _q-encoding_ is a character encoding–agnostic JavaScript implementation of [the `Q` encoding as defined by RFC 2047](https://tools.ietf.org/html/rfc2047#section-4.2). It can be used to encode data with any character encoding to its `Q`-encoded form, or the other way around (i.e. decoding).
4 |
5 | [An online demo is available.](https://mothereff.in/q)
6 |
7 | ## Installation
8 |
9 | Via [npm](https://www.npmjs.com/):
10 |
11 | ```bash
12 | npm install q-encoding
13 | ```
14 |
15 | Via [Bower](http://bower.io/):
16 |
17 | ```bash
18 | bower install q-encoding
19 | ```
20 |
21 | Via [Component](https://github.com/component/component):
22 |
23 | ```bash
24 | component install mathiasbynens/q-encoding
25 | ```
26 |
27 | In a browser:
28 |
29 | ```html
30 |
31 | ```
32 |
33 | In [Narwhal](http://narwhaljs.org/), [Node.js](https://nodejs.org/), and [RingoJS](http://ringojs.org/):
34 |
35 | ```js
36 | var q = require('q-encoding');
37 | ```
38 |
39 | In [Rhino](http://www.mozilla.org/rhino/):
40 |
41 | ```js
42 | load('q.js');
43 | ```
44 |
45 | Using an AMD loader like [RequireJS](http://requirejs.org/):
46 |
47 | ```js
48 | require(
49 | {
50 | 'paths': {
51 | 'q-encoding': 'path/to/q-encoding'
52 | }
53 | },
54 | ['q-encoding'],
55 | function(q) {
56 | console.log(q);
57 | }
58 | );
59 | ```
60 |
61 | ## API
62 |
63 | ### `q.version`
64 |
65 | A string representing the semantic version number.
66 |
67 | ### `q.encode(input)`
68 |
69 | This function takes an encoded byte string (the `input` parameter) and `Q`-encodes it. Each item in the input string represents an octet as per the desired character encoding. Here’s an example that uses UTF-8:
70 |
71 | ```js
72 | var utf8 = require('utf8');
73 |
74 | q.encode(utf8.encode('foo = bar'));
75 | // → 'foo_=3D_bar'
76 |
77 | q.encode(utf8.encode('Iñtërnâtiônàlizætiøn☃💩'));
78 | // → 'I=C3=B1t=C3=ABrn=C3=A2ti=C3=B4n=C3=A0liz=C3=A6ti=C3=B8n=E2=98=83=F0=9F=92=A9'
79 | ```
80 |
81 | ### `q.decode(text)`
82 |
83 | This function takes a `Q`-encoded string of text (the `text` parameter) and `Q`-decodes it. The return value is a ‘byte string’, i.e. a string of which each item represents an octet as per the character encoding that’s being used. Here’s an example that uses UTF-8:
84 |
85 | ```js
86 | var utf8 = require('utf8');
87 |
88 | utf8.decode(q.decode('foo_=3D_bar'));
89 | // → 'foo = bar'
90 |
91 | utf8.decode(q.decode('I=C3=B1t=C3=ABrn=C3=A2ti=C3=B4n=C3=A0liz=C3=A6ti=C3=B8n=E2=98=83=F0=9F=92=A9'));
92 | // → 'Iñtërnâtiônàlizætiøn☃💩'
93 | ```
94 |
95 | ### Using the `q` binary
96 |
97 | To use the `q` binary in your shell, simply install _q-encoding_ globally using npm:
98 |
99 | ```bash
100 | npm install -g q-encoding
101 | ```
102 |
103 | After that, you’ll be able to use `q` on the command line. Note that while the _q-encoding_ library itself is character encoding–agnostic, the command-line tool applies the UTF-8 character encoding on all input.
104 |
105 | ```bash
106 | $ q --encode 'foo = bar'
107 | foo_=3D_bar
108 |
109 | $ q --decode 'foo_=3D_bar'
110 | foo = bar
111 | ```
112 |
113 | Read a local text file, `Quoted-Printable`-encode it, and save the result to a new file:
114 |
115 | ```bash
116 | $ q --encode < foo.txt > foo-q.txt
117 | ```
118 |
119 | Or do the same with an online text file:
120 |
121 | ```bash
122 | $ curl -sL 'https://mths.be/brh' | q --encode > q.txt
123 | ```
124 |
125 | Or, the opposite — read a local file containing a `Quoted-Printable`-encoded message, decode it back to plain text, and save the result to a new file:
126 |
127 | ```bash
128 | $ q --decode < q.txt > original.txt
129 | ```
130 |
131 | See `q --help` for the full list of options.
132 |
133 | ## Support
134 |
135 | _q-encoding_ is designed to work in at least Node.js v0.10.0, Narwhal 0.3.2, RingoJS 0.8-0.9, PhantomJS 1.9.0, Rhino 1.7RC4, as well as old and modern versions of Chrome, Firefox, Safari, Opera, and Internet Explorer.
136 |
137 | ## Unit tests & code coverage
138 |
139 | After cloning this repository, run `npm install` to install the dependencies needed for development and testing. You may want to install Istanbul _globally_ using `npm install istanbul -g`.
140 |
141 | Once that’s done, you can run the unit tests in Node using `npm test` or `node tests/tests.js`. To run the tests in Rhino, Ringo, Narwhal, and web browsers as well, use `grunt test`.
142 |
143 | To generate the code coverage report, use `grunt cover`.
144 |
145 | ## Author
146 |
147 | | [](https://twitter.com/mathias "Follow @mathias on Twitter") |
148 | |---|
149 | | [Mathias Bynens](https://mathiasbynens.be/) |
150 |
151 | ## License
152 |
153 | _q-encoding_ is available under the [MIT](https://mths.be/mit) license.
154 |
--------------------------------------------------------------------------------
/bin/q:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | (function() {
3 |
4 | var fs = require('fs');
5 | var utf8 = require('utf8');
6 | var q = require('../q.js');
7 | var strings = process.argv.splice(2);
8 | var stdin = process.stdin;
9 | var data;
10 | var timeout;
11 | var action;
12 | var options = {};
13 | var log = console.log;
14 |
15 | var main = function() {
16 | var option = strings[0];
17 | var count = 0;
18 |
19 | if (/^(?:-h|--help|undefined)$/.test(option)) {
20 | log(
21 | 'q v%s - https://mths.be/q',
22 | q.version
23 | );
24 | log([
25 | '\nEncode or decode messages using the `Q` encoding (and UTF-8).',
26 | '\nUsage:\n',
27 | '\tq [-e | --encode] string',
28 | '\tq [-d | --decode] string',
29 | '\tq [-v | --version]',
30 | '\tq [-h | --help]',
31 | '\nExamples:\n',
32 | '\tq --encode \'foo = bar ©\'',
33 | '\techo \'foo_=3D_bar_=C2=A9\' | q --decode',
34 | ].join('\n'));
35 | return process.exit(1);
36 | }
37 |
38 | if (/^(?:-v|--version)$/.test(option)) {
39 | log('v%s', q.version);
40 | return process.exit(1);
41 | }
42 |
43 | strings.forEach(function(string) {
44 | // Process options
45 | if (string == '-e' || string == '--encode') {
46 | action = 'encode';
47 | return;
48 | }
49 | if (string == '-d' || string == '--decode') {
50 | action = 'decode';
51 | return;
52 | }
53 | // Process string(s)
54 | var result;
55 | if (!action) {
56 | log('Error: q requires at least one option and a string argument.');
57 | log('Try `q --help` for more information.');
58 | return process.exit(1);
59 | }
60 | try {
61 | if (action == 'encode') {
62 | result = q.encode(utf8.encode(string, options));
63 | } else if (action == 'decode') {
64 | result = utf8.decode(q.decode(string, options));
65 | }
66 | log(result);
67 | count++;
68 | } catch (exception) {
69 | log(exception.message + '\n');
70 | log('Error: failed to %s.', action);
71 | log('If you think this is a bug in q-encoding, please report it:');
72 | log('https://github.com/mathiasbynens/q-encoding/issues/new');
73 | log('\nStack trace using q-encoding@%s:\n', q.version);
74 | log(exception.stack);
75 | return process.exit(1);
76 | }
77 | });
78 | if (!count) {
79 | log('Error: q requires a string argument.');
80 | log('Try `q --help` for more information.');
81 | return process.exit(1);
82 | }
83 | // Return with exit status 0 outside of the `forEach` loop, in case
84 | // multiple strings were passed in.
85 | return process.exit(0);
86 | };
87 |
88 | if (stdin.isTTY) {
89 | // handle shell arguments
90 | main();
91 | } else {
92 | // Either the script is called from within a non-TTY context, or `stdin`
93 | // content is being piped in.
94 | if (!process.stdout.isTTY) {
95 | // The script was called from a non-TTY context. This is a rather uncommon
96 | // use case we don’t actively support. However, we don’t want the script
97 | // to wait forever in such cases, so…
98 | timeout = setTimeout(function() {
99 | // …if no piped data arrived after a whole minute, handle shell
100 | // arguments instead.
101 | main();
102 | }, 60000);
103 | }
104 | data = '';
105 | stdin.on('data', function(chunk) {
106 | clearTimeout(timeout);
107 | data += chunk;
108 | });
109 | stdin.on('end', function() {
110 | strings.push(data.trim());
111 | main();
112 | });
113 | stdin.resume();
114 | }
115 |
116 | }());
117 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "q-encoding",
3 | "version": "1.0.0",
4 | "main": "q.js",
5 | "ignore": [
6 | "bin",
7 | "coverage",
8 | "man",
9 | "tests",
10 | ".*",
11 | "component.json",
12 | "Gruntfile.js",
13 | "node_modules",
14 | "package.json"
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/component.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "q-encoding",
3 | "version": "1.0.0",
4 | "description": "A robust & character encoding–agnostic JavaScript implementation of the `Q` encoding as defined by RFC 2047.",
5 | "repo": "mathiasbynens/q-encoding",
6 | "license": "MIT",
7 | "scripts": [
8 | "q.js"
9 | ],
10 | "main": "q.js",
11 | "keywords": [
12 | "decode",
13 | "decoding",
14 | "encode",
15 | "encoding",
16 | "q-decode",
17 | "q-encode",
18 | "q-encoding",
19 | "string"
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/man/q.1:
--------------------------------------------------------------------------------
1 | .Dd May 5, 2014
2 | .Dt q 1
3 | .Sh NAME
4 | .Nm q
5 | .Nd encode or decode messages using the `Q` encoding
6 | .Sh SYNOPSIS
7 | .Nm
8 | .Op Fl e | -encode Ar string
9 | .br
10 | .Op Fl d | -decode Ar string
11 | .br
12 | .Op Fl v | -version
13 | .br
14 | .Op Fl h | -help
15 | .Sh DESCRIPTION
16 | .Nm
17 | encode or decode messages using the `Q` content transfer encoding.
18 | .Sh OPTIONS
19 | .Bl -ohang -offset
20 | .It Sy "--encode"
21 | Encode a string of text using UTF-8 and then using the `Q` encoding.
22 | .It Sy "--decode"
23 | Decode a string of text using the `Q` encoding, and UTF-8-decode the result.
24 | .It Sy "-v, --version"
25 | Print q's version.
26 | .It Sy "-h, --help"
27 | Show the help screen.
28 | .El
29 | .Sh EXIT STATUS
30 | The
31 | .Nm q
32 | utility exits with one of the following values:
33 | .Pp
34 | .Bl -tag -width flag -compact
35 | .It Li 0
36 | .Nm
37 | successfully encoded/decoded the input and printed the result.
38 | .It Li 1
39 | .Nm
40 | wasn't instructed to encode/decode anything (for example, the
41 | .Ar --help
42 | flag was set); or, an error occurred.
43 | .El
44 | .Sh EXAMPLES
45 | .Bl -ohang -offset
46 | .It Sy "q --encode 'foo = bar'"
47 | Print an encoded version of the given string.
48 | .It Sy "q --decode 'foo=3Dbar'"
49 | Print the decoded version of the given `Quoted-Printable`-encoded message.
50 | .It Sy "echo\ 'foo = bar'\ |\ q --encode"
51 | Print the encoded version of the string that gets piped in.
52 | .El
53 | .Sh BUGS
54 | q's bug tracker is located at .
55 | .Sh AUTHOR
56 | Mathias Bynens
57 | .Sh WWW
58 |
59 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "q-encoding",
3 | "version": "1.0.0",
4 | "description": "A robust & character encoding–agnostic JavaScript implementation of the `Q` encoding as defined by RFC 2047.",
5 | "homepage": "https://mths.be/q",
6 | "main": "q.js",
7 | "bin": {
8 | "q": "bin/q"
9 | },
10 | "man": "man/q.1",
11 | "keywords": [
12 | "decode",
13 | "decoding",
14 | "encode",
15 | "encoding",
16 | "q-decode",
17 | "q-encode",
18 | "q-encoding",
19 | "string"
20 | ],
21 | "license": "MIT",
22 | "author": {
23 | "name": "Mathias Bynens",
24 | "url": "https://mathiasbynens.be/"
25 | },
26 | "repository": {
27 | "type": "git",
28 | "url": "https://github.com/mathiasbynens/q-encoding.git"
29 | },
30 | "bugs": "https://github.com/mathiasbynens/q-encoding/issues",
31 | "files": [
32 | "LICENSE-MIT.txt",
33 | "q.js",
34 | "bin/",
35 | "man/"
36 | ],
37 | "directories": {
38 | "bin": "bin",
39 | "man": "man",
40 | "test": "tests"
41 | },
42 | "scripts": {
43 | "test": "node tests/tests.js"
44 | },
45 | "dependencies": {
46 | "utf8": "^2.1.2"
47 | },
48 | "devDependencies": {
49 | "grunt": "^0.4.5",
50 | "grunt-shell": "^1.1.2",
51 | "grunt-template": "^0.2.3",
52 | "istanbul": "^0.4.5",
53 | "qunit-extras": "^1.4.1",
54 | "qunitjs": "~1.11.0",
55 | "regenerate": "^1.2.1",
56 | "requirejs": "^2.1.16"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/q.js:
--------------------------------------------------------------------------------
1 | /*! https://mths.be/q v1.0.0 by @mathias | MIT license */
2 | ;(function(root) {
3 |
4 | // Detect free variables `exports`.
5 | var freeExports = typeof exports == 'object' && exports;
6 |
7 | // Detect free variable `module`.
8 | var freeModule = typeof module == 'object' && module &&
9 | module.exports == freeExports && module;
10 |
11 | // Detect free variable `global`, from Node.js or Browserified code, and use
12 | // it as `root`.
13 | var freeGlobal = typeof global == 'object' && global;
14 | if (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal) {
15 | root = freeGlobal;
16 | }
17 |
18 | /*--------------------------------------------------------------------------*/
19 |
20 | // https://tools.ietf.org/html/rfc2047#section-4.2
21 | var stringFromCharCode = String.fromCharCode;
22 | var decode = function(input) {
23 | return input
24 | // Decode `_` into a space. This is character-encoding-independent;
25 | // see https://tools.ietf.org/html/rfc2047#section-4.2, item 2.
26 | .replace(/_/g, ' ')
27 | // Decode escape sequences of the form `=XX` where `XX` is any
28 | // combination of two hexidecimal digits. For optimal compatibility,
29 | // lowercase hexadecimal digits are supported as well. See
30 | // https://tools.ietf.org/html/rfc2045#section-6.7, note 1.
31 | .replace(/=([a-fA-F0-9]{2})/g, function($0, $1) {
32 | var codePoint = parseInt($1, 16);
33 | return stringFromCharCode(codePoint);
34 | });
35 | };
36 |
37 | var regexUnsafeSymbols = /[\0-\x1F"-\),\.:-@\[-\^`\{-\uFFFF]/g;
38 | var encode = function(string) {
39 | // Note: this assumes the input is already encoded into octets (e.g. using
40 | // UTF-8), and that the resulting octets are within the extended ASCII
41 | // range.
42 | return string
43 | // Encode symbols that are definitely unsafe (i.e. unsafe in any context).
44 | .replace(regexUnsafeSymbols, function(symbol) {
45 | if (symbol > '\xFF') {
46 | throw RangeError(
47 | '`q.encode()` expects extended ASCII input only. Don\u2019t ' +
48 | 'forget to encode the input first using a character encoding ' +
49 | 'like UTF-8.'
50 | );
51 | }
52 | var codePoint = symbol.charCodeAt(0);
53 | var hexadecimal = codePoint.toString(16).toUpperCase();
54 | return '=' + ('0' + hexadecimal).slice(-2);
55 | })
56 | // Encode spaces as `_`, as it’s shorter than `=20`.
57 | .replace(/\x20/g, '_');
58 | };
59 |
60 | var q = {
61 | 'encode': encode,
62 | 'decode': decode,
63 | 'version': '1.0.0'
64 | };
65 |
66 | // Some AMD build optimizers, like r.js, check for specific condition patterns
67 | // like the following:
68 | if (
69 | typeof define == 'function' &&
70 | typeof define.amd == 'object' &&
71 | define.amd
72 | ) {
73 | define(function() {
74 | return q;
75 | });
76 | } else if (freeExports && !freeExports.nodeType) {
77 | if (freeModule) { // in Node.js or RingoJS v0.8.0+
78 | freeModule.exports = q;
79 | } else { // in Narwhal or RingoJS v0.7.0-
80 | for (var key in q) {
81 | q.hasOwnProperty(key) && (freeExports[key] = q[key]);
82 | }
83 | }
84 | } else { // in Rhino or a web browser
85 | root.q = q;
86 | }
87 |
88 | }(this));
89 |
--------------------------------------------------------------------------------
/scripts/export-data.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs');
2 | var regenerate = require('regenerate');
3 |
4 | // Let’s start with the safe/unsafe symbols in `Quoted-Printable` encoding.
5 | // https://tools.ietf.org/html/rfc2045#section-6.7
6 | //
7 | // safe-char :=
9 | // ; Characters not listed as "mail-safe" in
10 | // ; RFC 2049 are also not recommended.
11 | // hex-octet := "=" 2(DIGIT / "A" / "B" / "C" / "D" / "E" / "F")
12 | // ; Octet must be used for characters > 127, =,
13 | // ; SPACEs or TABs at the ends of lines, and is
14 | // ; recommended for any character not listed in
15 | // ; RFC 2049 as "mail-safe".
16 | //
17 | // https://tools.ietf.org/html/rfc2047#section-5 restricts this much
18 | // more severely in the case quoting is used for a “word” in an email header:
19 | //
20 | // In this case the set of characters that may be used in a "Q"-encoded
21 | // 'encoded-word' is restricted to: . An 'encoded-word' that appears within a
24 | // 'phrase' MUST be separated from any adjacent 'word', 'text' or
25 | // 'special' by 'linear-white-space'.
26 |
27 | var safeSymbols = regenerate()
28 | .addRange('A', 'Z')
29 | .addRange('a', 'z') // lower case ASCII
30 | .addRange('0', '9') // decimal digits
31 | .add('!', '*', '+', '-', '/', '_');
32 | var definitelyUnsafeSymbols = regenerate()
33 | .addRange(0x0, 0x10FFFF)
34 | // Note: the script assumes the input is already encoded into octets (e.g.
35 | // using UTF-8), and that the resulting octets are within the extended ASCII
36 | // range. Thus, there is no need to match astral symbols.
37 | .removeRange(0x010000, 0x10FFFF)
38 | .remove(safeSymbols)
39 | .remove(' '); // Note: space is excluded because it’s special-cased.
40 | // https://mathiasbynens.be/notes/javascript-encoding#surrogate-pairs
41 |
42 | module.exports = {
43 | 'unsafeSymbols': definitelyUnsafeSymbols.toString({ 'bmpOnly': true }),
44 | 'version': JSON.parse(fs.readFileSync('package.json', 'utf-8')).version
45 | };
46 |
--------------------------------------------------------------------------------
/src/q.js:
--------------------------------------------------------------------------------
1 | /*! https://mths.be/q v<%= version %> by @mathias | MIT license */
2 | ;(function(root) {
3 |
4 | // Detect free variables `exports`.
5 | var freeExports = typeof exports == 'object' && exports;
6 |
7 | // Detect free variable `module`.
8 | var freeModule = typeof module == 'object' && module &&
9 | module.exports == freeExports && module;
10 |
11 | // Detect free variable `global`, from Node.js or Browserified code, and use
12 | // it as `root`.
13 | var freeGlobal = typeof global == 'object' && global;
14 | if (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal) {
15 | root = freeGlobal;
16 | }
17 |
18 | /*--------------------------------------------------------------------------*/
19 |
20 | // https://tools.ietf.org/html/rfc2047#section-4.2
21 | var stringFromCharCode = String.fromCharCode;
22 | var decode = function(input) {
23 | return input
24 | // Decode `_` into a space. This is character-encoding-independent;
25 | // see https://tools.ietf.org/html/rfc2047#section-4.2, item 2.
26 | .replace(/_/g, ' ')
27 | // Decode escape sequences of the form `=XX` where `XX` is any
28 | // combination of two hexidecimal digits. For optimal compatibility,
29 | // lowercase hexadecimal digits are supported as well. See
30 | // https://tools.ietf.org/html/rfc2045#section-6.7, note 1.
31 | .replace(/=([a-fA-F0-9]{2})/g, function($0, $1) {
32 | var codePoint = parseInt($1, 16);
33 | return stringFromCharCode(codePoint);
34 | });
35 | };
36 |
37 | var regexUnsafeSymbols = /<%= unsafeSymbols %>/g;
38 | var encode = function(string) {
39 | // Note: this assumes the input is already encoded into octets (e.g. using
40 | // UTF-8), and that the resulting octets are within the extended ASCII
41 | // range.
42 | return string
43 | // Encode symbols that are definitely unsafe (i.e. unsafe in any context).
44 | .replace(regexUnsafeSymbols, function(symbol) {
45 | if (symbol > '\xFF') {
46 | throw RangeError(
47 | '`q.encode()` expects extended ASCII input only. Don\u2019t ' +
48 | 'forget to encode the input first using a character encoding ' +
49 | 'like UTF-8.'
50 | );
51 | }
52 | var codePoint = symbol.charCodeAt(0);
53 | var hexadecimal = codePoint.toString(16).toUpperCase();
54 | return '=' + ('0' + hexadecimal).slice(-2);
55 | })
56 | // Encode spaces as `_`, as it’s shorter than `=20`.
57 | .replace(/\x20/g, '_');
58 | };
59 |
60 | var q = {
61 | 'encode': encode,
62 | 'decode': decode,
63 | 'version': '<%= version %>'
64 | };
65 |
66 | // Some AMD build optimizers, like r.js, check for specific condition patterns
67 | // like the following:
68 | if (
69 | typeof define == 'function' &&
70 | typeof define.amd == 'object' &&
71 | define.amd
72 | ) {
73 | define(function() {
74 | return q;
75 | });
76 | } else if (freeExports && !freeExports.nodeType) {
77 | if (freeModule) { // in Node.js or RingoJS v0.8.0+
78 | freeModule.exports = q;
79 | } else { // in Narwhal or RingoJS v0.7.0-
80 | for (var key in q) {
81 | q.hasOwnProperty(key) && (freeExports[key] = q[key]);
82 | }
83 | }
84 | } else { // in Rhino or a web browser
85 | root.q = q;
86 | }
87 |
88 | }(this));
89 |
--------------------------------------------------------------------------------
/tests/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | q-encoding test suite
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
23 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/tests/tests.js:
--------------------------------------------------------------------------------
1 | (function(root) {
2 | 'use strict';
3 |
4 | var noop = Function.prototype;
5 |
6 | var load = (typeof require == 'function' && !(root.define && define.amd)) ?
7 | require :
8 | (!root.document && root.java && root.load) || noop;
9 |
10 | var QUnit = (function() {
11 | return root.QUnit || (
12 | root.addEventListener || (root.addEventListener = noop),
13 | root.setTimeout || (root.setTimeout = noop),
14 | root.QUnit = load('../node_modules/qunitjs/qunit/qunit.js') || root.QUnit,
15 | addEventListener === noop && delete root.addEventListener,
16 | root.QUnit
17 | );
18 | }());
19 |
20 | var qe = load('../node_modules/qunit-extras/qunit-extras.js');
21 | if (qe) {
22 | qe.runInContext(root);
23 | }
24 |
25 | // The `q` object to test
26 | var q = root.q || (root.q = (
27 | q = load('../q.js') || root.q,
28 | q = q.q || q
29 | ));
30 |
31 | // The `utf8` object to be used in tests
32 | var utf8 = root.utf8 || (root.utf8 = (
33 | utf8 = load('../node_modules/utf8/utf8.js') || root.utf8,
34 | utf8 = utf8.utf8 || utf8
35 | ));
36 |
37 | /*--------------------------------------------------------------------------*/
38 |
39 | // `throws` is a reserved word in ES3; alias it to avoid errors
40 | var raises = QUnit.assert['throws'];
41 |
42 | // explicitly call `QUnit.module()` instead of `module()`
43 | // in case we are in a CLI environment
44 | QUnit.module('q');
45 |
46 | // UTF-8 '=C2=A1Hola, se=C3=B1or!' → '\xA1Hola, se\xF1or!'
47 |
48 | test('q.encode', function() {
49 | equal(
50 | q.encode(utf8.encode('If you believe that truth=beauty, then surely mathematics is the most beautiful branch of philosophy.')),
51 | 'If_you_believe_that_truth=3Dbeauty=2C_then_surely_mathematics_is_the_most_beautiful_branch_of_philosophy=2E',
52 | 'Equals sign'
53 | );
54 | equal(
55 | q.encode(utf8.encode('Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Typi non habent claritatem insitam; est usus legentis in iis qui facit eorum claritatem. Investigationes demonstraverunt lectores legere me lius quod ii legunt saepius. Claritas est etiam processus dynamicus, qui sequitur mutationem consuetudium lectorum. Mirum est notare quam littera gothica, quam nunc putamus parum claram, anteposuerit litterarum formas humanitatis per seacula quarta decima et quinta decima. Eodem modo typi, qui nunc nobis videntur parum clari, fiant sollemnes in futurum.')),
56 | 'Lorem_ipsum_dolor_sit_amet=2C_consectetuer_adipiscing_elit=2C_sed_diam_nonummy_nibh_euismod_tincidunt_ut_laoreet_dolore_magna_aliquam_erat_volutpat=2E_Ut_wisi_enim_ad_minim_veniam=2C_quis_nostrud_exerci_tation_ullamcorper_suscipit_lobortis_nisl_ut_aliquip_ex_ea_commodo_consequat=2E_Duis_autem_vel_eum_iriure_dolor_in_hendrerit_in_vulputate_velit_esse_molestie_consequat=2C_vel_illum_dolore_eu_feugiat_nulla_facilisis_at_vero_eros_et_accumsan_et_iusto_odio_dignissim_qui_blandit_praesent_luptatum_zzril_delenit_augue_duis_dolore_te_feugait_nulla_facilisi=2E_Nam_liber_tempor_cum_soluta_nobis_eleifend_option_congue_nihil_imperdiet_doming_id_quod_mazim_placerat_facer_possim_assum=2E_Typi_non_habent_claritatem_insitam=3B_est_usus_legentis_in_iis_qui_facit_eorum_claritatem=2E_Investigationes_demonstraverunt_lectores_legere_me_lius_quod_ii_legunt_saepius=2E_Claritas_est_etiam_processus_dynamicus=2C_qui_sequitur_mutationem_consuetudium_lectorum=2E_Mirum_est_notare_quam_littera_gothica=2C_quam_nunc_putamus_parum_claram=2C_anteposuerit_litterarum_formas_humanitatis_per_seacula_quarta_decima_et_quinta_decima=2E_Eodem_modo_typi=2C_qui_nunc_nobis_videntur_parum_clari=2C_fiant_sollemnes_in_futurum=2E',
57 | 'Long text'
58 | );
59 | equal(
60 | q.encode(utf8.encode('foo ')),
61 | 'foo_',
62 | 'Trailing space'
63 | );
64 | equal(
65 | q.encode(utf8.encode('foo\t')),
66 | 'foo=09',
67 | 'Trailing tab'
68 | );
69 | equal(
70 | q.encode(utf8.encode('foo\r\nbar')),
71 | 'foo=0D=0Abar',
72 | 'CRLF'
73 | );
74 | equal(
75 | q.encode(utf8.encode('fooI\xF1t\xEBrn\xE2ti\xF4n\xE0liz\xE6ti\xF8n\u2603\uD83D\uDCA9bar')),
76 | 'fooI=C3=B1t=C3=ABrn=C3=A2ti=C3=B4n=C3=A0liz=C3=A6ti=C3=B8n=E2=98=83=F0=9F=92=A9bar',
77 | 'Supports UTF-8-encoded input'
78 | );
79 | equal(
80 | q.encode('foo\0bar\xFFbaz'), // Note: no UTF-8-encoding
81 | 'foo=00bar=FFbaz',
82 | 'Lowest and highest octet values (U+0000 and U+00FF)'
83 | );
84 | equal(
85 | q.encode('ooh: ahh'),
86 | 'ooh=3A_ahh',
87 | 'colons'
88 | );
89 | raises(
90 | function() {
91 | // Note: “forgot” to UTF-8-encode first
92 | q.encode('fooI\xF1t\xEBrn\xE2ti\xF4n\xE0liz\xE6ti\xF8n\u2603\uD83D\uDCA9bar')
93 | },
94 | RangeError,
95 | 'Invalid input (input must be character-encoded into octets using any encoding)'
96 | );
97 | });
98 |
99 | test('q.decode', function() {
100 | equal(
101 | utf8.decode(q.decode('If_you_believe_that_truth=3Dbeauty,_then_surely_mathematics_is_the_most_beautiful_branch_of_philosophy.')),
102 | 'If you believe that truth=beauty, then surely mathematics is the most beautiful branch of philosophy.',
103 | 'Equals sign'
104 | );
105 | equal(
106 | utf8.decode(q.decode('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')),
107 | 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
108 | '76 * 2 characters'
109 | );
110 | equal(
111 | utf8.decode(q.decode('Now\'s_the_time_for_all_folk_to_come_to_the_aid_of_their_country.')),
112 | 'Now\'s the time for all folk to come to the aid of their country.',
113 | 'Soft line break example from the RFC'
114 | );
115 | equal(
116 | utf8.decode(q.decode('fooI=C3=B1t=C3=ABrn=C3=A2ti=C3=B4n=C3=A0liz=C3=A6ti=C3=B8n=E2=98=83=F0=9F=92=A9bar')),
117 | 'fooI\xF1t\xEBrn\xE2ti\xF4n\xE0liz\xE6ti\xF8n\u2603\uD83D\uDCA9bar',
118 | 'UTF-8-decoding Q-decoded UTF-8-encoded content'
119 | );
120 | });
121 |
122 | /*--------------------------------------------------------------------------*/
123 |
124 | // configure QUnit and call `QUnit.start()` for
125 | // Narwhal, Node.js, PhantomJS, Rhino, and RingoJS
126 | if (!root.document || root.phantom) {
127 | QUnit.config.noglobals = true;
128 | QUnit.start();
129 | }
130 | }(typeof global == 'object' && global || this));
131 |
--------------------------------------------------------------------------------