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:32 UTC

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

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)) {