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();
+ });
+};