--wip-- [skip ci]
diff --git a/packages/dom/src/range/index.ts b/packages/dom/src/range/index.ts
index 011e994..be44f30 100644
--- a/packages/dom/src/range/index.ts
+++ b/packages/dom/src/range/index.ts
@@ -18,4 +18,27 @@
* under the License.
*/
+import type { RangeSelector } from './selector';
+import { makeCreateRangeSelectorMatcher } from './match';
+
export * from './match';
+export * from './selector';
+
+export function withRange<T>(
+ createMatcher: (selector: T) => (scope: Range) => AsyncIterable<Range>,
+): (selector: T | RangeSelector<T>) => (scope: Range) => AsyncIterable<Range> {
+ const createRangeMatcher = makeCreateRangeSelectorMatcher(createMatcher);
+ return function createMatcherWithRange(selector) {
+ if ('type' in selector && selector.type === 'RangeSelector') {
+ return createRangeMatcher(selector);
+ }
+ return createMatcher(selector as T);
+ };
+}
+
+export function withRangeRecursive<T>(
+ createMatcher: (selector: T) => (scope: Range) => AsyncIterable<Range>,
+): (selector: T | RangeSelector<T>) => (scope: Range) => AsyncIterable<Range> {
+ const inner = withRange(createMatcher);
+ return withRange(inner);
+}
diff --git a/packages/dom/src/range/match.ts b/packages/dom/src/range/match.ts
index bc2d601..cfa297b 100644
--- a/packages/dom/src/range/match.ts
+++ b/packages/dom/src/range/match.ts
@@ -19,12 +19,7 @@
*/
import { product } from './cartesian';
-
-export interface RangeSelector<T> {
- type: 'RangeSelector';
- startSelector: T;
- endSelector: T;
-}
+import type { RangeSelector } from './selector';
export function makeCreateRangeSelectorMatcher<T>(
createMatcher: (selector: T) => (scope: Range) => AsyncIterable<Range>,
diff --git a/packages/dom/src/range/selector.ts b/packages/dom/src/range/selector.ts
new file mode 100644
index 0000000..7690e76
--- /dev/null
+++ b/packages/dom/src/range/selector.ts
@@ -0,0 +1,25 @@
+/**
+ * @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 RangeSelector<T> {
+ type: 'RangeSelector';
+ startSelector: T;
+ endSelector: T;
+}
diff --git a/packages/selector/src/index.ts b/packages/selector/src/index.ts
index 99c2840..deca7b1 100644
--- a/packages/selector/src/index.ts
+++ b/packages/selector/src/index.ts
@@ -18,46 +18,83 @@
* under the License.
*/
+type Selector = { type: string };
+
+type Matcher<TScope, TMatch> = (scope: TScope) => AsyncIterable<TMatch>;
+type MatcherCreator<TSelector extends Selector, TScope, TMatch> = (
+ selector: TSelector,
+) => Matcher<TScope, TMatch>;
+
+type Plugin<TSelector extends Selector, TScope, TMatch> = (
+ next: MatcherCreator<TSelector, TScope, TMatch>,
+ recurse: MatcherCreator<TSelector, TScope, TMatch>,
+) => MatcherCreator<TSelector, TScope, TMatch>;
+
+export function composeMatcherCreator<
+ TSelector extends Selector,
+ TScope,
+ TMatch extends TScope
+>(
+ ...plugins: Array<Plugin<TSelector, TScope, TMatch>>
+): MatcherCreator<TSelector, TScope, TMatch> {
+ function innerMatcherCreator(selector: TSelector): Matcher<TScope, TMatch> {
+ throw new TypeError(`Unhandled selector. Selector type: ${selector.type}`);
+ }
+
+ function outerMatcherCreator(selector: TSelector): Matcher<TScope, TMatch> {
+ return composedMatcherCreator(selector);
+ }
+
+ const composedMatcherCreator = plugins.reduceRight(
+ (matcherCreator, plugin) => plugin(matcherCreator, outerMatcherCreator),
+ innerMatcherCreator,
+ );
+
+ return outerMatcherCreator;
+}
+
+type MatcherCreatorMap<TSelector extends Selector, TScope, TMatch> = {
+ [K: string]: MatcherCreator<TSelector, TScope, TMatch>;
+};
+
+export function mapSelectorTypes<TSelector extends Selector, TScope, TMatch>(
+ matcherCreators: MatcherCreatorMap<TSelector, TScope, TMatch>,
+): Plugin<TSelector, TScope, TMatch> {
+ return function mapSelectorTypesPlugin(next) {
+ return function (selector) {
+ const matcherCreator = matcherCreators[selector.type];
+
+ if (matcherCreator) {
+ return matcherCreator(selector);
+ }
+
+ return next(selector);
+ };
+ };
+}
+
export function withRefinement<
- TSelector,
- TSelectorScope,
- TRefinement,
- TRefinementScope,
+ TSelector extends Selector & { refinedBy?: TSelector },
+ TScope,
TMatch
>(
- createMatcher: (
- selector: TSelector,
- ) => (scope: TSelectorScope) => AsyncIterable<TRefinementScope>,
- createRefiner: (
- selector: TRefinement,
- ) => (scope: TRefinementScope) => AsyncIterable<TMatch>,
-): (
- selector: TSelector & { refinedBy?: TRefinement },
-) => (scope: TSelectorScope) => AsyncIterable<TRefinementScope | TMatch> {
+ next: (selector: TSelector) => (scope: TScope) => AsyncIterable<TScope>,
+ recurse: (selector: TSelector) => (scope: TScope) => AsyncIterable<TMatch>,
+): MatcherCreator<TSelector, TScope, TMatch> {
return function createMatcherWithRefinement(selector) {
const { refinedBy } = selector;
+ const matcher = next(selector);
if (refinedBy) {
- const match = createMatcher(selector);
- const refine = createRefiner(refinedBy);
+ const refine = recurse(refinedBy);
return async function* matchAll(scope) {
- for await (const subScope of match(scope)) {
+ for await (const subScope of matcher(scope)) {
yield* refine(subScope);
}
};
}
- return createMatcher(selector);
+ return matcher;
};
}
-
-export function withRecursiveRefinement<TSelector, TScope>(
- createMatcher: (
- selector: TSelector & { refinedBy?: TSelector },
- ) => (scope: TScope) => AsyncIterable<TScope>,
-): (
- selector: TSelector & { refinedBy?: TSelector },
-) => (scope: TScope) => AsyncIterable<TScope> {
- return withRefinement(createMatcher, createMatcher);
-}
diff --git a/web/demo/index.js b/web/demo/index.js
index 18c9983..a7f2816 100644
--- a/web/demo/index.js
+++ b/web/demo/index.js
@@ -21,12 +21,15 @@
/* global info, module, source, target */
import {
- makeCreateRangeSelectorMatcher,
createTextQuoteSelectorMatcher,
describeTextQuote,
highlightRange,
} from '@annotator/dom';
-import { withRecursiveRefinement } from '@annotator/selector';
+import {
+ composeMatcherCreator,
+ mapSelectorTypes,
+ withRefinement,
+} from '@annotator/selector';
const EXAMPLE_SELECTORS = [
{
@@ -91,18 +94,12 @@
target.normalize();
}
-const createMatcher = withRecursiveRefinement((selector) => {
- const innerCreateMatcher = {
+const createMatcher = composeMatcherCreator(
+ withRefinement,
+ mapSelectorTypes({
TextQuoteSelector: createTextQuoteSelectorMatcher,
- RangeSelector: makeCreateRangeSelectorMatcher(createMatcher),
- }[selector.type];
-
- if (!innerCreateMatcher) {
- throw new Error(`Unsupported selector type: ${selector.type}`);
- }
-
- return innerCreateMatcher(selector);
-});
+ }),
+);
async function anchor(selector) {
const scope = document.createRange();