You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@annotator.apache.org by ge...@apache.org on 2020/04/17 17:47:30 UTC

[incubator-annotator] branch typescript created (now 18a7158)

This is an automated email from the ASF dual-hosted git repository.

gerben pushed a change to branch typescript
in repository https://gitbox.apache.org/repos/asf/incubator-annotator.git.


      at 18a7158  Convert code to typscript

This branch includes the following new commits:

     new 3d8739d  Configure for typescript
     new 18a7158  Convert code to typscript

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[incubator-annotator] 01/02: Configure for typescript

Posted by ge...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

gerben pushed a commit to branch typescript
in repository https://gitbox.apache.org/repos/asf/incubator-annotator.git

commit 3d8739d39dbc75a7f7558481aef5a5da2881d0bf
Author: Gerben <ge...@treora.com>
AuthorDate: Thu Apr 16 18:02:03 2020 +0200

    Configure for typescript
---
 .mocharc.js           |  2 +-
 babel-register.js     |  3 +++
 babel.config.js       |  5 +++-
 nyc.config.js         |  4 ++--
 package.json          |  9 +++++---
 tsconfig.json         | 21 +++++++++++++++++
 web/webpack.config.js |  9 +++++---
 yarn.lock             | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++
 8 files changed, 107 insertions(+), 10 deletions(-)

diff --git a/.mocharc.js b/.mocharc.js
index 4354b8f..2200690 100644
--- a/.mocharc.js
+++ b/.mocharc.js
@@ -19,5 +19,5 @@
  */
 
 module.exports = {
-  require: ['@babel/register', 'chai/register-assert'],
+  require: ['./babel-register.js', 'chai/register-assert'],
 };
diff --git a/babel-register.js b/babel-register.js
new file mode 100644
index 0000000..929499a
--- /dev/null
+++ b/babel-register.js
@@ -0,0 +1,3 @@
+const register = require('@babel/register').default;
+
+register({ extensions: ['.ts', '.js'] });
diff --git a/babel.config.js b/babel.config.js
index 92f6ba4..dcd45ea 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -55,6 +55,9 @@ module.exports = api => {
       ...(DEV ? [['module-resolver', resolverOptions]] : []),
       ...(TEST ? ['istanbul'] : []),
     ],
-    presets: [['@babel/env', envOptions]],
+    presets: [
+      ['@babel/env', envOptions],
+      '@babel/preset-typescript',
+    ],
   };
 };
diff --git a/nyc.config.js b/nyc.config.js
index e43ea32..08d36cf 100644
--- a/nyc.config.js
+++ b/nyc.config.js
@@ -20,8 +20,8 @@
 
 module.exports = {
   all: true,
-  include: ['packages/*/src/**/*.js'],
+  include: ['packages/*/src/**/*.[jt]s'],
   instrument: false,
   sourceMap: false,
-  require: ['@babel/register'],
+  require: ['./babel-register.js'],
 };
diff --git a/package.json b/package.json
index 6c493d7..07e02ba 100644
--- a/package.json
+++ b/package.json
@@ -16,7 +16,7 @@
   },
   "scripts": {
     "build": "yarn run build:cjs && yarn run build:esm && yarn run build:misc",
-    "build:babel": "lerna exec --parallel -- babel --root-mode upward",
+    "build:babel": "lerna exec --parallel -- babel --extensions '.js,.ts' --root-mode upward",
     "build:cjs": "cross-env BABEL_ENV=cjs yarn build:babel -d lib src",
     "build:esm": "cross-env BABEL_ENV=esm yarn build:babel -d esm src",
     "build:misc": "lerna exec -- cp ../../LICENSE ../../NOTICE .",
@@ -25,8 +25,9 @@
     "prepare": "lerna run prepare",
     "prepublishOnly": "yarn run build",
     "start": "yarn run web:server",
-    "test": "cross-env BABEL_ENV=test nyc mocha packages/*/test/**/*.js",
-    "validate": "cross-env BABEL_ENV=test mocha test/**/*.js",
+    "test": "cross-env BABEL_ENV=test nyc mocha packages/*/test/**/*.[jt]s",
+    "typecheck": "tsc --noEmit || true",
+    "validate": "cross-env BABEL_ENV=test mocha test/**/*.[jt]s",
     "web:build": "webpack  --config=web/webpack.config.js --mode development",
     "web:server": "webpack-dev-server --config=web/webpack.config.js --hot --mode development"
   },
@@ -40,6 +41,7 @@
     "@babel/core": "^7.8.7",
     "@babel/plugin-transform-runtime": "^7.8.3",
     "@babel/preset-env": "^7.8.7",
+    "@babel/preset-typescript": "^7.9.0",
     "@babel/register": "^7.8.6",
     "ajv": "^6.11.0",
     "babel-eslint": "^10.0.3",
@@ -68,6 +70,7 @@
     "prettier": "^1.19.1",
     "resolve": "^1.15.0",
     "rimraf": "^3.0.0",
+    "typescript": "^3.8.3",
     "web-annotation-tests": "https://github.com/w3c/web-annotation-tests",
     "webpack": "^4.41.5",
     "webpack-cli": "^3.3.10",
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..2e42fbc
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,21 @@
+{
+    "compilerOptions": {
+        "noImplicitAny": true,
+        "target": "ES2017",
+        "lib": [
+            "ES2020",
+            "DOM",
+            "DOM.Iterable"
+        ],
+        "moduleResolution": "Node",
+        "allowSyntheticDefaultImports": true,
+        "typeRoots": [
+            "./node_modules/@types",
+            "./@types"
+        ]
+    },
+    "files": [
+        "packages/dom/src/index.ts",
+        "packages/selector/src/index.ts",
+    ]
+}
diff --git a/web/webpack.config.js b/web/webpack.config.js
index 7876d94..8ae81e8 100644
--- a/web/webpack.config.js
+++ b/web/webpack.config.js
@@ -31,19 +31,22 @@ module.exports = {
     test: [
       './test/index.html',
       'chai/register-assert',
-      'mocha-loader!multi-entry-loader?include=./packages/*/test/**/*.js!',
+      'mocha-loader!multi-entry-loader?include=./packages/*/test/**/*.[jt]s!',
     ],
   },
+  resolve: {
+    extensions: ['.ts', '.js'],
+  },
   devtool: 'inline-source-map',
   module: {
     rules: [
       {
-        test: /\.js$/,
+        test: /\.[jt]s$/,
         exclude: /node_modules/,
         use: 'babel-loader',
       },
       {
-        exclude: /\.js$/,
+        exclude: /\.[jt]s$/,
         use: [
           {
             loader: 'file-loader',
diff --git a/yarn.lock b/yarn.lock
index 1d7ce46..722c24d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -148,6 +148,18 @@
     levenary "^1.1.1"
     semver "^5.5.0"
 
+"@babel/helper-create-class-features-plugin@^7.8.3":
+  version "7.9.5"
+  resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.9.5.tgz#79753d44017806b481017f24b02fd4113c7106ea"
+  integrity sha512-IipaxGaQmW4TfWoXdqjY0TzoXQ1HRS0kPpEgvjosb3u7Uedcq297xFqDQiCcQtRRwzIMif+N1MLVI8C5a4/PAA==
+  dependencies:
+    "@babel/helper-function-name" "^7.9.5"
+    "@babel/helper-member-expression-to-functions" "^7.8.3"
+    "@babel/helper-optimise-call-expression" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/helper-replace-supers" "^7.8.6"
+    "@babel/helper-split-export-declaration" "^7.8.3"
+
 "@babel/helper-create-regexp-features-plugin@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.8.3.tgz#c774268c95ec07ee92476a3862b75cc2839beb79"
@@ -191,6 +203,15 @@
     "@babel/template" "^7.8.3"
     "@babel/types" "^7.8.3"
 
+"@babel/helper-function-name@^7.9.5":
+  version "7.9.5"
+  resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.9.5.tgz#2b53820d35275120e1874a82e5aabe1376920a5c"
+  integrity sha512-JVcQZeXM59Cd1qanDUxv9fgJpt3NeKUaqBqUEvfmQ+BCOKq2xUgaWZW2hr0dkbyJgezYuplEoh5knmrnS68efw==
+  dependencies:
+    "@babel/helper-get-function-arity" "^7.8.3"
+    "@babel/template" "^7.8.3"
+    "@babel/types" "^7.9.5"
+
 "@babel/helper-get-function-arity@^7.7.4":
   version "7.7.4"
   resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.7.4.tgz#cb46348d2f8808e632f0ab048172130e636005f0"
@@ -315,6 +336,11 @@
   dependencies:
     "@babel/types" "^7.8.3"
 
+"@babel/helper-validator-identifier@^7.9.5":
+  version "7.9.5"
+  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz#90977a8e6fbf6b431a7dc31752eee233bf052d80"
+  integrity sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==
+
 "@babel/helper-wrap-function@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.8.3.tgz#9dbdb2bb55ef14aaa01fe8c99b629bd5352d8610"
@@ -497,6 +523,13 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
+"@babel/plugin-syntax-typescript@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.8.3.tgz#c1f659dda97711a569cef75275f7e15dcaa6cabc"
+  integrity sha512-GO1MQ/SGGGoiEXY0e0bSpHimJvxqB7lktLLIq2pv8xG7WZ8IMEle74jIe1FhprHBWjwjZtXHkycDLZXIWM5Wfg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
 "@babel/plugin-transform-arrow-functions@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.8.3.tgz#82776c2ed0cd9e1a49956daeb896024c9473b8b6"
@@ -744,6 +777,15 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
+"@babel/plugin-transform-typescript@^7.9.0":
+  version "7.9.4"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.9.4.tgz#4bb4dde4f10bbf2d787fce9707fb09b483e33359"
+  integrity sha512-yeWeUkKx2auDbSxRe8MusAG+n4m9BFY/v+lPjmQDgOFX5qnySkUY5oXzkp6FwPdsYqnKay6lorXYdC0n3bZO7w==
+  dependencies:
+    "@babel/helper-create-class-features-plugin" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-syntax-typescript" "^7.8.3"
+
 "@babel/plugin-transform-unicode-regex@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.8.3.tgz#0cef36e3ba73e5c57273effb182f46b91a1ecaad"
@@ -815,6 +857,14 @@
     levenary "^1.1.1"
     semver "^5.5.0"
 
+"@babel/preset-typescript@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.9.0.tgz#87705a72b1f0d59df21c179f7c3d2ef4b16ce192"
+  integrity sha512-S4cueFnGrIbvYJgwsVFKdvOmpiL0XGw9MFW9D0vgRys5g36PBhZRL8NX8Gr2akz8XRtzq6HuDXPD/1nniagNUg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-transform-typescript" "^7.9.0"
+
 "@babel/register@^7.8.6":
   version "7.8.6"
   resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.8.6.tgz#a1066aa6168a73a70c35ef28cc5865ccc087ea69"
@@ -956,6 +1006,15 @@
     lodash "^4.17.13"
     to-fast-properties "^2.0.0"
 
+"@babel/types@^7.9.5":
+  version "7.9.5"
+  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.5.tgz#89231f82915a8a566a703b3b20133f73da6b9444"
+  integrity sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg==
+  dependencies:
+    "@babel/helper-validator-identifier" "^7.9.5"
+    lodash "^4.17.13"
+    to-fast-properties "^2.0.0"
+
 "@evocateur/libnpmaccess@^3.1.2":
   version "3.1.2"
   resolved "https://registry.yarnpkg.com/@evocateur/libnpmaccess/-/libnpmaccess-3.1.2.tgz#ecf7f6ce6b004e9f942b098d92200be4a4b1c845"
@@ -9698,6 +9757,11 @@ typedarray@^0.0.6:
   resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
   integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
 
+typescript@^3.8.3:
+  version "3.8.3"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061"
+  integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==
+
 uglify-js@^3.1.4:
   version "3.6.0"
   resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.6.0.tgz#704681345c53a8b2079fb6cec294b05ead242ff5"


[incubator-annotator] 02/02: Convert code to typscript

Posted by ge...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

gerben pushed a commit to branch typescript
in repository https://gitbox.apache.org/repos/asf/incubator-annotator.git

commit 18a7158f6c88375e0fa407c1443bdb85b22a02e6
Author: Gerben <ge...@treora.com>
AuthorDate: Fri Apr 17 18:38:30 2020 +0200

    Convert code to typscript
    
    Adopt the word ‘matcher’ to distinguish the selector functions from
    selector objects. We could choose another word like ‘locator’ or
    whatever if desired.
---
 @types/cartesian/index.d.ts                        |  3 +
 @types/dom-node-iterator/index.d.ts                |  4 +
 @types/dom-seek/index.d.ts                         |  3 +
 babel.config.js                                    |  2 +-
 packages/dom/src/{cartesian.js => cartesian.ts}    | 20 +++--
 packages/dom/src/{css.js => css.ts}                |  6 +-
 .../src/{highlight-range.js => highlight-range.ts} | 38 +++++----
 packages/dom/src/{index.js => index.ts}            |  0
 packages/dom/src/{range.js => range.ts}            | 26 ++++---
 packages/dom/src/{scope.js => scope.ts}            | 13 ++--
 packages/dom/src/{text-quote.js => text-quote.ts}  | 90 +++++++++++++---------
 packages/dom/src/types.ts                          |  5 ++
 packages/dom/test/cartesian.js                     |  2 +-
 packages/selector/src/{index.js => index.ts}       | 30 ++++++--
 packages/selector/src/types.ts                     | 25 ++++++
 web/demo/index.js                                  | 18 ++---
 16 files changed, 186 insertions(+), 99 deletions(-)

diff --git a/@types/cartesian/index.d.ts b/@types/cartesian/index.d.ts
new file mode 100644
index 0000000..9fc47e3
--- /dev/null
+++ b/@types/cartesian/index.d.ts
@@ -0,0 +1,3 @@
+declare module 'cartesian' {
+  export default function cartesian<T>(list: Array<Array<T>> | { [k: string]: Array<T> }): Array<Array<T>>;
+}
diff --git a/@types/dom-node-iterator/index.d.ts b/@types/dom-node-iterator/index.d.ts
new file mode 100644
index 0000000..0e10887
--- /dev/null
+++ b/@types/dom-node-iterator/index.d.ts
@@ -0,0 +1,4 @@
+declare module 'dom-node-iterator' {
+  let createNodeIterator: Document['createNodeIterator'];
+  export default createNodeIterator;
+}
diff --git a/@types/dom-seek/index.d.ts b/@types/dom-seek/index.d.ts
new file mode 100644
index 0000000..0ba0753
--- /dev/null
+++ b/@types/dom-seek/index.d.ts
@@ -0,0 +1,3 @@
+declare module 'dom-seek' {
+  export default function seek(iter: NodeIterator, where: number | Node): number;
+}
diff --git a/babel.config.js b/babel.config.js
index dcd45ea..0c052cc 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -36,7 +36,7 @@ module.exports = api => {
   // Used for resolving source files during development.
   let resolverOptions = {
     alias: {
-      '^@annotator/(.+)$': '@annotator/\\1/src/index.js',
+      '^@annotator/(.+)$': '@annotator/\\1/src/index.ts',
     },
   };
 
diff --git a/packages/dom/src/cartesian.js b/packages/dom/src/cartesian.ts
similarity index 80%
rename from packages/dom/src/cartesian.js
rename to packages/dom/src/cartesian.ts
index e800c2b..d2b44b7 100644
--- a/packages/dom/src/cartesian.js
+++ b/packages/dom/src/cartesian.ts
@@ -20,7 +20,7 @@
 
 import cartesianArrays from 'cartesian';
 
-export async function* product(...iterables) {
+export async function* product<T>(...iterables: AsyncGenerator<T>[]): AsyncGenerator<Array<T>, void, undefined> {
   // We listen to all iterators in parallel, while logging all the values they
   // produce. Whenever an iterator produces a value, we produce and yield all
   // combinations of that value with the logged values from other iterators.
@@ -28,28 +28,27 @@ export async function* product(...iterables) {
 
   const iterators = iterables.map(iterable => iterable[Symbol.asyncIterator]());
   // Initialise an empty log for each iterable.
-  const logs = iterables.map(() => []);
+  const logs: T[][] = iterables.map(() => []);
 
   const nextValuePromises = iterators.map((iterator, iterableNr) =>
     iterator
       .next()
-      .then(async ({ value, done }) => ({ value: await value, done }))
       .then(
         // Label the result with iterableNr, to know which iterable produced
         // this value after Promise.race below.
-        ({ value, done }) => ({ value, done, iterableNr }),
+        nextResult => ({ nextResult, iterableNr }),
       ),
   );
 
   // Keep listening as long as any of the iterables is not yet exhausted.
   while (nextValuePromises.some(p => p !== null)) {
     // Wait until any of the active iterators has produced a new value.
-    const { value, done, iterableNr } = await Promise.race(
+    const { nextResult, iterableNr } = await Promise.race(
       nextValuePromises.filter(p => p !== null),
     );
 
     // If this iterable was exhausted, stop listening to it and move on.
-    if (done) {
+    if (nextResult.done === true) {
       nextValuePromises[iterableNr] = null;
       continue;
     }
@@ -57,17 +56,16 @@ export async function* product(...iterables) {
     // Produce all combinations of the received value with the logged values
     // from the other iterables.
     const arrays = [...logs];
-    arrays[iterableNr] = [value];
-    const combinations = cartesianArrays(arrays);
+    arrays[iterableNr] = [nextResult.value];
+    const combinations: T[][] = cartesianArrays(arrays);
 
     // Append the received value to the right log.
-    logs[iterableNr] = [...logs[iterableNr], value];
+    logs[iterableNr] = [...logs[iterableNr], nextResult.value];
 
     // Start listening for the next value of this iterable.
     nextValuePromises[iterableNr] = iterators[iterableNr]
       .next()
-      .then(async ({ value, done }) => ({ value: await value, done }))
-      .then(({ value, done }) => ({ value, done, iterableNr }));
+      .then(nextResult => ({ nextResult, iterableNr }));
 
     // Yield each of the produced combinations separately.
     yield* combinations;
diff --git a/packages/dom/src/css.js b/packages/dom/src/css.ts
similarity index 80%
rename from packages/dom/src/css.js
rename to packages/dom/src/css.ts
index 3a7c6c1..004158c 100644
--- a/packages/dom/src/css.js
+++ b/packages/dom/src/css.ts
@@ -18,8 +18,10 @@
  * under the License.
  */
 
-export function createCssSelector(selector) {
-  return async function* matchAll(scope) {
+import { CssSelector, Matcher } from "../../selector/src";
+
+export function createCssSelectorMatcher(selector: CssSelector): Matcher<Document, Element> {
+  return async function* matchAll(scope: Document) {
     yield* scope.querySelectorAll(selector.value);
   };
 }
diff --git a/packages/dom/src/highlight-range.js b/packages/dom/src/highlight-range.ts
similarity index 84%
rename from packages/dom/src/highlight-range.js
rename to packages/dom/src/highlight-range.ts
index 57f76ee..d46cead 100644
--- a/packages/dom/src/highlight-range.js
+++ b/packages/dom/src/highlight-range.ts
@@ -28,14 +28,18 @@
 //   unusable afterwards
 // - tagName: the element used to wrap text nodes. Defaults to 'mark'.
 // - attributes: an Object defining any attributes to be set on the wrapper elements.
-export function highlightRange(range, tagName = 'mark', attributes = {}) {
+export function highlightRange(
+  range: Range,
+  tagName: string = 'mark',
+  attributes: Record<string, string> = {}
+): () => void {
   if (range.collapsed) return;
 
   // First put all nodes in an array (splits start and end nodes if needed)
   const nodes = textNodesInRange(range);
 
   // Highlight each node
-  const highlightElements = [];
+  const highlightElements: HTMLElement[] = [];
   for (const node of nodes) {
     const highlightElement = wrapNodeInHighlight(node, tagName, attributes);
     highlightElements.push(highlightElement);
@@ -52,10 +56,10 @@ export function highlightRange(range, tagName = 'mark', attributes = {}) {
 }
 
 // Return an array of the text nodes in the range. Split the start and end nodes if required.
-function textNodesInRange(range) {
+function textNodesInRange(range: Range): Text[] {
   // If the start or end node is a text node and only partly in the range, split it.
   if (
-    range.startContainer.nodeType === Node.TEXT_NODE &&
+    isTextNode(range.startContainer) &&
     range.startOffset > 0
   ) {
     const endOffset = range.endOffset; // (this may get lost when the splitting the node)
@@ -67,7 +71,7 @@ function textNodesInRange(range) {
     range.setStart(createdNode, 0);
   }
   if (
-    range.endContainer.nodeType === Node.TEXT_NODE &&
+    isTextNode(range.endContainer) &&
     range.endOffset < range.endContainer.length
   ) {
     range.endContainer.splitText(range.endOffset);
@@ -77,10 +81,12 @@ function textNodesInRange(range) {
   const walker = range.startContainer.ownerDocument.createTreeWalker(
     range.commonAncestorContainer,
     NodeFilter.SHOW_TEXT,
-    node =>
-      range.intersectsNode(node)
-        ? NodeFilter.FILTER_ACCEPT
-        : NodeFilter.FILTER_REJECT,
+    {
+      acceptNode: node =>
+        range.intersectsNode(node)
+          ? NodeFilter.FILTER_ACCEPT
+          : NodeFilter.FILTER_REJECT
+    },
   );
   walker.currentNode = range.startContainer;
 
@@ -98,16 +104,16 @@ function textNodesInRange(range) {
   //   }
   // }
 
-  const nodes = [];
-  if (walker.currentNode.nodeType === Node.TEXT_NODE)
+  const nodes: Text[] = [];
+  if (isTextNode(walker.currentNode))
     nodes.push(walker.currentNode);
   while (walker.nextNode() && range.comparePoint(walker.currentNode, 0) !== 1)
-    nodes.push(walker.currentNode);
+    nodes.push(walker.currentNode as Text);
   return nodes;
 }
 
 // Replace [node] with <tagName ...attributes>[node]</tagName>
-function wrapNodeInHighlight(node, tagName, attributes) {
+function wrapNodeInHighlight(node: Node, tagName: string, attributes: Record<string, string>): HTMLElement {
   const highlightElement = node.ownerDocument.createElement(tagName);
   Object.keys(attributes).forEach(key => {
     highlightElement.setAttribute(key, attributes[key]);
@@ -119,7 +125,7 @@ function wrapNodeInHighlight(node, tagName, attributes) {
 }
 
 // Remove a highlight element created with wrapNodeInHighlight.
-function removeHighlight(highlightElement) {
+function removeHighlight(highlightElement: HTMLElement) {
   // If it has somehow been removed already, there is nothing to be done.
   if (!highlightElement.parentNode) return;
   if (highlightElement.childNodes.length === 1) {
@@ -138,3 +144,7 @@ function removeHighlight(highlightElement) {
     highlightElement.remove();
   }
 }
+
+function isTextNode(node: Node): node is Text {
+  return node.nodeType === Node.TEXT_NODE
+}
diff --git a/packages/dom/src/index.js b/packages/dom/src/index.ts
similarity index 100%
rename from packages/dom/src/index.js
rename to packages/dom/src/index.ts
diff --git a/packages/dom/src/range.js b/packages/dom/src/range.ts
similarity index 63%
rename from packages/dom/src/range.js
rename to packages/dom/src/range.ts
index 51b1483..e465386 100644
--- a/packages/dom/src/range.js
+++ b/packages/dom/src/range.ts
@@ -18,19 +18,23 @@
  * under the License.
  */
 
-import { ownerDocument } from './scope.js';
-import { product } from './cartesian.js';
-
-export function createRangeSelectorCreator(createSelector) {
-  return function createRangeSelector(selector) {
-    const startSelector = createSelector(selector.startSelector);
-    const endSelector = createSelector(selector.endSelector);
-
-    return async function* matchAll(scope) {
+import { ownerDocument } from './scope';
+import { product } from './cartesian';
+import { RangeSelector, Selector } from '../../selector/src/types';
+import { DomMatcher, DomScope } from './types';
+
+export function makeCreateRangeSelectorMatcher(
+  createMatcher: <T extends Selector>(selector: T) => DomMatcher
+): (selector: RangeSelector) => DomMatcher {
+  return function createRangeSelectorMatcher(selector: RangeSelector) {
+    const startMatcher = createMatcher(selector.startSelector);
+    const endMatcher = createMatcher(selector.endSelector);
+
+    return async function* matchAll(scope: DomScope) {
       const document = ownerDocument(scope);
 
-      const startMatches = startSelector(scope);
-      const endMatches = endSelector(scope);
+      const startMatches = startMatcher(scope);
+      const endMatches = endMatcher(scope);
 
       const pairs = product(startMatches, endMatches);
 
diff --git a/packages/dom/src/scope.js b/packages/dom/src/scope.ts
similarity index 81%
rename from packages/dom/src/scope.js
rename to packages/dom/src/scope.ts
index 007618d..2b112ff 100644
--- a/packages/dom/src/scope.js
+++ b/packages/dom/src/scope.ts
@@ -18,15 +18,16 @@
  * under the License.
  */
 
-export function ownerDocument(scope) {
-  if ('commonAncestorContainer' in scope) {
-    return scope.commonAncestorContainer.ownerDocument;
-  }
+import { DomScope } from './types';
 
-  return scope.ownerDocument;
+export function ownerDocument(scope: DomScope): Document {
+  if ('commonAncestorContainer' in scope)
+    return scope.commonAncestorContainer.ownerDocument;
+  else
+    return scope.ownerDocument;
 }
 
-export function rangeFromScope(scope) {
+export function rangeFromScope(scope: DomScope | null): Range {
   if ('commonAncestorContainer' in scope) {
     return scope;
   }
diff --git a/packages/dom/src/text-quote.js b/packages/dom/src/text-quote.ts
similarity index 71%
rename from packages/dom/src/text-quote.js
rename to packages/dom/src/text-quote.ts
index 21a530e..01a3989 100644
--- a/packages/dom/src/text-quote.js
+++ b/packages/dom/src/text-quote.ts
@@ -21,26 +21,22 @@
 import createNodeIterator from 'dom-node-iterator';
 import seek from 'dom-seek';
 
-import { ownerDocument, rangeFromScope } from './scope.js';
+import { TextQuoteSelector } from '../../selector/src';
+import { DomScope, DomMatcher } from './types';
+import { ownerDocument, rangeFromScope } from './scope';
 
-// Node constants
-const TEXT_NODE = 3;
-
-// NodeFilter constants
-const SHOW_TEXT = 4;
-
-function firstTextNodeInRange(range) {
+function firstTextNodeInRange(range: Range): Text {
   const { startContainer } = range;
 
-  if (startContainer.nodeType === TEXT_NODE) return startContainer;
+  if (isTextNode(startContainer)) return startContainer;
 
   const root = range.commonAncestorContainer;
-  const iter = createNodeIterator(root, SHOW_TEXT);
-  return iter.nextNode();
+  const iter = createNodeIterator(root, NodeFilter.SHOW_TEXT);
+  return iter.nextNode() as Text;
 }
 
-export function createTextQuoteSelector(selector) {
-  return async function* matchAll(scope) {
+export function createTextQuoteSelectorMatcher(selector: TextQuoteSelector): DomMatcher {
+  return async function* matchAll(scope: DomScope) {
     const document = ownerDocument(scope);
     const range = rangeFromScope(scope);
     const root = range.commonAncestorContainer;
@@ -51,12 +47,12 @@ export function createTextQuoteSelector(selector) {
     const suffix = selector.suffix || '';
     const pattern = prefix + exact + suffix;
 
-    const iter = createNodeIterator(root, SHOW_TEXT);
+    const iter = createNodeIterator(root, NodeFilter.SHOW_TEXT);
 
     let fromIndex = 0;
     let referenceNodeIndex = 0;
 
-    if (range.startContainer.nodeType === TEXT_NODE) {
+    if (isTextNode(range.startContainer)) {
       referenceNodeIndex -= range.startOffset;
     }
 
@@ -122,32 +118,49 @@ export function createTextQuoteSelector(selector) {
   };
 }
 
-export async function describeTextQuote(range, scope = null) {
-  scope = rangeFromScope(scope || ownerDocument(range).documentElement);
+export async function describeTextQuote(
+  range: Range,
+  scope: DomScope = null
+): Promise<TextQuoteSelector> {
+  const exact = range.toString();
+
+  const result: TextQuoteSelector = { type: 'TextQuoteSelector', exact };
 
-  const root = scope.commonAncestorContainer;
-  const text = scope.toString();
+  const { prefix, suffix } = await calculateContextForDisambiguation(range, result, scope);
+  result.prefix = prefix;
+  result.suffix = suffix;
 
-  const exact = range.toString();
-  const selector = createTextQuoteSelector({ exact });
+  return result
+}
 
-  const iter = createNodeIterator(root, SHOW_TEXT);
+async function calculateContextForDisambiguation(
+  range: Range,
+  selector: TextQuoteSelector,
+  scope: DomScope
+): Promise<{ prefix?: string, suffix?: string }> {
+  const scopeAsRange = rangeFromScope(scope || ownerDocument(range).documentElement);
+  const root = scopeAsRange.commonAncestorContainer;
+  const text = scopeAsRange.toString();
+
+  const matcher = createTextQuoteSelectorMatcher(selector);
+
+  const iter = createNodeIterator(root, NodeFilter.SHOW_TEXT);
 
   const startNode = firstTextNodeInRange(range);
   const startIndex =
-    range.startContainer.nodeType === TEXT_NODE
+    isTextNode(range.startContainer)
       ? seek(iter, startNode) + range.startOffset
       : seek(iter, startNode);
-  const endIndex = startIndex + exact.length;
+  const endIndex = startIndex + selector.exact.length;
 
-  const affixLengthPairs = [];
+  const affixLengthPairs: Array<[number, number]> = [];
 
-  for await (const match of selector(scope)) {
-    const matchIter = createNodeIterator(root, SHOW_TEXT);
+  for await (const match of matcher(scopeAsRange)) {
+    const matchIter = createNodeIterator(root, NodeFilter.SHOW_TEXT);
 
     const matchStartNode = firstTextNodeInRange(match);
     const matchStartIndex =
-      match.startContainer.nodeType === TEXT_NODE
+      isTextNode(match.startContainer)
         ? seek(matchIter, matchStartNode) + match.startOffset
         : seek(matchIter, matchStartNode);
     const matchEndIndex = matchStartIndex + match.toString().length;
@@ -174,24 +187,23 @@ export async function describeTextQuote(range, scope = null) {
   }
 
   // Construct and return an unambiguous selector.
-  const result = { type: 'TextQuoteSelector', exact };
-
+  let prefix, suffix;
   if (affixLengthPairs.length) {
     const [prefixLength, suffixLength] = minimalSolution(affixLengthPairs);
 
     if (prefixLength > 0 && startIndex > 0) {
-      result.prefix = text.substring(startIndex - prefixLength, startIndex);
+      prefix = text.substring(startIndex - prefixLength, startIndex);
     }
 
     if (suffixLength > 0 && endIndex < text.length) {
-      result.suffix = text.substring(endIndex, endIndex + suffixLength);
+      suffix = text.substring(endIndex, endIndex + suffixLength);
     }
   }
 
-  return result;
+  return { prefix, suffix };
 }
 
-function overlap(text1, text2) {
+function overlap(text1: string, text2: string) {
   let count = 0;
 
   while (count < text1.length && count < text2.length) {
@@ -204,7 +216,7 @@ function overlap(text1, text2) {
   return count;
 }
 
-function overlapRight(text1, text2) {
+function overlapRight(text1: string, text2: string) {
   let count = 0;
 
   while (count < text1.length && count < text2.length) {
@@ -217,9 +229,9 @@ function overlapRight(text1, text2) {
   return count;
 }
 
-function minimalSolution(requirements) {
+function minimalSolution(requirements: Array<[number, number]>): [number, number] {
   // Build all the pairs and order them by their sums.
-  const pairs = requirements.flatMap(l => requirements.map(r => [l[0], r[1]]));
+  const pairs = requirements.flatMap(l => requirements.map<[number, number]>(r => [l[0], r[1]]));
   pairs.sort((a, b) => a[0] + a[1] - (b[0] + b[1]));
 
   // Find the first pair that satisfies every requirement.
@@ -233,3 +245,7 @@ function minimalSolution(requirements) {
   // Return the largest pairing (unreachable).
   return pairs[pairs.length - 1];
 }
+
+function isTextNode(node: Node): node is Text {
+  return node.nodeType === Node.TEXT_NODE
+}
diff --git a/packages/dom/src/types.ts b/packages/dom/src/types.ts
new file mode 100644
index 0000000..db6d018
--- /dev/null
+++ b/packages/dom/src/types.ts
@@ -0,0 +1,5 @@
+import { Matcher } from '../../selector/src';
+
+export type DomScope = Node | Range
+
+export type DomMatcher = Matcher<DomScope, Range>
diff --git a/packages/dom/test/cartesian.js b/packages/dom/test/cartesian.js
index a56f9c8..c5cfd23 100644
--- a/packages/dom/test/cartesian.js
+++ b/packages/dom/test/cartesian.js
@@ -18,7 +18,7 @@
  * under the License.
  */
 
-import { product } from '../src/cartesian.js';
+import { product } from '../src/cartesian';
 
 async function* gen1() {
   yield 1;
diff --git a/packages/selector/src/index.js b/packages/selector/src/index.ts
similarity index 50%
rename from packages/selector/src/index.js
rename to packages/selector/src/index.ts
index c9c100d..8135f6e 100644
--- a/packages/selector/src/index.js
+++ b/packages/selector/src/index.ts
@@ -18,20 +18,36 @@
  * under the License.
  */
 
-export function makeRefinable(selectorCreator) {
-  return function createSelector(source) {
-    const selector = selectorCreator(source);
+import { Selector, Matcher } from './types';
 
-    if (source.refinedBy) {
-      const refiningSelector = createSelector(source.refinedBy);
+export * from './types';
+
+export function makeRefinable<
+  // Any subtype of Selector can be made refinable; but note we limit the value
+  // of refinedBy because it must also be accepted by matcherCreator.
+  TSelector extends (Selector & { refinedBy: TSelector }),
+  TScope,
+  // To enable refinement, the implementation’s Match object must be usable as a
+  // Scope object itself.
+  TMatch extends TScope,
+>(
+  matcherCreator: (selector: TSelector) => Matcher<TScope, TMatch>,
+): (selector: TSelector) => Matcher<TScope, TMatch> {
+  return function createMatcherWithRefinement(
+    sourceSelector: TSelector
+  ): Matcher<TScope, TMatch> {
+    const matcher = matcherCreator(sourceSelector);
+
+    if (sourceSelector.refinedBy) {
+      const refiningSelector = createMatcherWithRefinement(sourceSelector.refinedBy);
 
       return async function* matchAll(scope) {
-        for await (const match of selector(scope)) {
+        for await (const match of matcher(scope)) {
           yield* refiningSelector(match);
         }
       };
     }
 
-    return selector;
+    return matcher;
   };
 }
diff --git a/packages/selector/src/types.ts b/packages/selector/src/types.ts
new file mode 100644
index 0000000..4f620a8
--- /dev/null
+++ b/packages/selector/src/types.ts
@@ -0,0 +1,25 @@
+export interface Selector {
+  refinedBy?: Selector,
+}
+
+export interface CssSelector extends Selector {
+  type: 'CssSelector',
+  value: string,
+}
+
+export interface TextQuoteSelector extends Selector {
+  type: 'TextQuoteSelector',
+  exact: string,
+  prefix?: string,
+  suffix?: string,
+}
+
+export interface RangeSelector extends Selector {
+  type: 'RangeSelector',
+  startSelector: Selector,
+  endSelector: Selector,
+}
+
+export interface Matcher<TScope, TMatch> {
+  (scope: TScope): AsyncGenerator<TMatch, void, void>,
+}
diff --git a/web/demo/index.js b/web/demo/index.js
index 47f7f30..b9e2533 100644
--- a/web/demo/index.js
+++ b/web/demo/index.js
@@ -21,8 +21,8 @@
 /* global info, module, source, target */
 
 import {
-  createRangeSelectorCreator,
-  createTextQuoteSelector,
+  makeCreateRangeSelectorMatcher,
+  createTextQuoteSelectorMatcher,
   describeTextQuote,
   highlightRange,
 } from '@annotator/dom';
@@ -91,21 +91,21 @@ function cleanup() {
   target.normalize();
 }
 
-const createSelector = makeRefinable(selector => {
-  const selectorCreator = {
-    TextQuoteSelector: createTextQuoteSelector,
-    RangeSelector: createRangeSelectorCreator(createSelector),
+const createMatcher = makeRefinable(selector => {
+  const innerCreateMatcher = {
+    TextQuoteSelector: createTextQuoteSelectorMatcher,
+    RangeSelector: makeCreateRangeSelectorMatcher(createMatcher),
   }[selector.type];
 
-  if (selectorCreator == null) {
+  if (!innerCreateMatcher) {
     throw new Error(`Unsupported selector type: ${selector.type}`);
   }
 
-  return selectorCreator(selector);
+  return innerCreateMatcher(selector);
 });
 
 async function anchor(selector) {
-  const matchAll = createSelector(selector);
+  const matchAll = createMatcher(selector);
   const ranges = [];
 
   for await (const range of matchAll(target)) {