|
- import type { TextQuoteSelector } from '@apache-annotator/selector';
- import type { BodyObject, WebAnnotation } from './WebAnnotation.js';
- import { asArray, asSingleValue, OnlyOne } from './multiplicity-utils.js';
-
- /**
- * Turn a partial annotation into a ‘well-formed’ WebAnnotation.
- *
- * It sets the following properties, if absent in the given stub:
- * - `@context` as required
- * - `type` as required, to `'Annotation'`
- * - `created` as recommended (to the current time)
- * - `target` to `'about:invalid'`
- *
- * @returns A shallow clone of the given annotation stub, with the missing
- * properties added.
- */
- export function completeAnnotationStub(annotationStub: Partial<WebAnnotation>) {
- const webAnnotation: WebAnnotation = {
- '@context': 'http://www.w3.org/ns/anno.jsonld',
- type: 'Annotation',
- created: new Date().toISOString(),
- id: '',
- target: 'about:invalid',
- ...annotationStub,
- };
-
- return webAnnotation;
- }
-
- /**
- * Get the name of the creator. If there are multiple, returns the first.
- * Assumes the creator is a nested Agent object: if the creator a string
- * (presumably the URL of an Agent node), `undefined` is returned.
- */
- export function getSingleCreatorName(
- annotationOrBody: WebAnnotation | BodyObject,
- ): string | undefined {
- const creator = asSingleValue(annotationOrBody.creator);
- if (typeof creator === 'string') return undefined;
- return asSingleValue(creator?.name ?? creator?.nickname);
- }
-
- /**
- * Check whether the annotation likely targets the given URL.
- *
- * The word “likely” is used because, in its comparison, this ignores the URL
- * scheme, fragment and query parameters.
- *
- * Note that, strictly speaking, a URL should be treated as an opaque string.
- * In practice, it may however be useful to consider URLs as ‘likely equivalent’
- * in order to apply annotations targeting one URL to the document with the
- * very similar URL. Apply with caution: Especially a different query may,
- * depending on the website at hand, result in very different documents.
- */
- export function targetsUrl(target: WebAnnotation['target'], url: string) {
- return getTargetUrls(target).some((targetUrl) => sameishUrl(targetUrl, url));
- }
-
- // Compare URLs while ignoring the scheme, fragment identifier, query parameter and trailing slash.
- function sameishUrl(url1: string, url2: string) {
- return normaliseUrl(url1) === normaliseUrl(url2);
- }
-
- function normaliseUrl(url: string) {
- url = url
- .split('#')[0]
- .split('?')[0]
- .replace(/^[a-zA-Z0-9.+-]+:\/\//, '');
- if (url.endsWith('/')) url = url.slice(0, -1);
- return url;
- }
-
- /**
- * Get the URLs of the resources that the annotation targets, for all its
- * targets.
- */
- export function getTargetUrls(target: WebAnnotation['target']): string[] {
- return unique(asArray(target).map(getTargetUrl));
- }
-
- /**
- * Get the URL of the resource that the annotation targets, for a single
- * target.
- */
- export function getTargetUrl(target: OnlyOne<WebAnnotation['target']>): string {
- if (typeof target === 'string') {
- // This string *could* be referring to a non-nested SpecificResource that
- // then contains the actual target URL. But we are not able to fetch that
- // now, and simply assume the string refers to the target document.
- return target;
- }
- // Specific Resource
- if ('source' in target) return target.source;
- // External Resource
- return target.id;
- }
-
- /**
- * Get the exact quotes that the annotation targets using a TextQuoteSelector,
- * if any.
- */
- export function getTargetQuotes(target: WebAnnotation['target']): string[] {
- const quotes = unique(asArray(target).map(getTargetQuote)).filter(
- (s) => s !== undefined,
- ) as string[];
- return quotes;
- }
-
- /**
- * Get the exact quote that a single target of an annotation targets using a
- * TextQuoteSelector, if any.
- */
- export function getTargetQuote(target: OnlyOne<WebAnnotation['target']>): string | undefined {
- if (typeof target === 'string') return undefined;
- if ('selector' in target) {
- // Find if target.selector is/has a TextQuoteSelector.
- const selectors = asArray(target.selector);
- const textQuoteSelector = selectors.find(selector => {
- if (typeof selector === 'string') {
- // The selector is not nested in the annotation. But we are not able to
- // fetch it now, and will thus have to ignore this selector.
- return false;
- }
- return selector.type === 'TextQuoteSelector';
- }) as TextQuoteSelector | undefined;
- if (textQuoteSelector)
- return textQuoteSelector.exact;
- }
- }
-
- function unique<T>(a: T[]): T[] {
- return [...new Set(a)];
- }
|