diff --git a/src/core/async.ts b/src/core/async.ts index 28a8b03..2e99281 100644 --- a/src/core/async.ts +++ b/src/core/async.ts @@ -6,3 +6,5 @@ export async function findAsync(arr: Array, asyncCallback: (item: T) => Pr export const mapAsync = async (arr: Array, asyncCallback: (item: T) => Promise): Promise> => Promise.all(arr.map(asyncCallback)) + +export const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) diff --git a/src/core/collections.ts b/src/core/collections.ts new file mode 100644 index 0000000..57df522 --- /dev/null +++ b/src/core/collections.ts @@ -0,0 +1 @@ +export const filterNotEmpty = (array: T[]): T[] => array.filter(x => x) diff --git a/src/core/env.ts b/src/core/env.ts new file mode 100644 index 0000000..e87255a --- /dev/null +++ b/src/core/env.ts @@ -0,0 +1 @@ +export const localLanguage = () => navigator.language.substr(0,2) diff --git a/src/resolver/url-match-pattern.ts b/src/resolver/url-match-pattern.ts index 7f16e1b..b2111b0 100644 --- a/src/resolver/url-match-pattern.ts +++ b/src/resolver/url-match-pattern.ts @@ -65,7 +65,7 @@ const URL_match_pattern: Resolver = { const id = applicable[0].value const ci = applicable[0].valueIsCaseInsensitive - return await this.getEntityByRegexedId(prop, id, ci) + return this.getEntityByRegexedId(prop, id, ci) }, getEntityByRegexedId: async function(prop, id, ci = false) { const cached = await this.checkIfCached(prop, id) @@ -106,7 +106,7 @@ const URL_match_pattern: Resolver = { cache.externalIDCache = {} } cache.externalIDCache[this.formCacheKey(prop, id)] = entityId - browser.storage.local.set(cache) + return browser.storage.local.set(cache) } } diff --git a/src/sidebar/components/add-property.tsx b/src/sidebar/components/add-property.tsx new file mode 100644 index 0000000..6aad0a7 --- /dev/null +++ b/src/sidebar/components/add-property.tsx @@ -0,0 +1,127 @@ +import './express/express.css' +import * as browser from 'webextension-polyfill' +import {useEffect, useRef, useState} from 'react' + +interface Suggestion { + id: string + datatype: string + label: string + description: string +} + +//todo move +export function useComponentVisible(ref, initialIsVisible = false) { + const [isComponentVisible, setIsComponentVisible] = useState(initialIsVisible) + // const ref = useRef(null) + + const handleClickOutside = (event) => { + if (ref.current && !ref.current.contains(event.target)) { + setIsComponentVisible(false) + } + } + + useEffect(() => { + document.addEventListener('click', handleClickOutside, true) + return () => { + document.removeEventListener('click', handleClickOutside, true) + } + }, []) + + return [isComponentVisible, setIsComponentVisible] +} + +export const AddProperty = () => { + const [suggestions, setSuggestions] = useState>([]) + const [searchString, setSearchString] = useState('') + const [selectedDescription, setSelectedDescription] = useState('') + const autoCompleteRef = useRef(null) + const [showSuggestions, setShowSuggestions] = useComponentVisible(autoCompleteRef, false) + + const updateAutocomplete = async function (e) { + setSearchString(e.target.value) + setShowSuggestions(true) + + const response = await fetch(`https://www.wikidata.org/w/api.php?action=wbsearchentities&search=${e.target.value}&format=json&errorformat=plaintext&language=en&uselang=en&type=property`) + const result = await response.json() + console.log(result) + if (result.search) setSuggestions(result.search) + } + + // todo hide suggestion on blur + return
+
+ + + {showSuggestions && +
    setShowSuggestions(false)} + > + {suggestions.map(it => +
  • { + setSearchString(it.label) + setSelectedDescription(it.description) + setShowSuggestions(false) + }} + > + {it.label} +
    {it.description + ' ' + it.datatype}
    +
  • )} +
} +
{selectedDescription}
+
+
+} + + +// const updateAutocompleteO = async function (e) { +// let response = await fetch(`https://www.wikidata.org/w/api.php?action=wbsearchentities&search=${e.target.value}&format=json&errorformat=plaintext&language=en&uselang=en&type=property`) +// response = await response.json() +// for (const suggestion of response.search) { +// +// const item = document.createElement('li') +// if (!supportedProperties.includes(suggestion.datatype)) { +// item.classList.add('express__autocomplete-option--unsupported') +// item.setAttribute('title', 'Data type not supported') +// } else { +// item.setAttribute('tabindex', '0') +// const activationEvent = () => { +// wrapper.setAttribute('data-prop', suggestion.id) +// wrapper.setAttribute('data-datatype', suggestion.datatype) +// +// browser.storage.local.set({ +// lastUsedProp: { +// prop: suggestion.id, +// name: suggestion.label, +// desc: suggestion.description, +// datatype: suggestion.datatype, +// }, +// }) +// wrapper.dispatchEvent(new Event('change')) +// } +// item.addEventListener('keydown', (e) => { +// if (e.key === 'Enter') { +// activationEvent() +// } +// }) +// } +// } +// } + +const supportedProperties = [ + 'monolingualtext', + 'string', + 'wikibase-item', + 'wikibase-lexeme', + 'wikibase-property', +] diff --git a/src/sidebar/components/claims.tsx b/src/sidebar/components/claims.tsx new file mode 100644 index 0000000..a2dc850 --- /dev/null +++ b/src/sidebar/components/claims.tsx @@ -0,0 +1,45 @@ +import {useEffect, useRef} from 'react' +import {Claim, renderStatement} from "../render-claims" +import {AddProperty} from "./add-property" + +export interface ClaimsProps { + statements: {id: string, claims: Claim[]}[] + renderingCache: any +} + +export const Claims = (props: ClaimsProps) => { + return
+ + +
+} + +// Integration based on https://reactjs.org/docs/integrating-with-other-libraries.html +export const ExistingClaims = ({statements, renderingCache}: ClaimsProps) => { + const reference = useRef(null) + useEffect(() => { + const rendered = statements + .map(it => renderStatement(it.claims, renderingCache)) + .filter(Boolean) + .map(claim => claim.rendered) + + reference.current?.append(...rendered) + }) + + return
+} + +/** + * todo have a shortcut + * in "edit mode" - automatically add an empty claim field when prev is filled + * see example in https://github.com/openshift/angular-key-value-editor + * + * saving incrementally vs saving on exiting edit mode? + */ +const AddClaims = () => { + return
+ + +
+ +} diff --git a/src/sidebar/components/express/express.tpl.js b/src/sidebar/components/express/express.tpl.js index d4cf255..9ed76b7 100644 --- a/src/sidebar/components/express/express.tpl.js +++ b/src/sidebar/components/express/express.tpl.js @@ -4,6 +4,9 @@ import { getValueByLang } from '../../get-value-by-lang.js'; import { getAutodesc } from '../../get-autodesc.js'; import browser from 'webextension-polyfill' +import './express.css' + + const express = (vars) => { const supportedProperties = [ @@ -87,12 +90,6 @@ const express = (vars) => { main.appendChild(input); - let style = document.createElement('link'); - style.setAttribute('rel', "stylesheet"); - style.setAttribute('href', "components/express/express.css"); - - wrapper.appendChild(style); - main.appendChild(autocomplete); main.appendChild(desc); @@ -182,11 +179,8 @@ const express = (vars) => { selection: selection, options: options, composer: composer, + // todo pass in setLanguage instead languagePicker: languagePicker, - loadingFinished: function() { - progress.remove(); - input.focus(); - } } } diff --git a/src/sidebar/components/join/join.tpl.js b/src/sidebar/components/join/join.tpl.js index af0a000..13628ae 100644 --- a/src/sidebar/components/join/join.tpl.js +++ b/src/sidebar/components/join/join.tpl.js @@ -3,10 +3,10 @@ import { getAutodesc } from '../../get-autodesc.js'; let joinCounter = 0; -const join = (vars) => { +const join = (vars) => { let wrapper = document.createElement('div'); wrapper.classList.add('join'); - + let humanField = document.createElement('input'); humanField.classList.add('join__field'); humanField.setAttribute('type', 'search'); @@ -50,7 +50,7 @@ const join = (vars) => { }); } - if (suggestions) { + if (suggestions) { for (let suggestion of suggestions) { let item = document.createElement('button'); item.setAttribute('tabindex', '0'); @@ -76,13 +76,12 @@ const join = (vars) => { }); } } - for (let placeholder of proposals.querySelectorAll('.join__proposal__desc--placeholder')) { + for (let placeholder of proposals.querySelectorAll('.join__proposal__desc--placeholder')) { let entityWithoutDesc = placeholder.parentNode.getAttribute('data-entity'); placeholder.innerText = await getAutodesc(entityWithoutDesc); } } - //humanField.addEventListener('change', updateList); humanField.addEventListener('focus', updateList); humanField.addEventListener('keyup', updateList); diff --git a/src/sidebar/entity.js b/src/sidebar/entity.js index de1d71a..30edd27 100644 --- a/src/sidebar/entity.js +++ b/src/sidebar/entity.js @@ -1,23 +1,24 @@ import {getAutodesc} from './get-autodesc.js' -import {resolveBreadcrumbs, resolveIdLinksPlaceholder, resolvePlaceholders} from './resolve-placeholders.js' +import {resolveBreadcrumbs, resolveIdLinksPlaceholder} from './resolve-placeholders.js' import {getAliasesByLang, getValueByLang} from './get-value-by-lang.js' -import {groupClaims} from './group-claims.js' import {templates} from './components/templates.tpl.js' import {wikidataGetEntity} from '../wd-get-entity.js' -import {ApplyFormatters} from './formatters.js' import {AddLemmaAffix} from './lemma-afixes.js' import browser from 'webextension-polyfill' -import { PrependNav } from './prepend-nav.js'; -import { getDeducedSenseClaims } from './deduce-sense-statements.js'; +import {PrependNav} from './prepend-nav.js' +import {getDeducedSenseClaims} from './deduce-sense-statements.js' +import * as ReactDOM from 'react-dom' +import {Claims} from './components/claims' +import {localLanguage} from '../core/env' +import {groupClaims} from "./group-claims" +import {ApplyFormatters} from "./formatters" +import {renderStatements} from "./render-claims" +import {delay} from "../core/async" if (history.length > 1 || window != window.top) { PrependNav(); } -const lang = navigator.language.substr(0,2); -const footnoteStorage = {}; -let refCounter = {}; - let cache = {} function highlightWord(el, text) { @@ -47,305 +48,229 @@ if (window.location.search) { } } -function dateToString(value) { - let wiso = value.time; - let prec = value.precision; - - if (prec <= 6) { - return false; +function createLemmaEnsign(entity) { + let labels = document.createDocumentFragment() + for (let lang in entity.lemmas) { + let lemma = AddLemmaAffix(entity.lemmas[lang].value, { + category: entity.lexicalCategory, + lang: entity.language, + gender: typeof entity.claims?.P5185 === 'object' ? entity.claims?.P5185[0]?.mainsnak?.datavalue?.value?.id : null, + }) + + if (labels.childNodes.length !== 0) { + labels.appendChild(document.createTextNode(' ‧ ')) + } + labels.appendChild(lemma) } - let suffix = wiso.startsWith('-') ? ' BCE' : ''; + let lexemeDescription = document.createDocumentFragment() - let pad = function (i) { - if (i < 10) { - return '0' + i; - } - return i; - } + lexemeDescription.appendChild(templates.placeholder({ + entity: entity.language, + }, cache)) - let iso = wiso - .replace(/^(\+|-)/, '') - .replace(/Z$/, '') - .replace(/^(\d+)-00/, '$1-01') - .replace(/^(\d+)-(\d+)-00/, '$1-$2-01') - + 'Z'; - - let date = new Date(iso); - - let output = []; - if (prec === 7) { - let text = date.getFullYear().toString().slice(0, -2) + 'XX' + suffix; - return templates.proxy({ - query: ` - SELECT ?innerText WHERE { - ?century wdt:P31 wd:Q578. - ?century wdt:P585 "${ iso }Z"^^xsd:dateTime. - SERVICE wikibase:label - { - bd:serviceParam wikibase:language "[AUTO_LANGUAGE],${ lang }". - ?century rdfs:label ?innerText. - } - } - LIMIT 1`, - text: text, - }); - } else if (prec === 8) { - let text = date.getFullYear().toString().slice(0, -1) + 'X'; - return templates.proxy({ - query: ` - SELECT ?innerText WHERE { - ?decade wdt:P31 wd:Q39911. - ?decade wdt:P585 "${ iso }Z"^^xsd:dateTime. - SERVICE wikibase:label - { - bd:serviceParam wikibase:language "[AUTO_LANGUAGE],${ lang }". - ?decade rdfs:label ?innerText. - } - } - LIMIT 1`, - text: text, - }); - } else { - if (prec > 8) { - output.push(date.getUTCFullYear()); - } - if (prec > 9) { - output.push(pad(date.getUTCMonth() + 1)); - } - if (prec > 10) { - output.push(pad(date.getUTCDate())); - } - } + lexemeDescription.appendChild(document.createTextNode(', ')) + + lexemeDescription.appendChild(templates.placeholder({ + entity: entity.lexicalCategory, + }, cache)) - return document.createTextNode(output.join('-') + suffix); + return templates.ensign({ + revid: entity.lastrevid, + id: entity.id, + label: labels, + description: {text: lexemeDescription}, + }) } -function insertAfter(referenceNode, newNode) { - referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); +function createCanonicalMeta(id) { + let metaCanon = document.createElement('meta') + metaCanon.setAttribute('name', 'canonical') + metaCanon.setAttribute('content', 'https://www.wikidata.org/wiki/' + id) + return metaCanon } -function renderStatements(snak, references, type, target, scope, delta) { - let valueType = snak.datatype ? snak.datatype : snak.datavalue.type ; - if (type === 'preformatted') { - target.appendChild(snak.datavalue.value); - } - if (type === 'value' || scope === 'reference') { - if (valueType === "time") { - let date = dateToString(snak.datavalue.value); - if (date) { - target.appendChild(templates.time({ - text: date, - })); - } - } - if (valueType === "wikibase-item" || valueType === "wikibase-entityid" || valueType === "wikibase-lexeme" || valueType === "wikibase-form" || valueType === "wikibase-sense") { - let vid = snak.datavalue.value.id; - if (snak.datavalue.parents) { - target.appendChild(templates.breadcrumbsPlaceholder(snak.datavalue.parents)); - } - target.appendChild(templates.placeholder({ - entity: vid, - }, cache)); - } - if (valueType === "external-id") { - target.appendChild(templates.code(snak.datavalue.value)); - } - if (valueType === "string") { - target.appendChild(document.createTextNode(snak.datavalue.value)); - } - if (valueType === "url") { - let humanReadable = snak.datavalue.value; - target.appendChild(templates.urlLink(snak.datavalue.value)); - } - if (valueType === 'quantity') { - target.appendChild(templates.unitNumber({ - number: snak.datavalue.value.amount, - unit: snak?.datavalue?.value?.unit, - })); - } - if (valueType === "globe-coordinate" || valueType === 'globecoordinate') { - target.appendChild(templates.mercator({ - lat: snak.datavalue.value.latitude, - lon: snak.datavalue.value.longitude, - pre: snak.datavalue.value.precision, - height: 500, - width: 500, - })); - } - if (valueType === "monolingualtext") { - target.appendChild(templates.title({ - text: snak.datavalue.value.text, - lang: snak.datavalue.value.language - })); - } - if (valueType === "commonsMedia") { - let name = encodeURIComponent(snak.datavalue.value); - if (name.match(/\.svg$/i)) { - target.appendChild(templates.image({ - src: `https://commons.wikimedia.org/wiki/Special:FilePath/${ name }` - })); - } else if (name.match(/\.(jpe?g|png|gif|tiff?|stl)$/i)) { - target.appendChild(templates.picture({ - srcSet: { - 250: `https://commons.wikimedia.org/wiki/Special:FilePath/${ name }?width=250px`, - 501: `https://commons.wikimedia.org/wiki/Special:FilePath/${ name }?width=501px`, - 801: `https://commons.wikimedia.org/wiki/Special:FilePath/${ name }?width=801px`, - 1068: `https://commons.wikimedia.org/wiki/Special:FilePath/${ name }?width=1068px`, - } - })); - } else if (name.match(/\.(wav|og[ga])$/i)) { - target.appendChild(templates.audio({ - src: `https://commons.wikimedia.org/wiki/Special:FilePath/${ name }` - })); - } else if (name.match(/\.webm$/i)) { - target.appendChild(templates.video({ - poster: `https://commons.wikimedia.org/wiki/Special:FilePath/${ name }?width=801px`, - src: `https://commons.wikimedia.org/wiki/Special:FilePath/${ name }` - })); - } - } - } else if(type === 'novalue') { - target.appendChild(document.createTextNode('—')); - } else if(type === 'somevalue') { - target.appendChild(document.createTextNode('?')); - } - if (target) { - target.appendChild(document.createTextNode('\xa0')); - let sup = document.createElement('sup'); - let c = 0; - for (let reference of references) { - if (c > 0) { - sup.appendChild(document.createTextNode('/')); - } - sup.appendChild(reference); - c++; - } - if (sup.hasChildNodes()) { - target.appendChild(sup); +function createDescriptionMeta(description) { + let metaDesc = document.createElement('meta') + metaDesc.setAttribute('name', 'description') + metaDesc.setAttribute('content', description) + return metaDesc +} + +function createKeywordsMeta(aliases) { + let metaKeys = document.createElement('meta') + metaKeys.setAttribute('name', 'keywords') + metaKeys.setAttribute('content', aliases.join(', ')) + return metaKeys +} + +function createTitleFragment(entity, initialDescription) { + let titleFragment = document.createElement('div') + let hasDescription = initialDescription !== false + + const setTitle = (description) => { + if (titleFragment.firstChild) { + titleFragment.removeChild(titleFragment.firstChild) } + titleFragment.appendChild(templates.ensign({ + revid: entity.lastrevid, + id: entity.id, + label: getValueByLang(entity, 'labels', entity.title), + description: { + text: description, + provisional: !hasDescription, + }, + })) } - if (valueType === "external-id" && snak.snaktype === "value") { - target.appendChild(templates.idLinksPlaceholder(snak.property, snak.datavalue.value)); - } - if (scope === 'statement' && typeof delta != 'undefined' && delta.hasOwnProperty('qualifiers')) { - let qualifiers = []; - for (let prop of Object.keys(delta.qualifiers)) { - let qvalues = []; - for (let qv of delta.qualifiers[prop]) { - let qualvalue = new DocumentFragment(); - renderStatements(qv,[], qv.snaktype, qualvalue, 'qualifier'); - qvalues.push(qualvalue); - } - qualifiers.push({ - prop: templates.placeholder({ entity: prop }), - vals: qvalues, - }); - } - target.appendChild(templates.annote(qualifiers)); + setTitle(initialDescription) + + if (!hasDescription && 'claims' in entity && 'P31' in entity.claims) { + (async () => { + setTitle(await getAutodesc(entity.id)) + })() } + return titleFragment } -function renderStatement(value) { - if (value[0].mainsnak) { - let pid = value[0].mainsnak.property; - let label = templates.placeholder({ - entity: pid, - }, cache); - - let values = []; - let hasPreferred = false; - for (let delta of value) { - if (delta.rank == "preferred") { - hasPreferred = true; - } +const createActionElements = id => + templates.actions('Actions', [ + { + link: 'add.html?' + id, + moji: './icons/u270Eu002B-addStatement.svg', + title: 'Add a statement', + desc: 'Extract data from this website', + callback: (e) => { + browser.runtime.sendMessage({ + type: 'open_adder', + entity: id, + }) + }, + }, + { + link: '#nocache', + moji: './icons/u2B6E-refresh.svg', + title: 'Reload data', + desc: 'Display an uncached version', + callback: (e) => { + location.hash = '#nocache' + location.reload() + }, + }, + { + link: 'links.html?' + id, + moji: './icons/u2BA9u1F4C4uFE0E-articleRedirect.svg', + title: 'What links here', + desc: 'A list of item that link to this', + }, + { + link: 'improve.html?' + id, + moji: './icons/u2728-specialPages.svg', + title: 'Improve', + desc: 'Automatic suggestions on how to improve this item', + }, + ]) + +const refCounter = {} + +function createReferenceItem(ref) { + let listItem + const refvalues = [] + if (typeof refCounter[ref.hash] === 'undefined') { + listItem = document.createElement('li') + refCounter[ref.hash] = { + item: listItem, } - for (const [key, delta] of Object.entries(value)) { - if (delta.rank == "deprecated" || (delta.rank == "normal" && hasPreferred)) { - delete value[key]; - } - } - for (let delta of value) { - if (delta && delta.hasOwnProperty('mainsnak') && delta.mainsnak) { - let thisvalue = new DocumentFragment(); - let type = delta.mainsnak.snaktype; - let refs = []; - if (delta.references) { - for (let ref of delta.references) { - let listItem; - let refvalues = []; - if (typeof refCounter[ref.hash] === 'undefined') { - listItem = document.createElement('li'); - refCounter[ref.hash] = { - item: listItem, - } - let formatted = ApplyFormatters(ref.snaks, 'reference') + const formatted = ApplyFormatters(ref.snaks, 'reference') + for (const key in formatted) { + for (const refthing of formatted[key]) { + if (refthing.datavalue) { + const refvalue = new DocumentFragment() - for (let key in formatted) { - for (let refthing of formatted[key]) { - if (refthing.datavalue) { - let refvalue = new DocumentFragment(); - - renderStatements(refthing, [], refthing.datavalue.type, refvalue, 'reference'); - refvalues.push(refvalue); - } - } - if (key.match(/^P\d+/)) { - let refStatement = templates.proof({ - prop: templates.placeholder({ - entity: key, - }, cache), - vals: refvalues, - }); - listItem.appendChild(refStatement); - } else { - for (let refvalue of refvalues) { - listItem.appendChild(refvalue) - } - } - } - } else { - listItem = refCounter[ref.hash].item; - } - listItem.setAttribute('id', ref.hash); - refs.push(templates.footnoteRef({ - text: '▐', - link: '#' + ref.hash, - title: listItem.innerText, - })); - footnoteStorage[ref.hash] = { - content: listItem, - }; - } + refvalue.append(...renderStatements(refthing, [], refthing.datavalue.type, 'reference', undefined, cache)) + refvalues.push(refvalue) + } + } + if (key.match(/^P\d+/)) { + const refStatement = templates.proof({ + prop: templates.placeholder({ + entity: key, + }, cache), + vals: refvalues, + }) + listItem.appendChild(refStatement) + } else { + for (const refvalue of refvalues) { + listItem.appendChild(refvalue) } + } + } + } else { + listItem = refCounter[ref.hash].item + } + listItem.setAttribute('id', ref.hash) + return listItem +} - renderStatements(delta.mainsnak, refs, type, thisvalue, 'statement', delta); +function collectFootnotes(statements) { + const footnotes = statements + .flatMap(it => it.claims) + .filter(it => it?.mainsnak && it?.references) + .flatMap(it => it.references) + .map(ref => + [ref.hash, {content: createReferenceItem(ref)}], + ) + return Object.fromEntries(footnotes) +} - values.push(thisvalue); +function getHigherPriorityClaims(claims) { + const hasPreferred = claims.find(it => it.rank == 'preferred') + const highPriority = (it) => !hasPreferred && it.rank == 'normal' || it.rank == 'preferred' + + return claims.filter(highPriority) +} - } - } - let statement = templates.remark({ - prop: templates.placeholder({ - entity: pid, - }, cache), - vals: values, - id: pid, - }); - - let firstValue = value.find(x=>x!==undefined); - - if (firstValue) { - return { - rendered: statement, - type: firstValue.mainsnak.snaktype, - }; +const getStatementsToRender = statements => + groupClaims(statements).map(it => ({ + id: it.id, + claims: getHigherPriorityClaims(it.claims), + })) + +async function renderFootnotes(content, footnoteStorage) { + /** + * todo a better way + * the issue right now is that things are rendered asynchronously in React (via useEffect) + * and as footnote rendering relies on elements existing in the DOM, + * it'll fail to render + * + * delay(0) pushes this back to async queue, but this is likely not reliable + * + * one potential way to deal with this is to send event whe main claims are rendered + * another to have these both under react management + */ + await delay(0) + let footnotes = content.querySelectorAll('.footnote') + + let references = document.createElement('ol') + + let footnoteNumber = 1 + Array.from(footnotes).reduce((k, footnote) => { + let footnoteId = footnote.getAttribute('href').substr(1) + let referenceItem = footnoteStorage[footnoteId].content + if (!footnoteStorage[footnoteId].number) { + references.appendChild(referenceItem) + footnoteStorage[footnoteId].number = footnoteNumber + footnoteNumber++ } - } + footnote.innerText = footnoteStorage[footnoteId].number + content.appendChild(references) + footnote.addEventListener('mouseover', () => { + footnote.setAttribute('title', referenceItem.innerText) + }) + }, 0) } function updateView(id, useCache = true) { @@ -363,136 +288,37 @@ function updateView(id, useCache = true) { let wrapper = document.createElement('div'); if (e.lemmas) { - let labels = document.createDocumentFragment(); - for (let lang in e.lemmas) { - let lemma = AddLemmaAffix(e.lemmas[lang].value, { - category: e.lexicalCategory, - lang: e.language, - gender: typeof e.claims?.P5185 === 'object' ? e.claims?.P5185[0]?.mainsnak?.datavalue?.value?.id : null, - }); - - if (labels.childNodes.length !== 0) { - labels.appendChild(document.createTextNode(' ‧ ')); - } - labels.appendChild(lemma); - } - - let lexemeDescription = document.createDocumentFragment(); - - lexemeDescription.appendChild(templates.placeholder({ - entity: e.language - }, cache)); - - lexemeDescription.appendChild(document.createTextNode(', ')); - - lexemeDescription.appendChild(templates.placeholder({ - entity: e.lexicalCategory - }, cache)); - - wrapper.appendChild(templates.ensign({ - revid: e.lastrevid, - id: id, - label: labels, - description: { text: lexemeDescription }, - })); + wrapper.appendChild(createLemmaEnsign(e)) } - - - let metaCanon = document.createElement('meta'); - metaCanon.setAttribute('name', 'canonical'); - metaCanon.setAttribute('content', 'https://www.wikidata.org/wiki/' + id); - document.head.appendChild(metaCanon); + document.head.appendChild(createCanonicalMeta(id)) if (e.labels || e.descriptions) { - document.title = getValueByLang(e, 'labels', e.title); - let description = getValueByLang(e, 'descriptions', false); + document.title = getValueByLang(e, 'labels', e.title) - let hasDescription = description != false; - let titleFragment = document.createElement('div'); - wrapper.appendChild(titleFragment); + let description = getValueByLang(e, 'descriptions', false) if (!description) { description = '???'; } else { - let metaDesc = document.createElement('meta'); - metaDesc.setAttribute('name', 'description'); - metaDesc.setAttribute('content', description); - document.head.appendChild(metaDesc); - } - - const setTitle = (description) => { - if (titleFragment.firstChild) { - titleFragment.removeChild(titleFragment.firstChild); - } - titleFragment.appendChild(templates.ensign({ - revid: e.lastrevid, - id: id, - label: getValueByLang(e, 'labels', e.title), - description: { - text: description, - provisional: !hasDescription - }, - })); + document.head.appendChild(createDescriptionMeta(description)) } + let titleFragment = createTitleFragment(e, description) - setTitle(description); - - if (!hasDescription && 'claims' in e && 'P31' in e.claims) { - (async () => { - description = await getAutodesc(id); - setTitle(description); - })() - } + wrapper.appendChild(titleFragment); } let aliases = getAliasesByLang(e); if (aliases) { - let metaKeys = document.createElement('meta'); - metaKeys.setAttribute('name', 'keywords'); - metaKeys.setAttribute('content', aliases.join(', ')); - document.head.appendChild(metaKeys); + document.head.appendChild(createKeywordsMeta(aliases)) } - footer.appendChild(templates.actions('Actions', [ - { - link: 'add.html?' + id, - moji: './icons/u270Eu002B-addStatement.svg', - title: 'Add a statement', - desc: 'Extract data from this website', - callback: (e) => { - browser.runtime.sendMessage({ - type: 'open_adder', - entity: id, - }); - } - }, - { - link: '#nocache', - moji: './icons/u2B6E-refresh.svg', - title: 'Reload data', - desc: 'Display an uncached version', - callback: (e) => { - location.hash = '#nocache'; - location.reload(); - }, - }, - { - link: 'links.html?' + id, - moji: './icons/u2BA9u1F4C4uFE0E-articleRedirect.svg', - title: 'What links here', - desc: 'A list of item that link to this', - }, - { - link: 'improve.html?' + id, - moji: './icons/u2728-specialPages.svg', - title: 'Improve', - desc: 'Automatic suggestions on how to improve this item', - }, - ])); + footer.appendChild(createActionElements(id)) let identifiers = document.createElement('div'); + identifiers.className = 'identifiers' let items = document.createElement('div'); + items.className = 'items' let glosses = document.createElement('div'); wrapper.appendChild(glosses); @@ -501,17 +327,12 @@ function updateView(id, useCache = true) { content.appendChild(wrapper); if (e.claims || e.statements) { - let statements = await enrichStatements(e.claims ? e.claims : e.statements); - for (let prop of groupClaims(statements)) { - let statement = renderStatement(statements[prop]); - if (statement) { - if (statement.type !== "external-id") { - items.appendChild(statement.rendered); - } else { - identifiers.appendChild(statement.rendered); - } - } - } + const statements = await enrichStatements(e.claims ? e.claims : e.statements); + + const statementsToRender = getStatementsToRender(statements) + + ReactDOM.render(, items, ) + renderFootnotes(content, collectFootnotes(statementsToRender)) } if (e['senses']) { const singleSense = e['senses'].length === 1; @@ -738,7 +559,7 @@ function updateView(id, useCache = true) { let section = document.createElement('section'); let heading = document.createElement('h2'); let headingText = templates.placeholder({ - json: `https://www.wikidata.org/w/api.php?action=parse&page=Translations:Help:Data_type/87/${lang}&disableeditsection=true&format=json`, + json: `https://www.wikidata.org/w/api.php?action=parse&page=Translations:Help:Data_type/87/${(localLanguage())}&disableeditsection=true&format=json`, type: 'span', extractor: (input) => { if (input?.parse?.text?.['*']) { @@ -763,29 +584,10 @@ function updateView(id, useCache = true) { } } + // Let react render main set of claims first + await delay(0) - let footnotes = content.querySelectorAll('.footnote'); - - let references = document.createElement('ol'); - - let footnoteNumber = 1; - Array.from(footnotes).reduce((k, footnote) => { - let footnoteId = footnote.getAttribute('href').substr(1); - let referenceItem = footnoteStorage[footnoteId].content; - if (!footnoteStorage[footnoteId].number) { - references.appendChild(referenceItem); - footnoteStorage[footnoteId].number = footnoteNumber; - footnoteNumber++; - } - footnote.innerText = footnoteStorage[footnoteId].number; - content.appendChild(references); - footnote.addEventListener('mouseover', () => { - footnote.setAttribute('title', referenceItem.innerText); - }); - }, 0); - - resolvePlaceholders(); - resolveBreadcrumbs(cache); + resolveBreadcrumbs(cache) resolveIdLinksPlaceholder(); })(); diff --git a/src/sidebar/group-claims.js b/src/sidebar/group-claims.js index fe47b62..0a39ea2 100644 --- a/src/sidebar/group-claims.js +++ b/src/sidebar/group-claims.js @@ -27,7 +27,7 @@ function groupClaims(claims) { { name: 'Naming', properties: [ - 'P6333', // title of broader work + 'P6333', // title of broader work 'P1448', // official name 'P1476', // title 'P1680', // subtitle @@ -39,7 +39,7 @@ function groupClaims(claims) { { name: 'Work Credit', properties: [ - 'P50', // author + 'P50', // author 'P58', // screenwriter 'P3174', // art director 'P161', // cast member @@ -167,7 +167,12 @@ function groupClaims(claims) { } } - return sorted.concat(remaining); + return sorted + .concat(remaining) + .map(id => ({ + id, + claims: claims[id], + })) } export { groupClaims } diff --git a/src/sidebar/links.js b/src/sidebar/links.js index 3983ac1..cfbfdfd 100644 --- a/src/sidebar/links.js +++ b/src/sidebar/links.js @@ -1,6 +1,5 @@ import { getRelatedItems } from './get-related-items.js'; import { templates } from './components/templates.tpl.js'; -import { resolvePlaceholders } from './resolve-placeholders.js'; import { PrependNav } from './prepend-nav.js'; PrependNav(); @@ -69,6 +68,5 @@ async function updateView(id, useCache = true) { vals: reverseProps[prop].values, }); whatLinksHere.appendChild(statement); - resolvePlaceholders(); } } diff --git a/src/sidebar/render-claims.ts b/src/sidebar/render-claims.ts new file mode 100644 index 0000000..351091c --- /dev/null +++ b/src/sidebar/render-claims.ts @@ -0,0 +1,271 @@ +import {templates} from './components/templates.tpl' +import {filterNotEmpty} from '../core/collections' +import {localLanguage} from '../core/env' + +const lang = localLanguage() + +function dateToString(value) { + const wiso = value.time + const prec = value.precision + + if (prec <= 6) { + return false + } + + const suffix = wiso.startsWith('-') ? ' BCE' : '' + + const pad = function (i) { + if (i < 10) { + return '0' + i + } + return i + } + + const iso = wiso + .replace(/^(\+|-)/, '') + .replace(/Z$/, '') + .replace(/^(\d+)-00/, '$1-01') + .replace(/^(\d+)-(\d+)-00/, '$1-$2-01') + + 'Z' + + const date = new Date(iso) + + const output = [] + if (prec === 7) { + const text = date.getFullYear().toString().slice(0, -2) + 'XX' + suffix + return templates.proxy({ + query: ` + SELECT ?innerText WHERE { + ?century wdt:P31 wd:Q578. + ?century wdt:P585 "${iso}Z"^^xsd:dateTime. + SERVICE wikibase:label + { + bd:serviceParam wikibase:language "[AUTO_LANGUAGE],${lang}". + ?century rdfs:label ?innerText. + } + } + LIMIT 1`, + text: text, + }) + } else if (prec === 8) { + const text = date.getFullYear().toString().slice(0, -1) + 'X' + return templates.proxy({ + query: ` + SELECT ?innerText WHERE { + ?decade wdt:P31 wd:Q39911. + ?decade wdt:P585 "${iso}Z"^^xsd:dateTime. + SERVICE wikibase:label + { + bd:serviceParam wikibase:language "[AUTO_LANGUAGE],${lang}". + ?decade rdfs:label ?innerText. + } + } + LIMIT 1`, + text: text, + }) + } else { + if (prec > 8) { + output.push(date.getUTCFullYear()) + } + if (prec > 9) { + output.push(pad(date.getUTCMonth() + 1)) + } + if (prec > 10) { + output.push(pad(date.getUTCDate())) + } + } + + return document.createTextNode(output.join('-') + suffix) +} + +function renderReferences(references) { + const sup = document.createElement('sup') + let c = 0 + for (const reference of references) { + if (c > 0) { + sup.appendChild(document.createTextNode('/')) + } + sup.appendChild(reference) + c++ + } + + return filterNotEmpty([ + document.createTextNode('\xa0'), + sup.hasChildNodes() ? sup : null, + ]) +} + +function renderQualifiers(scope, delta, renderingCache: any) { + if (!(scope === 'statement' && typeof delta != 'undefined' && delta.hasOwnProperty('qualifiers'))) { + return [] + } + + const qualifiers = [] + for (const prop of Object.keys(delta.qualifiers)) { + const qvalues = [] + for (const qv of delta.qualifiers[prop]) { + const qualvalue = new DocumentFragment() + qualvalue.append(...renderStatements(qv, [], qv.snaktype, 'qualifier', undefined, renderingCache)) + qvalues.push(qualvalue) + } + + qualifiers.push({ + prop: templates.placeholder({entity: prop}), + vals: qvalues, + }) + } + + return [templates.annote(qualifiers)] +} + +function renderIdLinks(valueType, snak) { + if (valueType === 'external-id' && snak.snaktype === 'value') { + return [templates.idLinksPlaceholder(snak.property, snak.datavalue.value)] + } + + return [] +} + +function renderStatementCore(snak, type, scope, valueType, renderingCache: any) { + if (type === 'preformatted') { + return [snak.datavalue.value] + } + if (type === 'value' || scope === 'reference') { + if (valueType === 'time') { + const date = dateToString(snak.datavalue.value) + if (date) { + return [templates.time({ + text: date, + })] + } + } + if (valueType === 'wikibase-item' || valueType === 'wikibase-entityid' || valueType === 'wikibase-lexeme' || valueType === 'wikibase-form' || valueType === 'wikibase-sense') { + const vid = snak.datavalue.value.id + return filterNotEmpty([ + snak.datavalue.parents ? templates.breadcrumbsPlaceholder(snak.datavalue.parents) : null, + templates.placeholder({entity: vid}, renderingCache), + ]) + } + if (valueType === 'external-id') { + return [templates.code(snak.datavalue.value)] + } + if (valueType === 'string') { + return [document.createTextNode(snak.datavalue.value)] + } + if (valueType === 'url') { + return [templates.urlLink(snak.datavalue.value)] + } + if (valueType === 'quantity') { + return [templates.unitNumber({ + number: snak.datavalue.value.amount, + unit: snak?.datavalue?.value?.unit, + })] + } + if (valueType === 'globe-coordinate' || valueType === 'globecoordinate') { + return [templates.mercator({ + lat: snak.datavalue.value.latitude, + lon: snak.datavalue.value.longitude, + pre: snak.datavalue.value.precision, + height: 500, + width: 500, + })] + } + if (valueType === 'monolingualtext') { + return [templates.title({ + text: snak.datavalue.value.text, + lang: snak.datavalue.value.language, + })] + } + if (valueType === 'commonsMedia') { + const name = encodeURIComponent(snak.datavalue.value) + if (name.match(/\.svg$/i)) { + return [templates.image({ + src: `https://commons.wikimedia.org/wiki/Special:FilePath/${name}`, + })] + } else if (name.match(/\.(jpe?g|png|gif|tiff?|stl)$/i)) { + return [templates.picture({ + srcSet: { + 250: `https://commons.wikimedia.org/wiki/Special:FilePath/${name}?width=250px`, + 501: `https://commons.wikimedia.org/wiki/Special:FilePath/${name}?width=501px`, + 801: `https://commons.wikimedia.org/wiki/Special:FilePath/${name}?width=801px`, + 1068: `https://commons.wikimedia.org/wiki/Special:FilePath/${name}?width=1068px`, + }, + })] + } else if (name.match(/\.(wav|og[ga])$/i)) { + return [templates.audio({ + src: `https://commons.wikimedia.org/wiki/Special:FilePath/${name}`, + })] + } else if (name.match(/\.webm$/i)) { + return [templates.video({ + poster: `https://commons.wikimedia.org/wiki/Special:FilePath/${name}?width=801px`, + src: `https://commons.wikimedia.org/wiki/Special:FilePath/${name}`, + })] + } + } + } else if (type === 'novalue') { + return [document.createTextNode('—')] + } else if (type === 'somevalue') { + return [document.createTextNode('?')] + } + return [] +} + +export function renderStatements(snak, references, type, scope, delta, renderingCache: any) { + const valueType = snak.datatype ? snak.datatype : snak.datavalue.type + return [ + ...renderStatementCore(snak, type, scope, valueType, renderingCache), + ...renderReferences(references), + ...renderIdLinks(valueType, snak), + ...renderQualifiers(scope, delta, renderingCache), + ] +} + + + +export interface Claim { + rank: 'deprecated' | 'normal' | 'preferred' + mainsnak: any + references: any + type: string + // todo improve +} + +export function renderStatement(claims: Claim[], renderingCache: any) { + if (!claims[0]?.mainsnak) return + + const pid = claims[0].mainsnak.property + const values = [] + for (const delta of claims) { + if (delta?.mainsnak) { + const thisvalue = new DocumentFragment() + const type = delta.mainsnak.snaktype + const refs = [] + if (delta.references) { + for (const ref of delta.references) { + refs.push(templates.footnoteRef({ + text: '▐', + link: '#' + ref.hash, + })) + } + } + + thisvalue.append(...renderStatements(delta.mainsnak, refs, type, 'statement', delta, renderingCache)) + values.push(thisvalue) + + } + } + const statement = templates.remark({ + prop: templates.placeholder({ + entity: pid, + }, renderingCache), + vals: values, + id: pid, + }) + const firstValue = claims.find(x => x !== undefined) + if (firstValue) { + return { + rendered: statement, + type: firstValue.mainsnak.snaktype, + } + } +} diff --git a/src/sidebar/resolve-placeholders.js b/src/sidebar/resolve-placeholders.js index 0c8184c..ebd613b 100644 --- a/src/sidebar/resolve-placeholders.js +++ b/src/sidebar/resolve-placeholders.js @@ -1,10 +1,7 @@ -import { wikidataGetEntity } from '../wd-get-entity.js'; import { getParents, Breadcrumbs } from './get-parents.js'; import { templates } from './components/templates.tpl.js'; import { getFormatterUrls } from './get-formatter-urls.js'; -import { sparqlQuery } from '../sqarql-query.js'; import { breadcrumbs } from './components/breadcrumbs/breadcrumbs.tpl.js'; -import { getValueByLang } from './get-value-by-lang.js'; function getLink(entityId) { let ns = entityId.charAt(0); @@ -16,10 +13,6 @@ function getLink(entityId) { return prefixes[ns] + entityId; } -function resolvePlaceholders(scope) { - -} - function resolveBreadcrumbs(cache) { let placeholders = document.querySelectorAll('.breadcrumbs[data-child-id]'); @@ -44,7 +37,6 @@ function resolveBreadcrumbs(cache) { }, cache)); } placeholder.parentNode.replaceChild(breadcrumbs(trail), placeholder); - resolvePlaceholders(placeholder.parentNode); })(); }, 0); } @@ -79,4 +71,4 @@ function resolveIdLinksPlaceholder() { }, 0); } -export { getLink, resolvePlaceholders, resolveIdLinksPlaceholder, resolveBreadcrumbs } +export { getLink, resolveIdLinksPlaceholder, resolveBreadcrumbs } diff --git a/src/wd-get-entity.js b/src/wd-get-entity.js index 509f425..730e93d 100644 --- a/src/wd-get-entity.js +++ b/src/wd-get-entity.js @@ -42,7 +42,7 @@ async function wikidataGetEntity(id, usecache = true, returnSingle = false) { cache: usecache ? 'default' : 'reload', }); response = await response.json(); - let cached = await addToLabelCache(id, response.entities); + await addToLabelCache(id, response.entities); return returnSingle ? response.entities[id] : response.entities; } catch(error) { throw ['Fetch Error :-S', error];