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 { // 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, document = window.document, ): Promise { // 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 >; type DomScope = Node | Range; type DomMatch = Element | Range; type DomMatcher = Matcher; // TODO fix type issues const createMatcher: (selector: SupportedSelector) => DomMatcher = // @ts-ignore makeRefinable((selector) => { const createMatcherFunctions = { CssSelector: createCssSelectorMatcher, TextQuoteSelector: createTextQuoteSelectorMatcher, TextPositionSelector: createTextPositionSelectorMatcher, RangeSelector: // @ts-ignore makeCreateRangeSelectorMatcher(createMatcher), }; const innerCreateMatcher = createMatcherFunctions[selector.type]; // @ts-ignore return innerCreateMatcher(selector); }); async function matchSelector( selector: Selector, scope: DomScope = window.document, ): Promise { 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 & { refinedBy?: T }; interface RangeSelector extends Selector { type: 'RangeSelector'; startSelector: T; endSelector: T; } function makeCreateRangeSelectorMatcher( createMatcher: ( selector: T, ) => Matcher, ): (selector: RangeSelector) => Matcher { // @ts-ignore return AAmakeCreateRangeSelectorMatcher(createMatcher); }