UNPKG

@kit-data-manager/pid-component

Version:

The PID-Component is a web component that can be used to evaluate and display FAIR Digital Objects, PIDs, ORCiDs, and possibly other identifiers in a user-friendly way. It is easily extensible to support other identifier types.

1,316 lines (1,291 loc) 77.5 kB
/*! * * Copyright 2024 Karlsruhe Institute of Technology. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); const index = require('./index-0c94ea0c.js'); class GenericIdentifierType { constructor(value, settings) { this._settings = []; this._items = []; this._actions = []; this._value = value; this._settings = settings; } get settings() { return this._settings; } set settings(value) { this._settings = value; } get items() { return this._items; } get actions() { return this._actions; } get value() { return this._value; } get data() { return undefined; } renderBody() { return undefined; } } class DateType extends GenericIdentifierType { getSettingsKey() { return 'DateType'; } hasCorrectFormat() { const regex = new RegExp('^([0-9]{4})-([0]?[1-9]|1[0-2])-([0-2][0-9]|3[0-1])(T([0-1][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9](.[0-9]*)?(Z|([+|-]([0-1][0-9]|2[0-3]):[0-5][0-9])){1}))$'); return regex.test(this.value); } init() { this._date = new Date(this.value); return; } isResolvable() { return false; } renderPreview() { return index.h("span", null, this._date.toLocaleString()); } } let cacheInstance; async function open() { if ('caches' in self) { cacheInstance = await caches.open('pid-component'); } } async function cachedFetch(url, init) { await open(); if (cacheInstance) { const response = await cacheInstance.match(url); if (response) { return response.json(); } else { let response; const parts = url.split('://'); if (parts[0] !== 'https') { response = await fetch(`https://${parts[1]}`, init); if (!response) { console.log(`404 for https://${parts[1]} - trying http://${parts[1]}`); response = await fetch(`http://${parts[1]}`, init); } } else { response = await fetch(url, init); } await cacheInstance.put(url, response.clone()); return response.json(); } } else { const response = await fetch(url, init); return response.json(); } } async function clearCache() { if (cacheInstance) { await cacheInstance.delete('pid-component'); } } class ORCIDInfo { constructor(orcid, ORCiDJSON, familyName, givenNames, employments, preferredLocale, biography, emails, keywords, researcherUrls, country) { this._orcid = orcid; this._familyName = familyName; this._givenNames = givenNames; this._employments = employments; this._preferredLocale = preferredLocale; this._biography = biography; this._emails = emails; this._keywords = keywords; this._researcherUrls = researcherUrls; this._country = country; this._ORCiDJSON = ORCiDJSON; } get orcid() { return this._orcid; } get familyName() { return this._familyName; } get givenNames() { return this._givenNames; } get ORCiDJSON() { return this._ORCiDJSON; } get employments() { return this._employments; } get preferredLocale() { return this._preferredLocale; } get biography() { return this._biography; } get emails() { return this._emails; } get keywords() { return this._keywords; } get researcherUrls() { return this._researcherUrls; } get country() { return this._country; } static isORCiD(text) { const regex = new RegExp('^(https://orcid.org/)?[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{3}[0-9X]$'); return regex.test(text); } static async getORCiDInfo(orcid) { if (!ORCIDInfo.isORCiD(orcid)) throw new Error('Invalid input'); if (orcid.match('^(https://orcid.org/)?[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{3}[0-9X]$') !== null) orcid = orcid.replace('https://orcid.org/', ''); const rawOrcidJSON = await cachedFetch(`https://pub.orcid.org/v3.0/${orcid}`, { headers: { Accept: 'application/json', }, }); let familyName = ''; let givenNames = []; try { familyName = rawOrcidJSON['person']['name']['family-name']['value']; } catch (e) { console.debug(e); } try { givenNames = rawOrcidJSON['person']['name']['given-names']['value']; } catch (e) { console.debug(e); } const affiliations = rawOrcidJSON['activities-summary']['employments']['affiliation-group']; const employments = []; try { for (let i = 0; i < affiliations.length; i++) { const employmentSummary = affiliations[i]['summaries'][0]['employment-summary']; const employment = new Employment(new Date(), null, '', ''); if (employmentSummary['start-date'] !== null) employment.startDate = new Date(employmentSummary['start-date']['year']['value'], employmentSummary['start-date']['month']['value'], employmentSummary['start-date']['day']['value']); if (employmentSummary['end-date'] !== null) employment.endDate = new Date(employmentSummary['end-date']['year']['value'], employmentSummary['end-date']['month']['value'], employmentSummary['end-date']['day']['value']); employment.organization = employmentSummary['organization']['name']; employment.department = employmentSummary['department-name']; employments.push(employment); } } catch (e) { console.debug(e); } const preferredLocale = rawOrcidJSON['preferences']['locale'] !== null ? rawOrcidJSON['preferences']['locale'] : undefined; const biography = rawOrcidJSON['person']['biography'] !== null ? rawOrcidJSON['person']['biography']['content'] : undefined; const emails = []; if (rawOrcidJSON['person']['emails']['email'] !== null) { for (const email of rawOrcidJSON['person']['emails']['email']) { emails.push({ email: email['email'], primary: email['primary'], verified: email['verified'], }); } } const keywords = []; if (rawOrcidJSON['person']['keywords']['keyword'] !== null) { for (const keyword of rawOrcidJSON['person']['keywords']['keyword']) { keywords.push({ content: keyword['content'], index: keyword['display-index'], }); } keywords.sort((a, b) => a.index - b.index); } const researcherUrls = []; if (rawOrcidJSON['person']['researcher-urls']['researcher-url'] !== null) { for (const researcherUrl of rawOrcidJSON['person']['researcher-urls']['researcher-url']) { researcherUrls.push({ url: researcherUrl['url']['value'], name: researcherUrl['url-name'], index: researcherUrl['display-index'], }); } researcherUrls.sort((a, b) => a.index - b.index); } const country = rawOrcidJSON['person']['addresses']['address'].length > 0 ? rawOrcidJSON['person']['addresses']['address'][0]['country']['value'] : undefined; return new ORCIDInfo(orcid, rawOrcidJSON, familyName, givenNames, employments, preferredLocale, biography, emails, keywords, researcherUrls, country); } static fromJSON(serialized) { const data = JSON.parse(serialized); const employments = data.employments.map(employment => Employment.fromJSON(employment)); return new ORCIDInfo(data.orcid, data.ORCiDJSON, data.familyName, data.givenNames, employments, data.preferredLocale, data.biography, data.emails, data.keywords, data.researcherUrls, data.country); } getAffiliationsAt(date) { const affiliations = []; for (const employment of this._employments) { if (employment.startDate <= date && employment.endDate === null) affiliations.push(employment); if (employment.startDate <= date && employment.endDate !== null && employment.endDate >= date) affiliations.push(employment); } return affiliations; } getAffiliationAsString(affiliation, showDepartment = true) { if (affiliation === undefined || affiliation.organization === null) return undefined; else { if (showDepartment && affiliation.department !== null) return `${affiliation.organization} [${affiliation.department}]`; else return affiliation.organization; } } toObject() { return { orcid: this._orcid, ORCiDJSON: this._ORCiDJSON, familyName: this._familyName, givenNames: this._givenNames, employments: this._employments.map(employment => JSON.stringify(employment.toObject())), preferredLocale: this._preferredLocale, biography: this._biography, emails: this._emails, keywords: this._keywords, researcherUrls: this._researcherUrls, country: this._country, }; } } class Employment { constructor(startDate, endDate, organization, department) { this.startDate = startDate; this.endDate = endDate; this.organization = organization; this.department = department; } static fromJSON(serialized) { const data = JSON.parse(serialized); const startDate = new Date(data.startDate); const endDate = data.endDate === null ? null : new Date(data.endDate); return new Employment(startDate, endDate, data.organization, data.department); } toObject() { return { startDate: this.startDate, endDate: this.endDate, organization: this.organization, department: this.department, }; } } class FoldableItem { constructor(priority, keyTitle, value, keyTooltip, keyLink, valueRegex, renderDynamically) { this._estimatedTypePriority = 0; this._priority = priority; this._keyTitle = keyTitle; this._value = value; this._keyTooltip = keyTooltip; this._keyLink = keyLink; this._valueRegex = valueRegex; this._renderDynamically = renderDynamically; if (renderDynamically) this._estimatedTypePriority = renderers.length; else this._estimatedTypePriority = Parser.getEstimatedPriority(this._value); } get priority() { return this._priority; } get keyTitle() { return this._keyTitle; } get value() { return this._value; } get keyTooltip() { return this._keyTooltip; } get keyLink() { return this._keyLink; } get valueRegex() { return this._valueRegex; } get renderDynamically() { return this._renderDynamically; } get estimatedTypePriority() { return this._estimatedTypePriority; } isValidValue() { return this._valueRegex.test(this._value); } } class FoldableAction { constructor(priority, title, link, style) { this._priority = priority; this._title = title; this._link = link; this._style = style; } get priority() { return this._priority; } get title() { return this._title; } get link() { return this._link; } get style() { return this._style; } } class ORCIDType extends GenericIdentifierType { constructor() { super(...arguments); this.affiliationAt = new Date(Date.now()); this.showAffiliation = true; } get data() { return JSON.stringify(this._orcidInfo.toObject()); } hasCorrectFormat() { return ORCIDInfo.isORCiD(this.value); } async init(data) { if (data !== undefined) { this._orcidInfo = ORCIDInfo.fromJSON(data); console.debug('reload ORCIDInfo from data', this._orcidInfo); } else { this._orcidInfo = await ORCIDInfo.getORCiDInfo(this.value); console.debug('load ORCIDInfo from API', this._orcidInfo); } if (this.settings) { for (const i of this.settings) { switch (i['name']) { case 'affiliationAt': this.affiliationAt = new Date(i['value']); break; case 'showAffiliation': this.showAffiliation = i['value']; break; } } } this.items.push(new FoldableItem(0, 'ORCiD', this._orcidInfo.orcid, 'ORCiD is a free service for researchers to distinguish themselves by creating a unique personal identifier.', 'https://orcid.org', undefined, true)); try { const familyName = this._orcidInfo.familyName; if (familyName) { new FoldableItem(1, 'Family Name', this._orcidInfo.familyName, 'The family name of the person.'); } } catch (e) { console.log('Failed to obtain family name from ORCiD record.', e); } try { const givenNames = this._orcidInfo.givenNames; if (givenNames) { new FoldableItem(2, 'Given Names', this._orcidInfo.givenNames.toString(), 'The given names of the person.'); } } catch (e) { console.log('Failed to obtain given names from ORCiD record.', e); } this.actions.push(new FoldableAction(0, 'Open ORCiD profile', `https://orcid.org/${this._orcidInfo.orcid}`, 'primary')); try { const affiliations = this._orcidInfo.getAffiliationsAt(new Date(Date.now())); for (const data of affiliations) { const affiliation = this._orcidInfo.getAffiliationAsString(data); if (affiliation !== undefined && affiliation.length > 2) this.items.push(new FoldableItem(50, 'Current Affiliation', affiliation, 'The current affiliation of the person.', undefined, undefined, false)); } } catch (e) { console.log('Failed to obtain affiliations from ORCiD record.', e); } if (this._orcidInfo.getAffiliationsAt(this.affiliationAt) !== this._orcidInfo.getAffiliationsAt(new Date()) && this.affiliationAt.toLocaleDateString('en-US') !== new Date().toLocaleDateString('en-US')) { const affiliationsThen = this._orcidInfo.getAffiliationsAt(this.affiliationAt); for (const data of affiliationsThen) { const affiliation = this._orcidInfo.getAffiliationAsString(data); this.items.push(new FoldableItem(49, 'Affiliation at ' + this.affiliationAt.toLocaleDateString('en-US', { year: 'numeric', month: 'numeric', day: 'numeric', }), affiliation, 'The affiliation of the person at the given date.', undefined, undefined, false)); } } if (this._orcidInfo.emails) { const primary = this._orcidInfo.emails.filter(email => email.primary)[0]; const other = this._orcidInfo.emails.filter(email => !email.primary); if (primary) { this.items.push(new FoldableItem(20, 'Primary E-Mail address', primary.email, 'The primary e-mail address of the person.')); this.actions.push(new FoldableAction(0, 'Send E-Mail', `mailto:${primary.email}`, 'secondary')); } if (other.length > 0) this.items.push(new FoldableItem(70, 'Other E-Mail addresses', other.map(email => email.email).join(', '), 'All other e-mail addresses of the person.')); if (this._orcidInfo.preferredLocale) this.items.push(new FoldableItem(25, 'Preferred Language', this._orcidInfo.preferredLocale, 'The preferred locale/language of the person.')); for (const url of this._orcidInfo.researcherUrls) { this.items.push(new FoldableItem(100, url.name, url.url, 'A link to a website specified by the person.')); } if (this._orcidInfo.keywords.length > 50) this.items.push(new FoldableItem(60, 'Keywords', this._orcidInfo.keywords.map(keyword => keyword.content).join(', '), 'Keywords specified by the person.', undefined, undefined, false)); if (this._orcidInfo.biography) this.items.push(new FoldableItem(200, 'Biography', this._orcidInfo.biography, 'The biography of the person.', undefined, undefined, false)); if (this._orcidInfo.country) this.items.push(new FoldableItem(30, 'Country', this._orcidInfo.country, 'The country of the person.')); } } isResolvable() { return this._orcidInfo.ORCiDJSON !== undefined; } renderPreview() { return (index.h("span", { class: 'inline-flex items-center font-mono flex-nowrap align-top' }, index.h("svg", { version: "1.1", xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 256 256", class: 'h-5 p-0.5 mr-1 flex-none items-center' }, index.h("style", { type: "text/css" }, `.st0{fill:#A6CE39;}`, `.st1{fill:#FFFFFF;}`), index.h("path", { class: "st0", d: "M256,128c0,70.7-57.3,128-128,128C57.3,256,0,198.7,0,128C0,57.3,57.3,0,128,0C198.7,0,256,57.3,256,128z" }), index.h("g", null, index.h("path", { class: "st1", d: "M86.3,186.2H70.9V79.1h15.4v48.4V186.2z" }), index.h("path", { class: "st1", d: "M108.9,79.1h41.6c39.6,0,57,28.3,57,53.6c0,27.5-21.5,53.6-56.8,53.6h-41.8V79.1z M124.3,172.4h24.5\n c34.9,0,42.9-26.5,42.9-39.7c0-21.5-13.7-39.7-43.7-39.7h-23.7V172.4z" }), index.h("path", { class: "st1", d: "M88.7,56.8c0,5.5-4.5,10.1-10.1,10.1c-5.6,0-10.1-4.6-10.1-10.1c0-5.6,4.5-10.1,10.1-10.1\n C84.2,46.7,88.7,51.3,88.7,56.8z" }))), index.h("span", { class: 'flex-none px-1 items-center' }, this._orcidInfo.familyName, ", ", this._orcidInfo.givenNames, ' ', this.showAffiliation && this._orcidInfo.getAffiliationsAt(new Date()).length > 0 ? `(${this._orcidInfo.getAffiliationAsString(this._orcidInfo.getAffiliationsAt(new Date())[0], false)}${this._orcidInfo.getAffiliationsAt(this.affiliationAt).length > 0 && this.affiliationAt.toLocaleDateString() !== new Date().toLocaleDateString() && this._orcidInfo.getAffiliationsAt(this.affiliationAt)[0].organization !== this._orcidInfo.getAffiliationsAt(new Date())[0].organization ? `, then: ${this._orcidInfo.getAffiliationsAt(this.affiliationAt)[0].organization}` : ''})` : ''))); } getSettingsKey() { return 'ORCIDType'; } } class PID { constructor(prefix, suffix) { this._prefix = prefix; this._suffix = suffix; } get prefix() { return this._prefix; } get suffix() { return this._suffix; } static isPID(text) { return new RegExp('^([0-9A-Za-z])+.([0-9A-Za-z])+/([!-~])+$').test(text); } static getPIDFromString(pid) { if (!PID.isPID(pid)) throw new Error('Invalid input'); const pidSplit = pid.split('/'); return new PID(pidSplit[0], pidSplit[1]); } static fromJSON(serialized) { const data = JSON.parse(serialized); return new PID(data.prefix, data.suffix); } toString() { return `${this.prefix}/${this.suffix}`; } isResolvable() { return !unresolvables.has(this) && !this.prefix.toUpperCase().match('^(0$|0\\.|HS_|10320$)'); } async resolve() { if (unresolvables.has(this)) return undefined; else if (handleMap.has(this)) return handleMap.get(this); else { const rawJson = await cachedFetch(`https://hdl.handle.net/api/handles/${this.prefix}/${this.suffix}#resolve`); console.log(rawJson); const valuePromises = rawJson.values.map(async (value) => { const type = (async () => { if (PID.isPID(value.type)) { const pid = PID.getPIDFromString(value.type); const dataType = await PIDDataType.resolveDataType(pid); return dataType instanceof PIDDataType ? dataType : pid; } return value.type; })(); return { index: value.index, type: await type, data: value.data, ttl: value.ttl, timestamp: Date.parse(value.timestamp), }; }); const values = await Promise.all(valuePromises); const record = new PIDRecord(this, values); handleMap.set(this, record); return record; } } toObject() { return { prefix: this.prefix, suffix: this.suffix, }; } } const locationType = new PID('10320', 'loc'); class PIDDataType { constructor(pid, name, description, redirectURL, regex) { this._pid = pid; this._name = name; this._description = description; this._regex = regex; this._redirectURL = redirectURL; } get pid() { return this._pid; } get name() { return this._name; } get description() { return this._description; } get redirectURL() { return this._redirectURL; } get regex() { return this._regex; } static async resolveDataType(pid) { if (typeMap.has(pid)) return typeMap.get(pid); if (!pid.isResolvable()) { console.debug(`PID ${pid.toString()} has been marked as unresolvable`); return undefined; } const pidRecord = await pid.resolve(); if (pidRecord === undefined) { console.debug(`PID ${pid.toString()} could not be resolved via the API`); unresolvables.add(pid); return undefined; } const tempDataType = { name: '', description: '', redirectURL: '', ePICJSON: {} }; for (let i = 0; i < pidRecord.values.length; i++) { const currentValue = pidRecord.values[i]; if (currentValue.type === locationType || currentValue.type.toString() === locationType.toString()) { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(currentValue.data.value, 'text/xml'); const xmlLocations = xmlDoc.getElementsByTagName('location'); for (let j = 0; j < xmlLocations.length; j++) { const newLocation = { href: xmlLocations[j].getAttribute('href'), weight: undefined, view: undefined, resolvedData: undefined, }; try { newLocation.weight = parseInt(xmlLocations[j].getAttribute('weight')); } catch (ignored) { } try { newLocation.view = xmlLocations[j].getAttribute('view'); } catch (ignored) { } try { if (newLocation.view === 'json') { newLocation.resolvedData = await cachedFetch(newLocation.href); tempDataType.ePICJSON = newLocation.resolvedData; tempDataType.name = newLocation.resolvedData['name']; tempDataType.description = newLocation.resolvedData['description']; } else { tempDataType.redirectURL = newLocation.href; } } catch (ignored) { } } } } try { const type = new PIDDataType(pid, tempDataType.name, tempDataType.description, tempDataType.redirectURL, tempDataType.regex); typeMap.set(pid, type); return type; } catch (e) { console.error(e); return undefined; } } static fromJSON(serialized) { const data = JSON.parse(serialized); return new PIDDataType(PID.fromJSON(data.pid), data.name, data.description, data.redirectURL, data.regex); } toObject() { return { pid: JSON.stringify(this._pid.toObject()), name: this._name, description: this._description, redirectURL: this._redirectURL, regex: this._regex, }; } } class PIDRecord { constructor(pid, values) { this._values = []; this._pid = pid; this._values = values; } get pid() { return this._pid; } get values() { return this._values; } static fromJSON(serialized) { const data = JSON.parse(serialized); const values = data.values.map(value => { const parsed = JSON.parse(value); const parsedType = JSON.parse(parsed.type); let type; if (parsedType.pidDataType !== undefined) { type = PIDDataType.fromJSON(parsedType.pidDataType); } else if (parsedType.pid !== undefined) { type = PID.fromJSON(parsedType.pid); } else { type = parsedType.string; } const data = JSON.parse(parsed.data); return { index: parsed.index, type: type, data: data, ttl: parsed.ttl, timestamp: parsed.timestamp, }; }); return new PIDRecord(PID.fromJSON(data.pid), values); } toObject() { return { pid: JSON.stringify(this._pid.toObject()), values: this._values.map(value => JSON.stringify({ index: value.index, type: JSON.stringify({ pid: value.type instanceof PID ? JSON.stringify(value.type.toObject()) : undefined, pidDataType: value.type instanceof PIDDataType ? JSON.stringify(value.type.toObject()) : undefined, string: typeof value.type == 'string' ? value.type : undefined, }), data: JSON.stringify(value.data), ttl: value.ttl, timestamp: value.timestamp, })), }; } } class HandleType extends GenericIdentifierType { constructor() { super(...arguments); this._parts = []; } get data() { return JSON.stringify(this._pidRecord.toObject()); } hasCorrectFormat() { return PID.isPID(this.value); } async init(data) { if (data !== undefined) { this._pidRecord = PIDRecord.fromJSON(data); this._parts = await Promise.all([ { text: this._pidRecord.pid.prefix, nextExists: true, }, { text: this._pidRecord.pid.suffix, nextExists: false, }, ]); console.debug('reload PIDRecord from data', this._pidRecord); } else { const pid = PID.getPIDFromString(this.value); this._parts = [ { text: pid.prefix, nextExists: true, }, { text: pid.suffix, nextExists: false, }, ]; this._pidRecord = await pid.resolve(); console.debug('load PIDRecord from API', this._pidRecord); } for (const value of this._pidRecord.values) { if (value.type instanceof PIDDataType) { this.items.push(new FoldableItem(0, value.type.name, value.data.value, value.type.description, value.type.redirectURL, value.type.regex)); } } this.actions.push(new FoldableAction(0, 'Open in FAIR-DOscope', `https://kit-data-manager.github.io/fairdoscope/?pid=${this._pidRecord.pid.toString()}`, 'primary')); this.actions.push(new FoldableAction(0, 'View in Handle.net registry', `https://hdl.handle.net/${this._pidRecord.pid.toString()}`, 'secondary')); return; } isResolvable() { return this._pidRecord.values.length > 0; } renderPreview() { return (index.h("span", { class: 'font-mono bg-inherit font-bold rounded-md' }, this._parts.map(element => { return (index.h("span", { class: 'font-bold font-mono' }, index.h("color-highlight", { text: element.text }), index.h("span", { class: 'font-mono font-bold text-gray-800 mx-0.5' }, element.nextExists ? '/' : ''))); }))); } getSettingsKey() { return 'HandleType'; } } class EmailType extends GenericIdentifierType { getSettingsKey() { return 'EmailType'; } hasCorrectFormat() { const regex = /^(([\w\-.]+@([\w-]+\.)+[\w-]{2,})(\s*,\s*)?)*$/gm; return regex.test(this.value); } init() { return; } isResolvable() { return false; } renderPreview() { return (index.h("span", { class: 'items-center' }, this.value .split(new RegExp(/\s*,\s*/)) .filter(email => email.length > 0) .map(email => { return (index.h("a", { href: 'mailto:' + email, rel: 'noopener noreferrer', target: "_blank", class: 'items-center inline-flex font-mono text-sm text-blue-400 border border-slate-400 bg-white/60 rounded-md px-1 py-0.5' }, index.h("svg", { xmlns: "http://www.w3.org/2000/svg", fill: "none", "aria-hidden": "true", viewBox: "0 0 24 24", "stroke-width": "1", stroke: "black", height: "20px", class: 'mr-2' }, index.h("path", { "stroke-linecap": "round", "stroke-linejoin": "round", d: "M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75" })), index.h("span", { class: 'ml-2' }, email))); }))); } } class URLType extends GenericIdentifierType { getSettingsKey() { return 'URLType'; } hasCorrectFormat() { const regex = new RegExp('^http(s)?:(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?$'); return regex.test(this.value); } init() { return; } isResolvable() { return false; } renderPreview() { return (index.h("a", { href: this.value, target: "_blank", rel: 'noopener noreferrer', class: 'font-mono text-sm text-blue-400' }, this.value)); } } class FallbackType extends GenericIdentifierType { hasCorrectFormat() { return true; } init() { return; } isResolvable() { return false; } renderPreview() { return index.h("span", null, this.value); } getSettingsKey() { return 'FallbackType'; } } class LocaleType extends GenericIdentifierType { getSettingsKey() { return 'LocaleType'; } hasCorrectFormat() { const regex = /^([a-zA-Z]{2})(-[A-Z]{2})?$/; return regex.test(this.value); } init() { return; } isResolvable() { return false; } renderPreview() { return index.h("locale-visualization", { locale: this.value, showFlag: true }); } } const renderers = [ { priority: 0, key: 'DateType', constructor: DateType, }, { priority: 1, key: 'ORCIDType', constructor: ORCIDType, }, { priority: 2, key: 'HandleType', constructor: HandleType, }, { priority: 3, key: 'EmailType', constructor: EmailType, }, { priority: 4, key: 'URLType', constructor: URLType, }, { priority: 5, key: 'LocaleType', constructor: LocaleType, }, { priority: 5, key: 'FallbackType', constructor: FallbackType, }, ]; const typeMap = new Map(); const handleMap = new Map(); const unresolvables = new Set(); class Parser { static getEstimatedPriority(value) { let priority = 0; for (let i = 0; i < renderers.length; i++) { const obj = new renderers[i].constructor(value); if (obj.hasCorrectFormat()) { priority = i; break; } } return priority; } static async getBestFit(value, settings) { var _a; let bestFit = new renderers[renderers.length - 1].constructor(value); for (let i = renderers.length - 1; i >= 0; i--) { const obj = new renderers[i].constructor(value); if (obj.hasCorrectFormat()) bestFit = obj; } try { const settingsKey = bestFit.getSettingsKey(); const settingsValues = (_a = settings.find(value => value.type === settingsKey)) === null || _a === void 0 ? void 0 : _a.values; if (settingsValues) bestFit.settings = settingsValues; } catch (e) { console.warn('Error while adding settings to object:', e); } await bestFit.init(); return bestFit; } } const instanceOfAny = (object, constructors) => constructors.some((c) => object instanceof c); let idbProxyableTypes; let cursorAdvanceMethods; // This is a function to prevent it throwing up in node environments. function getIdbProxyableTypes() { return (idbProxyableTypes || (idbProxyableTypes = [ IDBDatabase, IDBObjectStore, IDBIndex, IDBCursor, IDBTransaction, ])); } // This is a function to prevent it throwing up in node environments. function getCursorAdvanceMethods() { return (cursorAdvanceMethods || (cursorAdvanceMethods = [ IDBCursor.prototype.advance, IDBCursor.prototype.continue, IDBCursor.prototype.continuePrimaryKey, ])); } const transactionDoneMap = new WeakMap(); const transformCache = new WeakMap(); const reverseTransformCache = new WeakMap(); function promisifyRequest(request) { const promise = new Promise((resolve, reject) => { const unlisten = () => { request.removeEventListener('success', success); request.removeEventListener('error', error); }; const success = () => { resolve(wrap(request.result)); unlisten(); }; const error = () => { reject(request.error); unlisten(); }; request.addEventListener('success', success); request.addEventListener('error', error); }); // This mapping exists in reverseTransformCache but doesn't doesn't exist in transformCache. This // is because we create many promises from a single IDBRequest. reverseTransformCache.set(promise, request); return promise; } function cacheDonePromiseForTransaction(tx) { // Early bail if we've already created a done promise for this transaction. if (transactionDoneMap.has(tx)) return; const done = new Promise((resolve, reject) => { const unlisten = () => { tx.removeEventListener('complete', complete); tx.removeEventListener('error', error); tx.removeEventListener('abort', error); }; const complete = () => { resolve(); unlisten(); }; const error = () => { reject(tx.error || new DOMException('AbortError', 'AbortError')); unlisten(); }; tx.addEventListener('complete', complete); tx.addEventListener('error', error); tx.addEventListener('abort', error); }); // Cache it for later retrieval. transactionDoneMap.set(tx, done); } let idbProxyTraps = { get(target, prop, receiver) { if (target instanceof IDBTransaction) { // Special handling for transaction.done. if (prop === 'done') return transactionDoneMap.get(target); // Make tx.store return the only store in the transaction, or undefined if there are many. if (prop === 'store') { return receiver.objectStoreNames[1] ? undefined : receiver.objectStore(receiver.objectStoreNames[0]); } } // Else transform whatever we get back. return wrap(target[prop]); }, set(target, prop, value) { target[prop] = value; return true; }, has(target, prop) { if (target instanceof IDBTransaction && (prop === 'done' || prop === 'store')) { return true; } return prop in target; }, }; function replaceTraps(callback) { idbProxyTraps = callback(idbProxyTraps); } function wrapFunction(func) { // Due to expected object equality (which is enforced by the caching in `wrap`), we // only create one new func per func. // Cursor methods are special, as the behaviour is a little more different to standard IDB. In // IDB, you advance the cursor and wait for a new 'success' on the IDBRequest that gave you the // cursor. It's kinda like a promise that can resolve with many values. That doesn't make sense // with real promises, so each advance methods returns a new promise for the cursor object, or // undefined if the end of the cursor has been reached. if (getCursorAdvanceMethods().includes(func)) { return function (...args) { // Calling the original function with the proxy as 'this' causes ILLEGAL INVOCATION, so we use // the original object. func.apply(unwrap(this), args); return wrap(this.request); }; } return function (...args) { // Calling the original function with the proxy as 'this' causes ILLEGAL INVOCATION, so we use // the original object. return wrap(func.apply(unwrap(this), args)); }; } function transformCachableValue(value) { if (typeof value === 'function') return wrapFunction(value); // This doesn't return, it just creates a 'done' promise for the transaction, // which is later returned for transaction.done (see idbObjectHandler). if (value instanceof IDBTransaction) cacheDonePromiseForTransaction(value); if (instanceOfAny(value, getIdbProxyableTypes())) return new Proxy(value, idbProxyTraps); // Return the same value back if we're not going to transform it. return value; } function wrap(value) { // We sometimes generate multiple promises from a single IDBRequest (eg when cursoring), because // IDB is weird and a single IDBRequest can yield many responses, so these can't be cached. if (value instanceof IDBRequest) return promisifyRequest(value); // If we've already transformed this value before, reuse the transformed value. // This is faster, but it also provides object equality. if (transformCache.has(value)) return transformCache.get(value); const newValue = transformCachableValue(value); // Not all types are transformed. // These may be primitive types, so they can't be WeakMap keys. if (newValue !== value) { transformCache.set(value, newValue); reverseTransformCache.set(newValue, value); } return newValue; } const unwrap = (value) => reverseTransformCache.get(value); /** * Open a database. * * @param name Name of the database. * @param version Schema version. * @param callbacks Additional callbacks. */ function openDB(name, version, { blocked, upgrade, blocking, terminated } = {}) { const request = indexedDB.open(name, version); const openPromise = wrap(request); if (upgrade) { request.addEventListener('upgradeneeded', (event) => { upgrade(wrap(request.result), event.oldVersion, event.newVersion, wrap(request.transaction), event); }); } if (blocked) { request.addEventListener('blocked', (event) => blocked( // Casting due to https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1405 event.oldVersion, event.newVersion, event)); } openPromise .then((db) => { if (terminated) db.addEventListener('close', () => terminated()); if (blocking) { db.addEventListener('versionchange', (event) => blocking(event.oldVersion, event.newVersion, event)); } }) .catch(() => { }); return openPromise; } const readMethods = ['get', 'getKey', 'getAll', 'getAllKeys', 'count']; const writeMethods = ['put', 'add', 'delete', 'clear']; const cachedMethods = new Map(); function getMethod(target, prop) { if (!(target instanceof IDBDatabase && !(prop in target) && typeof prop === 'string')) { return; } if (cachedMethods.get(prop)) return cachedMethods.get(prop); const targetFuncName = prop.replace(/FromIndex$/, ''); const useIndex = prop !== targetFuncName; const isWrite = writeMethods.includes(targetFuncName); if ( // Bail if the target doesn't exist on the target. Eg, getAll isn't in Edge. !(targetFuncName in (useIndex ? IDBIndex : IDBObjectStore).prototype) || !(isWrite || readMethods.includes(targetFuncName))) { return; } const method = async function (storeName, ...args) { // isWrite ? 'readwrite' : undefined gzipps better, but fails in Edge :( const tx = this.transaction(storeName, isWrite ? 'readwrite' : 'readonly'); let target = tx.store; if (useIndex) target = target.index(args.shift()); // Must reject if op rejects. // If it's a write operation, must reject if tx.done rejects. // Must reject with op rejection first. // Must resolve with op value. // Must handle both promises (no unhandled rejections) return (await Promise.all([ target[targetFuncName](...args), isWrite && tx.done, ]))[0]; }; cachedMethods.set(prop, method); return method; } replaceTraps((oldTraps) => ({ ...oldTraps, get: (target, prop, receiver) => getMethod(target, prop) || oldTraps.get(target, prop, receiver), has: (target, prop) => !!getMethod(target, prop) || oldTraps.has(target, prop), })); const advanceMethodProps = ['continue', 'continuePrimaryKey', 'advance']; const methodMap = {}; const advanceResults = new WeakMap(); const ittrProxiedCursorToOriginalProxy = new WeakMap(); const cursorIteratorTraps = { get(target, prop) { if (!advanceMethodProps.includes(prop)) return target[prop]; let cachedFunc = methodMap[prop]; if (!cachedFunc) { cachedFunc = methodMap[prop] = function (...args) { advanceResults.set(this, ittrProxiedCursorToOriginalProxy.get(this)[prop](...args)); }; } return cachedFunc; }, }; async function* iterate(...args) { // tslint:disable-next-line:no-this-assignment let cursor = this; if (!(cursor instanceof IDBCursor)) { cursor = await cursor.openCursor(...args); } if (!cursor) return; cursor = cursor; const proxiedCursor = new Proxy(cursor, cursorIteratorTraps); ittrProxiedCursorToOriginalProxy.set(proxiedCursor, cursor); // Map this double-proxy back to the original, so other cursor methods work. reverseTransformCache.set(proxiedCursor, unwrap(cursor)); while (cursor) { yield proxiedCursor; // If one of the advancing methods was not called, call continue(). cursor = await (advanceResults.get(proxiedCursor) || cursor.continue()); advanceResults.delete(proxiedCursor); } } function isIteratorProp(target, prop) { return ((prop === Symbol.asyncIterator && instanceOfAny(target, [IDBIndex, IDBObjectStore, IDBCursor])) || (prop === 'iterate' && instanceOfAny(target, [IDBIndex, IDBObjectStore]))); } replaceTraps((oldTraps) => ({ ...oldTraps, get(target, prop, receiver) { if (isIteratorProp(target, prop)) return iterate; return oldTraps.get(target, prop, receiver); }, has(target, prop) { return isIteratorProp(target, prop) || oldTraps.has(target, prop); }, })); const dbName = 'pid-component'; const dbVersion = undefined; class Database { constructor() { this.dbPromise = openDB(dbName, dbVersion, { upgrade(db) { const entityStore = db.createObjectStore('entities', { keyPath: 'value', }); entityStore.createIndex('by-context', 'context', { unique: false }); const relationStore = db.createObjectStore('relations', { autoIncrement: true, }); relationStore.createIndex('by-start', 'start', { unique: false }); relationStore.createIndex('by-description', 'description', { unique: false }); relationStore.createIndex('by-end', 'end', { unique: false }); }, }); } async addEntity(renderer) { const context = document.documentURI; const db = await this.dbPromise; await db .add('entities', { value: renderer.value, rendererKey: renderer.getSettingsKey(), context: context, lastAccess: new Date(), lastData: renderer.data, }) .catch(reason => { if (reason.name === 'ConstraintError') { console.debug('Entity already exists', reason); } else console.error('Could not add entity', reason); }); console.debug('added entity', renderer); const tx = db.transaction('relations', 'readwrite'); const promises = []; for (const item of renderer.items) { const relation = { start: renderer.value, description: item.keyTitle, end: item.value, }; const index = tx.store.index('by-start'); let cursor = await index.openCursor(); while (cursor) { if (cursor.value.start === relation.start && cursor.value.end === relation.end && cursor.value.description === relation.description) { return; } cursor = await cursor.continue(); } promises.push(tx.store.add(relation)); } promises.push(tx.done); await Promise.all(promises); console.debug('added relations', promises); } async getEntity(value, settings) { var _a, _b; try { const db = await this.dbPromise; const entity = await db.get('entities', value); if (entity !== undefined) { console.debug('Found entity for value in db', entity, value); const entitySettings = (_a = settings.find(value => value.type === entity.rendererKey)) === null || _a === void 0 ? void 0 : _a.values; const ttl = entitySettings === null || entitySettings === void 0 ? void 0 : entitySettings.find(value => value.name === 'ttl'); if (ttl != undefined && ttl.value != undefined && (new Date().getTime() - entity.lastAccess.getTime() > ttl.value || ttl.value === 0)) { console.log('TTL expired! Deleting entry in db', ttl.value, new Date().getTime() - entity.lastAccess.getTime()); await this.deleteEntity(value); } else { console.log('TTL not expired