|
- import {
- createCssSelectorMatcher,
- createTextPositionSelectorMatcher,
- createTextQuoteSelectorMatcher,
- makeCreateRangeSelectorMatcher as AAmakeCreateRangeSelectorMatcher,
- } from '@apache-annotator/dom';
- import {
- makeRefinable,
- Matcher,
- TextPositionSelector,
- TextQuoteSelector,
- } from '@apache-annotator/selector';
- import { asArray } from 'web-annotation-utils';
- import type { OnlyOne, Selector, WebAnnotation } from 'web-annotation-utils';
-
- /**
- * Find the Elements and/or Ranges in the document the annotation targets, if
- * any.
- *
- * This supports the following selector types:
- * - CssSelector
- * - TextQuoteSelector
- * - TextPositionSelector
- * - RangeSelector
- */
- export async function findTargetsInDocument(
- target: WebAnnotation['target'],
- document = window.document,
- ): Promise<DomMatch[]> {
- // Process all targets (there may be multiple)
- const targets = await Promise.all(
- asArray(target).map((target) => findTargetInDocument(target, document)),
- );
-
- // A target might match in multiple places (e.g. TextQuoteSelector), each of which counts as a target.
- return targets.flat();
- }
-
- /**
- * Find the Elements and/or Ranges in the document the annotation targets, if
- * any, given a single target.
- *
- * This supports the following selector types:
- * - CssSelector
- * - TextQuoteSelector
- * - TextPositionSelector
- * - RangeSelector
- */
- export async function findTargetInDocument(
- target: OnlyOne<WebAnnotation['target']>,
- document = window.document,
- ): Promise<DomMatch[]> {
- // If it targets the whole document, there are no targets.
- if (typeof target === 'string') {
- // This string *could* be referring to a non-nested node that contains the actual target URL.
- // We’re not going to fetch that here, but simply assume it refers to the target document.
- return [];
- }
-
- // An External Resource: also targets the whole document.
- if (!('source' in target)) return [];
-
- // A SpecificResource without a selector, no fun either.
- if (!target.selector) return [];
-
- // The selector could be an external node. We’ll not bother fetching that here.
- if (typeof target.selector === 'string') {
- throw new Error(
- 'Annotation target does not include its selector; fetching it is not implemented.',
- );
- }
-
- // Use the first selector we understand. (“Multiple Selectors SHOULD select the same content”)
- // TODO Take the more precise one; retry with others if the first fails; perhaps combine e.g. Position+Quote for speedup.
- const selector = asArray(target.selector).find(
- (selector) => selector.type && selector.type in supportedSelectorTypes,
- );
- if (!selector) return [];
-
- const targetInDom = await matchSelector(selector, document);
- return targetInDom;
- }
-
- const supportedSelectorTypes = {
- CssSelector: null,
- TextQuoteSelector: null,
- TextPositionSelector: null,
- RangeSelector: null,
- };
-
- type SupportedSelector = MaybeRefined<
- TextQuoteSelector | TextPositionSelector | RangeSelector<SupportedSelector>
- >;
-
- type DomScope = Node | Range;
- type DomMatch = Element | Range;
- type DomMatcher = Matcher<DomScope, DomMatch>;
-
- // TODO fix type issues
-
- const createMatcher: (selector: SupportedSelector) => DomMatcher =
- // @ts-ignore
- makeRefinable<SupportedSelector, DomScope, DomMatch>((selector) => {
- const createMatcherFunctions = {
- CssSelector: createCssSelectorMatcher,
- TextQuoteSelector: createTextQuoteSelectorMatcher,
- TextPositionSelector: createTextPositionSelectorMatcher,
- RangeSelector:
- // @ts-ignore
- makeCreateRangeSelectorMatcher<SupportedSelector>(createMatcher),
- };
- const innerCreateMatcher = createMatcherFunctions[selector.type];
-
- // @ts-ignore
- return innerCreateMatcher(selector);
- });
-
- async function matchSelector(
- selector: Selector,
- scope: DomScope = window.document,
- ): Promise<DomMatch[]> {
- if (!(selector.type && selector.type in supportedSelectorTypes))
- throw new Error(`Unsupported selector type: ${selector.type}`);
-
- const matches: DomMatch[] = [];
- const matchGenerator = createMatcher(selector as SupportedSelector)(scope);
- for await (const match of matchGenerator) {
- matches.push(match);
- }
- return matches;
- }
-
- // Type modifications for Apache Annotator (TODO apply upstream)
-
- type MaybeRefined<T extends Selector> = T & { refinedBy?: T };
-
- interface RangeSelector<T extends Selector = Selector> extends Selector {
- type: 'RangeSelector';
- startSelector: T;
- endSelector: T;
- }
-
- function makeCreateRangeSelectorMatcher<T extends Selector>(
- createMatcher: <TMatch extends Node | Range>(
- selector: T,
- ) => Matcher<Node | Range, TMatch>,
- ): (selector: RangeSelector<T>) => Matcher<DomScope, Range> {
- // @ts-ignore
- return AAmakeCreateRangeSelectorMatcher(createMatcher);
- }
|