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

[incubator-annotator] branch eliminate-extraneous-interfaces created (now 13aa9b2)

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

randall pushed a change to branch eliminate-extraneous-interfaces
in repository https://gitbox.apache.org/repos/asf/incubator-annotator.git.


      at 13aa9b2  Eliminate extraneous interfaces

This branch includes the following new commits:

     new 13aa9b2  Eliminate extraneous interfaces

The 1 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/01: Eliminate extraneous interfaces

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

randall pushed a commit to branch eliminate-extraneous-interfaces
in repository https://gitbox.apache.org/repos/asf/incubator-annotator.git

commit 13aa9b2ba4482be5381c1ae85b592f72d10ce782
Author: Randall Leeds <ra...@apache.org>
AuthorDate: Sat Aug 22 20:32:56 2020 -0700

    Eliminate extraneous interfaces
    
    Make several changes to eliminate unnecessary type definitions.
    
    Remove the centrally-exported selector interfaces from the selector
    package and migrate them to where they are used. The selector package
    need not commit to the existence of any particular set of selectors. In
    the future, the apache-annotator package may consolidate the existing
    selector definitions and export them under a unified namespace.
    
    Remove the matcher interface in favor of being explicit and verbose
    about function types.
    
    Simplify the type signature for makeRefinable. The selector is defined
    as an interface, thus callers are free to pass anything that extends it,
    so there is no need to parametrize its type. Use the same type
    definition for the input and output of the selector creator.
    
    Define the DOM match functions in terms of Range and place the burden on
    the caller to pass a Range. Remove the unused scope functions. This
    change may hurt the developer experience, but that will require some
    experience and feedback.
    
    Remove the "type" field from all selector definitions, since the match
    functions do not use this information. Applications may decide to
    include type information in selectors, but it is not necessary to
    dictate the specific way to achieve this. In the future, utility
    functions in the selector package may dictate this by providing a typed
    selector creator that delegates to implementations based on a type
    field.
---
 packages/dom/package.json                          |  3 --
 packages/dom/src/css.ts                            |  8 ++--
 packages/dom/src/highlight-range.ts                |  3 +-
 packages/dom/src/range/match.ts                    | 25 ++++++-----
 packages/dom/src/scope.ts                          | 39 ----------------
 packages/dom/src/text-quote/describe.ts            | 52 ++++++++--------------
 packages/dom/src/text-quote/index.ts               |  2 +
 packages/dom/src/text-quote/match.ts               | 25 +++++------
 .../dom/src/{types.ts => text-quote/selector.ts}   | 10 ++---
 packages/dom/test/text-quote/describe-cases.ts     | 10 +----
 packages/dom/test/text-quote/describe.test.ts      | 10 +++--
 packages/dom/test/text-quote/match-cases.ts        | 25 +----------
 packages/dom/test/text-quote/match.test.ts         | 37 +++++++++++----
 packages/selector/src/index.ts                     | 42 +++++++----------
 packages/selector/src/types.ts                     | 45 -------------------
 web/demo/index.js                                  |  5 ++-
 16 files changed, 114 insertions(+), 227 deletions(-)

diff --git a/packages/dom/package.json b/packages/dom/package.json
index e2b198e..6704d56 100644
--- a/packages/dom/package.json
+++ b/packages/dom/package.json
@@ -22,9 +22,6 @@
     "core-js": "^3.6.4",
     "dom-seek": "^5.1.0"
   },
-  "devDependencies": {
-    "@annotator/selector": "^0.1.0"
-  },
   "engines": {
     "node": "^10 || ^11 || ^12 || >=13.7"
   },
diff --git a/packages/dom/src/css.ts b/packages/dom/src/css.ts
index 28ffb94..9c5c832 100644
--- a/packages/dom/src/css.ts
+++ b/packages/dom/src/css.ts
@@ -18,12 +18,14 @@
  * under the License.
  */
 
-import type { CssSelector, Matcher } from '@annotator/selector';
+export interface CssSelector {
+  value: string;
+}
 
 export function createCssSelectorMatcher(
   selector: CssSelector,
-): Matcher<Document, Element> {
-  return async function* matchAll(scope: Document) {
+): (scope: ParentNode) => AsyncIterable<Element> {
+  return async function* matchAll(scope) {
     yield* scope.querySelectorAll(selector.value);
   };
 }
diff --git a/packages/dom/src/highlight-range.ts b/packages/dom/src/highlight-range.ts
index e7b9b8e..c4928c9 100644
--- a/packages/dom/src/highlight-range.ts
+++ b/packages/dom/src/highlight-range.ts
@@ -74,7 +74,8 @@ function textNodesInRange(range: Range): Text[] {
 
   // Collect the text nodes.
   const document =
-    range.startContainer.ownerDocument || (range.startContainer as Document);
+    range.commonAncestorContainer.ownerDocument ??
+    (range.commonAncestorContainer as Document);
   const walker = document.createTreeWalker(
     range.commonAncestorContainer,
     NodeFilter.SHOW_TEXT,
diff --git a/packages/dom/src/range/match.ts b/packages/dom/src/range/match.ts
index df87db7..bc2b5b8 100644
--- a/packages/dom/src/range/match.ts
+++ b/packages/dom/src/range/match.ts
@@ -18,22 +18,25 @@
  * under the License.
  */
 
-import type { RangeSelector, Selector } from '@annotator/selector';
-
-import { ownerDocument } from '../scope';
-import type { DomMatcher, DomScope } from '../types';
-
 import { product } from './cartesian';
 
-export function makeCreateRangeSelectorMatcher(
-  createMatcher: <T extends Selector>(selector: T) => DomMatcher,
-): (selector: RangeSelector) => DomMatcher {
-  return function createRangeSelectorMatcher(selector: RangeSelector) {
+export interface RangeSelector<T> {
+  startSelector: T;
+  endSelector: T;
+}
+
+export function makeCreateRangeSelectorMatcher<T>(
+  createMatcher: (selector: T) => (scope: Range) => AsyncIterable<Range>,
+): (selector: RangeSelector<T>) => (scope: Range) => AsyncIterable<Range> {
+  return function createRangeSelectorMatcher(selector) {
     const startMatcher = createMatcher(selector.startSelector);
     const endMatcher = createMatcher(selector.endSelector);
 
-    return async function* matchAll(scope: DomScope) {
-      const document = ownerDocument(scope);
+    return async function* matchAll(scope: Range) {
+      const { commonAncestorContainer } = scope;
+      const document =
+        commonAncestorContainer.ownerDocument ??
+        (commonAncestorContainer as Document);
 
       const startMatches = startMatcher(scope);
       const endMatches = endMatcher(scope);
diff --git a/packages/dom/src/scope.ts b/packages/dom/src/scope.ts
deleted file mode 100644
index e107212..0000000
--- a/packages/dom/src/scope.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-/**
- * @license
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import type { DomScope } from './types';
-
-export function ownerDocument(scope: DomScope): Document {
-  const node = isRange(scope) ? scope.commonAncestorContainer : scope;
-  return node.ownerDocument || (node as Document);
-}
-
-export function rangeFromScope(scope: DomScope): Range {
-  if (isRange(scope)) {
-    return scope;
-  }
-  const range = ownerDocument(scope).createRange();
-  range.selectNodeContents(scope);
-  return range;
-}
-
-function isRange(scope: DomScope): scope is Range {
-  return 'collapsed' in scope;
-}
diff --git a/packages/dom/src/text-quote/describe.ts b/packages/dom/src/text-quote/describe.ts
index b048914..ef4cd5f 100644
--- a/packages/dom/src/text-quote/describe.ts
+++ b/packages/dom/src/text-quote/describe.ts
@@ -19,46 +19,33 @@
  */
 
 import seek from 'dom-seek';
-import type { TextQuoteSelector } from '@annotator/selector';
 
-import type { DomScope } from '../types';
-import { ownerDocument, rangeFromScope } from '../scope';
+import type { TextQuoteSelector } from './selector';
 
 export async function describeTextQuote(
   range: Range,
-  scope: DomScope = ownerDocument(range).documentElement,
+  scope: Range = range,
 ): Promise<TextQuoteSelector> {
   range = range.cloneRange();
 
   // Take the part of the range that falls within the scope.
-  const scopeAsRange = rangeFromScope(scope);
-  if (!scopeAsRange.isPointInRange(range.startContainer, range.startOffset))
-    range.setStart(scopeAsRange.startContainer, scopeAsRange.startOffset);
-  if (!scopeAsRange.isPointInRange(range.endContainer, range.endOffset))
-    range.setEnd(scopeAsRange.endContainer, scopeAsRange.endOffset);
-
-  const exact = range.toString();
-
-  const result: TextQuoteSelector = { type: 'TextQuoteSelector', exact };
-
-  const { prefix, suffix } = calculateContextForDisambiguation(
-    range,
-    result,
-    scope,
-  );
-  result.prefix = prefix;
-  result.suffix = suffix;
-
-  return result;
+  if (!scope.isPointInRange(range.startContainer, range.startOffset))
+    range.setStart(scope.startContainer, scope.startOffset);
+  if (!scope.isPointInRange(range.endContainer, range.endOffset))
+    range.setEnd(scope.endContainer, scope.endOffset);
+
+  return {
+    exact: range.toString(),
+    ...calculateContextForDisambiguation(range, scope),
+  };
 }
 
 function calculateContextForDisambiguation(
   range: Range,
-  selector: TextQuoteSelector,
-  scope: DomScope,
+  scope: Range,
 ): { prefix?: string; suffix?: string } {
-  const exactText = selector.exact;
-  const scopeText = rangeFromScope(scope).toString();
+  const exactText = range.toString();
+  const scopeText = scope.toString();
   const targetStartIndex = getRangeTextPosition(range, scope);
   const targetEndIndex = targetStartIndex + exactText.length;
 
@@ -153,23 +140,20 @@ function minimalSolution(
 }
 
 // Get the index of the first character of range within the text of scope.
-function getRangeTextPosition(range: Range, scope: DomScope): number {
-  const scopeAsRange = rangeFromScope(scope);
+function getRangeTextPosition(range: Range, scope: Range): number {
   const iter = document.createNodeIterator(
-    scopeAsRange.commonAncestorContainer,
+    scope.commonAncestorContainer,
     NodeFilter.SHOW_TEXT,
     {
       acceptNode(node: Text) {
         // Only reveal nodes within the range
-        return scopeAsRange.intersectsNode(node)
+        return scope.intersectsNode(node)
           ? NodeFilter.FILTER_ACCEPT
           : NodeFilter.FILTER_REJECT;
       },
     },
   );
-  const scopeOffset = isTextNode(scopeAsRange.startContainer)
-    ? scopeAsRange.startOffset
-    : 0;
+  const scopeOffset = isTextNode(scope.startContainer) ? scope.startOffset : 0;
   if (isTextNode(range.startContainer))
     return seek(iter, range.startContainer) + range.startOffset - scopeOffset;
   else return seek(iter, firstTextNodeInRange(range)) - scopeOffset;
diff --git a/packages/dom/src/text-quote/index.ts b/packages/dom/src/text-quote/index.ts
index bb73732..8cd40b3 100644
--- a/packages/dom/src/text-quote/index.ts
+++ b/packages/dom/src/text-quote/index.ts
@@ -20,3 +20,5 @@
 
 export * from './describe';
 export * from './match';
+
+export type { TextQuoteSelector } from './selector';
diff --git a/packages/dom/src/text-quote/match.ts b/packages/dom/src/text-quote/match.ts
index a78d057..2cf1567 100644
--- a/packages/dom/src/text-quote/match.ts
+++ b/packages/dom/src/text-quote/match.ts
@@ -18,19 +18,20 @@
  * under the License.
  */
 
-import type { TextQuoteSelector } from '@annotator/selector';
 import seek from 'dom-seek';
 
-import type { DomScope, DomMatcher } from '../types';
-import { ownerDocument, rangeFromScope } from '../scope';
+import type { TextQuoteSelector } from './selector';
 
 export function createTextQuoteSelectorMatcher(
   selector: TextQuoteSelector,
-): DomMatcher {
-  return async function* matchAll(scope: DomScope) {
-    const document = ownerDocument(scope);
-    const scopeAsRange = rangeFromScope(scope);
-    const scopeText = scopeAsRange.toString();
+): (scope: Range) => AsyncIterable<Range> {
+  return async function* matchAll(scope) {
+    const { commonAncestorContainer, startContainer, startOffset } = scope;
+    const document =
+      commonAncestorContainer.ownerDocument ??
+      (commonAncestorContainer as Document);
+
+    const scopeText = scope.toString();
 
     const exact = selector.exact;
     const prefix = selector.prefix || '';
@@ -38,12 +39,12 @@ export function createTextQuoteSelectorMatcher(
     const searchPattern = prefix + exact + suffix;
 
     const iter = document.createNodeIterator(
-      scopeAsRange.commonAncestorContainer,
+      commonAncestorContainer,
       NodeFilter.SHOW_TEXT,
       {
         acceptNode(node: Text) {
           // Only reveal nodes within the range; and skip any empty text nodes.
-          return scopeAsRange.intersectsNode(node) && node.length > 0
+          return scope.intersectsNode(node) && node.length > 0
             ? NodeFilter.FILTER_ACCEPT
             : NodeFilter.FILTER_REJECT;
         },
@@ -51,9 +52,7 @@ export function createTextQuoteSelectorMatcher(
     );
 
     // The index of the first character of iter.referenceNode inside the text.
-    let referenceNodeIndex = isTextNode(scopeAsRange.startContainer)
-      ? -scopeAsRange.startOffset
-      : 0;
+    let referenceNodeIndex = isTextNode(startContainer) ? -startOffset : 0;
 
     let fromIndex = 0;
     while (fromIndex <= scopeText.length) {
diff --git a/packages/dom/src/types.ts b/packages/dom/src/text-quote/selector.ts
similarity index 85%
rename from packages/dom/src/types.ts
rename to packages/dom/src/text-quote/selector.ts
index 9bfc80a..2965896 100644
--- a/packages/dom/src/types.ts
+++ b/packages/dom/src/text-quote/selector.ts
@@ -18,8 +18,8 @@
  * under the License.
  */
 
-import type { Matcher } from '@annotator/selector';
-
-export type DomScope = Node | Range;
-
-export type DomMatcher = Matcher<DomScope, Range>;
+export interface TextQuoteSelector {
+  exact: string;
+  prefix?: string;
+  suffix?: string;
+}
diff --git a/packages/dom/test/text-quote/describe-cases.ts b/packages/dom/test/text-quote/describe-cases.ts
index 7d43a5f..9dcf720 100644
--- a/packages/dom/test/text-quote/describe-cases.ts
+++ b/packages/dom/test/text-quote/describe-cases.ts
@@ -18,8 +18,7 @@
  * under the License.
  */
 
-import type { TextQuoteSelector } from '@annotator/selector';
-
+import type { TextQuoteSelector } from '../../src';
 import { RangeInfo } from '../utils';
 
 export const testCases: {
@@ -38,7 +37,6 @@ export const testCases: {
       endOffset: 20,
     },
     expected: {
-      type: 'TextQuoteSelector',
       exact: 'dolor am',
       prefix: '',
       suffix: '',
@@ -53,7 +51,6 @@ export const testCases: {
       endOffset: 26,
     },
     expected: {
-      type: 'TextQuoteSelector',
       exact: 'anno',
       prefix: 'to ',
       suffix: '',
@@ -68,7 +65,6 @@ export const testCases: {
       endOffset: 11,
     },
     expected: {
-      type: 'TextQuoteSelector',
       exact: 'tate',
       prefix: '',
       suffix: ' ',
@@ -83,7 +79,6 @@ export const testCases: {
       endOffset: 2,
     },
     expected: {
-      type: 'TextQuoteSelector',
       exact: 'to',
       prefix: '',
       suffix: ' annotate ',
@@ -98,7 +93,6 @@ export const testCases: {
       endOffset: 30,
     },
     expected: {
-      type: 'TextQuoteSelector',
       exact: 'tate',
       prefix: 'to anno',
       suffix: '',
@@ -113,7 +107,6 @@ export const testCases: {
       endOffset: 11,
     },
     expected: {
-      type: 'TextQuoteSelector',
       exact: '',
       prefix: 'e',
       suffix: ' ',
@@ -128,7 +121,6 @@ export const testCases: {
       endOffset: 2,
     },
     expected: {
-      type: 'TextQuoteSelector',
       exact: 'annota',
       prefix: 'to ',
       suffix: '',
diff --git a/packages/dom/test/text-quote/describe.test.ts b/packages/dom/test/text-quote/describe.test.ts
index 9219147..2679be4 100644
--- a/packages/dom/test/text-quote/describe.test.ts
+++ b/packages/dom/test/text-quote/describe.test.ts
@@ -32,7 +32,9 @@ describe('describeTextQuote', () => {
   for (const [name, { html, range, expected }] of Object.entries(testCases)) {
     it(`works for case: ${name}`, async () => {
       const doc = domParser.parseFromString(html, 'text/html');
-      const result = await describeTextQuote(hydrateRange(range, doc), doc);
+      const scope = doc.createRange();
+      scope.selectNodeContents(doc);
+      const result = await describeTextQuote(hydrateRange(range, doc), scope);
       assert.deepEqual(result, expected);
     });
   }
@@ -45,7 +47,6 @@ describe('describeTextQuote', () => {
     scope.setEnd(evaluateXPath(doc, '//b/text()'), 30); // "not to annotate"
     const result = await describeTextQuote(hydrateRange(range, doc), scope);
     assert.deepEqual(result, {
-      type: 'TextQuoteSelector',
       exact: 'anno',
       prefix: '', // no prefix needed in this scope.
       suffix: '',
@@ -60,7 +61,6 @@ describe('describeTextQuote', () => {
     scope.setEnd(evaluateXPath(doc, '//b/text()'), 17); // "ipsum dolor"
     const result = await describeTextQuote(hydrateRange(range, doc), scope);
     assert.deepEqual(result, {
-      type: 'TextQuoteSelector',
       exact: 'dolor',
       prefix: '',
       suffix: '',
@@ -85,9 +85,11 @@ describe('describeTextQuote', () => {
     for (const [name, { html, selector, expected }] of applicableTestCases) {
       it(`case: '${name}'`, async () => {
         const doc = domParser.parseFromString(html, 'text/html');
+        const scope = doc.createRange();
+        scope.selectNodeContents(doc);
         for (const rangeInfo of expected) {
           const range = hydrateRange(rangeInfo, doc);
-          const result = await describeTextQuote(range, doc);
+          const result = await describeTextQuote(range, scope);
           assert.equal(result.exact, selector.exact);
           // Our result may have a different combination of prefix/suffix; only check for obvious inconsistency.
           if (selector.prefix && result.prefix)
diff --git a/packages/dom/test/text-quote/match-cases.ts b/packages/dom/test/text-quote/match-cases.ts
index 099802c..c66b4e9 100644
--- a/packages/dom/test/text-quote/match-cases.ts
+++ b/packages/dom/test/text-quote/match-cases.ts
@@ -18,8 +18,7 @@
  * under the License.
  */
 
-import type { TextQuoteSelector } from '@annotator/selector';
-
+import type { TextQuoteSelector } from '../../src';
 import { RangeInfo } from '../utils';
 
 export const testCases: {
@@ -32,7 +31,6 @@ export const testCases: {
   simple: {
     html: '<b>lorem ipsum dolor amet yada yada</b>',
     selector: {
-      type: 'TextQuoteSelector',
       exact: 'dolor am',
     },
     expected: [
@@ -47,7 +45,6 @@ export const testCases: {
   'first characters': {
     html: '<b>lorem ipsum dolor amet yada yada</b>',
     selector: {
-      type: 'TextQuoteSelector',
       exact: 'lorem ipsum',
     },
     expected: [
@@ -62,7 +59,6 @@ export const testCases: {
   'last characters': {
     html: '<b>lorem ipsum dolor amet yada yada</b>',
     selector: {
-      type: 'TextQuoteSelector',
       exact: 'yada yada',
     },
     expected: [
@@ -77,7 +73,6 @@ export const testCases: {
   'across elements': {
     html: '<b>lorem <i>ipsum</i> dolor <u>amet</u> yada yada</b>',
     selector: {
-      type: 'TextQuoteSelector',
       exact: 'dolor am',
     },
     expected: [
@@ -92,7 +87,6 @@ export const testCases: {
   'exact element contents': {
     html: '<b>lorem <i>ipsum dolor</i> amet yada yada</b>',
     selector: {
-      type: 'TextQuoteSelector',
       exact: 'ipsum dolor',
     },
     expected: [
@@ -108,7 +102,6 @@ export const testCases: {
     html:
       '<head><title>The title</title></head><b>lorem ipsum dolor amet yada yada</b>',
     selector: {
-      type: 'TextQuoteSelector',
       exact: 'title',
     },
     expected: [
@@ -123,7 +116,6 @@ export const testCases: {
   'two matches': {
     html: '<b>lorem ipsum dolor amet yada yada</b>',
     selector: {
-      type: 'TextQuoteSelector',
       exact: 'yada',
     },
     expected: [
@@ -144,7 +136,6 @@ export const testCases: {
   'overlapping matches': {
     html: '<b>bananas</b>',
     selector: {
-      type: 'TextQuoteSelector',
       exact: 'ana',
     },
     expected: [
@@ -165,7 +156,6 @@ export const testCases: {
   'no matches': {
     html: '<b>lorem ipsum dolor amet yada yada</b>',
     selector: {
-      type: 'TextQuoteSelector',
       exact: 'holy grail',
     },
     expected: [],
@@ -173,7 +163,6 @@ export const testCases: {
   'with prefix': {
     html: '<b>lorem ipsum dolor amet yada yada</b>',
     selector: {
-      type: 'TextQuoteSelector',
       exact: 'yada',
       prefix: 't ',
     },
@@ -189,7 +178,6 @@ export const testCases: {
   'with suffix': {
     html: '<b>lorem ipsum dolor amet yada yada</b>',
     selector: {
-      type: 'TextQuoteSelector',
       exact: 'o',
       suffix: 'l',
     },
@@ -205,7 +193,6 @@ export const testCases: {
   'with prefix and suffix': {
     html: '<b>lorem ipsum dolor amet yada yada</b>',
     selector: {
-      type: 'TextQuoteSelector',
       exact: 'o',
       prefix: 'l',
       suffix: 're',
@@ -222,7 +209,6 @@ export const testCases: {
   'with prefix and suffix, two matches': {
     html: '<b>lorem ipsum dolor amet yada yada</b>',
     selector: {
-      type: 'TextQuoteSelector',
       exact: 'o',
       prefix: 'l',
       suffix: 'r',
@@ -245,7 +231,6 @@ export const testCases: {
   'with prefix, no matches': {
     html: '<b>lorem ipsum dolor amet yada yada</b>',
     selector: {
-      type: 'TextQuoteSelector',
       exact: 'dolor',
       prefix: 'oopsum ',
     },
@@ -254,7 +239,6 @@ export const testCases: {
   'with suffix, no matches': {
     html: '<b>lorem ipsum dolor amet yada yada</b>',
     selector: {
-      type: 'TextQuoteSelector',
       exact: 'dolor',
       suffix: ' amot',
     },
@@ -263,7 +247,6 @@ export const testCases: {
   'with suffix, no matches due to whitespace': {
     html: '<b>lorem ipsum dolor amet yada yada</b>',
     selector: {
-      type: 'TextQuoteSelector',
       exact: 'dolor',
       suffix: 'a',
     },
@@ -272,7 +255,6 @@ export const testCases: {
   'with empty prefix and suffix': {
     html: '<b>lorem ipsum dolor amet yada yada</b>',
     selector: {
-      type: 'TextQuoteSelector',
       exact: 'dolor am',
       prefix: '',
       suffix: '',
@@ -289,7 +271,6 @@ export const testCases: {
   'empty quote': {
     html: '<b>lorem</b>',
     selector: {
-      type: 'TextQuoteSelector',
       exact: '',
     },
     // A five character string contains six spots to find an empty string
@@ -305,7 +286,6 @@ export const testCases: {
   'empty quote, with prefix': {
     html: '<b>lorem ipsum dolor amet yada yada</b>',
     selector: {
-      type: 'TextQuoteSelector',
       exact: '',
       prefix: 'dolor',
     },
@@ -321,7 +301,6 @@ export const testCases: {
   'empty quote, with suffix': {
     html: '<b>lorem ipsum dolor amet yada yada</b>',
     selector: {
-      type: 'TextQuoteSelector',
       exact: '',
       suffix: 'i',
     },
@@ -337,7 +316,6 @@ export const testCases: {
   'empty quote, with prefix and suffix': {
     html: '<b>lorem ipsum dolor amet yada yada</b>',
     selector: {
-      type: 'TextQuoteSelector',
       exact: '',
       prefix: 'lorem ',
       suffix: 'ipsum',
@@ -354,7 +332,6 @@ export const testCases: {
   'empty quote, no matches': {
     html: '<b>lorem ipsum dolor amet yada yada</b>',
     selector: {
-      type: 'TextQuoteSelector',
       exact: '',
       prefix: 'X',
     },
diff --git a/packages/dom/test/text-quote/match.test.ts b/packages/dom/test/text-quote/match.test.ts
index 78b1239..ee8b1ef 100644
--- a/packages/dom/test/text-quote/match.test.ts
+++ b/packages/dom/test/text-quote/match.test.ts
@@ -19,10 +19,9 @@
  */
 
 import { assert } from 'chai';
-import type { TextQuoteSelector } from '@annotator/selector';
 
+import type { TextQuoteSelector } from '../../src';
 import { createTextQuoteSelectorMatcher } from '../../src/text-quote/match';
-import type { DomScope } from '../../src/types';
 import { evaluateXPath, RangeInfo } from '../utils';
 
 import { testCases } from './match-cases';
@@ -35,13 +34,21 @@ describe('createTextQuoteSelectorMatcher', () => {
   )) {
     it(`works for case: '${name}'`, async () => {
       const doc = domParser.parseFromString(html, 'text/html');
-      await testMatcher(doc, doc, selector, expected);
+
+      const scope = doc.createRange();
+      scope.selectNodeContents(doc);
+
+      await testMatcher(doc, scope, selector, expected);
     });
   }
 
   it('handles adjacent text nodes', async () => {
     const { html, selector } = testCases['simple'];
     const doc = domParser.parseFromString(html, 'text/html');
+
+    const scope = doc.createRange();
+    scope.selectNodeContents(doc);
+
     const textNode = evaluateXPath(doc, '//b/text()') as Text;
 
     for (let index = textNode.length - 1; index > 0; index--)
@@ -49,7 +56,7 @@ describe('createTextQuoteSelectorMatcher', () => {
     // console.log([...textNode.parentNode.childNodes].map(node => node.textContent))
     // → 'l',  'o', 'r', 'e', 'm', …
 
-    await testMatcher(doc, doc, selector, [
+    await testMatcher(doc, scope, selector, [
       {
         startContainerXPath: '//b/text()[13]',
         startOffset: 0,
@@ -63,6 +70,9 @@ describe('createTextQuoteSelectorMatcher', () => {
     const { html, selector } = testCases['simple'];
     const doc = domParser.parseFromString(html, 'text/html');
 
+    const scope = doc.createRange();
+    scope.selectNodeContents(doc);
+
     const textNode = evaluateXPath(doc, '//b/text()') as Text;
     textNode.splitText(textNode.length);
     textNode.splitText(20);
@@ -75,7 +85,7 @@ describe('createTextQuoteSelectorMatcher', () => {
     // console.log([...textNode.parentNode.childNodes].map(node => node.textContent))
     // → '', 'lorem ipsum ', '', 'dolor', '', ' am', '', 'et yada yada', ''
 
-    await testMatcher(doc, doc, selector, [
+    await testMatcher(doc, scope, selector, [
       {
         startContainerXPath: '//b/text()[4]', // "dolor"
         startOffset: 0,
@@ -89,24 +99,33 @@ describe('createTextQuoteSelectorMatcher', () => {
     const { html, selector, expected } = testCases['simple'];
     const doc = domParser.parseFromString(html, 'text/html');
 
-    await testMatcher(doc, evaluateXPath(doc, '//b'), selector, expected);
+    const scope = doc.createRange();
+    scope.selectNodeContents(evaluateXPath(doc, '//b'));
+
+    await testMatcher(doc, scope, selector, expected);
   });
 
   it('works with parent of text as scope, when matching its first characters', async () => {
     const { html, selector, expected } = testCases['first characters'];
     const doc = domParser.parseFromString(html, 'text/html');
 
-    await testMatcher(doc, evaluateXPath(doc, '//b'), selector, expected);
+    const scope = doc.createRange();
+    scope.selectNodeContents(evaluateXPath(doc, '//b'));
+
+    await testMatcher(doc, scope, selector, expected);
   });
 
   it('works with parent of text as scope, when matching its first characters, with an empty text node', async () => {
     const { html, selector } = testCases['first characters'];
     const doc = domParser.parseFromString(html, 'text/html');
 
+    const scope = doc.createRange();
+    scope.selectNodeContents(evaluateXPath(doc, '//b'));
+
     const textNode = evaluateXPath(doc, '//b/text()') as Text;
     textNode.splitText(0);
 
-    await testMatcher(doc, evaluateXPath(doc, '//b'), selector, [
+    await testMatcher(doc, scope, selector, [
       {
         startContainerXPath: '//b/text()[2]',
         startOffset: 0,
@@ -179,7 +198,7 @@ describe('createTextQuoteSelectorMatcher', () => {
 
 async function testMatcher(
   doc: Document,
-  scope: DomScope,
+  scope: Range,
   selector: TextQuoteSelector,
   expected: RangeInfo[],
 ) {
diff --git a/packages/selector/src/index.ts b/packages/selector/src/index.ts
index c66bd94..8dfcb8b 100644
--- a/packages/selector/src/index.ts
+++ b/packages/selector/src/index.ts
@@ -18,39 +18,29 @@
  * under the License.
  */
 
-import type { Matcher, Selector } from './types';
-
-export type { Matcher, Selector } from './types';
-export type { CssSelector, RangeSelector, TextQuoteSelector } from './types';
+export interface Selector {
+  refinedBy?: Selector;
+}
 
-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);
+export function makeRefinable<T>(
+  createMatcher: (selector: Selector) => (scope: T) => AsyncIterable<T>,
+): (selector: Selector) => (scope: T) => AsyncIterable<T> {
+  return function createMatcherWithRefinement(selector) {
+    const { refinedBy } = selector;
 
-    if (sourceSelector.refinedBy) {
-      const refiningSelector = createMatcherWithRefinement(
-        sourceSelector.refinedBy,
-      );
+    if (refinedBy) {
+      const subScopes = createMatcher(selector);
+      const refinement = createMatcherWithRefinement(refinedBy) as (
+        scope: T,
+      ) => AsyncIterable<T>;
 
       return async function* matchAll(scope) {
-        for await (const match of matcher(scope)) {
-          yield* refiningSelector(match);
+        for await (const subScope of subScopes(scope)) {
+          yield* refinement(subScope);
         }
       };
     }
 
-    return matcher;
+    return createMatcher(selector);
   };
 }
diff --git a/packages/selector/src/types.ts b/packages/selector/src/types.ts
deleted file mode 100644
index fc4f64b..0000000
--- a/packages/selector/src/types.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-/**
- * @license
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-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 6de6493..25fa5b4 100644
--- a/web/demo/index.js
+++ b/web/demo/index.js
@@ -105,10 +105,13 @@ const createMatcher = makeRefinable((selector) => {
 });
 
 async function anchor(selector) {
+  const scope = document.createRange();
+  scope.selectNodeContents(target);
+
   const matchAll = createMatcher(selector);
   const ranges = [];
 
-  for await (const range of matchAll(target)) {
+  for await (const range of matchAll(scope)) {
     ranges.push(range);
   }