A simple mechanism for using Web Annotation in web browsers.

web-annotation-discovery/example-code/ discovery.ts
170 lines
5.3 KiB

  1. /**
  2. * This code provides a reference implementation in TypeScript for the Web
  3. * Anotation Discovery mechanism.
  4. *
  5. * It implements functionality on the browser side, to detect annotations
  6. * embedded in the page, and to detect links to annotation ‘feeds’.
  7. */
  8. import MIMEType from 'whatwg-mimetype';
  9. /**
  10. * The type used here for Web Annotation objects. For valid input, the object
  11. * should include other properties (e.g. the target, id, …). However, this type
  12. * defines only the minimum outline required for the functions below; hence the
  13. * “should” in the name.
  14. */
  15. type ShouldBeAnnotation = {
  16. '@context':
  17. | 'http://www.w3.org/ns/anno.jsonld'
  18. | [...any, 'http://www.w3.org/ns/anno.jsonld', ...any];
  19. type: 'Annotation' | [...any, 'Annotation', ...any];
  20. };
  21. /**
  22. * Likewise for an Annotation Collection.
  23. */
  24. type ShouldBeAnnotationCollection = {
  25. '@context':
  26. | 'http://www.w3.org/ns/anno.jsonld'
  27. | [...any, 'http://www.w3.org/ns/anno.jsonld', ...any];
  28. type: 'AnnotationCollection' | [...any, 'AnnotationCollection', ...any];
  29. first?:
  30. | string
  31. | {
  32. items: Omit<ShouldBeAnnotation, '@context'>[];
  33. };
  34. };
  35. function isAnnotation(value: any): value is ShouldBeAnnotation {
  36. if (typeof value !== 'object') return false;
  37. const hasCorrectContext = asArray(value['@context']).some(
  38. (context) => context === 'http://www.w3.org/ns/anno.jsonld',
  39. );
  40. const hasCorrectType = asArray(value.type).some(
  41. (type) => type === 'Annotation',
  42. );
  43. return hasCorrectContext && hasCorrectType;
  44. }
  45. function isAnnotationCollection(
  46. value: any,
  47. ): value is ShouldBeAnnotationCollection {
  48. if (typeof value !== 'object') return false;
  49. const hasCorrectContext = asArray(value['@context']).some(
  50. (context) => context === 'http://www.w3.org/ns/anno.jsonld',
  51. );
  52. const hasCorrectType = asArray(value.type).some(
  53. (type) => type === 'AnnotationCollection',
  54. );
  55. return hasCorrectContext && hasCorrectType;
  56. }
  57. /**
  58. * Helper function to detect if a script/link has the media type. While an extract string match may be simple and tempting,
  59. * many type strings are equivalent. Some examples:
  60. *
  61. * application/ld+json;profile="http://www.w3.org/ns/anno.jsonld"
  62. * application/ld+json;profile="something and http://www.w3.org/ns/anno.jsonld"
  63. * application/ld+json;profile="\"with\\escapes\" http://www.w3.org/ns/anno.jsonld"
  64. * application/ld+json; charset="utf-8"; profile="http://www.w3.org/ns/anno.jsonld"
  65. */
  66. export function isAnnotationMimeType(type: string): boolean {
  67. let mimeType: MIMEType;
  68. try {
  69. mimeType = new MIMEType(type);
  70. } catch (error) {
  71. return false;
  72. }
  73. if (mimeType.essence !== 'application/ld+json') return false;
  74. const profile = mimeType.parameters.get('profile');
  75. if (!profile) return false;
  76. return profile.split(' ').includes('http://www.w3.org/ns/anno.jsonld');
  77. }
  78. /**
  79. * To discover annotations when navigating to a URL, simply check the content
  80. * type of the response.
  81. *
  82. * If positive, response.json() can be passed into getAnnotationsFromParsedJson().
  83. */
  84. export function responseContainsAnnotations(response: Response) {
  85. return (
  86. response.ok &&
  87. isAnnotationMimeType(response.headers.get('Content-Type') || '')
  88. );
  89. }
  90. export function getAnnotationsFromParsedJson(value: any): ShouldBeAnnotation[] {
  91. // The content could be one annotation, or a collection of annotations.
  92. if (isAnnotation(value)) {
  93. return [value];
  94. } else if (isAnnotationCollection(value) && typeof value.first === 'object') {
  95. return value.first.items.map((annotation) => ({
  96. '@context': value['@context'],
  97. ...annotation,
  98. }));
  99. } else {
  100. // Perhaps we got invalid data or an empty collection.
  101. return [];
  102. }
  103. }
  104. /**
  105. * Find annotations embedded as JSON within <script> tags.
  106. * See “Embedding Web Annotations in HTML - W3C Working Group Note 23 February 2017”
  107. * <https://www.w3.org/TR/annotation-html/>
  108. */
  109. export function discoverEmbeddedAnnotations(
  110. document = window.document,
  111. ): ShouldBeAnnotation[] {
  112. let scripts = [
  113. ...document.getElementsByTagName('script'),
  114. ] as HTMLScriptElement[];
  115. scripts = scripts.filter((script) => isAnnotationMimeType(script.type));
  116. const annotations: ShouldBeAnnotation[] = [];
  117. for (const script of scripts) {
  118. try {
  119. const parsed = JSON.parse(script.textContent ?? '');
  120. annotations.push(...getAnnotationsFromParsedJson(parsed));
  121. } catch (error) {
  122. console.error('Cannot read annotation(s) of script', script, error);
  123. }
  124. }
  125. return annotations;
  126. }
  127. /**
  128. * Find links to annotation feeds.
  129. *
  130. * An example of such a link:
  131. * <link
  132. * rel="alternate"
  133. * type='application/ld+json;profile="http://www.w3.org/ns/anno.jsonld"'
  134. * title="My annotation feed!"
  135. * href="https://myfeed.example/annotations/"
  136. * />
  137. */
  138. export function discoverAnnotationFeeds(document = window.document) {
  139. let links = [...document.getElementsByTagName('link')] as HTMLLinkElement[];
  140. links = links.filter(
  141. (link) =>
  142. link.relList.contains('alternate') && isAnnotationMimeType(link.type),
  143. );
  144. return links.map((link) => ({
  145. url: link.href,
  146. title: link.title,
  147. }));
  148. }
  149. /**
  150. * Helper function to treat a non-array value as an array with one item.
  151. */
  152. function asArray<T>(value: T | T[] | undefined): T[] {
  153. if (Array.isArray(value)) return value;
  154. if (value === undefined || value === null) return [];
  155. return [value];
  156. }