|
- /**
- * This code provides a reference implementation in TypeScript for the Web
- * Anotation Discovery mechanism.
- *
- * It implements functionality on the browser side, to detect annotations
- * embedded in the page, and to detect links to annotation ‘feeds’.
- */
-
- import MIMEType from 'whatwg-mimetype';
-
- /**
- * The type used here for Web Annotation objects. For valid input, the object
- * should include other properties (e.g. the target, id, …). However, this type
- * defines only the minimum outline required for the functions below; hence the
- * “should” in the name.
- */
- type ShouldBeAnnotation = {
- '@context':
- | 'http://www.w3.org/ns/anno.jsonld'
- | [...any, 'http://www.w3.org/ns/anno.jsonld', ...any];
- type: 'Annotation' | [...any, 'Annotation', ...any];
- };
-
- /**
- * Likewise for an Annotation Collection.
- */
- type ShouldBeAnnotationCollection = {
- '@context':
- | 'http://www.w3.org/ns/anno.jsonld'
- | [...any, 'http://www.w3.org/ns/anno.jsonld', ...any];
- type: 'AnnotationCollection' | [...any, 'AnnotationCollection', ...any];
- first?:
- | string
- | {
- items: Omit<ShouldBeAnnotation, '@context'>[];
- };
- };
-
- function isAnnotation(value: any): value is ShouldBeAnnotation {
- if (typeof value !== 'object') return false;
- const hasCorrectContext = asArray(value['@context']).some(
- (context) => context === 'http://www.w3.org/ns/anno.jsonld',
- );
- const hasCorrectType = asArray(value.type).some(
- (type) => type === 'Annotation',
- );
- return hasCorrectContext && hasCorrectType;
- }
-
- function isAnnotationCollection(
- value: any,
- ): value is ShouldBeAnnotationCollection {
- if (typeof value !== 'object') return false;
- const hasCorrectContext = asArray(value['@context']).some(
- (context) => context === 'http://www.w3.org/ns/anno.jsonld',
- );
- const hasCorrectType = asArray(value.type).some(
- (type) => type === 'AnnotationCollection',
- );
- return hasCorrectContext && hasCorrectType;
- }
-
- /**
- * Helper function to detect if a script/link has the media type. While an extract string match may be simple and tempting,
- * many type strings are equivalent. Some examples:
- *
- * application/ld+json;profile="http://www.w3.org/ns/anno.jsonld"
- * application/ld+json;profile="something and http://www.w3.org/ns/anno.jsonld"
- * application/ld+json;profile="\"with\\escapes\" http://www.w3.org/ns/anno.jsonld"
- * application/ld+json; charset="utf-8"; profile="http://www.w3.org/ns/anno.jsonld"
- */
- export function isAnnotationMimeType(type: string): boolean {
- let mimeType: MIMEType;
- try {
- mimeType = new MIMEType(type);
- } catch (error) {
- return false;
- }
- if (mimeType.essence !== 'application/ld+json') return false;
- const profile = mimeType.parameters.get('profile');
- if (!profile) return false;
- return profile.split(' ').includes('http://www.w3.org/ns/anno.jsonld');
- }
-
- /**
- * To discover annotations when navigating to a URL, simply check the content
- * type of the response.
- *
- * If positive, response.json() can be passed into getAnnotationsFromParsedJson().
- */
- export function responseContainsAnnotations(response: Response) {
- return (
- response.ok &&
- isAnnotationMimeType(response.headers.get('Content-Type') || '')
- );
- }
-
- export function getAnnotationsFromParsedJson(value: any): ShouldBeAnnotation[] {
- // The content could be one annotation, or a collection of annotations.
- if (isAnnotation(value)) {
- return [value];
- } else if (isAnnotationCollection(value) && typeof value.first === 'object') {
- return value.first.items.map((annotation) => ({
- '@context': value['@context'],
- ...annotation,
- }));
- } else {
- // Perhaps we got invalid data or an empty collection.
- return [];
- }
- }
-
- /**
- * Find annotations embedded as JSON within <script> tags.
- * See “Embedding Web Annotations in HTML - W3C Working Group Note 23 February 2017”
- * <https://www.w3.org/TR/annotation-html/>
- */
- export function discoverEmbeddedAnnotations(
- document = window.document,
- ): ShouldBeAnnotation[] {
- let scripts = [
- ...document.getElementsByTagName('script'),
- ] as HTMLScriptElement[];
- scripts = scripts.filter((script) => isAnnotationMimeType(script.type));
-
- const annotations: ShouldBeAnnotation[] = [];
- for (const script of scripts) {
- try {
- const parsed = JSON.parse(script.textContent ?? '');
- annotations.push(...getAnnotationsFromParsedJson(parsed));
- } catch (error) {
- console.error('Cannot read annotation(s) of script', script, error);
- }
- }
- return annotations;
- }
-
- /**
- * Find links to annotation feeds.
- *
- * An example of such a link:
- * <link
- * rel="alternate"
- * type='application/ld+json;profile="http://www.w3.org/ns/anno.jsonld"'
- * title="My annotation feed!"
- * href="https://myfeed.example/annotations/"
- * />
- */
- export function discoverAnnotationFeeds(document = window.document) {
- let links = [...document.getElementsByTagName('link')] as HTMLLinkElement[];
- links = links.filter(
- (link) =>
- link.relList.contains('alternate') && isAnnotationMimeType(link.type),
- );
-
- return links.map((link) => ({
- url: link.href,
- title: link.title,
- }));
- }
-
- /**
- * Helper function to treat a non-array value as an array with one item.
- */
- function asArray<T>(value: T | T[] | undefined): T[] {
- if (Array.isArray(value)) return value;
- if (value === undefined || value === null) return [];
- return [value];
- }
|