diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..de8c630 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,18 @@ +{ + "root": true, + + "extends": "@ljharb", + + "rules": { + "id-length": 0, + "new-cap": [2, { + "capIsNewExceptions": [ + "CodePointAt", + "RequireObjectCoercible", + "SymbolDescriptiveString", + "ToString", + "UTF16EncodeCodePoint", + ], + }], + }, +} diff --git a/.github/workflows/node-aught.yml b/.github/workflows/node-aught.yml new file mode 100644 index 0000000..f3cddd8 --- /dev/null +++ b/.github/workflows/node-aught.yml @@ -0,0 +1,18 @@ +name: 'Tests: node.js < 10' + +on: [pull_request, push] + +jobs: + tests: + uses: ljharb/actions/.github/workflows/node.yml@main + with: + range: '< 10' + type: minors + command: npm run tests-only + + node: + name: 'node < 10' + needs: [tests] + runs-on: ubuntu-latest + steps: + - run: 'echo tests completed' diff --git a/.github/workflows/node-esm.yml b/.github/workflows/node-esm.yml new file mode 100644 index 0000000..6d387f2 --- /dev/null +++ b/.github/workflows/node-esm.yml @@ -0,0 +1,9 @@ +name: 'Tests: node.js (ESM)' + +on: [pull_request, push] + +jobs: + tests: + uses: ljharb/actions/.github/workflows/node-esm.yml@main + with: + command: npm run tests-esm \ No newline at end of file diff --git a/.github/workflows/node-pretest.yml b/.github/workflows/node-pretest.yml new file mode 100644 index 0000000..765edf7 --- /dev/null +++ b/.github/workflows/node-pretest.yml @@ -0,0 +1,7 @@ +name: 'Tests: pretest/posttest' + +on: [pull_request, push] + +jobs: + tests: + uses: ljharb/actions/.github/workflows/pretest.yml@main diff --git a/.github/workflows/node-tens.yml b/.github/workflows/node-tens.yml new file mode 100644 index 0000000..b49ceb1 --- /dev/null +++ b/.github/workflows/node-tens.yml @@ -0,0 +1,18 @@ +name: 'Tests: node.js >= 10' + +on: [pull_request, push] + +jobs: + tests: + uses: ljharb/actions/.github/workflows/node.yml@main + with: + range: '>= 10' + type: minors + command: npm run tests-only + + node: + name: 'node >= 10' + needs: [tests] + runs-on: ubuntu-latest + steps: + - run: 'echo tests completed' diff --git a/.github/workflows/rebase.yml b/.github/workflows/rebase.yml new file mode 100644 index 0000000..5b6d04b --- /dev/null +++ b/.github/workflows/rebase.yml @@ -0,0 +1,15 @@ +name: Automatic Rebase + +on: [pull_request_target] + +jobs: + _: + name: "Automatic Rebase" + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - uses: ljharb/rebase@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/require-allow-edits.yml b/.github/workflows/require-allow-edits.yml new file mode 100644 index 0000000..7b842f8 --- /dev/null +++ b/.github/workflows/require-allow-edits.yml @@ -0,0 +1,12 @@ +name: Require “Allow Edits” + +on: [pull_request_target] + +jobs: + _: + name: "Require “Allow Edits”" + + runs-on: ubuntu-latest + + steps: + - uses: ljharb/require-allow-edits@main diff --git a/.gitignore b/.gitignore index 52ab08e..335819f 100644 --- a/.gitignore +++ b/.gitignore @@ -107,3 +107,5 @@ dist npm-shrinkwrap.json package-lock.json yarn.lock + +.npmignore diff --git a/.npmrc b/.npmrc index 43c97e7..eacea13 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,3 @@ package-lock=false +allow-same-version=true +message=v%s diff --git a/.nycrc b/.nycrc new file mode 100644 index 0000000..bdd626c --- /dev/null +++ b/.nycrc @@ -0,0 +1,9 @@ +{ + "all": true, + "check-coverage": false, + "reporter": ["text-summary", "text", "html", "json"], + "exclude": [ + "coverage", + "test" + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..03a962f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). diff --git a/README.md b/README.md index 8c3623a..f3a0ab1 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,83 @@ -# String.prototype.toWellFormed +# string.prototype.towellformed [![Version Badge][npm-version-svg]][package-url] + +[![github actions][actions-image]][actions-url] +[![coverage][codecov-image]][codecov-url] +[![dependency status][deps-svg]][deps-url] +[![dev dependency status][dev-deps-svg]][dev-deps-url] +[![License][license-image]][license-url] +[![Downloads][downloads-image]][downloads-url] + +[![npm badge][npm-badge-png]][package-url] + An ESnext spec-compliant `String.prototype.toWellFormed` shim/polyfill/replacement that works as far down as ES3. + +This package implements the [es-shim API](https://github.com/es-shims/api) interface. It works in an ES3-supported environment and complies with the proposed [spec](https://tc39.es/proposal-is-usv-string/). + +Because `String.prototype.toWellFormed` depends on a receiver (the `this` value), the main export takes the string to operate on as the first argument. + +## Getting started + +```sh +npm install --save string.prototype.towellformed +``` + +## Usage/Examples + +```js +var toWellFormed = require('string.prototype.towellformed'); +var assert = require('assert'); + +var leadingPoo = '\uD83D'; +var trailingPoo = '\uDCA9'; +var wholePoo = leadingPoo + trailingPoo; + +var replacementChar = '\uFFFD'; + +assert.equal(toWellFormed(wholePoo), wholePoo); +assert.equal(toWellFormed(leadingPoo), replacementChar); +assert.equal(toWellFormed(trailingPoo), replacementChar); +``` + +```js +var toWellFormed = require('string.prototype.towellformed'); +var assert = require('assert'); +/* when String#toWellFormed is not present */ +delete String.prototype.toWellFormed; +var shimmed = toWellFormed.shim(); + +assert.equal(shimmed, toWellFormed.getPolyfill()); +assert.deepEqual(wholePoo.toWellFormed(), toWellFormed(wholePoo)); +assert.deepEqual(leadingPoo.toWellFormed(), toWellFormed(leadingPoo)); +assert.deepEqual(trailingPoo.toWellFormed(), toWellFormed(trailingPoo)); +``` + +```js +var toWellFormed = require('string.prototype.towellformed'); +var assert = require('assert'); +/* when String#at is present */ +var shimmed = toWellFormed.shim(); + +assert.equal(shimmed, String.prototype.toWellFormed); +assert.deepEqual(wholePoo.toWellFormed(), toWellFormed(wholePoo)); +assert.deepEqual(leadingPoo.toWellFormed(), toWellFormed(leadingPoo)); +assert.deepEqual(trailingPoo.toWellFormed(), toWellFormed(trailingPoo)); +``` + +## Tests +Simply clone the repo, `npm install`, and run `npm test` + +[package-url]: https://npmjs.org/package/string.prototype.towellformed +[npm-version-svg]: https://versionbadg.es/es-shims/String.prototype.toWellFormed.svg +[deps-svg]: https://david-dm.org/es-shims/String.prototype.toWellFormed.svg +[deps-url]: https://david-dm.org/es-shims/String.prototype.toWellFormed +[dev-deps-svg]: https://david-dm.org/es-shims/String.prototype.toWellFormed/dev-status.svg +[dev-deps-url]: https://david-dm.org/es-shims/String.prototype.toWellFormed#info=devDependencies +[npm-badge-png]: https://nodei.co/npm/string.prototype.towellformed.png?downloads=true&stars=true +[license-image]: https://img.shields.io/npm/l/string.prototype.towellformed.svg +[license-url]: LICENSE +[downloads-image]: https://img.shields.io/npm/dm/string.prototype.towellformed.svg +[downloads-url]: https://npm-stat.com/charts.html?package=string.prototype.towellformed +[codecov-image]: https://codecov.io/gh/es-shims/String.prototype.toWellFormed/branch/main/graphs/badge.svg +[codecov-url]: https://app.codecov.io/gh/es-shims/String.prototype.toWellFormed/ +[actions-image]: https://img.shields.io/endpoint?url=https://github-actions-badge-u3jn4tfpocch.runkit.sh/es-shims/String.prototype.toWellFormed +[actions-url]: https://github.com/es-shims/String.prototype.toWellFormed/actions diff --git a/auto.js b/auto.js new file mode 100644 index 0000000..8ebf606 --- /dev/null +++ b/auto.js @@ -0,0 +1,3 @@ +'use strict'; + +require('./shim')(); diff --git a/implementation.js b/implementation.js new file mode 100644 index 0000000..a08e78d --- /dev/null +++ b/implementation.js @@ -0,0 +1,24 @@ +'use strict'; + +var CodePointAt = require('es-abstract/2022/CodePointAt'); +// var UTF16EncodeCodePoint = require('es-abstract/2022/UTF16EncodeCodePoint'); +var RequireObjectCoercible = require('es-abstract/2022/RequireObjectCoercible'); +var ToString = require('es-abstract/2022/ToString'); + +module.exports = function toWellFormed() { + var O = RequireObjectCoercible(this); // step 1 + var S = ToString(O); // step 2 + var strLen = S.length; // step 3 + var k = 0; // step 4 + var result = ''; // step 5 + while (k < strLen) { // step 6 + var cp = CodePointAt(S, k); // step 6.a + if (cp['[[IsUnpairedSurrogate]]']) { // step 6.b + result += '\uFFFD'; // step 6.b.i + } else { // step 6.c + result += cp['[[CodePoint]]']; // UTF16EncodeCodePoint(cp['[[CodePoint]]']); // step 6.c.i + } + k += cp['[[CodeUnitCount]]']; // step 6.d + } + return result; // step 7 +}; diff --git a/index.js b/index.js new file mode 100644 index 0000000..fff1034 --- /dev/null +++ b/index.js @@ -0,0 +1,24 @@ +'use strict'; + +var define = require('define-properties'); +var RequireObjectCoercible = require('es-abstract/2022/RequireObjectCoercible'); +var callBind = require('call-bind'); + +var implementation = require('./implementation'); + +var getPolyfill = require('./polyfill'); +var bound = callBind(getPolyfill()); + +var shim = require('./shim'); + +var boundShim = function toWellFormed(string) { + RequireObjectCoercible(string); + return bound(string); +}; +define(boundShim, { + getPolyfill: getPolyfill, + implementation: implementation, + shim: shim +}); + +module.exports = boundShim; diff --git a/index.mjs b/index.mjs new file mode 100644 index 0000000..917f04e --- /dev/null +++ b/index.mjs @@ -0,0 +1,15 @@ +import callBind from 'call-bind'; +import RequireObjectCoercible from 'es-abstract/2022/RequireObjectCoercible.js'; + +import getPolyfill from 'string.prototype.towellformed/polyfill'; + +const bound = callBind(getPolyfill()); + +export default function toWellFormed(string) { + RequireObjectCoercible(string); + return bound(string); +} + +export { default as getPolyfill } from 'string.prototype.towellformed/polyfill'; +export { default as implementation } from 'string.prototype.towellformed/implementation'; +export { default as shim } from 'string.prototype.towellformed/shim'; diff --git a/package.json b/package.json new file mode 100644 index 0000000..00e11b5 --- /dev/null +++ b/package.json @@ -0,0 +1,96 @@ +{ + "name": "string.prototype.towellformed", + "version": "0.0.0", + "description": "An ESnext spec-compliant `String.prototype.toWellFormed` shim/polyfill/replacement that works as far down as ES3.", + "main": "index.js", + "exports": { + ".": [ + { + "import": "./index.mjs", + "require": "./index.js", + "default": "./index.js" + }, + "./index.js" + ], + "./auto": "./auto.js", + "./polyfill": "./polyfill.js", + "./implementation": "./implementation.js", + "./shim": "./shim.js", + "./package.json": "./package.json" + }, + "directories": { + "test": "test" + }, + "scripts": { + "prepack": "npmignore --auto --commentLines=autogenerated", + "prepublish": "not-in-publish || npm run prepublishOnly", + "prepublishOnly": "safe-publish-latest", + "pretest": "npm run lint", + "lint": "eslint --ext=js,mjs .", + "postlint": "es-shim-api --bound", + "tests-only": "nyc tape 'test/**/*.js'", + "tests-esm": "nyc node test/index.mjs", + "test": "npm run tests-only && npm run tests-esm", + "posttest": "aud --production", + "version": "auto-changelog && git add CHANGELOG.md", + "postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\"" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/es-shims/String.prototype.toWellFormed.git" + }, + "keywords": [ + "javascript", + "ecmascript", + "polyfill", + "shim", + "es-shim API", + "unicode", + "string", + "surrogate", + "pair", + "well-formed", + "String.prototype.toWellFormed" + ], + "author": "Jordan Harband ", + "funding": { + "url": "https://github.com/sponsors/ljharb" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/es-shims/String.prototype.toWellFormed/issues" + }, + "homepage": "https://github.com/es-shims/String.prototype.toWellFormed#readme", + "devDependencies": { + "@es-shims/api": "^2.2.3", + "@ljharb/eslint-config": "^21.0.0", + "aud": "^2.0.0", + "auto-changelog": "^2.4.0", + "es-value-fixtures": "^1.4.2", + "eslint": "=8.8.0", + "functions-have-names": "^1.2.3", + "has-strict-mode": "^1.0.1", + "npmignore": "^0.3.0", + "nyc": "^10.3.2", + "safe-publish-latest": "^2.0.0", + "tape": "^5.6.0" + }, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.2" + }, + "auto-changelog": { + "output": "CHANGELOG.md", + "template": "keepachangelog", + "unreleased": false, + "commitLimit": false, + "backfillLimit": false, + "hideCredit": true + }, + "publishConfig": { + "ignore": [ + ".github/workflows" + ] + } +} diff --git a/polyfill.js b/polyfill.js new file mode 100644 index 0000000..2de8dc9 --- /dev/null +++ b/polyfill.js @@ -0,0 +1,7 @@ +'use strict'; + +var implementation = require('./implementation'); + +module.exports = function getPolyfill() { + return String.prototype.toWellFormed || implementation; +}; diff --git a/shim.js b/shim.js new file mode 100644 index 0000000..b4598e7 --- /dev/null +++ b/shim.js @@ -0,0 +1,14 @@ +'use strict'; + +var define = require('define-properties'); +var getPolyfill = require('./polyfill'); + +module.exports = function shimStringPrototypeToWellFormed() { + var polyfill = getPolyfill(); + define( + String.prototype, + { toWellFormed: polyfill }, + { toWellFormed: function () { return String.prototype.toWellFormed !== polyfill; } } + ); + return polyfill; +}; diff --git a/test/implementation.js b/test/implementation.js new file mode 100644 index 0000000..4dd2a4a --- /dev/null +++ b/test/implementation.js @@ -0,0 +1,20 @@ +'use strict'; + +var implementation = require('../implementation'); +var callBind = require('call-bind'); +var test = require('tape'); +var hasStrictMode = require('has-strict-mode')(); +var runTests = require('./tests'); + +test('as a function', function (t) { + t.test('bad first arg/receiver', { skip: !hasStrictMode }, function (st) { + /* eslint no-useless-call: 0 */ + st['throws'](function () { implementation.call(undefined); }, TypeError, 'undefined is not an object'); + st['throws'](function () { implementation.call(null); }, TypeError, 'null is not an object'); + st.end(); + }); + + runTests(callBind(implementation), t); + + t.end(); +}); diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..c4f13d5 --- /dev/null +++ b/test/index.js @@ -0,0 +1,17 @@ +'use strict'; + +var bound = require('../'); +var test = require('tape'); +var runTests = require('./tests'); + +test('as a function', function (t) { + t.test('bad receiver', function (st) { + st['throws'](bound.bind(null, undefined), TypeError, 'undefined is not an object'); + st['throws'](bound.bind(null, null), TypeError, 'null is not an object'); + st.end(); + }); + + runTests(bound, t); + + t.end(); +}); diff --git a/test/index.mjs b/test/index.mjs new file mode 100644 index 0000000..478a190 --- /dev/null +++ b/test/index.mjs @@ -0,0 +1,31 @@ +import bound from 'string.prototype.towellformed'; +import * as Module from 'string.prototype.towellformed'; +import test from 'tape'; +import runTests from './tests.js'; + +test('as a function', (t) => { + t.test('bad receiver', (st) => { + st.throws(() => bound(undefined), TypeError, 'undefined is not an object'); + st.throws(() => bound(null), TypeError, 'null is not an object'); + st.end(); + }); + + runTests(bound, t); + + t.end(); +}); + +test('named exports', async (t) => { + t.deepEqual( + Object.keys(Module).sort(), + ['default', 'shim', 'getPolyfill', 'implementation'].sort(), + 'has expected named exports', + ); + + const { shim, getPolyfill, implementation } = Module; + t.equal((await import('string.prototype.towellformed/shim')).default, shim, 'shim named export matches deep export'); + t.equal((await import('string.prototype.towellformed/implementation')).default, implementation, 'implementation named export matches deep export'); + t.equal((await import('string.prototype.towellformed/polyfill')).default, getPolyfill, 'getPolyfill named export matches deep export'); + + t.end(); +}); diff --git a/test/shimmed.js b/test/shimmed.js new file mode 100644 index 0000000..bfa8b52 --- /dev/null +++ b/test/shimmed.js @@ -0,0 +1,35 @@ +'use strict'; + +require('../auto'); + +var test = require('tape'); +var defineProperties = require('define-properties'); +var callBind = require('call-bind'); +var isEnumerable = Object.prototype.propertyIsEnumerable; +var functionsHaveNames = require('functions-have-names')(); +var hasStrictMode = require('has-strict-mode')(); + +var runTests = require('./tests'); + +test('shimmed', function (t) { + t.equal(String.prototype.toWellFormed.length, 0, 'String#toWellFormed has a length of 0'); + t.test('Function name', { skip: !functionsHaveNames }, function (st) { + st.equal(String.prototype.toWellFormed.name, 'toWellFormed', 'String#toWellFormed has name "toWellFormed"'); + st.end(); + }); + + t.test('enumerability', { skip: !defineProperties.supportsDescriptors }, function (et) { + et.equal(false, isEnumerable.call(String.prototype, 'toWellFormed'), 'String#toWellFormed is not enumerable'); + et.end(); + }); + + t.test('bad receiver', { skip: !hasStrictMode }, function (st) { + st['throws'](function () { return String.prototype.toWellFormed.call(undefined); }, TypeError, 'undefined is not an object'); + st['throws'](function () { return String.prototype.toWellFormed.call(null); }, TypeError, 'null is not an object'); + st.end(); + }); + + runTests(callBind(String.prototype.toWellFormed), t); + + t.end(); +}); diff --git a/test/tests.js b/test/tests.js new file mode 100644 index 0000000..632fc44 --- /dev/null +++ b/test/tests.js @@ -0,0 +1,45 @@ +'use strict'; + +var v = require('es-value-fixtures'); +var forEach = require('for-each'); +var inspect = require('object-inspect'); + +var SymbolDescriptiveString = require('es-abstract/2022/SymbolDescriptiveString'); + +var leadingPoo = '\uD83D'; +var trailingPoo = '\uDCA9'; +var wholePoo = leadingPoo + trailingPoo; + +var replacementChar = '\uFFFD'; + +module.exports = function (toWellFormed, t) { + t.test('well-formed strings', function (st) { + forEach(v.nonStrings.concat(v.strings, wholePoo), function (value) { + if (value != null) { // eslint-disable-line eqeqeq + var string = typeof value === 'symbol' ? SymbolDescriptiveString(value) : String(value); + st.equal( + toWellFormed(string), + string, + inspect(string) + ' (from ' + inspect(value) + ') is well-formed' + ); + } + }); + + st.end(); + }); + + t.test('not well-formed strings', function (st) { + st.equal( + toWellFormed(leadingPoo), + replacementChar, + 'a string with a leading surrogate but no trailing surrogate has the lone surrogate replaced as expected' + ); + st.equal( + toWellFormed(trailingPoo), + replacementChar, + 'a string with a trailing surrogate but no leading surrogate has the lone surrogate replaced as expected' + ); + + st.end(); + }); +};