Merge branch 'master' into simpler-matcher-creation
diff --git a/packages/dom/src/range/match.ts b/packages/dom/src/range/match.ts
index d6891c6..d0b6943 100644
--- a/packages/dom/src/range/match.ts
+++ b/packages/dom/src/range/match.ts
@@ -20,8 +20,10 @@
 
 import type {
   Matcher,
+  Plugin,
   RangeSelector,
   Selector,
+  MatcherCreator,
 } from '@apache-annotator/selector';
 import { ownerDocument } from '../owner-document';
 import { toRange } from '../to-range';
@@ -93,9 +95,7 @@
  * @public
  */
 export function makeCreateRangeSelectorMatcher(
-  createMatcher: <T extends Selector, TMatch extends Node | Range>(
-    selector: T,
-  ) => Matcher<Node | Range, TMatch>,
+  createMatcher: MatcherCreator<Node | Range, Node | Range>,
 ): (selector: RangeSelector) => Matcher<Node | Range, Range> {
   return function createRangeSelectorMatcher(selector) {
     const startMatcher = createMatcher(selector.startSelector);
@@ -122,3 +122,17 @@
     };
   };
 }
+
+export const supportRangeSelector: Plugin<Node | Range, Node | Range> = function supportRangeSelectorPlugin(
+  next,
+  recurse,
+) {
+  const createRangeSelectorMatcher = makeCreateRangeSelectorMatcher(recurse);
+  return function (selector: Selector) {
+    if (selector.type === 'RangeSelector') {
+      return createRangeSelectorMatcher(selector as RangeSelector);
+    } else {
+      return next(selector);
+    }
+  };
+};
diff --git a/packages/selector/src/index.ts b/packages/selector/src/index.ts
index e0a5e48..7c294ae 100644
--- a/packages/selector/src/index.ts
+++ b/packages/selector/src/index.ts
@@ -18,47 +18,77 @@
  * under the License.
  */
 
-import type { Matcher, Selector } from './types';
+import type { Matcher, Selector, SelectorType, MatcherCreator, Plugin } from './types';
 
-export type { Matcher, Selector } from './types';
-export type {
-  CssSelector,
-  RangeSelector,
-  TextPositionSelector,
-  TextQuoteSelector,
-} from './types';
 export * from './text';
+export * from './types';
+
+interface TypeToMatcherCreatorMap<TScope, TMatch> {
+  // [K: SelectorType]: MatcherCreator<TScope, TMatch>; // Gives errors further down. TypeScript’s fault?
+  [K: string]: MatcherCreator<TScope, TMatch> | undefined;
+}
+
+export function composeMatcherCreator<TScope, TMatch extends TScope>(
+  ...plugins: Array<Plugin<TScope, TMatch>>
+): MatcherCreator<TScope, TMatch> {
+  function innerMatcherCreator(selector: Selector): Matcher<TScope, TMatch> {
+    throw new TypeError(`Unhandled selector. Selector type: ${selector.type}`);
+  }
+
+  function outerMatcherCreator(selector: Selector): Matcher<TScope, TMatch> {
+    return composedMatcherCreator(selector);
+  }
+
+  const composedMatcherCreator = plugins.reduceRight(
+    (
+      matcherCreator: MatcherCreator<TScope, TMatch>,
+      plugin: Plugin<TScope, TMatch>
+    ) => plugin(matcherCreator, outerMatcherCreator),
+    innerMatcherCreator,
+  );
+
+  return outerMatcherCreator;
+}
+
+// A plugin with parameters (i.e. a function that returns a plugin)
+// Invokes the matcher implementation corresponding to the selector’s type.
+export function mapSelectorTypes<TScope, TMatch extends TScope>(
+  typeToMatcherCreator: TypeToMatcherCreatorMap<TScope, TMatch>,
+): Plugin<TScope, TMatch> {
+  return function mapSelectorTypesPlugin(next, recurse): MatcherCreator<TScope, TMatch> {
+    return function(selector: Selector): Matcher<TScope, TMatch> {
+      const type = selector.type;
+      if (type !== undefined) {
+        const matcherCreator = typeToMatcherCreator[type];
+        if (matcherCreator !== undefined)
+          return matcherCreator(selector);
+      }
+      // Not a know selector type; continue down the plugin chain.
+      return next(selector);
+    }
+  }
+}
 
 /**
- * Wrap a matcher creation function so that it supports refinement of selection.
+ * A plugin to support the Selector’s refinedBy field.
  *
  * See {@link https://www.w3.org/TR/2017/REC-annotation-model-20170223/#refinement-of-selection
  * | §4.2.9 Refinement of Selection} in the Web Annotation Data Model.
  *
- * @param matcherCreator - The function to wrap; it will be executed both for
- * {@link Selector}s passed to the returned wrapper function, and for any
- * refining Selector those might contain (and any refinement of that, etc.).
- *
  * @public
  */
-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> {
+export const supportRefinement: Plugin<any, any> =
+  function supportRefinementPlugin<TScope, TMatch extends TScope>(
+    next: MatcherCreator<TScope, TMatch>,
+    recurse: MatcherCreator<TScope, TMatch>,
+  ) {
   return function createMatcherWithRefinement(
-    sourceSelector: TSelector,
+    sourceSelector: Selector,
   ): Matcher<TScope, TMatch> {
-    const matcher = matcherCreator(sourceSelector);
+    const matcher = next(sourceSelector);
 
     if (sourceSelector.refinedBy) {
-      const refiningSelector = createMatcherWithRefinement(
+      const refiningSelector = recurse(
         sourceSelector.refinedBy,
       );
 
@@ -67,8 +97,8 @@
           yield* refiningSelector(match);
         }
       };
+    } else {
+      return matcher;
     }
-
-    return matcher;
   };
 }
diff --git a/packages/selector/src/types.ts b/packages/selector/src/types.ts
index fd59dfb..fff5199 100644
--- a/packages/selector/src/types.ts
+++ b/packages/selector/src/types.ts
@@ -35,9 +35,13 @@
    *
    * Corresponds to RDF property {@link http://www.w3.org/ns/oa#refinedBy}
    */
-  refinedBy?: Selector;
+  refinedBy?: this;
+
+  type?: SelectorType;
 }
 
+export type SelectorType = string; // not enumerating known options: we allow extensibility.
+
 /**
  * The {@link https://www.w3.org/TR/2017/REC-annotation-model-20170223/#css-selector
  * | CssSelector} of the Web Annotation Data Model.
@@ -103,3 +107,20 @@
 export interface Matcher<TScope, TMatch> {
   (scope: TScope): AsyncGenerator<TMatch, void, void>;
 }
+
+export type MatcherCreator<TScope, TMatch> = (selector: Selector) => Matcher<TScope, TMatch>;
+
+export type Plugin<TScope, TMatch> =
+  (
+    next: MatcherCreator<TScope, TMatch>,
+    recurse: MatcherCreator<TScope, TMatch>,
+  ) => typeof next;
+
+
+// Basic form of a plugin: (two equivalents)
+// const identity: Plugin<any, any> = (next, recurse) => next;
+// const identity: Plugin<any, any> = (next, recurse) => {
+//   return function (selector: Selector): Matcher<any, any> {
+//     return next(selector); // or do something else here.
+//   };
+// };
diff --git a/web/index.js b/web/index.js
index abb9a8b..d0c33f8 100644
--- a/web/index.js
+++ b/web/index.js
@@ -21,14 +21,18 @@
 /* global info, module, source, target, form */
 
 import {
-  makeCreateRangeSelectorMatcher,
   createTextQuoteSelectorMatcher,
   describeTextQuote,
+  supportRangeSelector,
   createTextPositionSelectorMatcher,
   describeTextPosition,
   highlightRange,
 } from '@apache-annotator/dom';
-import { makeRefinable } from '@apache-annotator/selector';
+import {
+  composeMatcherCreator,
+  mapSelectorTypes,
+  supportRefinement,
+} from '@apache-annotator/selector';
 
 const EXAMPLE_SELECTORS = [
   {
@@ -96,19 +100,14 @@
   info.innerText = '';
 }
 
-const createMatcher = makeRefinable((selector) => {
-  const innerCreateMatcher = {
+const createMatcher = composeMatcherCreator(
+  supportRefinement, // this plugin must come first: it needs to access the result of the ones below.
+  supportRangeSelector,
+  mapSelectorTypes({
     TextQuoteSelector: createTextQuoteSelectorMatcher,
     TextPositionSelector: createTextPositionSelectorMatcher,
-    RangeSelector: makeCreateRangeSelectorMatcher(createMatcher),
-  }[selector.type];
-
-  if (!innerCreateMatcher) {
-    throw new Error(`Unsupported selector type: ${selector.type}`);
-  }
-
-  return innerCreateMatcher(selector);
-});
+  }),
+);
 
 async function anchor(selector) {
   const matchAll = createMatcher(selector);