TypeScript types and utility functions for handling Web Annotations.

web-annotation-utils/lib/ wa-attribute-utils.js
115 lines
4.3 KiB

  1. import { asArray, asSingleValue } from './multiplicity-utils.js';
  2. /**
  3. * Turn a partial annotation into a ‘well-formed’ WebAnnotation.
  4. *
  5. * It sets the following properties, if absent in the given stub:
  6. * - `@context` as required
  7. * - `type` as required, to `'Annotation'`
  8. * - `created` as recommended (to the current time)
  9. * - `target` to `'about:invalid'`
  10. *
  11. * @returns A shallow clone of the given annotation stub, with the missing
  12. * properties added.
  13. */
  14. export function completeAnnotationStub(annotationStub) {
  15. const webAnnotation = Object.assign({ '@context': 'http://www.w3.org/ns/anno.jsonld', type: 'Annotation', created: new Date().toISOString(), id: '', target: 'about:invalid' }, annotationStub);
  16. return webAnnotation;
  17. }
  18. /**
  19. * Get the name of the creator. If there are multiple, returns the first.
  20. * Assumes the creator is a nested Agent object: if the creator a string
  21. * (presumably the URL of an Agent node), `undefined` is returned.
  22. */
  23. export function getSingleCreatorName(annotationOrBody) {
  24. var _a;
  25. const creator = asSingleValue(annotationOrBody.creator);
  26. if (typeof creator === 'string')
  27. return undefined;
  28. return asSingleValue((_a = creator === null || creator === void 0 ? void 0 : creator.name) !== null && _a !== void 0 ? _a : creator === null || creator === void 0 ? void 0 : creator.nickname);
  29. }
  30. /**
  31. * Check whether the annotation likely targets the given URL.
  32. *
  33. * The word “likely” is used because, in its comparison, this ignores the URL
  34. * scheme, fragment and query parameters.
  35. *
  36. * Note that, strictly speaking, a URL should be treated as an opaque string.
  37. * In practice, it may however be useful to consider URLs as ‘likely equivalent’
  38. * in order to apply annotations targeting one URL to the document with the
  39. * very similar URL. Apply with caution: Especially a different query may,
  40. * depending on the website at hand, result in very different documents.
  41. */
  42. export function targetsUrl(target, url) {
  43. return getTargetUrls(target).some((targetUrl) => sameishUrl(targetUrl, url));
  44. }
  45. // Compare URLs while ignoring the scheme, fragment identifier, query parameter and trailing slash.
  46. function sameishUrl(url1, url2) {
  47. return normaliseUrl(url1) === normaliseUrl(url2);
  48. }
  49. function normaliseUrl(url) {
  50. url = url
  51. .split('#')[0]
  52. .split('?')[0]
  53. .replace(/^[a-zA-Z0-9.+-]+:\/\//, '');
  54. if (url.endsWith('/'))
  55. url = url.slice(0, -1);
  56. return url;
  57. }
  58. /**
  59. * Get the URLs of the resources that the annotation targets, for all its
  60. * targets.
  61. */
  62. export function getTargetUrls(target) {
  63. return unique(asArray(target).map(getTargetUrl));
  64. }
  65. /**
  66. * Get the URL of the resource that the annotation targets, for a single
  67. * target.
  68. */
  69. export function getTargetUrl(target) {
  70. if (typeof target === 'string') {
  71. // This string *could* be referring to a non-nested SpecificResource that
  72. // then contains the actual target URL. But we are not able to fetch that
  73. // now, and simply assume the string refers to the target document.
  74. return target;
  75. }
  76. // Specific Resource
  77. if ('source' in target)
  78. return target.source;
  79. // External Resource
  80. return target.id;
  81. }
  82. /**
  83. * Get the exact quotes that the annotation targets using a TextQuoteSelector,
  84. * if any.
  85. */
  86. export function getTargetQuotes(target) {
  87. const quotes = unique(asArray(target).map(getTargetQuote)).filter((s) => s !== undefined);
  88. return quotes;
  89. }
  90. /**
  91. * Get the exact quote that a single target of an annotation targets using a
  92. * TextQuoteSelector, if any.
  93. */
  94. export function getTargetQuote(target) {
  95. if (typeof target === 'string')
  96. return undefined;
  97. if ('selector' in target) {
  98. // Find if target.selector is/has a TextQuoteSelector.
  99. const selectors = asArray(target.selector);
  100. const textQuoteSelector = selectors.find(selector => {
  101. if (typeof selector === 'string') {
  102. // The selector is not nested in the annotation. But we are not able to
  103. // fetch it now, and will thus have to ignore this selector.
  104. return false;
  105. }
  106. return selector.type === 'TextQuoteSelector';
  107. });
  108. if (textQuoteSelector)
  109. return textQuoteSelector.exact;
  110. }
  111. }
  112. function unique(a) {
  113. return [...new Set(a)];
  114. }