@museumwnf/viewer-core 2.14.0 → 2.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -9
- package/src/catalogue/dates.js +10 -0
- package/src/catalogue/legacySearch.js +12 -9
- package/src/catalogue/pagination.js +10 -3
- package/src/dxa/gallery/eraWords.js +8 -0
- package/src/dxa/gallery/useGalleryCollection.js +44 -9
- package/src/dxa/gallery/useGalleryItemDetail.js +3 -2
- package/src/dxa/gallery/useGalleryPartner.js +2 -2
- package/src/dxa/gallery/useGalleryTimeline.js +7 -5
- package/src/dxa/index.js +2 -1
- package/src/dxa/shared.js +7 -0
- package/src/i18n/I18nText.vue +4 -1
- package/src/i18n/markdown.js +34 -6
- package/src/index.js +1 -1
- package/src/testing/viteConfig.js +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@museumwnf/viewer-core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.16.0",
|
|
4
4
|
"description": "Application engine for MWNF websites: turns a dataset.config.js into a mounted Vue application.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
@@ -33,19 +33,19 @@
|
|
|
33
33
|
"test": "vitest run"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"marked": "^18.
|
|
36
|
+
"marked": "^18.1.3"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"vue": "^3.5.0",
|
|
40
40
|
"vue-router": "^4.5.0 || ^5.0.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@vitejs/plugin-vue": "^6.0.
|
|
44
|
-
"@vue/test-utils": "^2.
|
|
45
|
-
"jsdom": "^30.
|
|
46
|
-
"vite": "^8.
|
|
47
|
-
"vitest": "^
|
|
48
|
-
"vue": "^3.5.
|
|
49
|
-
"vue-router": "^5.
|
|
43
|
+
"@vitejs/plugin-vue": "^6.0.10",
|
|
44
|
+
"@vue/test-utils": "^2.5.1",
|
|
45
|
+
"jsdom": "^30.1.3",
|
|
46
|
+
"vite": "^8.3.5",
|
|
47
|
+
"vitest": "^5.1.0",
|
|
48
|
+
"vue": "^3.5.44",
|
|
49
|
+
"vue-router": "^5.4.0"
|
|
50
50
|
}
|
|
51
51
|
}
|
package/src/catalogue/dates.js
CHANGED
|
@@ -182,6 +182,16 @@ export function eraLabel(year, t) {
|
|
|
182
182
|
return year < 0 ? `${Math.abs(year)} ${t('catalogue.era.bc')}` : `${year} ${t('catalogue.era.ad')}`
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
/**
|
|
186
|
+
* `t`, with a family's own era words: `words` maps a `catalogue.era.*`
|
|
187
|
+
* entry to the one to read instead (the galleries' "A.D." and "B.C.",
|
|
188
|
+
* `GALLERY_ERA_WORDS`). Every other entry reads as before; no `words`, `t`.
|
|
189
|
+
*/
|
|
190
|
+
export function withEraWords(t, words) {
|
|
191
|
+
if (!words) return t
|
|
192
|
+
return (key, ...rest) => t(words[key] ?? key, ...rest)
|
|
193
|
+
}
|
|
194
|
+
|
|
185
195
|
/**
|
|
186
196
|
* A record's dates rounded outward to the century, the window legacy's
|
|
187
197
|
* "Timeline for this item" opens, so a single-year object still lands on a
|
|
@@ -56,15 +56,18 @@ function byStartDate(a, b) {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
|
-
* `fields`: `{ [field]: (record, text) => string | string[] }`,
|
|
60
|
-
* field of the form searches in a record and one of its
|
|
59
|
+
* `fields`: `{ [field]: (record, text, language) => string | string[] }`,
|
|
60
|
+
* what each field of the form searches in a record and one of its
|
|
61
|
+
* translations, given that translation's language (so a field can read a
|
|
62
|
+
* related record's text in the same language, as legacy's joins did).
|
|
61
63
|
* `countryFields`: the fields in which a word naming a country (in any of
|
|
62
64
|
* its names) also finds the records of that country.
|
|
63
65
|
* `glossaryFields`: the fields in which a glossary term also searches its
|
|
64
66
|
* spellings in the site's language.
|
|
65
|
-
* `score`: `{ [field]: (like, record, text, inCountry) => number }`,
|
|
66
|
-
* relevance a matching row adds, `like(value)` telling whether a value
|
|
67
|
-
* the row's own word. Summed over the rows, the best translation
|
|
67
|
+
* `score`: `{ [field]: (like, record, text, inCountry, language) => number }`,
|
|
68
|
+
* the relevance a matching row adds, `like(value)` telling whether a value
|
|
69
|
+
* holds the row's own word. Summed over the rows, the best translation
|
|
70
|
+
* counting.
|
|
68
71
|
*
|
|
69
72
|
* Returns `narrow(list, filters, helpers)` for a results spec (pass it
|
|
70
73
|
* `sort: false`): the records of `list` that match, in legacy's order.
|
|
@@ -88,13 +91,13 @@ export function useLegacySearch({
|
|
|
88
91
|
// translation object, so a file loaded again starts afresh.
|
|
89
92
|
const folded = new WeakMap()
|
|
90
93
|
|
|
91
|
-
function hayOf(field, record, text) {
|
|
94
|
+
function hayOf(field, record, text, language) {
|
|
92
95
|
let byField = folded.get(text)
|
|
93
96
|
if (!byField) folded.set(text, (byField = new Map()))
|
|
94
97
|
let hay = byField.get(field)
|
|
95
98
|
if (!hay) {
|
|
96
99
|
const getter = fields[field] ?? fields.keyword
|
|
97
|
-
byField.set(field, (hay = strings(getter(record, text)).map(collate)))
|
|
100
|
+
byField.set(field, (hay = strings(getter(record, text, language)).map(collate)))
|
|
98
101
|
}
|
|
99
102
|
return hay
|
|
100
103
|
}
|
|
@@ -161,13 +164,13 @@ export function useLegacySearch({
|
|
|
161
164
|
let relevance = 0
|
|
162
165
|
for (const row of active) {
|
|
163
166
|
const inCountry = row.countries.has(record.country_id)
|
|
164
|
-
const hay = hayOf(row.field, record, text)
|
|
167
|
+
const hay = hayOf(row.field, record, text, language)
|
|
165
168
|
const hit = inCountry || hay.some((h) => row.needles.some((n) => h.includes(n)))
|
|
166
169
|
if (result === null) result = hit
|
|
167
170
|
else if (String(row.cond).toUpperCase() === 'OR') result = result || hit
|
|
168
171
|
else result = result && hit
|
|
169
172
|
const rank = score[row.field]
|
|
170
|
-
if (rank) relevance += rank((value) => strings(value).some((v) => collate(v).includes(row.needle)), record, text, inCountry)
|
|
173
|
+
if (rank) relevance += rank((value) => strings(value).some((v) => collate(v).includes(row.needle)), record, text, inCountry, language)
|
|
171
174
|
}
|
|
172
175
|
if (result === null || result) best = Math.max(best ?? 0, relevance)
|
|
173
176
|
}
|
|
@@ -45,13 +45,20 @@ export function usePagination(list, { page = 1, size = DEFAULT_SIZE } = {}) {
|
|
|
45
45
|
/**
|
|
46
46
|
* Chronological order by `key` (a record's `start_date`), undated records
|
|
47
47
|
* last — or first, with `undated: 'first'`. Stable: two records of the same
|
|
48
|
-
* year keep the order they arrived in
|
|
48
|
+
* year keep the order they arrived in, or the order `tiebreak(a, b)` gives
|
|
49
|
+
* them (a family's own, e.g. the galleries' `legacyOrder`). Returns a new
|
|
50
|
+
* array.
|
|
49
51
|
*/
|
|
50
|
-
export function sortChronological(list, { key = 'start_date', undated = 'last' } = {}) {
|
|
52
|
+
export function sortChronological(list, { key = 'start_date', undated = 'last', tiebreak = null } = {}) {
|
|
51
53
|
const missing = undated === 'first' ? -Infinity : Infinity
|
|
52
54
|
const year = (record) => {
|
|
53
55
|
const value = record?.[key]
|
|
54
56
|
return typeof value === 'number' && Number.isFinite(value) ? value : missing
|
|
55
57
|
}
|
|
56
|
-
|
|
58
|
+
const byYear = (a, b) => {
|
|
59
|
+
const ya = year(a)
|
|
60
|
+
const yb = year(b)
|
|
61
|
+
return ya === yb ? 0 : ya - yb
|
|
62
|
+
}
|
|
63
|
+
return [...(list ?? [])].sort((a, b) => byYear(a, b) || (tiebreak ? tiebreak(a, b) : 0))
|
|
57
64
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Legacy's galleries wrote an era "A.D." and "B.C." (dxa-client: the timeline
|
|
2
|
+
// and collection year lists, the item sheet, the timeline results), where
|
|
3
|
+
// every other website writes "AD" and "BC". A gallery reads its own words
|
|
4
|
+
// for them (`withEraWords`, inventory-app#2423).
|
|
5
|
+
export const GALLERY_ERA_WORDS = {
|
|
6
|
+
'catalogue.era.ad': 'gallery.era.ad',
|
|
7
|
+
'catalogue.era.bc': 'gallery.era.bc',
|
|
8
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { projectLabel } from '../../conventions.js'
|
|
2
2
|
import { DATE_MODE, FACET_CATEGORIES, PAGE_SIZE } from '../shared.js'
|
|
3
3
|
|
|
4
|
-
export { DATE_MODE, FACET_CATEGORIES, FACET_LABEL_KEYS, PAGE_SIZE, useFacetLabels } from '../shared.js'
|
|
4
|
+
export { DATE_MODE, FACET_CATEGORIES, FACET_LABEL_KEYS, GALLERY_PAGE_SIZE, PAGE_SIZE, useFacetLabels } from '../shared.js'
|
|
5
5
|
|
|
6
6
|
// The catalogue spec: what a DXA gallery's lists filter and search on. The
|
|
7
7
|
// engine — query state, options, dates, pages, the boolean grammar — is
|
|
@@ -97,25 +97,41 @@ export function useGalleryCollection(data, config = {}) {
|
|
|
97
97
|
/**
|
|
98
98
|
* A record as viewer-layout's grid contract: the thumbnail, the name, the
|
|
99
99
|
* lines legacy's hover card carried (date, holder, place, source
|
|
100
|
-
* project).
|
|
100
|
+
* project). The same lines one by one (`date`, `partner`, `place`,
|
|
101
|
+
* `project`), for a card that sets each its own way, as legacy's
|
|
102
|
+
* ObjectGrid did.
|
|
101
103
|
*/
|
|
102
104
|
function tile(item, t) {
|
|
103
105
|
const text = tr('items', item.id, defaultLang)
|
|
106
|
+
const date = text.dates ?? ''
|
|
107
|
+
const partner = labelOf('partners', item.partner_id)
|
|
108
|
+
const place = [text.location, labelOf('countries', item.country_id)].filter(Boolean).join(', ')
|
|
109
|
+
const project = projectLabel(manifest, item.project_id, defaultLang) ?? ''
|
|
104
110
|
return {
|
|
105
111
|
id: item.id,
|
|
106
112
|
image: item.images?.[0]?.url ?? '',
|
|
107
113
|
imageAlt: labelOf('items', item.id),
|
|
108
114
|
name: mdInline(text.name ?? item.internal_name ?? ''),
|
|
109
|
-
meta: [
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
].filter(Boolean),
|
|
115
|
+
meta: [date, partner, place, `${t('catalogue.results.forProject')} ${project}`].filter(Boolean),
|
|
116
|
+
date,
|
|
117
|
+
partner,
|
|
118
|
+
place,
|
|
119
|
+
project,
|
|
115
120
|
to: itemRoute(item),
|
|
116
121
|
}
|
|
117
122
|
}
|
|
118
123
|
|
|
124
|
+
// Legacy's order among records of one year (a gallery page's
|
|
125
|
+
// `sort.tiebreak`, viewer-layout's dxa/gallery pages):
|
|
126
|
+
// dxa-api's `ORDER BY startDate, countryName, partnerName`, then MySQL's
|
|
127
|
+
// own order, which is the table's key: the record's legacy key, its
|
|
128
|
+
// numbers compared as numbers (inventory-app#2423).
|
|
129
|
+
function legacyOrder(a, b) {
|
|
130
|
+
return compareText(labelOf('countries', a.country_id), labelOf('countries', b.country_id))
|
|
131
|
+
|| compareText(labelOf('partners', a.partner_id), labelOf('partners', b.partner_id))
|
|
132
|
+
|| compareKeys(a.backward_compatibility, b.backward_compatibility)
|
|
133
|
+
}
|
|
134
|
+
|
|
119
135
|
// ── The results page, as a spec ──────────────────────────────────────
|
|
120
136
|
//
|
|
121
137
|
// What viewer-layout's `CatalogueResultsView` renders on
|
|
@@ -166,7 +182,26 @@ export function useGalleryCollection(data, config = {}) {
|
|
|
166
182
|
}
|
|
167
183
|
|
|
168
184
|
return {
|
|
169
|
-
FACETS, hasEveryTag, haystack, tile, collectionResults,
|
|
185
|
+
FACETS, hasEveryTag, haystack, tile, legacyOrder, collectionResults,
|
|
170
186
|
countryIdForCode, tagIdForLegacy, tagLabelForLegacy,
|
|
171
187
|
}
|
|
172
188
|
}
|
|
189
|
+
|
|
190
|
+
function compareText(a, b) {
|
|
191
|
+
return String(a ?? '').localeCompare(String(b ?? ''), 'en', { sensitivity: 'base' })
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Two legacy keys (`mwnf3:objects:ISL:dz:Mus01:24`), segment by segment, a
|
|
195
|
+
// number against a number as numbers.
|
|
196
|
+
function compareKeys(a, b) {
|
|
197
|
+
const left = String(a ?? '').split(':')
|
|
198
|
+
const right = String(b ?? '').split(':')
|
|
199
|
+
for (let i = 0; i < Math.max(left.length, right.length); i++) {
|
|
200
|
+
const x = left[i] ?? ''
|
|
201
|
+
const y = right[i] ?? ''
|
|
202
|
+
if (x === y) continue
|
|
203
|
+
if (/^\d+$/.test(x) && /^\d+$/.test(y)) return Number(x) - Number(y)
|
|
204
|
+
return x.localeCompare(y, 'en')
|
|
205
|
+
}
|
|
206
|
+
return 0
|
|
207
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { eraLabel, roundOutward } from '../../catalogue/dates.js'
|
|
1
|
+
import { eraLabel, roundOutward, withEraWords } from '../../catalogue/dates.js'
|
|
2
|
+
import { GALLERY_ERA_WORDS } from './eraWords.js'
|
|
2
3
|
import { useSiteConfig } from '../../siteConfig.js'
|
|
3
4
|
import { useGallerySheet } from './useGallerySheet.js'
|
|
4
5
|
|
|
@@ -72,7 +73,7 @@ export function useGalleryItemDetail(data, timeline) {
|
|
|
72
73
|
},
|
|
73
74
|
events: (country) => findEvents({ country, begin: from, end: to }),
|
|
74
75
|
range: [from, to],
|
|
75
|
-
era: (year) => eraLabel(year, ctx.t),
|
|
76
|
+
era: (year) => eraLabel(year, withEraWords(ctx.t, GALLERY_ERA_WORDS)),
|
|
76
77
|
searchTo: (country, [begin, end]) => ({ name: 'timeline-results', query: { country, begin, end } }),
|
|
77
78
|
}
|
|
78
79
|
}
|
|
@@ -59,7 +59,7 @@ export function useGalleryPartner(data, collection, config = {}) {
|
|
|
59
59
|
// results and the timeline gallery use. `scope` is built per-page in the
|
|
60
60
|
// site's own PartnerObjects.vue, over the route's own id —
|
|
61
61
|
// `CatalogueResultsView` takes no record id of its own, unlike
|
|
62
|
-
// `RecordView`.
|
|
62
|
+
// `RecordView`. Legacy's page (PartnerObjects.vue) printed no count.
|
|
63
63
|
const partnerObjects = {
|
|
64
64
|
entity: 'items',
|
|
65
65
|
sort: { undated: 'first' },
|
|
@@ -70,7 +70,7 @@ export function useGalleryPartner(data, collection, config = {}) {
|
|
|
70
70
|
empty: 'gallery.partner.noObjects',
|
|
71
71
|
pagination: { jump: true },
|
|
72
72
|
record: (item, { t }) => tile(item, t),
|
|
73
|
-
summary: (
|
|
73
|
+
summary: () => [],
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
return { partnerList, partnerSheet, partnerObjects }
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { dateRange, eraLabel, roundOutward, yearBucketsFromRange } from '../../catalogue/dates.js'
|
|
1
|
+
import { dateRange, eraLabel, roundOutward, withEraWords, yearBucketsFromRange } from '../../catalogue/dates.js'
|
|
2
|
+
import { GALLERY_ERA_WORDS } from './eraWords.js'
|
|
2
3
|
import { useTimelineEvents } from '../../catalogue/timeline.js'
|
|
3
4
|
import { PAGE_SIZE } from '../shared.js'
|
|
4
5
|
|
|
@@ -84,9 +85,10 @@ export function useGalleryTimeline(data, collection, config = {}) {
|
|
|
84
85
|
route: 'timeline-results',
|
|
85
86
|
controls: [
|
|
86
87
|
{ key: 'country' },
|
|
87
|
-
{ key: 'begin', options: (ctx) => yearBucketsFromRange(ctx.years.min, ctx.years.max, ctx.t) },
|
|
88
|
-
{ key: 'end', options: (ctx) => yearBucketsFromRange(ctx.years.min, ctx.years.max, ctx.t) },
|
|
88
|
+
{ key: 'begin', options: (ctx) => yearBucketsFromRange(ctx.years.min, ctx.years.max, withEraWords(ctx.t, GALLERY_ERA_WORDS)) },
|
|
89
|
+
{ key: 'end', options: (ctx) => yearBucketsFromRange(ctx.years.min, ctx.years.max, withEraWords(ctx.t, GALLERY_ERA_WORDS)) },
|
|
89
90
|
],
|
|
91
|
+
eraWords: GALLERY_ERA_WORDS,
|
|
90
92
|
gallery: {
|
|
91
93
|
route: 'timeline-gallery',
|
|
92
94
|
items: (ctx) => timelineGalleryItems(ctx.filters),
|
|
@@ -127,8 +129,8 @@ export function useGalleryTimeline(data, collection, config = {}) {
|
|
|
127
129
|
value: countryId ? labelOf('countries', countryId) : t('timeline.form.allCountries'),
|
|
128
130
|
}]
|
|
129
131
|
if (begin != null || end != null) {
|
|
130
|
-
const from = begin != null ? eraLabel(begin, t) : t('timeline.form.earliest')
|
|
131
|
-
const to = end != null ? eraLabel(end, t) : t('timeline.form.latest')
|
|
132
|
+
const from = begin != null ? eraLabel(begin, withEraWords(t, GALLERY_ERA_WORDS)) : t('timeline.form.earliest')
|
|
133
|
+
const to = end != null ? eraLabel(end, withEraWords(t, GALLERY_ERA_WORDS)) : t('timeline.form.latest')
|
|
132
134
|
parts.push({ value: `${from} ${t('timeline.form.to')} ${to}` })
|
|
133
135
|
}
|
|
134
136
|
parts.push({ count: pageInfo.total, value: t('catalogue.results.objects') })
|
package/src/dxa/index.js
CHANGED
|
@@ -19,11 +19,12 @@
|
|
|
19
19
|
// const sheet = useGallerySheet(data)
|
|
20
20
|
|
|
21
21
|
// Shared verbatim by both families — see shared.js.
|
|
22
|
-
export { DATE_MODE, FACET_CATEGORIES, FACET_LABEL_KEYS, PAGE_SIZE, useFacetLabels } from './shared.js'
|
|
22
|
+
export { DATE_MODE, FACET_CATEGORIES, FACET_LABEL_KEYS, GALLERY_PAGE_SIZE, PAGE_SIZE, useFacetLabels } from './shared.js'
|
|
23
23
|
|
|
24
24
|
export { useGalleryData } from './gallery/useGalleryData.js'
|
|
25
25
|
export { useGalleryCollection } from './gallery/useGalleryCollection.js'
|
|
26
26
|
export { useGalleryTimeline } from './gallery/useGalleryTimeline.js'
|
|
27
|
+
export { GALLERY_ERA_WORDS } from './gallery/eraWords.js'
|
|
27
28
|
export { useGalleryPartner } from './gallery/useGalleryPartner.js'
|
|
28
29
|
export { useGallerySheet } from './gallery/useGallerySheet.js'
|
|
29
30
|
export { useGalleryItemDetail } from './gallery/useGalleryItemDetail.js'
|
package/src/dxa/shared.js
CHANGED
|
@@ -10,6 +10,13 @@ import { useI18n } from '../i18n/index.js'
|
|
|
10
10
|
/** Nine tiles a page, as legacy's grids showed. */
|
|
11
11
|
export const PAGE_SIZE = 9
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* A gallery's pages show twenty tiles: dxa-api's own page size
|
|
15
|
+
* (`API_PAGESIZE`), which the gallery client never overrode — measured on
|
|
16
|
+
* Ceramics' collection results, 20 tiles a page (inventory-app#2423).
|
|
17
|
+
*/
|
|
18
|
+
export const GALLERY_PAGE_SIZE = 20
|
|
19
|
+
|
|
13
20
|
/**
|
|
14
21
|
* Decision D5: the DXA sites test containment — `na <= start_date` and
|
|
15
22
|
* `nz >= coalesce(end_date, start_date)` in `Objects.blade.php` — so an
|
package/src/i18n/I18nText.vue
CHANGED
|
@@ -3,13 +3,16 @@ import { computed } from 'vue'
|
|
|
3
3
|
import { useI18n } from './index.js'
|
|
4
4
|
import { renderBlock } from './markdown.js'
|
|
5
5
|
|
|
6
|
+
// `blankLines`: each blank line between two blocks a `<br>` (renderBlock),
|
|
7
|
+
// for a page that sets legacy's line spacing.
|
|
6
8
|
const props = defineProps({
|
|
7
9
|
keypath: { type: String, required: true },
|
|
8
10
|
tag: { type: String, default: 'div' },
|
|
11
|
+
blankLines: { type: Boolean, default: false },
|
|
9
12
|
})
|
|
10
13
|
|
|
11
14
|
const { t } = useI18n()
|
|
12
|
-
const html = computed(() => renderBlock(t(props.keypath)))
|
|
15
|
+
const html = computed(() => renderBlock(t(props.keypath), { blankLines: props.blankLines }))
|
|
13
16
|
</script>
|
|
14
17
|
|
|
15
18
|
<template>
|
package/src/i18n/markdown.js
CHANGED
|
@@ -61,7 +61,30 @@ const record = new Marked({ async: false, gfm: true, breaks: true })
|
|
|
61
61
|
prose.use(escaping, noCode)
|
|
62
62
|
record.use(escaping, noCode)
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
// `blankLines` keeps the number of blank lines between two blocks. Legacy's
|
|
65
|
+
// text pages were HTML, a blank line a `<br>`, and some put two blank lines
|
|
66
|
+
// where others put one; Markdown reads any run of blank lines as one
|
|
67
|
+
// paragraph break. With it, each blank line of the source is a `<br>`
|
|
68
|
+
// between the blocks, and the page sets its paragraphs without margins (a
|
|
69
|
+
// gallery's Credits, inventory-app#2423). A run of N blank lines is one
|
|
70
|
+
// `space` token holding N + 1 newlines.
|
|
71
|
+
const blankLineSpacing = {
|
|
72
|
+
renderer: {
|
|
73
|
+
space(token) {
|
|
74
|
+
const newlines = String(token?.raw ?? '').split('\n').length - 1
|
|
75
|
+
return '<br>'.repeat(Math.max(0, newlines - 1))
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
const proseLines = new Marked({ async: false, gfm: true, breaks: false })
|
|
80
|
+
const recordLines = new Marked({ async: false, gfm: true, breaks: true })
|
|
81
|
+
proseLines.use(escaping, noCode, blankLineSpacing)
|
|
82
|
+
recordLines.use(escaping, noCode, blankLineSpacing)
|
|
83
|
+
|
|
84
|
+
const pipeline = (breaks, blankLines = false) => {
|
|
85
|
+
if (blankLines) return breaks ? recordLines : proseLines
|
|
86
|
+
return breaks ? record : prose
|
|
87
|
+
}
|
|
65
88
|
|
|
66
89
|
const REGEXP_SPECIAL = /[.*+?^${}()|[\]\\]/g
|
|
67
90
|
function escapeRegExp(text) {
|
|
@@ -153,8 +176,10 @@ function withGlossary(breaks, glossary) {
|
|
|
153
176
|
return entry ? (breaks ? entry.record : entry.prose) : pipeline(breaks)
|
|
154
177
|
}
|
|
155
178
|
|
|
156
|
-
|
|
157
|
-
|
|
179
|
+
// `blankLines` is for a page's own text, which has no glossary; a glossary
|
|
180
|
+
// wins over it.
|
|
181
|
+
function engineFor(breaks, glossary, blankLines = false) {
|
|
182
|
+
return glossary && glossary.length > 0 ? withGlossary(breaks, glossary) : pipeline(breaks, blankLines)
|
|
158
183
|
}
|
|
159
184
|
|
|
160
185
|
// The same key as the render pipelines, for the same reason: a glossary list
|
|
@@ -195,9 +220,12 @@ export function glossaryIdsIn(text, glossary) {
|
|
|
195
220
|
return [...found]
|
|
196
221
|
}
|
|
197
222
|
|
|
198
|
-
/**
|
|
199
|
-
|
|
200
|
-
|
|
223
|
+
/**
|
|
224
|
+
* A text as one or more paragraphs. `glossary`: `[{ id, spelling }]` to
|
|
225
|
+
* highlight. `blankLines`: each blank line between two blocks a `<br>`.
|
|
226
|
+
*/
|
|
227
|
+
export function renderBlock(text, { breaks = false, glossary, blankLines = false } = {}) {
|
|
228
|
+
return engineFor(breaks, glossary, blankLines).parse(String(text ?? ''))
|
|
201
229
|
}
|
|
202
230
|
|
|
203
231
|
/** A text without the surrounding paragraph, for a heading or a table cell. */
|
package/src/index.js
CHANGED
|
@@ -26,7 +26,7 @@ export { useListQuery } from './catalogue/listQuery.js'
|
|
|
26
26
|
export { paginate, sortChronological, usePagination } from './catalogue/pagination.js'
|
|
27
27
|
export { facetOptions, useFacets } from './catalogue/facets.js'
|
|
28
28
|
export {
|
|
29
|
-
centuryPresets, dateRange, eraLabel, inDateRange, roundOutward, yearBuckets, yearBucketsFromRange,
|
|
29
|
+
centuryPresets, dateRange, eraLabel, inDateRange, roundOutward, withEraWords, yearBuckets, yearBucketsFromRange,
|
|
30
30
|
} from './catalogue/dates.js'
|
|
31
31
|
export { useSearchLanguage } from './catalogue/searchLanguage.js'
|
|
32
32
|
export {
|
|
@@ -54,6 +54,9 @@ export function defineViewerConfig({
|
|
|
54
54
|
test: {
|
|
55
55
|
environment: 'jsdom',
|
|
56
56
|
testTimeout: 60000,
|
|
57
|
+
// Node 26 has a `localStorage` of its own, which hides jsdom's from the
|
|
58
|
+
// tests: without a storage file it is undefined.
|
|
59
|
+
execArgv: ['--no-experimental-webstorage'],
|
|
57
60
|
server: {
|
|
58
61
|
deps: {
|
|
59
62
|
// viewer-core ships .vue source; Node cannot load it unless Vitest
|