Skip to content

Commit

Permalink
OPDS: fix rendering of webpub language map and contributor objects
Browse files Browse the repository at this point in the history
  • Loading branch information
johnfactotum committed Dec 8, 2023
1 parent ceb153a commit f919f0c
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 58 deletions.
17 changes: 17 additions & 0 deletions src/format.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,23 @@ export const locales = GLib.get_language_names()
.map(glibcLocale).filter(x => x)
.map(locale => new Intl.Locale(locale, { hourCycle }))

// very naive, probably bad locale matcher
// replace this with `Intl.LocaleMatcher` once it's available
export const matchLocales = strs => {
const availableLocales = strs.map(makeLocale)
const matches = []
for (const a of locales) {
for (const [i, b] of availableLocales.entries()) {
if (!b) continue
if (a.language === b.language
&& (a.region && b.region ? a.region === b.region : true)
&& (a.script && b.script ? a.script === b.script : true))
matches.push(strs[i])
}
}
return matches
}

const percentFormat = new Intl.NumberFormat(locales, { style: 'percent' })
export const percent = x => percentFormat.format(x)

Expand Down
6 changes: 5 additions & 1 deletion src/library.js
Original file line number Diff line number Diff line change
Expand Up @@ -446,12 +446,16 @@ GObject.registerClass({
({ currency, value }) => format.price(currency, value)),
webView.provide('formatLanguage', format.language),
webView.provide('formatDate', format.date),
webView.provide('formatList', format.list),
webView.provide('matchLocales', format.matchLocales),
]
utils.connect(webView, {
'context-menu': () => false,
'load-changed': (webView, event) => {
if (event === WebKit.LoadEvent.FINISHED) {
webView.run(`globalThis.uiText = ${JSON.stringify(uiText)}`)
const lang = format.locales[0].baseName
webView.run(`globalThis.uiText = ${JSON.stringify(uiText)}
document.documentElement.lang = "${lang}"`)
.catch(e => console.error(e))
for (const f of initFuncs) f()

Expand Down
97 changes: 55 additions & 42 deletions src/opds/opds.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,15 +101,9 @@ const filterNS = ns => ns
? name => el => el.namespaceURI === ns && el.localName === name
: name => el => el.localName === name

const langToLocales = lang => {
try { return new Intl.Locale(lang) }
catch { return 'en' }
}

const formatElementList = (locales, els) => {
const formatElementList = async els => {
const arr = els.slice(0)
return new Intl.ListFormat(locales, { type: 'conjunction' })
.format(els.map(() => '%s'))
return (await globalThis.formatList(els.map(() => '%s')))
.split(/(%s)/g)
.map(str => str === '%s' ? arr.shift() : document.createTextNode(str))
}
Expand Down Expand Up @@ -279,7 +273,7 @@ const getPublication = (entry, filter) => {
?? children.find(filterDC('date')))?.textContent,
language: children.find(filterDC('language'))?.textContent,
identifier: children.find(filterDC('identifier'))?.textContent,
subjects: children.filter(filter('category')).map(category => ({
subject: children.filter(filter('category')).map(category => ({
name: category.getAttribute('label'),
code: category.getAttribute('term'),
})),
Expand Down Expand Up @@ -346,6 +340,36 @@ const getFeed = doc => {
}
}

const renderLanguageMap = async x => {
if (!x) return ''
if (typeof x === 'string') return x
const keys = Object.keys(x)
return x[(await globalThis.matchLocales(keys))[0]] ?? x.en ?? x[keys[0]]
}

const renderLinkedObject = (object, baseURL) => {
const a = document.createElement('a')
if (object.links?.length) {
for (const link of object.links) if (isOPDSCatalog(link.type)) {
a.href = '?url=' + encodeURIComponent(resolveURL(link.href, baseURL))
return a
}
a.href = resolveURL(object.links[0].href, baseURL)
}
return a
}

const renderContributor = async (contributor, baseURL) => {
if (!contributor) return
const as = await Promise.all([contributor ?? []].flat().map(async contributor => {
const a = renderLinkedObject(contributor, baseURL)
a.innerText = typeof contributor === 'string' ? contributor
: await renderLanguageMap(contributor.name)
return a
}))
return as.length <= 1 ? as : await formatElementList(as)
}

const renderAcquisitionButton = async (rel, links, callback) => {
const label = globalThis.uiText.acq[rel] ?? globalThis.uiText.acq[REL.ACQ]
const priceData = links[0].properties?.price
Expand Down Expand Up @@ -416,14 +440,14 @@ const renderFacets = (facets, baseURL) => facets.map(({ metadata, links }) => {
return section
})

const renderGroups = (groups, baseURL) => groups.flatMap(({ metadata, links, publications, navigation }) => {
const renderGroups = async (groups, baseURL) => (await Promise.all(groups.map(async ({ metadata, links, publications, navigation }) => {
const container = document.createElement('div')
container.classList.add('container')
container.replaceChildren(...(publications ?? navigation).map(item => {
container.replaceChildren(...await Promise.all((publications ?? navigation).map(async item => {
const isPub = 'metadata' in item
const el = document.createElement(isPub ? 'opds-pub' : 'opds-nav')
if (isPub) {
el.setAttribute('heading', item.metadata.title ?? '')
el.setAttribute('heading', await renderLanguageMap(item.metadata.title))
const src = resolveURL(item.images?.[0]?.href, baseURL)
if (src) el.setAttribute('image', src)
el.setAttribute('href', '#' + encodeURIComponent(JSON.stringify(item)))
Expand All @@ -435,7 +459,7 @@ const renderGroups = (groups, baseURL) => groups.flatMap(({ metadata, links, pu
? '?url=' + encodeURIComponent(href) : href)
}
return el
}))
})))
if (!metadata) return container

const div = document.createElement('div')
Expand All @@ -453,7 +477,7 @@ const renderGroups = (groups, baseURL) => groups.flatMap(({ metadata, links, pu
div.classList.add('carousel-header')
container.classList.add('carousel')
return [document.createElement('hr'), div, container]
})
}))).flat()

const entryMap = new Map()
globalThis.updateProgress = ({ progress, token }) =>
Expand Down Expand Up @@ -494,25 +518,12 @@ const renderPublication = async (pub, baseURL) => {
const src = resolveURL(pub.images?.[0]?.href, baseURL)
if (src) item.setAttribute('image', src)

item.setAttribute('heading', pub.metadata.title)
item.setAttribute('heading', await renderLanguageMap(pub.metadata.title))

const authors = document.createElement('div')
authors.slot = 'authors'
item.append(authors)
const authorAs = [pub.metadata.author ?? []].flat().map(author => {
const a = document.createElement('a')
a.innerText = typeof author === 'string' ? author : author.name
if (author.links?.length) {
for (const link of author.links) if (isOPDSCatalog(link.type)) {
a.href = '?url=' + encodeURIComponent(resolveURL(link.href, baseURL))
return a
}
a.href = resolveURL(author.links[0].href, baseURL)
}
return a
})
authors.append(...(authorAs.length <= 1 ? authorAs
: formatElementList(langToLocales(pub.metadata.language ?? 'en'), authorAs)))
authors.append(...await renderContributor(pub.metadata.author, baseURL))

const blob = pub.metadata[SYMBOL.CONTENT]
? renderContent(pub.metadata[SYMBOL.CONTENT].value, pub.metadata[SYMBOL.CONTENT].type, baseURL)
Expand All @@ -531,9 +542,10 @@ const renderPublication = async (pub, baseURL) => {
details.append(table)

for (const [k, v = pub.metadata[k]] of [
['publisher'],
['publisher', await renderContributor(pub.metadata.publisher, baseURL)],
['published', await globalThis.formatDate(pub.metadata.published)],
['language', await globalThis.formatLanguage(pub.metadata.language)],
['language', await Promise.all([pub.metadata.language ?? []].flat()
.map(x => globalThis.formatLanguage(x)))],
['identifier'],
]) {
if (!v) continue
Expand All @@ -542,40 +554,41 @@ const renderPublication = async (pub, baseURL) => {
const td = document.createElement('td')
tr.append(th, td)
th.textContent = globalThis.uiText.metadata[k]
td.textContent = v
if (v[0].nodeType != null) td.append(...v)
else td.textContent = v
if (v.length > 30) tr.classList.add('long')
table.append(tr)
}

const tags = document.createElement('div')
tags.role = 'list'
details.append(tags)
tags.append(...[pub.metadata.subjects ?? []].flat().map(subject => {
tags.append(...[pub.metadata.subject ?? []].flat().map(subject => {
const li = document.createElement('div')
li.role = 'listitem'
const icon = document.createElement('foliate-symbolic')
icon.setAttribute('src', '/icons/hicolor/scalable/actions/tag-symbolic.svg')
const span = document.createElement('span')
span.textContent = typeof subject === 'string' ? subject : subject.name ?? subject.code
li.append(icon, span)
const a = renderLinkedObject(subject, baseURL)
a.textContent = typeof subject === 'string' ? subject : subject.name ?? subject.code
li.append(icon, a)
return li
}))

return item
}

const renderFeed = (feed, baseURL) => {
const renderFeed = async (feed, baseURL) => {
const linksByRel = groupByArray(feed.links, link => link.rel)
const searchLink = linksByRel.get('search')
?.find(link => parseMediaType(link.type).mediaType === MIME.OPENSEARCH)
if (searchLink) document.body.dataset.searchUrl = resolveURL(searchLink.href, baseURL)
else delete document.body.dataset.searchUrl
globalThis.updateSearchURL()

document.querySelector('#feed h1').textContent = feed.metadata.title ?? ''
document.querySelector('#feed p').textContent = feed.metadata.subtitle ?? ''
document.querySelector('#feed h1').textContent = await renderLanguageMap(feed.metadata.title)
document.querySelector('#feed p').textContent = await renderLanguageMap(feed.metadata.subtitle)

document.querySelector('#feed main').append(...renderGroups(feed.groups, baseURL))
document.querySelector('#feed main').append(...await renderGroups(feed.groups, baseURL))
if (feed.facets)
document.querySelector('#nav').append(...renderFacets(feed.facets, baseURL))

Expand Down Expand Up @@ -679,7 +692,7 @@ try {
if (text.startsWith('<')) {
const doc = new DOMParser().parseFromString(text, MIME.XML)
const { documentElement: { localName } } = doc
if (localName === 'feed') renderFeed(getFeed(doc), url)
if (localName === 'feed') await renderFeed(getFeed(doc), url)
else if (localName === 'entry') throw new Error('todo')
else if (localName === 'OpenSearchDescription') renderOpenSearch(doc, url)
else throw new Error(`root element is <${localName}>; expected <feed> or <entry>`)
Expand All @@ -692,7 +705,7 @@ try {
publications ? { publications } : null,
...(feed.groups ?? []),
].filter(x => x)
renderFeed(feed, url)
await renderFeed(feed, url)
}
} catch (e) {
console.error(e)
Expand Down
17 changes: 2 additions & 15 deletions src/selection-tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { gettext as _ } from 'gettext'

import * as utils from './utils.js'
import { WebView } from './webview.js'
import { locales } from './format.js'
import { locales, matchLocales } from './format.js'

const getLanguage = lang => {
try {
Expand All @@ -24,20 +24,7 @@ const getGoogleTranslateLanguages = utils.memoize(() => {
// [...document.querySelector('table').querySelectorAll('tr')].map(tr => tr.querySelector('code')?.innerText).filter(x => x).map(x => `'${x}'`).join(', ')
const displayName = new Intl.DisplayNames(locales, { type: 'language' })
const langs = ['af', 'sq', 'am', 'ar', 'hy', 'as', 'ay', 'az', 'bm', 'eu', 'be', 'bn', 'bho', 'bs', 'bg', 'ca', 'ceb', 'zh-CN', 'zh-TW', 'co', 'hr', 'cs', 'da', 'dv', 'doi', 'nl', 'en', 'eo', 'et', 'ee', 'fil', 'fi', 'fr', 'fy', 'gl', 'ka', 'de', 'el', 'gn', 'gu', 'ht', 'ha', 'haw', 'he', 'hi', 'hmn', 'hu', 'is', 'ig', 'ilo', 'id', 'ga', 'it', 'ja', 'jv', 'kn', 'kk', 'km', 'rw', 'gom', 'ko', 'kri', 'ku', 'ckb', 'ky', 'lo', 'la', 'lv', 'ln', 'lt', 'lg', 'lb', 'mk', 'mai', 'mg', 'ms', 'ml', 'mt', 'mi', 'mr', 'mni-Mtei', 'lus', 'mn', 'my', 'ne', 'no', 'ny', 'or', 'om', 'ps', 'fa', 'pl', 'pt', 'pa', 'qu', 'ro', 'ru', 'sm', 'sa', 'gd', 'nso', 'sr', 'st', 'sn', 'sd', 'si', 'sk', 'sl', 'so', 'es', 'su', 'sw', 'sv', 'tl', 'tg', 'ta', 'tt', 'te', 'th', 'ti', 'ts', 'tr', 'tk', 'ak', 'uk', 'ur', 'ug', 'uz', 'vi', 'cy', 'xh', 'yi', 'yo', 'zu']
let defaultLang
for (const locale of locales.map(locale => locale.baseName)) {
if (locale === 'zh-CN' || locale === 'zh-TW') {
defaultLang = locale
break
}
const language = getLanguage(locale)
if (language === 'mni') {
defaultLang = 'mni-Mtei'
break
}
defaultLang = langs.find(lang => lang === language)
if (defaultLang) break
}
const defaultLang = matchLocales(langs)[0] ?? 'en'
return JSON.stringify([langs.map(lang => [lang, displayName.of(lang)]), defaultLang])
})

Expand Down

0 comments on commit f919f0c

Please sign in to comment.