import { asArray, asSingleValue } 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) { const webAnnotation = Object.assign({ '@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) { var _a; const creator = asSingleValue(annotationOrBody.creator); if (typeof creator === 'string') return undefined; 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); } /** * 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, url) { return getTargetUrls(target).some((targetUrl) => sameishUrl(targetUrl, url)); } // Compare URLs while ignoring the scheme, fragment identifier, query parameter and trailing slash. function sameishUrl(url1, url2) { return normaliseUrl(url1) === normaliseUrl(url2); } function normaliseUrl(url) { 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) { return unique(asArray(target).map(getTargetUrl)); } /** * Get the URL of the resource that the annotation targets, for a single * target. */ export function getTargetUrl(target) { 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) { const quotes = unique(asArray(target).map(getTargetQuote)).filter((s) => s !== undefined); return quotes; } /** * Get the exact quote that a single target of an annotation targets using a * TextQuoteSelector, if any. */ export function getTargetQuote(target) { 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'; }); if (textQuoteSelector) return textQuoteSelector.exact; } } function unique(a) { return [...new Set(a)]; }