diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..986df3b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +indent_style = tab +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[{README.md,*.yml,*.d.ts}] +indent_style = space +indent_size = 2 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..29966a2 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,31 @@ +name: run-checks + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + publish: + runs-on: ubuntu-latest + strategy: + matrix: + # Include all major maintenance + active LTS + current Node.js versions. + # https://github.com/nodejs/Release#release-schedule + node: [12, 14, 16] + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up Node.js + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node }} + - name: Install dependencies + run: npm install + - name: Build + run: npm run build + - name: Test + run: npm test diff --git a/.github/workflows/publish-on-tag.yml b/.github/workflows/publish-on-tag.yml index 10d7ad5..b0e5835 100644 --- a/.github/workflows/publish-on-tag.yml +++ b/.github/workflows/publish-on-tag.yml @@ -3,7 +3,7 @@ name: publish-on-tag on: push: tags: - - '*' + - '*' jobs: publish: @@ -11,6 +11,12 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2 + - name: Install dependencies + run: npm install + - name: Build + run: npm run build + - name: Test + run: npm test - name: Publish env: NPM_TOKEN: ${{secrets.NPM_TOKEN}} diff --git a/Gruntfile.js b/Gruntfile.js deleted file mode 100644 index 74cf2ee..0000000 --- a/Gruntfile.js +++ /dev/null @@ -1,93 +0,0 @@ -module.exports = function(grunt) { - - grunt.initConfig({ - 'shell': { - 'options': { - 'stdout': true, - 'stderr': true, - 'failOnError': true - }, - 'transform-data': { - 'command': 'npm run build' - }, - 'cover-html': { - 'command': 'istanbul cover --report "html" --verbose --dir "coverage" "tests/tests.js"' - }, - 'cover-coveralls': { - 'command': 'istanbul cover --verbose --dir "coverage" "tests/tests.js" && coveralls < coverage/lcov.info; rm -rf coverage/lcov*' - }, - 'test-narwhal': { - 'command': 'echo "Testing in Narwhal..."; export NARWHAL_OPTIMIZATION=-1; narwhal "tests/tests.js"' - }, - 'test-phantomjs': { - 'command': 'echo "Testing in PhantomJS..."; phantomjs "tests/tests.js"' - }, - 'test-rhino': { - 'command': 'echo "Testing in Rhino..."; rhino -opt -1 "tests.js"', - 'options': { - 'execOptions': { - 'cwd': 'tests' - } - } - }, - 'test-ringo': { - 'command': 'echo "Testing in Ringo..."; ringo -o -1 "tests/tests.js"' - }, - 'test-node': { - 'command': 'echo "Testing in Node..."; node "tests/tests.js"' - }, - 'test-browser': { - 'command': 'echo "Testing in a browser..."; open "tests/index.html"' - } - }, - 'template': { - 'build': { - 'options': { - 'data': function() { - return require('./scripts/export-data.js'); - } - }, - 'files': { - 'koi8-r.js': ['src/koi8-r.js'] - } - }, - 'build-tests': { - 'options': { - 'data': function() { - return require('./scripts/export-data.js'); - } - }, - 'files': { - 'tests/tests.js': ['tests/tests.src.js'] - } - } - } - }); - - grunt.loadNpmTasks('grunt-shell'); - grunt.loadNpmTasks('grunt-template'); - - grunt.registerTask('cover', 'shell:cover-html'); - grunt.registerTask('ci', [ - 'shell:test-narwhal', - 'shell:test-phantomjs', - 'shell:test-rhino', - 'shell:test-ringo', - 'shell:test-node' - ]); - grunt.registerTask('test', [ - 'ci', - 'shell:test-browser' - ]); - - grunt.registerTask('build', [ - 'shell:transform-data', - 'default' - ]); - - grunt.registerTask('default', [ - 'template', - 'shell:test-node' - ]); - -}; diff --git a/README.md b/README.md index 9224a7c..39f95b9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# koi8-r [![koi8-r on npm](https://img.shields.io/npm/v/koi8-r)](https://www.npmjs.com/package/koi8-r) +# koi8-r [![Build status](https://github.com/mathiasbynens/koi8-r/workflows/run-checks/badge.svg)](https://github.com/mathiasbynens/koi8-r/actions?query=workflow%3Arun-checks) [![koi8-r on npm](https://img.shields.io/npm/v/koi8-r)](https://www.npmjs.com/package/koi8-r) _koi8-r_ is a robust JavaScript implementation of [the koi8-r character encoding as defined by the Encoding Standard](https://encoding.spec.whatwg.org/#koi8-r). diff --git a/data/index.txt b/data/index.txt index f8d6dff..639e9c4 100644 --- a/data/index.txt +++ b/data/index.txt @@ -1,11 +1,8 @@ -# Any copyright is dedicated to the Public Domain. -# https://creativecommons.org/publicdomain/zero/1.0/ -# # For details on index index-koi8-r.txt see the Encoding Standard # https://encoding.spec.whatwg.org/ # # Identifier: c5497cd9071cb352c0e56b219154e539badf63de40b71578f09e2e11fe7d50ae -# Date: 2016-01-20 +# Date: 2018-01-06 0 0x2500 ─ (BOX DRAWINGS LIGHT HORIZONTAL) 1 0x2502 │ (BOX DRAWINGS LIGHT VERTICAL) diff --git a/package.json b/package.json index 348be12..ad4c72e 100644 --- a/package.json +++ b/package.json @@ -35,18 +35,12 @@ "scripts": { "download": "curl https://encoding.spec.whatwg.org/index-koi8-r.txt > data/index.txt", "build": "node scripts/transform-data.js", - "test": "node tests/tests.js" + "test": "node tests/tests.js", + "cover": "istanbul cover --report html --verbose --dir coverage tests/tests.js" }, "devDependencies": { - "coveralls": "^2.11.6", - "grunt": "^0.4.5", - "grunt-shell": "^1.1.2", - "grunt-template": "^0.2.3", "istanbul": "^0.4.2", - "jsesc": "^2.1.0", - "qunit-extras": "^1.4.5", - "qunitjs": "~1.11.0", - "requirejs": "^2.1.22", - "string.fromcodepoint": "^0.2.1" + "jsesc": "^3.0.2", + "lodash.template": "^4.5.0" } } diff --git a/scripts/export-data.js b/scripts/export-data.js index 8b9e2c9..8fa0665 100644 --- a/scripts/export-data.js +++ b/scripts/export-data.js @@ -1,18 +1,18 @@ -var fs = require('fs'); -var jsesc = require('jsesc'); +const fs = require('fs'); +const jsesc = require('jsesc'); function readJSON(path) { return JSON.parse(fs.readFileSync(path, 'utf-8')); } module.exports = { - 'labels': jsesc(readJSON('data/labels.json'), { - 'compact': false, - 'indentLevel': 2 + labels: jsesc(readJSON('data/labels.json'), { + compact: false, + indentLevel: 2, }), - 'encoded': jsesc(readJSON('data/encoded.json'), { 'wrap': true }), - 'decoded': jsesc(readJSON('data/decoded.json'), { 'wrap': true }), - 'indexByCodePoint': jsesc(readJSON('data/index-by-code-point.json')), - 'indexByPointer': jsesc(readJSON('data/index-by-pointer.json')), - 'version': readJSON('package.json').version + encoded: jsesc(readJSON('data/encoded.json'), { wrap: true }), + decoded: jsesc(readJSON('data/decoded.json'), { wrap: true }), + indexByCodePoint: jsesc(readJSON('data/index-by-code-point.json')), + indexByPointer: jsesc(readJSON('data/index-by-pointer.json')), + version: readJSON('package.json').version }; diff --git a/scripts/transform-data.js b/scripts/transform-data.js index 9871c23..3d27d51 100644 --- a/scripts/transform-data.js +++ b/scripts/transform-data.js @@ -1,43 +1,43 @@ -var fs = require('fs'); -var jsesc = require('jsesc'); -require('string.fromcodepoint'); +const fs = require('fs'); +const jsesc = require('jsesc'); +const template = require('lodash.template'); function format(object) { return jsesc(object, { - 'json': true, - 'compact': false + json: true, + compact: false, }) + '\n'; } function parse(source) { - var indexByCodePoint = {}; - var indexByPointer = {}; - var decoded = ''; - var encoded = ''; + const indexByCodePoint = {}; + const indexByPointer = {}; + let decoded = ''; + let encoded = ''; var lines = source.split('\n'); - lines.forEach(function(line) { - var data = line.trim().split('\t'); + for (const line of lines) { + const data = line.trim().split('\t'); if (data.length != 3) { - return; + continue; } - var pointer = Number(data[0]); - var codePoint = Number(data[1]); - var symbol = String.fromCodePoint(codePoint); + const pointer = Number(data[0]); + const codePoint = Number(data[1]); + const symbol = String.fromCodePoint(codePoint); decoded += symbol; encoded += String.fromCodePoint(pointer + 0x80); indexByCodePoint[codePoint] = pointer; indexByPointer[pointer] = symbol; - }); + } return { - 'decoded': decoded, - 'encoded': encoded, - 'indexByCodePoint': indexByCodePoint, - 'indexByPointer': indexByPointer + decoded: decoded, + encoded: encoded, + indexByCodePoint: indexByCodePoint, + indexByPointer: indexByPointer }; } -var source = fs.readFileSync('./data/index.txt', 'utf-8'); -var result = parse(source); +const source = fs.readFileSync('./data/index.txt', 'utf-8'); +const result = parse(source); fs.writeFileSync( './data/index-by-code-point.json', format(result.indexByCodePoint) @@ -54,3 +54,19 @@ fs.writeFileSync( './data/encoded.json', format(result.encoded) ); + +// tests/tests.src.js → tests/tests.js +const TEST_TEMPLATE = fs.readFileSync('./tests/tests.src.js', 'utf8'); +const createTest = template(TEST_TEMPLATE, { + interpolate: /<\%=([\s\S]+?)%\>/g, +}); +const testCode = createTest(require('./export-data.js')); +fs.writeFileSync('./tests/tests.js', testCode); + +// src/koi8-r.src.js -> koi8-r.js +const LIB_TEMPLATE = fs.readFileSync('./src/koi8-r.src.js', 'utf8'); +const createLib = template(LIB_TEMPLATE, { + interpolate: /<\%=([\s\S]+?)%\>/g, +}); +const libCode = createLib(require('./export-data.js')); +fs.writeFileSync('./koi8-r.js', libCode); diff --git a/src/koi8-r.js b/src/koi8-r.src.js similarity index 69% rename from src/koi8-r.js rename to src/koi8-r.src.js index 989b6ee..c7d7f2d 100644 --- a/src/koi8-r.js +++ b/src/koi8-r.src.js @@ -1,21 +1,5 @@ /*! https://mths.be/koi8-r v<%= version %> by @mathias | MIT license */ -;(function(root) { - - // Detect free variables `exports`. - var freeExports = typeof exports == 'object' && exports; - - // Detect free variable `module`. - var freeModule = typeof module == 'object' && module && - module.exports == freeExports && module; - - // Detect free variable `global`, from Node.js/io.js or Browserified code, - // and use it as `root`. - var freeGlobal = typeof global == 'object' && global; - if (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal) { - root = freeGlobal; - } - - /*--------------------------------------------------------------------------*/ +;(function() { var object = {}; var hasOwnProperty = object.hasOwnProperty; @@ -33,7 +17,7 @@ return '&#' + codePoint + ';'; } // Else, `mode == 'fatal'`. - throw Error(); + throw new Error(); }; // https://encoding.spec.whatwg.org/#single-byte-decoder @@ -113,32 +97,12 @@ }; var koi8r = { - 'encode': encode, - 'decode': decode, - 'labels': <%= labels %>, - 'version': '<%= version %>' + encode: encode, + decode: decode, + labels: <%= labels %>, + version: '<%= version %>', }; - // Some AMD build optimizers, like r.js, check for specific condition patterns - // like the following: - if ( - typeof define == 'function' && - typeof define.amd == 'object' && - define.amd - ) { - define(function() { - return koi8r; - }); - } else if (freeExports && !freeExports.nodeType) { - if (freeModule) { // in Node.js, io.js or RingoJS v0.8.0+ - freeModule.exports = koi8r; - } else { // in Narwhal or RingoJS v0.7.0- - for (var key in koi8r) { - koi8r.hasOwnProperty(key) && (freeExports[key] = koi8r[key]); - } - } - } else { // in Rhino or a web browser - root.koi8r = koi8r; - } + module.exports = koi8r; -}(this)); +}()); diff --git a/tests/tests.src.js b/tests/tests.src.js index 09e688e..dd9ffdc 100644 --- a/tests/tests.src.js +++ b/tests/tests.src.js @@ -1,158 +1,111 @@ -(function(root) { - 'use strict'; +const assert = require('assert'); - var noop = Function.prototype; +const koi8r = require('../koi8-r.js'); - var load = (typeof require == 'function' && !(root.define && define.amd)) ? - require : - (!root.document && root.java && root.load) || noop; +console.log('Testing `koi8r.encode`…'); +assert.strictEqual( + koi8r.encode('\0\x01\x02\x03\x04\x05\x06\x07\b\t\n\x0B\f\r\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7F'), + '\0\x01\x02\x03\x04\x05\x06\x07\b\t\n\x0B\f\r\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7F', + 'U+0000 to U+007F remain unchanged' +); +assert.strictEqual( + koi8r.encode(<%= decoded %>), + <%= encoded %>, + 'Encoding all other symbols in the character set' +); +assert.throws( + () => { + koi8r.encode('\uFFFF'); + }, + Error, + 'Encoding a code point that is invalid for this encoding throws an error in `fatal` mode, which is the implied default for `encode()`' +); +assert.throws( + () => { + koi8r.encode('\uFFFF', { mode: 'fatal' }); + }, + Error, + 'Encoding a code point that is invalid for this encoding throws an error in `fatal` mode' +); +assert.throws( + () => { + koi8r.encode('\uFFFF', { mode: 'FATAL' }); + }, + Error, + 'Mode names are case-insensitive' +); +assert.throws( + () => { + koi8r.encode('\uFFFF', { mode: 'fAtAl' }); + }, + Error, + 'Mode names are case-insensitive' +); +assert.strictEqual( + koi8r.encode('\uFFFF', { mode: 'html' }), + '￿', + 'Encoding a code point that is invalid for this encoding returns an HTML entity in `html` mode' +); +assert.strictEqual( + koi8r.encode('\uFFFF', { mode: 'HTML' }), + '￿', + 'Mode names are case-insensitive' +); +assert.strictEqual( + koi8r.encode('\uFFFF', { mode: 'hTmL' }), + '￿', + 'Mode names are case-insensitive' +); - var QUnit = (function() { - return root.QUnit || ( - root.addEventListener || (root.addEventListener = noop), - root.setTimeout || (root.setTimeout = noop), - root.QUnit = load('../node_modules/qunitjs/qunit/qunit.js') || root.QUnit, - addEventListener === noop && delete root.addEventListener, - root.QUnit - ); - }()); - - var qe = load('../node_modules/qunit-extras/qunit-extras.js'); - if (qe) { - qe.runInContext(root); - } - - // The `koi8r` object to test - var koi8r = root.koi8r || (root.koi8r = ( - koi8r = load('../koi8-r.js') || root.koi8r, - koi8r = koi8r.koi8r || koi8r - )); - - /*--------------------------------------------------------------------------*/ - - // `throws` is a reserved word in ES3; alias it to avoid errors - var raises = QUnit.assert['throws']; - - // explicitly call `QUnit.module()` instead of `module()` - // in case we are in a CLI environment - QUnit.module('koi8-r'); - - test('koi8r.encode', function() { - equal( - koi8r.encode('\0\x01\x02\x03\x04\x05\x06\x07\b\t\n\x0B\f\r\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7F'), - '\0\x01\x02\x03\x04\x05\x06\x07\b\t\n\x0B\f\r\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7F', - 'U+0000 to U+007F remain unchanged' - ); - equal( - koi8r.encode(<%= decoded %>), - <%= encoded %>, - 'Encoding all other symbols in the character set' - ); - raises( - function() { - koi8r.encode('\uFFFF'); - }, - Error, - 'Encoding a code point that is invalid for this encoding throws an error in `fatal` mode, which is the implied default for `encode()`' - ); - raises( - function() { - koi8r.encode('\uFFFF', { 'mode': 'fatal' }); - }, - Error, - 'Encoding a code point that is invalid for this encoding throws an error in `fatal` mode' - ); - raises( - function() { - koi8r.encode('\uFFFF', { 'mode': 'FATAL' }); - }, - Error, - 'Mode names are case-insensitive' - ); - raises( - function() { - koi8r.encode('\uFFFF', { 'mode': 'fAtAl' }); - }, - Error, - 'Mode names are case-insensitive' - ); - equal( - koi8r.encode('\uFFFF', { 'mode': 'html' }), - '￿', - 'Encoding a code point that is invalid for this encoding returns an HTML entity in `html` mode' - ); - equal( - koi8r.encode('\uFFFF', { 'mode': 'HTML' }), - '￿', - 'Mode names are case-insensitive' - ); - equal( - koi8r.encode('\uFFFF', { 'mode': 'hTmL' }), - '￿', - 'Mode names are case-insensitive' - ); - }); - - test('koi8r.decode', function() { - equal( - koi8r.decode('\0\x01\x02\x03\x04\x05\x06\x07\b\t\n\x0B\f\r\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7F'), - '\0\x01\x02\x03\x04\x05\x06\x07\b\t\n\x0B\f\r\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7F', - 'U+0000 to U+007F remain unchanged' - ); - equal( - koi8r.decode(<%= encoded %>), - <%= decoded %>, - 'Decoding all other symbols in the character set' - ); - equal( - koi8r.decode('\uFFFF'), - '\uFFFD', - 'Decoding a byte that is invalid for this encoding returns U+FFFD in `replacement` mode, which is the implied default for `decode()`' - ); - equal( - koi8r.decode('\uFFFF', { 'mode': 'replacement' }), - '\uFFFD', - 'Decoding a byte that is invalid for this encoding returns U+FFFD in `replacement` mode' - ); - equal( - koi8r.decode('\uFFFF', { 'mode': 'REPLACEMENT' }), - '\uFFFD', - 'Mode names are case-insensitive' - ); - equal( - koi8r.decode('\uFFFF', { 'mode': 'rEpLaCeMeNt' }), - '\uFFFD', - 'Mode names are case-insensitive' - ); - raises( - function() { - koi8r.decode('\uFFFF', { 'mode': 'fatal' }); - }, - Error, - 'Decoding a byte that is invalid for this encoding throws an error in `fatal` mode' - ); - raises( - function() { - koi8r.decode('\uFFFF', { 'mode': 'FATAL' }); - }, - Error, - 'Decoding a byte that is invalid for this encoding throws an error in `fatal` mode' - ); - raises( - function() { - koi8r.decode('\uFFFF', { 'mode': 'fAtAl' }); - }, - Error, - 'Mode names are case-insensitive' - ); - }); - - /*--------------------------------------------------------------------------*/ - - // configure QUnit and call `QUnit.start()` for - // Narwhal, Node.js, PhantomJS, Rhino, and RingoJS - if (!root.document || root.phantom) { - QUnit.config.noglobals = true; - QUnit.start(); - } -}(typeof global == 'object' && global || this)); +console.log('Testing `koi8r.decode`…'); +assert.strictEqual( + koi8r.decode('\0\x01\x02\x03\x04\x05\x06\x07\b\t\n\x0B\f\r\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7F'), + '\0\x01\x02\x03\x04\x05\x06\x07\b\t\n\x0B\f\r\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7F', + 'U+0000 to U+007F remain unchanged' +); +assert.strictEqual( + koi8r.decode(<%= encoded %>), + <%= decoded %>, + 'Decoding all other symbols in the character set' +); +assert.strictEqual( + koi8r.decode('\uFFFF'), + '\uFFFD', + 'Decoding a byte that is invalid for this encoding returns U+FFFD in `replacement` mode, which is the implied default for `decode()`' +); +assert.strictEqual( + koi8r.decode('\uFFFF', { mode: 'replacement' }), + '\uFFFD', + 'Decoding a byte that is invalid for this encoding returns U+FFFD in `replacement` mode' +); +assert.strictEqual( + koi8r.decode('\uFFFF', { mode: 'REPLACEMENT' }), + '\uFFFD', + 'Mode names are case-insensitive' +); +assert.strictEqual( + koi8r.decode('\uFFFF', { mode: 'rEpLaCeMeNt' }), + '\uFFFD', + 'Mode names are case-insensitive' +); +assert.throws( + () => { + koi8r.decode('\uFFFF', { mode: 'fatal' }); + }, + Error, + 'Decoding a byte that is invalid for this encoding throws an error in `fatal` mode' +); +assert.throws( + () => { + koi8r.decode('\uFFFF', { mode: 'FATAL' }); + }, + Error, + 'Decoding a byte that is invalid for this encoding throws an error in `fatal` mode' +); +assert.throws( + () => { + koi8r.decode('\uFFFF', { mode: 'fAtAl' }); + }, + Error, + 'Mode names are case-insensitive' +);