Skip to content

Commit 6240826

Browse files
committed
refactor(opds): migrate null to undefined, conditionally extract link properties by role
1 parent 7be0664 commit 6240826

File tree

1 file changed

+53
-74
lines changed

1 file changed

+53
-74
lines changed

opds.js

Lines changed: 53 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ const groupByArray = (arr, f) => {
5151

5252
// https://www.rfc-editor.org/rfc/rfc7231#section-3.1.1
5353
const parseMediaType = str => {
54-
if (!str) return null
54+
if (!str) return
5555
const [mediaType, ...ps] = str.split(/ *; */)
5656
return {
5757
mediaType: mediaType.toLowerCase(),
@@ -72,7 +72,7 @@ export const isOPDSCatalog = str => {
7272

7373
// ignore the namespace if it doesn't appear in document at all
7474
const useNS = (doc, ns) =>
75-
doc.lookupNamespaceURI(null) === ns || doc.lookupPrefix(ns) ? ns : null
75+
doc.lookupNamespaceURI(null) === ns || doc.lookupPrefix(ns) ? ns : undefined
7676

7777
const filterNS = ns => ns
7878
? name => el => el.namespaceURI === ns && el.localName === name
@@ -92,7 +92,7 @@ const getContent = el => {
9292

9393
const getTextContent = el => {
9494
const content = getContent(el)
95-
if (content?.type === 'text') return content?.value
95+
if (content?.type === 'text') return content.value
9696
}
9797

9898
const getSummary = (a, b) => getTextContent(a) ?? getTextContent(b)
@@ -110,7 +110,7 @@ const getDirectChildren = (el, ns, localName, tagName) => {
110110

111111
const getPrice = link => {
112112
const prices = getDirectChildren(link, NS.OPDS, 'price', 'opds:price')
113-
if (!prices.length) return null
113+
if (!prices.length) return
114114
const parsed = prices.map(price => ({
115115
currency: price.getAttribute('currencycode'),
116116
value: parseFloat(price.textContent),
@@ -133,61 +133,45 @@ const getIndirectAcquisition = el => {
133133
}
134134

135135
const getLink = link => {
136-
const obj = {
137-
rel: link.getAttribute('rel')?.split(/ +/),
138-
href: link.getAttribute('href'),
139-
type: link.getAttribute('type'),
140-
title: link.getAttribute('title'),
141-
properties: {},
142-
}
143-
144-
// --- Prices & Indirect Acquisitions ---
145-
const price = getPrice(link)
146-
if (price) obj.properties.price = price
147-
148-
const indirectAcquisition = getIndirectAcquisition(link)
149-
if (indirectAcquisition.length) obj.properties.indirectAcquisition = indirectAcquisition
136+
const relAttr = link.getAttribute('rel')
137+
const rel = relAttr ? relAttr.split(/ +/) : undefined
150138

151-
// --- Facet Grouping ---
152-
const facetGroup = link.getAttributeNS(NS.OPDS, 'facetGroup') || link.getAttribute('opds:facetGroup')
153-
if (facetGroup) obj[FACET_GROUP] = facetGroup
139+
const isAcquisition = rel?.some(r => r.startsWith(REL.ACQ) || r === 'preview')
140+
const isStream = rel?.includes(REL.STREAM)
141+
const isFacet = rel?.includes(REL.FACET)
154142

155143
// Map OPDS 1.x active facets to OPDS 2.0 "self" link
156144
const activeFacet = link.getAttributeNS(NS.OPDS, 'activeFacet') || link.getAttribute('opds:activeFacet')
157-
if (activeFacet === 'true') {
158-
obj.rel = [obj.rel ?? []].flat().concat('self')
159-
}
145+
const mappedRel = activeFacet === 'true' ? [rel ?? []].flat().concat('self') : rel
160146

161-
// --- Pagination / Facet Counters ---
162147
// Maps OPDS 1.x thr:count seamlessly to OPDS 2.0 properties.numberOfItems
163148
const thrCount = link.getAttributeNS(NS.THR, 'count') || link.getAttribute('thr:count')
149+
// Support for systems that incorrectly use standard `count` for facet hints
164150
const fallbackCount = link.getAttribute('count')
165-
const isStream = obj.rel?.includes(REL.STREAM)
166-
167-
if (thrCount != null) {
168-
obj.properties.numberOfItems = Number(thrCount)
169-
} else if (!isStream && fallbackCount != null) {
170-
// Support for systems that incorrectly use standard `count` for facet hints
171-
obj.properties.numberOfItems = Number(fallbackCount)
172-
}
173151

174152
// --- OPDS-PSE Extensions ---
175-
// Kept explicitly inside properties to map to OPDS 2.x standard extension mechanism
176153
const pseCount = link.getAttributeNS(NS.PSE, 'count') || link.getAttribute('pse:count')
177-
if (pseCount != null) {
178-
obj.properties['pse:count'] = Number(pseCount)
179-
} else if (isStream && fallbackCount != null) {
180-
obj.properties['pse:count'] = Number(fallbackCount)
181-
}
182-
183154
const pseLastRead = link.getAttributeNS(NS.PSE, 'lastRead') || link.getAttribute('pse:lastRead')
184-
if (pseLastRead != null) obj.properties['pse:lastRead'] = Number(pseLastRead)
185-
186155
const pseLastReadDate = link.getAttributeNS(NS.PSE, 'lastReadDate') || link.getAttribute('pse:lastReadDate')
187-
if (pseLastReadDate != null) obj.properties['pse:lastReadDate'] = pseLastReadDate
188-
// ---------------------------
189156

190-
// Clean up empty properties
157+
const obj = {
158+
rel: mappedRel,
159+
href: link.getAttribute('href'),
160+
type: link.getAttribute('type'),
161+
title: link.getAttribute('title'),
162+
// --- Facet Grouping ---
163+
[FACET_GROUP]: link.getAttributeNS(NS.OPDS, 'facetGroup') || link.getAttribute('opds:facetGroup'),
164+
properties: {
165+
price: (isAcquisition || isStream) ? getPrice(link) : undefined,
166+
indirectAcquisition: (isAcquisition || isStream) ? getIndirectAcquisition(link) : [],
167+
// --- Pagination / Facet Counters ---
168+
numberOfItems: thrCount != null ? Number(thrCount) : (isFacet && fallbackCount != null) ? Number(fallbackCount) : undefined,
169+
'pse:count': isStream ? Number(pseCount || fallbackCount) || undefined : undefined,
170+
'pse:lastRead': isStream && pseLastRead != null ? Number(pseLastRead) : undefined,
171+
'pse:lastReadDate': isStream ? pseLastReadDate : undefined,
172+
}
173+
}
174+
191175
if (Object.keys(obj.properties).length === 0) delete obj.properties
192176

193177
return obj
@@ -219,8 +203,7 @@ export const getPublication = entry => {
219203
author: children.filter(filter('author')).map(getPerson),
220204
contributor: children.filter(filter('contributor')).map(getPerson),
221205
publisher: children.find(filterDC('publisher'))?.textContent,
222-
published: (children.find(filterDCTERMS('issued'))
223-
?? children.find(filterDC('date')))?.textContent,
206+
published: (children.find(filterDCTERMS('issued')) ?? children.find(filterDC('date')))?.textContent,
224207
language: children.find(filterDC('language'))?.textContent,
225208
identifier: children.find(filterDC('identifier'))?.textContent,
226209
subject: children.filter(filter('category')).map(category => ({
@@ -229,8 +212,7 @@ export const getPublication = entry => {
229212
scheme: category.getAttribute('scheme'),
230213
})),
231214
rights: children.find(filter('rights'))?.textContent ?? '',
232-
[SYMBOL.CONTENT]: getContent(children.find(filter('content'))
233-
?? children.find(filter('summary'))),
215+
[SYMBOL.CONTENT]: getContent(children.find(filter('content')) ?? children.find(filter('summary'))),
234216
},
235217
links,
236218
images: REL.COVER.concat(REL.THUMBNAIL)
@@ -249,7 +231,7 @@ export const getFeed = doc => {
249231
const filterFH = filterNS(NS.FH)
250232
const filterOS = filterNS(NS.OS)
251233

252-
const groupedItems = new Map([[null, []]])
234+
const groupedItems = new Map([[undefined, []]])
253235
const groupLinkMap = new Map()
254236
for (const entry of entries) {
255237
const children = Array.from(entry.children)
@@ -260,7 +242,7 @@ export const getFeed = doc => {
260242

261243
const groupLinks = linksByRel.get(REL.GROUP) ?? linksByRel.get('collection')
262244
const groupLink = groupLinks?.length
263-
? groupLinks.find(link => groupedItems.has(link.href)) ?? groupLinks[0] : null
245+
? groupLinks.find(link => groupedItems.has(link.href)) ?? groupLinks[0] : undefined
264246
if (groupLink && !groupLinkMap.has(groupLink.href))
265247
groupLinkMap.set(groupLink.href, groupLink)
266248

@@ -272,13 +254,13 @@ export const getFeed = doc => {
272254
children.find(filter('content'))),
273255
})
274256

275-
const arr = groupedItems.get(groupLink?.href ?? null)
257+
const arr = groupedItems.get(groupLink?.href)
276258
if (arr) arr.push(item)
277259
else groupedItems.set(groupLink.href, [item])
278260
}
279261
const [items, ...groups] = Array.from(groupedItems, ([key, items]) => {
280262
const itemsKey = items[0]?.metadata ? 'publications' : 'navigation'
281-
if (key == null) return { [itemsKey]: items }
263+
if (key === undefined) return { [itemsKey]: items }
282264
const link = groupLinkMap.get(key)
283265
return {
284266
metadata: {
@@ -290,39 +272,36 @@ export const getFeed = doc => {
290272
}
291273
})
292274

293-
const metadata = {
294-
title: children.find(filter('title'))?.textContent,
295-
subtitle: children.find(filter('subtitle'))?.textContent,
296-
}
297-
298275
// --- OPDS 2.0 Pagination (derived from OpenSearch / RFC 5005) ---
299276
const totalResults = children.find(filterOS('totalResults'))?.textContent
300277
const itemsPerPage = children.find(filterOS('itemsPerPage'))?.textContent
301278
const startIndex = children.find(filterOS('startIndex'))?.textContent
302279

303-
if (totalResults != null) metadata.numberOfItems = Number(totalResults)
304-
if (itemsPerPage != null) metadata.itemsPerPage = Number(itemsPerPage)
280+
let currentPage
305281
if (startIndex != null && itemsPerPage != null) {
306282
const start = Number(startIndex)
307283
const items = Number(itemsPerPage)
308284
// Resolves typical 1-based offset to a page number
309-
metadata.currentPage = Math.floor((start > 0 ? start - 1 : 0) / items) + 1
285+
currentPage = Math.floor((start > 0 ? start - 1 : 0) / items) + 1
310286
}
311287

312-
const isComplete = !!children.find(filterFH('complete'))
313-
const isArchive = !!children.find(filterFH('archive'))
314-
// ----------------------------------------------------------------
315-
316288
return {
317-
metadata,
289+
metadata: {
290+
title: children.find(filter('title'))?.textContent,
291+
subtitle: children.find(filter('subtitle'))?.textContent,
292+
numberOfItems: totalResults != null ? Number(totalResults) : undefined,
293+
itemsPerPage: itemsPerPage != null ? Number(itemsPerPage) : undefined,
294+
currentPage
295+
},
318296
links,
319-
isComplete,
320-
isArchive,
297+
isComplete: !!children.find(filterFH('complete')),
298+
isArchive: !!children.find(filterFH('archive')),
321299
...items,
322300
groups,
323301
facets: Array.from(
324302
groupByArray(linksByRel.get(REL.FACET) ?? [], link => link[FACET_GROUP]),
325-
([facet, links]) => ({ metadata: { title: facet }, links })),
303+
([facet, links]) => ({ metadata: { title: facet }, links })
304+
),
326305
}
327306
}
328307

@@ -332,7 +311,7 @@ export const getSearch = async link => {
332311
metadata: {
333312
title: link.title,
334313
},
335-
search: map => replace(link.href, map.get(null)),
314+
search: map => replace(link.href, map.get(undefined)),
336315
params: Array.from(getVariables(link.href), name => ({ name })),
337316
}
338317
}
@@ -363,14 +342,14 @@ export const getOpenSearch = doc => {
363342
description: children.find(filter('Description'))?.textContent,
364343
},
365344
search: map => template.replace(regex, (_, prefix, param) => {
366-
const namespace = prefix ? $url.lookupNamespaceURI(prefix) : null
367-
const ns = namespace === defaultNS ? null : namespace
345+
const namespace = prefix ? $url.lookupNamespaceURI(prefix) : undefined
346+
const ns = namespace === defaultNS ? undefined : namespace
368347
const val = map.get(ns)?.get(param)
369348
return encodeURIComponent(val ? val : (!ns ? defaultMap.get(param) ?? '' : ''))
370349
}),
371350
params: Array.from(template.matchAll(regex), ([, prefix, param, optional]) => {
372-
const namespace = prefix ? $url.lookupNamespaceURI(prefix) : null
373-
const ns = namespace === defaultNS ? null : namespace
351+
const namespace = prefix ? $url.lookupNamespaceURI(prefix) : undefined
352+
const ns = namespace === defaultNS ? undefined : namespace
374353
return {
375354
ns, name: param,
376355
required: !optional,

0 commit comments

Comments
 (0)