Browser extension that demonstrates the Web Annotation Discovery mechanism: subscribe to people’s annotation collections/‘feeds’, to see their notes on the web; and create & publish annotations yourself.

web-annotation-discovery-we.../src/util/ dom-selectors.ts
151 lines
4.7 KiB

  1. import {
  2. createCssSelectorMatcher,
  3. createTextPositionSelectorMatcher,
  4. createTextQuoteSelectorMatcher,
  5. makeCreateRangeSelectorMatcher as AAmakeCreateRangeSelectorMatcher,
  6. } from '@apache-annotator/dom';
  7. import {
  8. makeRefinable,
  9. Matcher,
  10. TextPositionSelector,
  11. TextQuoteSelector,
  12. } from '@apache-annotator/selector';
  13. import { asArray } from 'web-annotation-utils';
  14. import type { OnlyOne, Selector, WebAnnotation } from 'web-annotation-utils';
  15. /**
  16. * Find the Elements and/or Ranges in the document the annotation targets, if
  17. * any.
  18. *
  19. * This supports the following selector types:
  20. * - CssSelector
  21. * - TextQuoteSelector
  22. * - TextPositionSelector
  23. * - RangeSelector
  24. */
  25. export async function findTargetsInDocument(
  26. target: WebAnnotation['target'],
  27. document = window.document,
  28. ): Promise<DomMatch[]> {
  29. // Process all targets (there may be multiple)
  30. const targets = await Promise.all(
  31. asArray(target).map((target) => findTargetInDocument(target, document)),
  32. );
  33. // A target might match in multiple places (e.g. TextQuoteSelector), each of which counts as a target.
  34. return targets.flat();
  35. }
  36. /**
  37. * Find the Elements and/or Ranges in the document the annotation targets, if
  38. * any, given a single target.
  39. *
  40. * This supports the following selector types:
  41. * - CssSelector
  42. * - TextQuoteSelector
  43. * - TextPositionSelector
  44. * - RangeSelector
  45. */
  46. export async function findTargetInDocument(
  47. target: OnlyOne<WebAnnotation['target']>,
  48. document = window.document,
  49. ): Promise<DomMatch[]> {
  50. // If it targets the whole document, there are no targets.
  51. if (typeof target === 'string') {
  52. // This string *could* be referring to a non-nested node that contains the actual target URL.
  53. // We’re not going to fetch that here, but simply assume it refers to the target document.
  54. return [];
  55. }
  56. // An External Resource: also targets the whole document.
  57. if (!('source' in target)) return [];
  58. // A SpecificResource without a selector, no fun either.
  59. if (!target.selector) return [];
  60. // The selector could be an external node. We’ll not bother fetching that here.
  61. if (typeof target.selector === 'string') {
  62. throw new Error(
  63. 'Annotation target does not include its selector; fetching it is not implemented.',
  64. );
  65. }
  66. // Use the first selector we understand. (“Multiple Selectors SHOULD select the same content”)
  67. // TODO Take the more precise one; retry with others if the first fails; perhaps combine e.g. Position+Quote for speedup.
  68. const selector = asArray(target.selector).find(
  69. (selector) => selector.type && selector.type in supportedSelectorTypes,
  70. );
  71. if (!selector) return [];
  72. const targetInDom = await matchSelector(selector, document);
  73. return targetInDom;
  74. }
  75. const supportedSelectorTypes = {
  76. CssSelector: null,
  77. TextQuoteSelector: null,
  78. TextPositionSelector: null,
  79. RangeSelector: null,
  80. };
  81. type SupportedSelector = MaybeRefined<
  82. TextQuoteSelector | TextPositionSelector | RangeSelector<SupportedSelector>
  83. >;
  84. type DomScope = Node | Range;
  85. type DomMatch = Element | Range;
  86. type DomMatcher = Matcher<DomScope, DomMatch>;
  87. // TODO fix type issues
  88. const createMatcher: (selector: SupportedSelector) => DomMatcher =
  89. // @ts-ignore
  90. makeRefinable<SupportedSelector, DomScope, DomMatch>((selector) => {
  91. const createMatcherFunctions = {
  92. CssSelector: createCssSelectorMatcher,
  93. TextQuoteSelector: createTextQuoteSelectorMatcher,
  94. TextPositionSelector: createTextPositionSelectorMatcher,
  95. RangeSelector:
  96. // @ts-ignore
  97. makeCreateRangeSelectorMatcher<SupportedSelector>(createMatcher),
  98. };
  99. const innerCreateMatcher = createMatcherFunctions[selector.type];
  100. // @ts-ignore
  101. return innerCreateMatcher(selector);
  102. });
  103. async function matchSelector(
  104. selector: Selector,
  105. scope: DomScope = window.document,
  106. ): Promise<DomMatch[]> {
  107. if (!(selector.type && selector.type in supportedSelectorTypes))
  108. throw new Error(`Unsupported selector type: ${selector.type}`);
  109. const matches: DomMatch[] = [];
  110. const matchGenerator = createMatcher(selector as SupportedSelector)(scope);
  111. for await (const match of matchGenerator) {
  112. matches.push(match);
  113. }
  114. return matches;
  115. }
  116. // Type modifications for Apache Annotator (TODO apply upstream)
  117. type MaybeRefined<T extends Selector> = T & { refinedBy?: T };
  118. interface RangeSelector<T extends Selector = Selector> extends Selector {
  119. type: 'RangeSelector';
  120. startSelector: T;
  121. endSelector: T;
  122. }
  123. function makeCreateRangeSelectorMatcher<T extends Selector>(
  124. createMatcher: <TMatch extends Node | Range>(
  125. selector: T,
  126. ) => Matcher<Node | Range, TMatch>,
  127. ): (selector: RangeSelector<T>) => Matcher<DomScope, Range> {
  128. // @ts-ignore
  129. return AAmakeCreateRangeSelectorMatcher(createMatcher);
  130. }