├── .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 [![Build status](https://travis-ci.org/mathiasbynens/q-encoding.svg?branch=master)](https://travis-ci.org/mathiasbynens/q-encoding) [![Dependency status](https://gemnasium.com/mathiasbynens/q-encoding.svg)](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 | | [![twitter/mathias](https://gravatar.com/avatar/24e08a9ea84deb17ae121074d0f17125?s=70)](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 | --------------------------------------------------------------------------------