@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,231 +1,231 @@
1
- import { computed, ref, toValue, watch } from 'vue'
2
- import { useDataPackage } from '../composables/useDataPackage.js'
3
- import { entityRef } from '../composables/useEntities.js'
4
- import { useRecordLanguage } from '../composables/useRecordLanguage.js'
5
- import { glossaryIdsIn, renderBlock, renderInline, renderPlain } from '../i18n/markdown.js'
6
-
7
- const BASE_LANGUAGE = 'en'
8
-
9
- // A record page does the same things on every website before it shows a
10
- // single field: decide which language the record is read in, load that
11
- // language for the record's entity and for the entities its sheet names
12
- // (dynasties, glossary, partners), gather the glossary terms the record
13
- // reaches in that language for the renderer, and say when it is ready.
14
- // Seven pages wrote that choreography by hand. This is the one place it is
15
- // written, on top of `useRecordLanguage`, which decides the language.
16
-
17
- // A term's row in one language: its spellings and definition from
18
- // `rows`/`fallback` (English), its headword from `term` (the glossary
19
- // record itself, language-independent) — `null` when the term has neither,
20
- // which happens for a headword-only fixture row with no translation file at
21
- // all. Shared by `glossaryTermsFor` (walks a record's own `glossary_ids`)
22
- // and `glossaryTermsForText` (walks the whole glossary, filtered by what
23
- // the text contains), so the two agree on what a term row looks like.
24
- function termRow(id, term, rows, fallback) {
25
- const t = rows[id] ?? fallback[id] ?? {}
26
- const spellings = (t.spellings ?? []).map((s) => String(s).trim()).filter(Boolean)
27
- const word = term?.word ?? spellings[0] ?? ''
28
- if (!word && spellings.length === 0) return null
29
- return { id, word, definition: t.definition ?? '', spellings: spellings.length ? spellings : [word] }
30
- }
31
-
32
- /**
33
- * `{ id, word, definition, spellings }` for each glossary term of `record`
34
- * (its `glossary_ids`), with the spellings and definition in `language`,
35
- * falling back to English, then to the term's own headword.
36
- */
37
- export function glossaryTermsFor(record, language, { entity = 'glossary', translations, byId } = {}) {
38
- const ids = record?.glossary_ids ?? []
39
- if (ids.length === 0) return []
40
- const rows = translations(entity, language)
41
- const fallback = translations(entity, BASE_LANGUAGE)
42
- const out = []
43
- for (const id of ids) {
44
- const row = termRow(id, byId?.get(id), rows, fallback)
45
- if (row) out.push(row)
46
- }
47
- return out
48
- }
49
-
50
- /**
51
- * `glossaryTermsFor`'s counterpart for a text with no `glossary_ids` column
52
- * to read — a dynasty history, a theme essay: every glossary term whose
53
- * spelling occurs in `text`, matched the same word-bounded way `md`/
54
- * `renderBlock` highlight it, so what this returns and what the text is
55
- * rendered with agree. The candidate list — the whole glossary in
56
- * `language`, English behind it — is the same for every text asked about
57
- * in that language; `glossaryIdsIn` is what carries the caching (see
58
- * `markdown.js`), so building it here costs one pass over the glossary,
59
- * not a rebuilt regex.
60
- */
61
- export function glossaryTermsForText(text, language, { entity = 'glossary' } = {}) {
62
- const value = String(text ?? '')
63
- if (!value) return []
64
- const { translations } = useDataPackage()
65
- const rows = translations(entity, language)
66
- const fallback = translations(entity, BASE_LANGUAGE)
67
- const byId = new Map()
68
- const candidates = []
69
- for (const term of entityRef(entity).value ?? []) {
70
- const row = termRow(term.id, term, rows, fallback)
71
- if (!row) continue
72
- byId.set(term.id, row)
73
- for (const spelling of row.spellings) candidates.push({ id: term.id, spelling })
74
- }
75
- return glossaryIdsIn(value, candidates).map((id) => byId.get(id))
76
- }
77
-
78
- /** The `[{ id, spelling }]` list the renderers highlight, one entry per spelling. */
79
- export function glossaryEntries(terms) {
80
- const out = []
81
- for (const term of terms ?? []) {
82
- for (const spelling of term.spellings) out.push({ id: term.id, spelling, definition: term.definition, word: term.word })
83
- }
84
- return out
85
- }
86
-
87
- /**
88
- * The record page's engine.
89
- *
90
- * `record` is a ref, computed or getter. `entity` is the record's entity;
91
- * `translations` names the other entities the sheet reads in the record's
92
- * language (`['dynasties', 'glossary', 'partners']`). Returns what
93
- * `useRecordLanguage` returns — `language`, `languages`, `dir`, `select`,
94
- * `reset` — plus `text` (the record's translation in the active language,
95
- * falling back to English), `ready` (every named load done for this record
96
- * and language), `terms` and `glossary` (the record's glossary terms, and
97
- * the spelling list the renderers take), and `attribution` when asked for.
98
- *
99
- * `attribution: ['author', 'copy_editor']` names fields that are proper
100
- * names rather than text: when the active language's row lacks them, the
101
- * first other language of the record that has them supplies them. This is
102
- * the one place a page reads across languages, and it exists because the
103
- * importer files some records' credits on one language only.
104
- */
105
- export function useRecordSheet(record, { entity, translations: related = [], attribution: attributionFields = [], languages } = {}) {
106
- if (!entity) throw new Error('useRecordSheet: entity is required')
107
- const pkg = useDataPackage()
108
- const language = useRecordLanguage(record, { entity, languages })
109
- const ready = ref(false)
110
- const attribution = ref({})
111
- const glossaryRecords = related.includes('glossary') ? entityRef('glossary') : null
112
-
113
- const text = computed(() => {
114
- const current = toValue(record)
115
- return current ? pkg.tr(entity, current.id, language.language.value, BASE_LANGUAGE) : {}
116
- })
117
-
118
- async function resolveAttribution(current, lang) {
119
- const own = pkg.tr(entity, current.id, lang, BASE_LANGUAGE)
120
- if (attributionFields.some((field) => own[field])) return {}
121
- for (const code of language.languages.value) {
122
- if (code === lang) continue
123
- await pkg.loadTranslations(entity, code)
124
- const row = pkg.translations(entity, code)[current.id] ?? {}
125
- if (attributionFields.some((field) => row[field])) {
126
- const found = { from: code }
127
- for (const field of attributionFields) if (row[field]) found[field] = row[field]
128
- return found
129
- }
130
- }
131
- return {}
132
- }
133
-
134
- let generation = 0
135
- async function load() {
136
- const current = toValue(record)
137
- const lang = language.language.value
138
- const mine = ++generation
139
- ready.value = false
140
- attribution.value = {}
141
- if (!current) {
142
- ready.value = true
143
- return
144
- }
145
- // The record language and English both: `tr` falls back to English for a
146
- // record or a term that has no row in the active language, and a fallback
147
- // that has not been loaded is a blank, not a fallback.
148
- const languages = lang === BASE_LANGUAGE ? [lang] : [lang, BASE_LANGUAGE]
149
- await Promise.all(
150
- [entity, ...related].flatMap((name) => languages.map((code) => pkg.loadTranslations(name, code))),
151
- )
152
- if (mine !== generation) return
153
- if (attributionFields.length) {
154
- const found = await resolveAttribution(current, lang)
155
- if (mine !== generation) return
156
- attribution.value = found
157
- }
158
- ready.value = true
159
- }
160
-
161
- watch(() => [toValue(record)?.id, language.language.value], load, { immediate: true })
162
-
163
- const glossaryIndex = computed(() =>
164
- glossaryRecords ? new Map((glossaryRecords.value ?? []).map((term) => [term.id, term])) : new Map(),
165
- )
166
- const terms = computed(() =>
167
- glossaryTermsFor(toValue(record), language.language.value, {
168
- translations: pkg.translations,
169
- byId: glossaryIndex.value,
170
- }),
171
- )
172
- const glossary = computed(() => glossaryEntries(terms.value))
173
-
174
- return { ...language, text, ready, terms, glossary, attribution }
175
- }
176
-
177
- // ── The field engine ───────────────────────────────────────────────────────
178
- //
179
- // A sheet is a list of labelled values, in an order, with the empty ones
180
- // dropped. Which fields, in what order, under which labels, is the site's:
181
- // "Materials/techniques" on Islamic Art and "Type" on a gallery name the
182
- // same field and are both legacy facts. The spec is data; this turns it into
183
- // rows and renders every value through the one pipeline, with the glossary.
184
-
185
- const RENDER = {
186
- inline: (value, ctx) => renderInline(value, { glossary: ctx.glossary }),
187
- block: (value, ctx) => renderBlock(value, { breaks: true, glossary: ctx.glossary }),
188
- plain: (value) => renderPlain(value),
189
- link: (value) => String(value),
190
- custom: (value) => value,
191
- }
192
-
193
- function present(value) {
194
- if (value == null) return false
195
- if (typeof value === 'string') return value.trim() !== ''
196
- if (Array.isArray(value)) return value.length > 0
197
- return true
198
- }
199
-
200
- /**
201
- * `[{ key, label, render, value, html }]` from a spec and a context.
202
- *
203
- * A spec entry is `{ key, label, value, render = 'inline', when }`: `label`
204
- * a text (already looked up) or `(ctx) => text`; `value` a function of the
205
- * context returning the raw value, or the name of a field of `ctx.text`;
206
- * `render` one of `inline`, `block`, `plain` (rendered through the pipeline
207
- * with `ctx.glossary`), `link` (the value is an address) or `custom` (the
208
- * value is handed back as it is, for a slot); `when(ctx)` gates the row.
209
- * Rows whose value is empty are dropped. `ctx` is whatever the site passes —
210
- * typically `{ record, text, glossary, ...helpers }`.
211
- */
212
- export function sheetRows(spec, ctx = {}) {
213
- const rows = []
214
- for (const entry of spec ?? []) {
215
- if (!entry || (entry.when && !entry.when(ctx))) continue
216
- const raw = typeof entry.value === 'function' ? entry.value(ctx) : ctx.text?.[entry.value ?? entry.key]
217
- if (!present(raw)) continue
218
- const render = entry.render ?? 'inline'
219
- const renderer = RENDER[render]
220
- if (!renderer) throw new Error(`sheetRows: unknown render "${render}" on "${entry.key}"`)
221
- const value = Array.isArray(raw) && render !== 'custom' ? raw.join(entry.join ?? ', ') : raw
222
- rows.push({
223
- key: entry.key,
224
- label: typeof entry.label === 'function' ? entry.label(ctx) : (entry.label ?? entry.key),
225
- render,
226
- value,
227
- html: render === 'custom' || render === 'link' ? null : renderer(value, ctx),
228
- })
229
- }
230
- return rows
231
- }
1
+ import { computed, ref, toValue, watch } from 'vue'
2
+ import { useDataPackage } from '../composables/useDataPackage.js'
3
+ import { entityRef } from '../composables/useEntities.js'
4
+ import { useRecordLanguage } from '../composables/useRecordLanguage.js'
5
+ import { glossaryIdsIn, renderBlock, renderInline, renderPlain } from '../i18n/markdown.js'
6
+
7
+ const BASE_LANGUAGE = 'en'
8
+
9
+ // A record page does the same things on every website before it shows a
10
+ // single field: decide which language the record is read in, load that
11
+ // language for the record's entity and for the entities its sheet names
12
+ // (dynasties, glossary, partners), gather the glossary terms the record
13
+ // reaches in that language for the renderer, and say when it is ready.
14
+ // Seven pages wrote that choreography by hand. This is the one place it is
15
+ // written, on top of `useRecordLanguage`, which decides the language.
16
+
17
+ // A term's row in one language: its spellings and definition from
18
+ // `rows`/`fallback` (English), its headword from `term` (the glossary
19
+ // record itself, language-independent) — `null` when the term has neither,
20
+ // which happens for a headword-only fixture row with no translation file at
21
+ // all. Shared by `glossaryTermsFor` (walks a record's own `glossary_ids`)
22
+ // and `glossaryTermsForText` (walks the whole glossary, filtered by what
23
+ // the text contains), so the two agree on what a term row looks like.
24
+ function termRow(id, term, rows, fallback) {
25
+ const t = rows[id] ?? fallback[id] ?? {}
26
+ const spellings = (t.spellings ?? []).map((s) => String(s).trim()).filter(Boolean)
27
+ const word = term?.word ?? spellings[0] ?? ''
28
+ if (!word && spellings.length === 0) return null
29
+ return { id, word, definition: t.definition ?? '', spellings: spellings.length ? spellings : [word] }
30
+ }
31
+
32
+ /**
33
+ * `{ id, word, definition, spellings }` for each glossary term of `record`
34
+ * (its `glossary_ids`), with the spellings and definition in `language`,
35
+ * falling back to English, then to the term's own headword.
36
+ */
37
+ export function glossaryTermsFor(record, language, { entity = 'glossary', translations, byId } = {}) {
38
+ const ids = record?.glossary_ids ?? []
39
+ if (ids.length === 0) return []
40
+ const rows = translations(entity, language)
41
+ const fallback = translations(entity, BASE_LANGUAGE)
42
+ const out = []
43
+ for (const id of ids) {
44
+ const row = termRow(id, byId?.get(id), rows, fallback)
45
+ if (row) out.push(row)
46
+ }
47
+ return out
48
+ }
49
+
50
+ /**
51
+ * `glossaryTermsFor`'s counterpart for a text with no `glossary_ids` column
52
+ * to read — a dynasty history, a theme essay: every glossary term whose
53
+ * spelling occurs in `text`, matched the same word-bounded way `md`/
54
+ * `renderBlock` highlight it, so what this returns and what the text is
55
+ * rendered with agree. The candidate list — the whole glossary in
56
+ * `language`, English behind it — is the same for every text asked about
57
+ * in that language; `glossaryIdsIn` is what carries the caching (see
58
+ * `markdown.js`), so building it here costs one pass over the glossary,
59
+ * not a rebuilt regex.
60
+ */
61
+ export function glossaryTermsForText(text, language, { entity = 'glossary' } = {}) {
62
+ const value = String(text ?? '')
63
+ if (!value) return []
64
+ const { translations } = useDataPackage()
65
+ const rows = translations(entity, language)
66
+ const fallback = translations(entity, BASE_LANGUAGE)
67
+ const byId = new Map()
68
+ const candidates = []
69
+ for (const term of entityRef(entity).value ?? []) {
70
+ const row = termRow(term.id, term, rows, fallback)
71
+ if (!row) continue
72
+ byId.set(term.id, row)
73
+ for (const spelling of row.spellings) candidates.push({ id: term.id, spelling })
74
+ }
75
+ return glossaryIdsIn(value, candidates).map((id) => byId.get(id))
76
+ }
77
+
78
+ /** The `[{ id, spelling }]` list the renderers highlight, one entry per spelling. */
79
+ export function glossaryEntries(terms) {
80
+ const out = []
81
+ for (const term of terms ?? []) {
82
+ for (const spelling of term.spellings) out.push({ id: term.id, spelling, definition: term.definition, word: term.word })
83
+ }
84
+ return out
85
+ }
86
+
87
+ /**
88
+ * The record page's engine.
89
+ *
90
+ * `record` is a ref, computed or getter. `entity` is the record's entity;
91
+ * `translations` names the other entities the sheet reads in the record's
92
+ * language (`['dynasties', 'glossary', 'partners']`). Returns what
93
+ * `useRecordLanguage` returns — `language`, `languages`, `dir`, `select`,
94
+ * `reset` — plus `text` (the record's translation in the active language,
95
+ * falling back to English), `ready` (every named load done for this record
96
+ * and language), `terms` and `glossary` (the record's glossary terms, and
97
+ * the spelling list the renderers take), and `attribution` when asked for.
98
+ *
99
+ * `attribution: ['author', 'copy_editor']` names fields that are proper
100
+ * names rather than text: when the active language's row lacks them, the
101
+ * first other language of the record that has them supplies them. This is
102
+ * the one place a page reads across languages, and it exists because the
103
+ * importer files some records' credits on one language only.
104
+ */
105
+ export function useRecordSheet(record, { entity, translations: related = [], attribution: attributionFields = [], languages } = {}) {
106
+ if (!entity) throw new Error('useRecordSheet: entity is required')
107
+ const pkg = useDataPackage()
108
+ const language = useRecordLanguage(record, { entity, languages })
109
+ const ready = ref(false)
110
+ const attribution = ref({})
111
+ const glossaryRecords = related.includes('glossary') ? entityRef('glossary') : null
112
+
113
+ const text = computed(() => {
114
+ const current = toValue(record)
115
+ return current ? pkg.tr(entity, current.id, language.language.value, BASE_LANGUAGE) : {}
116
+ })
117
+
118
+ async function resolveAttribution(current, lang) {
119
+ const own = pkg.tr(entity, current.id, lang, BASE_LANGUAGE)
120
+ if (attributionFields.some((field) => own[field])) return {}
121
+ for (const code of language.languages.value) {
122
+ if (code === lang) continue
123
+ await pkg.loadTranslations(entity, code)
124
+ const row = pkg.translations(entity, code)[current.id] ?? {}
125
+ if (attributionFields.some((field) => row[field])) {
126
+ const found = { from: code }
127
+ for (const field of attributionFields) if (row[field]) found[field] = row[field]
128
+ return found
129
+ }
130
+ }
131
+ return {}
132
+ }
133
+
134
+ let generation = 0
135
+ async function load() {
136
+ const current = toValue(record)
137
+ const lang = language.language.value
138
+ const mine = ++generation
139
+ ready.value = false
140
+ attribution.value = {}
141
+ if (!current) {
142
+ ready.value = true
143
+ return
144
+ }
145
+ // The record language and English both: `tr` falls back to English for a
146
+ // record or a term that has no row in the active language, and a fallback
147
+ // that has not been loaded is a blank, not a fallback.
148
+ const languages = lang === BASE_LANGUAGE ? [lang] : [lang, BASE_LANGUAGE]
149
+ await Promise.all(
150
+ [entity, ...related].flatMap((name) => languages.map((code) => pkg.loadTranslations(name, code))),
151
+ )
152
+ if (mine !== generation) return
153
+ if (attributionFields.length) {
154
+ const found = await resolveAttribution(current, lang)
155
+ if (mine !== generation) return
156
+ attribution.value = found
157
+ }
158
+ ready.value = true
159
+ }
160
+
161
+ watch(() => [toValue(record)?.id, language.language.value], load, { immediate: true })
162
+
163
+ const glossaryIndex = computed(() =>
164
+ glossaryRecords ? new Map((glossaryRecords.value ?? []).map((term) => [term.id, term])) : new Map(),
165
+ )
166
+ const terms = computed(() =>
167
+ glossaryTermsFor(toValue(record), language.language.value, {
168
+ translations: pkg.translations,
169
+ byId: glossaryIndex.value,
170
+ }),
171
+ )
172
+ const glossary = computed(() => glossaryEntries(terms.value))
173
+
174
+ return { ...language, text, ready, terms, glossary, attribution }
175
+ }
176
+
177
+ // ── The field engine ───────────────────────────────────────────────────────
178
+ //
179
+ // A sheet is a list of labelled values, in an order, with the empty ones
180
+ // dropped. Which fields, in what order, under which labels, is the site's:
181
+ // "Materials/techniques" on Islamic Art and "Type" on a gallery name the
182
+ // same field and are both legacy facts. The spec is data; this turns it into
183
+ // rows and renders every value through the one pipeline, with the glossary.
184
+
185
+ const RENDER = {
186
+ inline: (value, ctx) => renderInline(value, { glossary: ctx.glossary }),
187
+ block: (value, ctx) => renderBlock(value, { breaks: true, glossary: ctx.glossary }),
188
+ plain: (value) => renderPlain(value),
189
+ link: (value) => String(value),
190
+ custom: (value) => value,
191
+ }
192
+
193
+ function present(value) {
194
+ if (value == null) return false
195
+ if (typeof value === 'string') return value.trim() !== ''
196
+ if (Array.isArray(value)) return value.length > 0
197
+ return true
198
+ }
199
+
200
+ /**
201
+ * `[{ key, label, render, value, html }]` from a spec and a context.
202
+ *
203
+ * A spec entry is `{ key, label, value, render = 'inline', when }`: `label`
204
+ * a text (already looked up) or `(ctx) => text`; `value` a function of the
205
+ * context returning the raw value, or the name of a field of `ctx.text`;
206
+ * `render` one of `inline`, `block`, `plain` (rendered through the pipeline
207
+ * with `ctx.glossary`), `link` (the value is an address) or `custom` (the
208
+ * value is handed back as it is, for a slot); `when(ctx)` gates the row.
209
+ * Rows whose value is empty are dropped. `ctx` is whatever the site passes —
210
+ * typically `{ record, text, glossary, ...helpers }`.
211
+ */
212
+ export function sheetRows(spec, ctx = {}) {
213
+ const rows = []
214
+ for (const entry of spec ?? []) {
215
+ if (!entry || (entry.when && !entry.when(ctx))) continue
216
+ const raw = typeof entry.value === 'function' ? entry.value(ctx) : ctx.text?.[entry.value ?? entry.key]
217
+ if (!present(raw)) continue
218
+ const render = entry.render ?? 'inline'
219
+ const renderer = RENDER[render]
220
+ if (!renderer) throw new Error(`sheetRows: unknown render "${render}" on "${entry.key}"`)
221
+ const value = Array.isArray(raw) && render !== 'custom' ? raw.join(entry.join ?? ', ') : raw
222
+ rows.push({
223
+ key: entry.key,
224
+ label: typeof entry.label === 'function' ? entry.label(ctx) : (entry.label ?? entry.key),
225
+ render,
226
+ value,
227
+ html: render === 'custom' || render === 'link' ? null : renderer(value, ctx),
228
+ })
229
+ }
230
+ return rows
231
+ }
package/src/rights.js CHANGED
@@ -1,53 +1,53 @@
1
- import { useRouter } from 'vue-router'
2
- import { mwnfLinks } from './conventions.js'
3
- import { useDataPackage } from './composables/useDataPackage.js'
4
- import { useSiteConfig } from './siteConfig.js'
5
-
6
- // A site's rights and the address of one of its pages — two facts a
7
- // citation needs that neither `citation()` nor the data package can know by
8
- // itself. The rights holder and terms come from the exporter's
9
- // `manifest.json` (inventory-app#1690 puts a `rights` block there); a
10
- // page's own address depends on where the site is deployed, which the
11
- // package cannot declare either. Both are read from one place a website
12
- // declares once — the manifest's `rights` block, and `site.origin` in
13
- // `dataset.config.js` — so a composed view asks here instead of assembling
14
- // its own guess.
15
-
16
- /**
17
- * `{ holder, termsUrl, attribution }` from the loaded data package's
18
- * `manifest.rights`. A package built before the block existed, or one that
19
- * carries none, has no holder or attribution to offer, and its terms
20
- * address falls back to the MWNF legal notice — the address every site
21
- * pointed at before a package could declare its own.
22
- */
23
- export function useSiteRights() {
24
- const { manifest } = useDataPackage()
25
- const rights = manifest.rights
26
- return {
27
- holder: rights?.rights_holder ?? null,
28
- termsUrl: rights?.terms_url ?? mwnfLinks.legalNotice,
29
- attribution: rights?.attribution ?? null,
30
- }
31
- }
32
-
33
- const TRAILING_SLASHES = /\/+$/
34
-
35
- /**
36
- * The address of `route` on this deployment: the declared `site.origin`
37
- * plus the hash the router would put in the location bar for it. Null when
38
- * the site declares no origin, so a page renders its citation without a
39
- * source address instead of one that resolves nowhere once copied out of
40
- * the app. `route` is either a route already resolved (its `fullPath` read
41
- * directly, so asking for the current page's own address costs no second
42
- * resolution) or a raw location, which the installed router resolves the
43
- * same way a `<router-link>` would.
44
- */
45
- export function sourceUrl(route) {
46
- const origin = String(useSiteConfig().site?.origin ?? '').replace(TRAILING_SLASHES, '')
47
- if (!origin) return null
48
- const fullPath =
49
- typeof route === 'object' && route !== null && typeof route.fullPath === 'string'
50
- ? route.fullPath
51
- : useRouter().resolve(route).fullPath
52
- return `${origin}/#${fullPath}`
53
- }
1
+ import { useRouter } from 'vue-router'
2
+ import { mwnfLinks } from './conventions.js'
3
+ import { useDataPackage } from './composables/useDataPackage.js'
4
+ import { useSiteConfig } from './siteConfig.js'
5
+
6
+ // A site's rights and the address of one of its pages — two facts a
7
+ // citation needs that neither `citation()` nor the data package can know by
8
+ // itself. The rights holder and terms come from the exporter's
9
+ // `manifest.json` (inventory-app#1690 puts a `rights` block there); a
10
+ // page's own address depends on where the site is deployed, which the
11
+ // package cannot declare either. Both are read from one place a website
12
+ // declares once — the manifest's `rights` block, and `site.origin` in
13
+ // `dataset.config.js` — so a composed view asks here instead of assembling
14
+ // its own guess.
15
+
16
+ /**
17
+ * `{ holder, termsUrl, attribution }` from the loaded data package's
18
+ * `manifest.rights`. A package built before the block existed, or one that
19
+ * carries none, has no holder or attribution to offer, and its terms
20
+ * address falls back to the MWNF legal notice — the address every site
21
+ * pointed at before a package could declare its own.
22
+ */
23
+ export function useSiteRights() {
24
+ const { manifest } = useDataPackage()
25
+ const rights = manifest.rights
26
+ return {
27
+ holder: rights?.rights_holder ?? null,
28
+ termsUrl: rights?.terms_url ?? mwnfLinks.legalNotice,
29
+ attribution: rights?.attribution ?? null,
30
+ }
31
+ }
32
+
33
+ const TRAILING_SLASHES = /\/+$/
34
+
35
+ /**
36
+ * The address of `route` on this deployment: the declared `site.origin`
37
+ * plus the hash the router would put in the location bar for it. Null when
38
+ * the site declares no origin, so a page renders its citation without a
39
+ * source address instead of one that resolves nowhere once copied out of
40
+ * the app. `route` is either a route already resolved (its `fullPath` read
41
+ * directly, so asking for the current page's own address costs no second
42
+ * resolution) or a raw location, which the installed router resolves the
43
+ * same way a `<router-link>` would.
44
+ */
45
+ export function sourceUrl(route) {
46
+ const origin = String(useSiteConfig().site?.origin ?? '').replace(TRAILING_SLASHES, '')
47
+ if (!origin) return null
48
+ const fullPath =
49
+ typeof route === 'object' && route !== null && typeof route.fullPath === 'string'
50
+ ? route.fullPath
51
+ : useRouter().resolve(route).fullPath
52
+ return `${origin}/#${fullPath}`
53
+ }