From 62e3f55c23611f25bbb03aba8b5c77e488537b72 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Tue, 13 Sep 2022 15:59:55 -0700 Subject: [PATCH] Initial implementation, tests, readme --- .eslintrc | 18 +++++ .github/workflows/node-aught.yml | 18 +++++ .github/workflows/node-esm.yml | 9 +++ .github/workflows/node-pretest.yml | 7 ++ .github/workflows/node-tens.yml | 18 +++++ .github/workflows/rebase.yml | 15 ++++ .github/workflows/require-allow-edits.yml | 12 +++ .gitignore | 2 + .npmrc | 2 + .nycrc | 9 +++ CHANGELOG.md | 6 ++ README.md | 83 +++++++++++++++++++- auto.js | 3 + implementation.js | 24 ++++++ index.js | 24 ++++++ index.mjs | 15 ++++ package.json | 96 +++++++++++++++++++++++ polyfill.js | 7 ++ shim.js | 14 ++++ test/implementation.js | 20 +++++ test/index.js | 17 ++++ test/index.mjs | 31 ++++++++ test/shimmed.js | 35 +++++++++ test/tests.js | 45 +++++++++++ 24 files changed, 529 insertions(+), 1 deletion(-) create mode 100644 .eslintrc create mode 100644 .github/workflows/node-aught.yml create mode 100644 .github/workflows/node-esm.yml create mode 100644 .github/workflows/node-pretest.yml create mode 100644 .github/workflows/node-tens.yml create mode 100644 .github/workflows/rebase.yml create mode 100644 .github/workflows/require-allow-edits.yml create mode 100644 .nycrc create mode 100644 CHANGELOG.md create mode 100644 auto.js create mode 100644 implementation.js create mode 100644 index.js create mode 100644 index.mjs create mode 100644 package.json create mode 100644 polyfill.js create mode 100644 shim.js create mode 100644 test/implementation.js create mode 100644 test/index.js create mode 100644 test/index.mjs create mode 100644 test/shimmed.js create mode 100644 test/tests.js 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(); + }); +};