@museumwnf/viewer-core 1.14.0 → 1.15.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.
Files changed (43) hide show
  1. package/LICENSE.md +44 -44
  2. package/README.md +658 -635
  3. package/package.json +50 -50
  4. package/src/AppRoot.vue +58 -58
  5. package/src/catalogue/dates.js +209 -209
  6. package/src/catalogue/facets.js +61 -61
  7. package/src/catalogue/keywordIndex.js +289 -289
  8. package/src/catalogue/listQuery.js +100 -100
  9. package/src/catalogue/pagination.js +57 -57
  10. package/src/catalogue/searchLanguage.js +32 -32
  11. package/src/catalogue/timeline.js +220 -220
  12. package/src/composables/useCatalogueData.js +141 -141
  13. package/src/composables/useDataPackage.js +98 -98
  14. package/src/composables/useEntities.js +100 -100
  15. package/src/composables/useRecordLanguage.js +71 -71
  16. package/src/conventions.js +252 -148
  17. package/src/createViewer.js +29 -29
  18. package/src/i18n/I18nText.vue +17 -17
  19. package/src/i18n/I18nTextInline.vue +17 -17
  20. package/src/i18n/index.js +88 -88
  21. package/src/i18n/language.js +127 -127
  22. package/src/i18n/markdown.js +265 -265
  23. package/src/index.js +51 -50
  24. package/src/injectionKeys.js +1 -1
  25. package/src/languages.js +49 -49
  26. package/src/legacy/index.js +58 -58
  27. package/src/record/collectionTree.js +307 -307
  28. package/src/record/derivations.js +154 -154
  29. package/src/record/partners.js +87 -87
  30. package/src/record/recordSheet.js +231 -231
  31. package/src/rights.js +53 -53
  32. package/src/router/index.js +138 -138
  33. package/src/siteConfig.js +37 -37
  34. package/src/styles/base.css +49 -49
  35. package/src/testing/checkOfferedLanguages.js +67 -67
  36. package/src/testing/index.js +3 -3
  37. package/src/testing/smoke.js +103 -103
  38. package/src/testing/viteConfig.js +90 -90
  39. package/src/views/DetailView.vue +53 -53
  40. package/src/views/HomeView.vue +20 -20
  41. package/src/views/ListView.vue +61 -61
  42. package/src/views/NotFoundView.vue +8 -8
  43. package/src/vite.js +8 -8
@@ -1,265 +1,265 @@
1
- import { Marked } from 'marked'
2
-
3
- // One Markdown pipeline for every text on every website, configured here and
4
- // nowhere else. Its own instance rather than the shared `marked` singleton, so
5
- // configuring it cannot change how a website renders anything else.
6
- //
7
- // Raw HTML is escaped rather than rendered. Texts are written by translators
8
- // through the GitHub web interface, and the rules they are given say Markdown
9
- // only; escaping is what makes that a guarantee instead of a request.
10
- //
11
- // This is the only place that guarantee lives. viewer-i18n's checker used to
12
- // reject HTML too, which added nothing here and was wrong about autolinks and
13
- // code spans; it no longer looks. Anything relaxed in this file is relaxed
14
- // everywhere, with nothing behind it.
15
-
16
- const ESCAPES = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }
17
-
18
- function escapeHtml(text) {
19
- return String(text).replace(/[&<>"']/g, (character) => ESCAPES[character])
20
- }
21
-
22
- // Raw HTML is escaped in both, because both render text that came from
23
- // somewhere else: a translator's file, or a museum record carried in from the
24
- // legacy system. Neither is a place markup may be authored.
25
- const escaping = {
26
- renderer: {
27
- html(token) {
28
- return escapeHtml(token?.raw ?? token?.text ?? token ?? '')
29
- },
30
- },
31
- }
32
-
33
- // `breaks` is the one difference, and it is a difference in the source rather
34
- // than in policy. A text in the dictionary is written as prose and wraps freely,
35
- // so a single newline means nothing. A record's field arrives from a database
36
- // where a newline was typed to be a line break, and dropping it runs the lines
37
- // together.
38
- const prose = new Marked({ async: false, gfm: true, breaks: false })
39
- const record = new Marked({ async: false, gfm: true, breaks: true })
40
- prose.use(escaping)
41
- record.use(escaping)
42
-
43
- const pipeline = (breaks) => (breaks ? record : prose)
44
-
45
- const REGEXP_SPECIAL = /[.*+?^${}()|[\]\\]/g
46
- function escapeRegExp(text) {
47
- return text.replace(REGEXP_SPECIAL, '\\$&')
48
- }
49
-
50
- // A word boundary that also works for a spelling starting or ending in an
51
- // accented letter — JS's own `\b` is `\w`-based (`[A-Za-z0-9_]`), so it does
52
- // not see "é" as a word character and misses the boundary right before one.
53
- const NOT_WORD = '[\\p{L}\\p{N}_]'
54
-
55
- /**
56
- * A marked inline extension: highlights every occurrence of a glossary
57
- * spelling as `<span class="gloss-term" data-gid="…">`, as a token the
58
- * parser produces rather than HTML smuggled through the source text — the
59
- * escaping above still applies to everything else. One instance per
60
- * distinct glossary (see the cache below); built once, matched against
61
- * every text rendered with that glossary.
62
- */
63
- function spellingIndex(glossary) {
64
- const bySpelling = new Map()
65
- for (const { id, spelling } of glossary) {
66
- const trimmed = spelling?.trim()
67
- if (!trimmed) continue
68
- bySpelling.set(trimmed.toLowerCase(), id)
69
- }
70
-
71
- // Longest spelling first, so "kufic script" wins over "kufic" when a text
72
- // contains both.
73
- const spellings = [...bySpelling.keys()].sort((a, b) => b.length - a.length)
74
- if (spellings.length === 0) return null
75
- return { bySpelling, alternation: spellings.map(escapeRegExp).join('|') }
76
- }
77
-
78
- function glossaryExtension(glossary) {
79
- const index = spellingIndex(glossary)
80
- if (!index) return null
81
- const { bySpelling, alternation } = index
82
- const findAnywhere = new RegExp(`(?<!${NOT_WORD})(?:${alternation})(?!${NOT_WORD})`, 'iu')
83
- const matchAtStart = new RegExp(`^(?:${alternation})(?!${NOT_WORD})`, 'iu')
84
-
85
- return {
86
- name: 'glossaryTerm',
87
- level: 'inline',
88
- start(src) {
89
- return findAnywhere.exec(src)?.index
90
- },
91
- tokenizer(src) {
92
- const match = matchAtStart.exec(src)
93
- if (!match) return undefined
94
- return {
95
- type: 'glossaryTerm',
96
- raw: match[0],
97
- text: match[0],
98
- id: bySpelling.get(match[0].toLowerCase()),
99
- }
100
- },
101
- renderer(token) {
102
- return `<span class="gloss-term" data-gid="${escapeHtml(token.id)}">${escapeHtml(token.text)}</span>`
103
- },
104
- }
105
- }
106
-
107
- // Keyed on the spelling list, not on which text is being rendered — every
108
- // field of one record shares the same glossary, and re-parsing that list
109
- // into a regex and a Marked instance for each field would be wasted work.
110
- const glossaryPipelines = new Map()
111
-
112
- function glossaryKey(glossary) {
113
- return JSON.stringify([...glossary].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)))
114
- }
115
-
116
- function withGlossary(breaks, glossary) {
117
- const key = glossaryKey(glossary)
118
- let entry = glossaryPipelines.get(key)
119
- if (entry === undefined) {
120
- const extension = glossaryExtension(glossary)
121
- if (!extension) {
122
- entry = null // no usable spelling in this list — fall back below
123
- } else {
124
- const withProse = new Marked({ async: false, gfm: true, breaks: false })
125
- const withRecord = new Marked({ async: false, gfm: true, breaks: true })
126
- withProse.use(escaping, { extensions: [extension] })
127
- withRecord.use(escaping, { extensions: [extension] })
128
- entry = { prose: withProse, record: withRecord }
129
- }
130
- glossaryPipelines.set(key, entry)
131
- }
132
- return entry ? (breaks ? entry.record : entry.prose) : pipeline(breaks)
133
- }
134
-
135
- function engineFor(breaks, glossary) {
136
- return glossary && glossary.length > 0 ? withGlossary(breaks, glossary) : pipeline(breaks)
137
- }
138
-
139
- // The same key as the render pipelines, for the same reason: a glossary list
140
- // is rebuilt into a regex once, however many texts get scanned against it.
141
- const glossaryMatchers = new Map()
142
-
143
- function matcherFor(glossary) {
144
- const key = glossaryKey(glossary)
145
- let matcher = glossaryMatchers.get(key)
146
- if (matcher === undefined) {
147
- const index = spellingIndex(glossary)
148
- matcher = index
149
- ? { bySpelling: index.bySpelling, regex: new RegExp(`(?<!${NOT_WORD})(?:${index.alternation})(?!${NOT_WORD})`, 'giu') }
150
- : null
151
- glossaryMatchers.set(key, matcher)
152
- }
153
- return matcher
154
- }
155
-
156
- /**
157
- * The ids of `glossary` (`[{ id, spelling }]`) whose spelling occurs in
158
- * `text`, matched the same word-bounded, case-insensitive way
159
- * `renderBlock`/`renderInline` highlight them — a text scanned this way and
160
- * the same text rendered with the same list agree on what counts as a hit.
161
- * The free-text counterpart to a record's own `glossary_ids`: a dynasty
162
- * history or a theme essay has no such column to read, so this scans the
163
- * whole glossary against the text instead. One id per term, in no
164
- * particular order.
165
- */
166
- export function glossaryIdsIn(text, glossary) {
167
- if (!glossary || glossary.length === 0) return []
168
- const matcher = matcherFor(glossary)
169
- if (!matcher) return []
170
- const found = new Set()
171
- for (const match of String(text ?? '').matchAll(matcher.regex)) {
172
- found.add(matcher.bySpelling.get(match[0].toLowerCase()))
173
- }
174
- return [...found]
175
- }
176
-
177
- /** A text as one or more paragraphs. `glossary`: `[{ id, spelling }]` to highlight. */
178
- export function renderBlock(text, { breaks = false, glossary } = {}) {
179
- return engineFor(breaks, glossary).parse(String(text ?? ''))
180
- }
181
-
182
- /** A text without the surrounding paragraph, for a heading or a table cell. */
183
- export function renderInline(text, { breaks = false, glossary } = {}) {
184
- return engineFor(breaks, glossary).parseInline(String(text ?? ''))
185
- }
186
-
187
- // Plain text is read off the token tree the same parser produces, never by
188
- // rewriting the source with a regex: what a Markdown text says is what its
189
- // tokens say. Raw HTML is dropped, since it was never content; an image is
190
- // its alt text; a break is a space.
191
- const BLOCKS = new Set(['paragraph', 'heading', 'blockquote', 'list', 'list_item', 'code', 'table', 'hr', 'html'])
192
-
193
- const ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ' }
194
- function decodeEntities(text) {
195
- return text.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (match, entity) => {
196
- if (entity[0] === '#') {
197
- const code =
198
- entity[1].toLowerCase() === 'x' ? parseInt(entity.slice(2), 16) : parseInt(entity.slice(1), 10)
199
- return Number.isFinite(code) ? String.fromCodePoint(code) : match
200
- }
201
- return ENTITIES[entity.toLowerCase()] ?? match
202
- })
203
- }
204
-
205
- function plainText(tokens) {
206
- let out = ''
207
- for (const token of tokens ?? []) {
208
- switch (token.type) {
209
- case 'html':
210
- break
211
- case 'image':
212
- out += token.text ?? ''
213
- break
214
- case 'br':
215
- case 'space':
216
- out += ' '
217
- break
218
- case 'list':
219
- out += plainText(token.items)
220
- break
221
- case 'table':
222
- out += plainText(token.header)
223
- for (const row of token.rows ?? []) out += plainText(row)
224
- break
225
- default:
226
- if (token.tokens?.length) out += plainText(token.tokens)
227
- else out += token.text ?? ''
228
- }
229
- if (BLOCKS.has(token.type)) out += ' '
230
- }
231
- return out
232
- }
233
-
234
- /** A text as plain text — for an `alt`, a `title`, an option label, a sort key. */
235
- export function renderPlain(text) {
236
- return decodeEntities(plainText(record.lexer(String(text ?? ''))))
237
- .replace(/\s+/g, ' ')
238
- .trim()
239
- }
240
-
241
- // `md`/`mdInline`/`mdStrip` are `renderBlock`/`renderInline`/`renderPlain`
242
- // under the one signature and the one convention every site wrote for
243
- // itself: a missing text is `''`, not an empty paragraph or "undefined", and
244
- // a record's own line breaks are kept unless a caller says otherwise — a
245
- // data-package field is typed with breaks on purpose, the way a dictionary
246
- // entry is not. `renderBlock`/`renderInline`/`renderPlain` stay, for a
247
- // caller that wants the pipeline's own default (`breaks: false`) instead.
248
-
249
- /** `renderBlock`, record convention: `''` for a missing text, line breaks kept by default. */
250
- export function md(text, { glossary, breaks = true } = {}) {
251
- if (!text) return ''
252
- return renderBlock(text, { breaks, glossary })
253
- }
254
-
255
- /** `renderInline`, the same convention, for a heading or a table cell. */
256
- export function mdInline(text, { glossary } = {}) {
257
- if (!text) return ''
258
- return renderInline(text, { glossary })
259
- }
260
-
261
- /** `renderPlain`, the same convention, for an `alt`, a `title`, a sort key. */
262
- export function mdStrip(text) {
263
- if (!text) return ''
264
- return renderPlain(text)
265
- }
1
+ import { Marked } from 'marked'
2
+
3
+ // One Markdown pipeline for every text on every website, configured here and
4
+ // nowhere else. Its own instance rather than the shared `marked` singleton, so
5
+ // configuring it cannot change how a website renders anything else.
6
+ //
7
+ // Raw HTML is escaped rather than rendered. Texts are written by translators
8
+ // through the GitHub web interface, and the rules they are given say Markdown
9
+ // only; escaping is what makes that a guarantee instead of a request.
10
+ //
11
+ // This is the only place that guarantee lives. viewer-i18n's checker used to
12
+ // reject HTML too, which added nothing here and was wrong about autolinks and
13
+ // code spans; it no longer looks. Anything relaxed in this file is relaxed
14
+ // everywhere, with nothing behind it.
15
+
16
+ const ESCAPES = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }
17
+
18
+ function escapeHtml(text) {
19
+ return String(text).replace(/[&<>"']/g, (character) => ESCAPES[character])
20
+ }
21
+
22
+ // Raw HTML is escaped in both, because both render text that came from
23
+ // somewhere else: a translator's file, or a museum record carried in from the
24
+ // legacy system. Neither is a place markup may be authored.
25
+ const escaping = {
26
+ renderer: {
27
+ html(token) {
28
+ return escapeHtml(token?.raw ?? token?.text ?? token ?? '')
29
+ },
30
+ },
31
+ }
32
+
33
+ // `breaks` is the one difference, and it is a difference in the source rather
34
+ // than in policy. A text in the dictionary is written as prose and wraps freely,
35
+ // so a single newline means nothing. A record's field arrives from a database
36
+ // where a newline was typed to be a line break, and dropping it runs the lines
37
+ // together.
38
+ const prose = new Marked({ async: false, gfm: true, breaks: false })
39
+ const record = new Marked({ async: false, gfm: true, breaks: true })
40
+ prose.use(escaping)
41
+ record.use(escaping)
42
+
43
+ const pipeline = (breaks) => (breaks ? record : prose)
44
+
45
+ const REGEXP_SPECIAL = /[.*+?^${}()|[\]\\]/g
46
+ function escapeRegExp(text) {
47
+ return text.replace(REGEXP_SPECIAL, '\\$&')
48
+ }
49
+
50
+ // A word boundary that also works for a spelling starting or ending in an
51
+ // accented letter — JS's own `\b` is `\w`-based (`[A-Za-z0-9_]`), so it does
52
+ // not see "é" as a word character and misses the boundary right before one.
53
+ const NOT_WORD = '[\\p{L}\\p{N}_]'
54
+
55
+ /**
56
+ * A marked inline extension: highlights every occurrence of a glossary
57
+ * spelling as `<span class="gloss-term" data-gid="…">`, as a token the
58
+ * parser produces rather than HTML smuggled through the source text — the
59
+ * escaping above still applies to everything else. One instance per
60
+ * distinct glossary (see the cache below); built once, matched against
61
+ * every text rendered with that glossary.
62
+ */
63
+ function spellingIndex(glossary) {
64
+ const bySpelling = new Map()
65
+ for (const { id, spelling } of glossary) {
66
+ const trimmed = spelling?.trim()
67
+ if (!trimmed) continue
68
+ bySpelling.set(trimmed.toLowerCase(), id)
69
+ }
70
+
71
+ // Longest spelling first, so "kufic script" wins over "kufic" when a text
72
+ // contains both.
73
+ const spellings = [...bySpelling.keys()].sort((a, b) => b.length - a.length)
74
+ if (spellings.length === 0) return null
75
+ return { bySpelling, alternation: spellings.map(escapeRegExp).join('|') }
76
+ }
77
+
78
+ function glossaryExtension(glossary) {
79
+ const index = spellingIndex(glossary)
80
+ if (!index) return null
81
+ const { bySpelling, alternation } = index
82
+ const findAnywhere = new RegExp(`(?<!${NOT_WORD})(?:${alternation})(?!${NOT_WORD})`, 'iu')
83
+ const matchAtStart = new RegExp(`^(?:${alternation})(?!${NOT_WORD})`, 'iu')
84
+
85
+ return {
86
+ name: 'glossaryTerm',
87
+ level: 'inline',
88
+ start(src) {
89
+ return findAnywhere.exec(src)?.index
90
+ },
91
+ tokenizer(src) {
92
+ const match = matchAtStart.exec(src)
93
+ if (!match) return undefined
94
+ return {
95
+ type: 'glossaryTerm',
96
+ raw: match[0],
97
+ text: match[0],
98
+ id: bySpelling.get(match[0].toLowerCase()),
99
+ }
100
+ },
101
+ renderer(token) {
102
+ return `<span class="gloss-term" data-gid="${escapeHtml(token.id)}">${escapeHtml(token.text)}</span>`
103
+ },
104
+ }
105
+ }
106
+
107
+ // Keyed on the spelling list, not on which text is being rendered — every
108
+ // field of one record shares the same glossary, and re-parsing that list
109
+ // into a regex and a Marked instance for each field would be wasted work.
110
+ const glossaryPipelines = new Map()
111
+
112
+ function glossaryKey(glossary) {
113
+ return JSON.stringify([...glossary].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)))
114
+ }
115
+
116
+ function withGlossary(breaks, glossary) {
117
+ const key = glossaryKey(glossary)
118
+ let entry = glossaryPipelines.get(key)
119
+ if (entry === undefined) {
120
+ const extension = glossaryExtension(glossary)
121
+ if (!extension) {
122
+ entry = null // no usable spelling in this list — fall back below
123
+ } else {
124
+ const withProse = new Marked({ async: false, gfm: true, breaks: false })
125
+ const withRecord = new Marked({ async: false, gfm: true, breaks: true })
126
+ withProse.use(escaping, { extensions: [extension] })
127
+ withRecord.use(escaping, { extensions: [extension] })
128
+ entry = { prose: withProse, record: withRecord }
129
+ }
130
+ glossaryPipelines.set(key, entry)
131
+ }
132
+ return entry ? (breaks ? entry.record : entry.prose) : pipeline(breaks)
133
+ }
134
+
135
+ function engineFor(breaks, glossary) {
136
+ return glossary && glossary.length > 0 ? withGlossary(breaks, glossary) : pipeline(breaks)
137
+ }
138
+
139
+ // The same key as the render pipelines, for the same reason: a glossary list
140
+ // is rebuilt into a regex once, however many texts get scanned against it.
141
+ const glossaryMatchers = new Map()
142
+
143
+ function matcherFor(glossary) {
144
+ const key = glossaryKey(glossary)
145
+ let matcher = glossaryMatchers.get(key)
146
+ if (matcher === undefined) {
147
+ const index = spellingIndex(glossary)
148
+ matcher = index
149
+ ? { bySpelling: index.bySpelling, regex: new RegExp(`(?<!${NOT_WORD})(?:${index.alternation})(?!${NOT_WORD})`, 'giu') }
150
+ : null
151
+ glossaryMatchers.set(key, matcher)
152
+ }
153
+ return matcher
154
+ }
155
+
156
+ /**
157
+ * The ids of `glossary` (`[{ id, spelling }]`) whose spelling occurs in
158
+ * `text`, matched the same word-bounded, case-insensitive way
159
+ * `renderBlock`/`renderInline` highlight them — a text scanned this way and
160
+ * the same text rendered with the same list agree on what counts as a hit.
161
+ * The free-text counterpart to a record's own `glossary_ids`: a dynasty
162
+ * history or a theme essay has no such column to read, so this scans the
163
+ * whole glossary against the text instead. One id per term, in no
164
+ * particular order.
165
+ */
166
+ export function glossaryIdsIn(text, glossary) {
167
+ if (!glossary || glossary.length === 0) return []
168
+ const matcher = matcherFor(glossary)
169
+ if (!matcher) return []
170
+ const found = new Set()
171
+ for (const match of String(text ?? '').matchAll(matcher.regex)) {
172
+ found.add(matcher.bySpelling.get(match[0].toLowerCase()))
173
+ }
174
+ return [...found]
175
+ }
176
+
177
+ /** A text as one or more paragraphs. `glossary`: `[{ id, spelling }]` to highlight. */
178
+ export function renderBlock(text, { breaks = false, glossary } = {}) {
179
+ return engineFor(breaks, glossary).parse(String(text ?? ''))
180
+ }
181
+
182
+ /** A text without the surrounding paragraph, for a heading or a table cell. */
183
+ export function renderInline(text, { breaks = false, glossary } = {}) {
184
+ return engineFor(breaks, glossary).parseInline(String(text ?? ''))
185
+ }
186
+
187
+ // Plain text is read off the token tree the same parser produces, never by
188
+ // rewriting the source with a regex: what a Markdown text says is what its
189
+ // tokens say. Raw HTML is dropped, since it was never content; an image is
190
+ // its alt text; a break is a space.
191
+ const BLOCKS = new Set(['paragraph', 'heading', 'blockquote', 'list', 'list_item', 'code', 'table', 'hr', 'html'])
192
+
193
+ const ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ' }
194
+ function decodeEntities(text) {
195
+ return text.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (match, entity) => {
196
+ if (entity[0] === '#') {
197
+ const code =
198
+ entity[1].toLowerCase() === 'x' ? parseInt(entity.slice(2), 16) : parseInt(entity.slice(1), 10)
199
+ return Number.isFinite(code) ? String.fromCodePoint(code) : match
200
+ }
201
+ return ENTITIES[entity.toLowerCase()] ?? match
202
+ })
203
+ }
204
+
205
+ function plainText(tokens) {
206
+ let out = ''
207
+ for (const token of tokens ?? []) {
208
+ switch (token.type) {
209
+ case 'html':
210
+ break
211
+ case 'image':
212
+ out += token.text ?? ''
213
+ break
214
+ case 'br':
215
+ case 'space':
216
+ out += ' '
217
+ break
218
+ case 'list':
219
+ out += plainText(token.items)
220
+ break
221
+ case 'table':
222
+ out += plainText(token.header)
223
+ for (const row of token.rows ?? []) out += plainText(row)
224
+ break
225
+ default:
226
+ if (token.tokens?.length) out += plainText(token.tokens)
227
+ else out += token.text ?? ''
228
+ }
229
+ if (BLOCKS.has(token.type)) out += ' '
230
+ }
231
+ return out
232
+ }
233
+
234
+ /** A text as plain text — for an `alt`, a `title`, an option label, a sort key. */
235
+ export function renderPlain(text) {
236
+ return decodeEntities(plainText(record.lexer(String(text ?? ''))))
237
+ .replace(/\s+/g, ' ')
238
+ .trim()
239
+ }
240
+
241
+ // `md`/`mdInline`/`mdStrip` are `renderBlock`/`renderInline`/`renderPlain`
242
+ // under the one signature and the one convention every site wrote for
243
+ // itself: a missing text is `''`, not an empty paragraph or "undefined", and
244
+ // a record's own line breaks are kept unless a caller says otherwise — a
245
+ // data-package field is typed with breaks on purpose, the way a dictionary
246
+ // entry is not. `renderBlock`/`renderInline`/`renderPlain` stay, for a
247
+ // caller that wants the pipeline's own default (`breaks: false`) instead.
248
+
249
+ /** `renderBlock`, record convention: `''` for a missing text, line breaks kept by default. */
250
+ export function md(text, { glossary, breaks = true } = {}) {
251
+ if (!text) return ''
252
+ return renderBlock(text, { breaks, glossary })
253
+ }
254
+
255
+ /** `renderInline`, the same convention, for a heading or a table cell. */
256
+ export function mdInline(text, { glossary } = {}) {
257
+ if (!text) return ''
258
+ return renderInline(text, { glossary })
259
+ }
260
+
261
+ /** `renderPlain`, the same convention, for an `alt`, a `title`, a sort key. */
262
+ export function mdStrip(text) {
263
+ if (!text) return ''
264
+ return renderPlain(text)
265
+ }
package/src/index.js CHANGED
@@ -1,50 +1,51 @@
1
- export { createViewer } from './createViewer.js'
2
- export { useCatalogueData } from './composables/useCatalogueData.js'
3
- export { useDataPackage } from './composables/useDataPackage.js'
4
- export { byId, entityRef, loadEntities, useEntities } from './composables/useEntities.js'
5
- export { resolveRecordLanguage, useRecordLanguage } from './composables/useRecordLanguage.js'
6
- export { createI18n, mergeMessages, useI18n, useLocale } from './i18n/index.js'
7
- export { isRtl, negotiateLanguage } from './i18n/language.js'
8
- export { md, mdInline, mdStrip, renderBlock, renderInline, renderPlain } from './i18n/markdown.js'
9
- export { languageLabels, offeredLanguages } from './languages.js'
10
- export { defaultScrollBehavior, resolveViews } from './router/index.js'
11
- export { mediaUrl, useSiteConfig } from './siteConfig.js'
12
- export { sourceUrl, useSiteRights } from './rights.js'
13
-
14
- // The list pages: query state, pagination, facets, dates, keyword search.
15
- export { useListQuery } from './catalogue/listQuery.js'
16
- export { paginate, sortChronological, usePagination } from './catalogue/pagination.js'
17
- export { facetOptions, useFacets } from './catalogue/facets.js'
18
- export {
19
- centuryPresets, dateRange, eraLabel, inDateRange, roundOutward, yearBuckets, yearBucketsFromRange,
20
- } from './catalogue/dates.js'
21
- export { useSearchLanguage } from './catalogue/searchLanguage.js'
22
- export {
23
- combineExpansions, countryExpansion, glossaryExpansion, parseBooleanQuery, useKeywordIndex,
24
- } from './catalogue/keywordIndex.js'
25
- export {
26
- effectiveYearTo, eventDateLabel, overlapsRange, useTimelineEvents,
27
- } from './catalogue/timeline.js'
28
-
29
- // The record page: the sheet engine and its derivations.
30
- export {
31
- glossaryEntries, glossaryTermsFor, glossaryTermsForText, sheetRows, useRecordSheet,
32
- } from './record/recordSheet.js'
33
- export {
34
- citation, relatedRecords, searchGlossary, timelineLinkFor, useGlossaryPopup, useRelatedRecords,
35
- } from './record/derivations.js'
36
- export { buildCollectionTree, collectionTreeFromThemes, useCollectionTree } from './record/collectionTree.js'
37
- export { groupByCountry, partnerHierarchy } from './record/partners.js'
38
-
39
- // Conventions every website had written for itself.
40
- export {
41
- PROJECT_ENTRIES, PROJECT_FAMILIES, mwnfLinks, projectFamily, projectName,
42
- sectionMeta, useFeaturedRecord, useProjectName, useSection,
43
- } from './conventions.js'
44
-
45
- export { default as I18nText } from './i18n/I18nText.vue'
46
- export { default as I18nTextInline } from './i18n/I18nTextInline.vue'
47
- export { default as HomeView } from './views/HomeView.vue'
48
- export { default as ListView } from './views/ListView.vue'
49
- export { default as DetailView } from './views/DetailView.vue'
50
- export { default as NotFoundView } from './views/NotFoundView.vue'
1
+ export { createViewer } from './createViewer.js'
2
+ export { useCatalogueData } from './composables/useCatalogueData.js'
3
+ export { useDataPackage } from './composables/useDataPackage.js'
4
+ export { byId, entityRef, loadEntities, useEntities } from './composables/useEntities.js'
5
+ export { resolveRecordLanguage, useRecordLanguage } from './composables/useRecordLanguage.js'
6
+ export { createI18n, mergeMessages, useI18n, useLocale } from './i18n/index.js'
7
+ export { isRtl, negotiateLanguage } from './i18n/language.js'
8
+ export { md, mdInline, mdStrip, renderBlock, renderInline, renderPlain } from './i18n/markdown.js'
9
+ export { languageLabels, offeredLanguages } from './languages.js'
10
+ export { defaultScrollBehavior, resolveViews } from './router/index.js'
11
+ export { mediaUrl, useSiteConfig } from './siteConfig.js'
12
+ export { sourceUrl, useSiteRights } from './rights.js'
13
+
14
+ // The list pages: query state, pagination, facets, dates, keyword search.
15
+ export { useListQuery } from './catalogue/listQuery.js'
16
+ export { paginate, sortChronological, usePagination } from './catalogue/pagination.js'
17
+ export { facetOptions, useFacets } from './catalogue/facets.js'
18
+ export {
19
+ centuryPresets, dateRange, eraLabel, inDateRange, roundOutward, yearBuckets, yearBucketsFromRange,
20
+ } from './catalogue/dates.js'
21
+ export { useSearchLanguage } from './catalogue/searchLanguage.js'
22
+ export {
23
+ combineExpansions, countryExpansion, glossaryExpansion, parseBooleanQuery, useKeywordIndex,
24
+ } from './catalogue/keywordIndex.js'
25
+ export {
26
+ effectiveYearTo, eventDateLabel, overlapsRange, useTimelineEvents,
27
+ } from './catalogue/timeline.js'
28
+
29
+ // The record page: the sheet engine and its derivations.
30
+ export {
31
+ glossaryEntries, glossaryTermsFor, glossaryTermsForText, sheetRows, useRecordSheet,
32
+ } from './record/recordSheet.js'
33
+ export {
34
+ citation, relatedRecords, searchGlossary, timelineLinkFor, useGlossaryPopup, useRelatedRecords,
35
+ } from './record/derivations.js'
36
+ export { buildCollectionTree, collectionTreeFromThemes, useCollectionTree } from './record/collectionTree.js'
37
+ export { groupByCountry, partnerHierarchy } from './record/partners.js'
38
+
39
+ // Conventions every website had written for itself.
40
+ export {
41
+ PROJECT_ENTRIES, PROJECT_FAMILIES, mwnfLinks, projectFamily, projectLabel,
42
+ projectLinks, projectName, sectionMeta, useFeaturedRecord, useProjectName,
43
+ useProjects, useSection,
44
+ } from './conventions.js'
45
+
46
+ export { default as I18nText } from './i18n/I18nText.vue'
47
+ export { default as I18nTextInline } from './i18n/I18nTextInline.vue'
48
+ export { default as HomeView } from './views/HomeView.vue'
49
+ export { default as ListView } from './views/ListView.vue'
50
+ export { default as DetailView } from './views/DetailView.vue'
51
+ export { default as NotFoundView } from './views/NotFoundView.vue'
@@ -1 +1 @@
1
- export const VIEWER_CONFIG = Symbol('viewer-config')
1
+ export const VIEWER_CONFIG = Symbol('viewer-config')