@uniweb/kit 0.9.6 → 0.9.8
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 +3 -2
- package/src/styled/Prose/index.jsx +12 -7
- package/src/xref/Ref.jsx +142 -0
- package/src/xref/index.js +27 -0
- package/src/xref/registry.js +217 -0
- package/src/xref/styles.js +103 -0
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/kit",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.8",
|
|
4
4
|
"description": "Standard component library for Uniweb foundations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./src/index.js",
|
|
8
|
+
"./xref": "./src/xref/index.js",
|
|
8
9
|
"./theme-tokens.css": "./src/theme-tokens.css"
|
|
9
10
|
},
|
|
10
11
|
"files": [
|
|
@@ -38,7 +39,7 @@
|
|
|
38
39
|
"fuse.js": "^7.0.0",
|
|
39
40
|
"shiki": "^3.0.0",
|
|
40
41
|
"tailwind-merge": "^2.6.0",
|
|
41
|
-
"@uniweb/core": "0.7.
|
|
42
|
+
"@uniweb/core": "0.7.8"
|
|
42
43
|
},
|
|
43
44
|
"peerDependencies": {
|
|
44
45
|
"react": "^18.0.0 || ^19.0.0",
|
|
@@ -133,15 +133,20 @@ function SequenceElement({ element, block }) {
|
|
|
133
133
|
case 'dataBlock':
|
|
134
134
|
return null
|
|
135
135
|
|
|
136
|
-
case '
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
136
|
+
case 'math': {
|
|
137
|
+
// Math from $$...$$ on its own line, ```math fence, or inline
|
|
138
|
+
// $...$ (the inline form doesn't reach the sequence walker — it
|
|
139
|
+
// rides inside paragraph HTML — but render the same way for
|
|
140
|
+
// safety in case a future caller surfaces it here). Mathml is
|
|
141
|
+
// pre-compiled at parse time; the browser renders real MathML
|
|
142
|
+
// natively. Foundations that want to style errors can target
|
|
143
|
+
// .temml-error inside this wrapper.
|
|
141
144
|
if (!element.mathml) return null
|
|
145
|
+
const display = element.display !== false
|
|
146
|
+
const Tag = display ? 'div' : 'span'
|
|
142
147
|
return (
|
|
143
|
-
<
|
|
144
|
-
className=
|
|
148
|
+
<Tag
|
|
149
|
+
className={display ? 'math-display' : 'math-inline'}
|
|
145
150
|
dangerouslySetInnerHTML={{ __html: element.mathml }}
|
|
146
151
|
/>
|
|
147
152
|
)
|
package/src/xref/Ref.jsx
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-reference renderer.
|
|
3
|
+
*
|
|
4
|
+
* Foundations that want `[#id]` cross-reference rendering import this
|
|
5
|
+
* component from `@uniweb/kit/xref` and register it in their
|
|
6
|
+
* foundation.js's `defaultInsets` map:
|
|
7
|
+
*
|
|
8
|
+
* import { Ref } from '@uniweb/kit/xref'
|
|
9
|
+
* export default {
|
|
10
|
+
* defaultInsets: { Ref },
|
|
11
|
+
* xref: { kinds: { ... } },
|
|
12
|
+
* // ...
|
|
13
|
+
* }
|
|
14
|
+
*
|
|
15
|
+
* Content-reader compiles `[#id]` markdown markers to
|
|
16
|
+
* `inset_ref { component: 'Ref', key: <id> }` regardless of foundation
|
|
17
|
+
* choice; the runtime's inset resolver looks up the component name in
|
|
18
|
+
* the foundation's defaultInsets to find this renderer.
|
|
19
|
+
*
|
|
20
|
+
* Resolution flow at render time:
|
|
21
|
+
* 1. Read `getXrefRegistry(block.website)` — the per-document
|
|
22
|
+
* registry the runtime populated from `{#id}` attributes when the
|
|
23
|
+
* foundation declared `xref:`.
|
|
24
|
+
* 2. Pick the active xref-style preset (foundation default + document
|
|
25
|
+
* override) and the kind metadata for the registered entry.
|
|
26
|
+
* 3. Render via the style's label + counter + locator.
|
|
27
|
+
*
|
|
28
|
+
* Multi-ref clusters (`[#a;#b]`) split on `;` and render same-kind
|
|
29
|
+
* groups using the style's `labelPlural`. Mixed-kind clusters fall
|
|
30
|
+
* back to comma-separated singular rendering with a console warning.
|
|
31
|
+
*
|
|
32
|
+
* Missing ids render `[?<id-with-typo>]` so the failing key is visible
|
|
33
|
+
* in the output — easier to debug than the bare `[?]` we use for
|
|
34
|
+
* missing cite keys.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import React from 'react'
|
|
38
|
+
import { getXrefRegistry } from './registry.js'
|
|
39
|
+
import { resolveXrefStyle, getKindMeta } from './styles.js'
|
|
40
|
+
|
|
41
|
+
function splitKeys(raw) {
|
|
42
|
+
return String(raw || '')
|
|
43
|
+
.split(';')
|
|
44
|
+
.map((k) => k.trim().replace(/^#/, ''))
|
|
45
|
+
.filter(Boolean)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function formatLocator(params) {
|
|
49
|
+
const { page, locator, label = 'page' } = params || {}
|
|
50
|
+
const value = page || locator
|
|
51
|
+
if (!value) return ''
|
|
52
|
+
const labels = {
|
|
53
|
+
page: 'p.',
|
|
54
|
+
chapter: 'chap.',
|
|
55
|
+
section: '§',
|
|
56
|
+
paragraph: '¶',
|
|
57
|
+
}
|
|
58
|
+
const lab = labels[label] || `${label}.`
|
|
59
|
+
return ` (${lab} ${value})`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function renderEntry(entry, kindMeta) {
|
|
63
|
+
const label = kindMeta?.label || ''
|
|
64
|
+
const sep = kindMeta?.sep ?? ' '
|
|
65
|
+
const counter = entry.counterText
|
|
66
|
+
return label ? `${label}${sep}${counter}` : counter
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function renderGroupSameKind(entries, kindMeta) {
|
|
70
|
+
if (entries.length === 1) {
|
|
71
|
+
return renderEntry(entries[0], kindMeta)
|
|
72
|
+
}
|
|
73
|
+
const label = kindMeta?.labelPlural || kindMeta?.label || ''
|
|
74
|
+
const sep = kindMeta?.sep ?? ' '
|
|
75
|
+
const counters = entries.map((e) => e.counterText)
|
|
76
|
+
let body
|
|
77
|
+
if (counters.length === 2) {
|
|
78
|
+
body = counters.join(' and ')
|
|
79
|
+
} else {
|
|
80
|
+
body = counters.slice(0, -1).join(', ') + ', and ' + counters[counters.length - 1]
|
|
81
|
+
}
|
|
82
|
+
return label ? `${label}${sep}${body}` : body
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function Ref({ params, block }) {
|
|
86
|
+
const website = block?.website
|
|
87
|
+
const registry = getXrefRegistry(website)
|
|
88
|
+
const entries = registry?.entries || {}
|
|
89
|
+
const styleName = website?.config?.book?.xrefStyle || 'humanities'
|
|
90
|
+
const style = resolveXrefStyle(styleName, website?.config)
|
|
91
|
+
|
|
92
|
+
const ids = splitKeys(params?.key)
|
|
93
|
+
if (ids.length === 0) {
|
|
94
|
+
return <span className="xref xref--missing" title="No id">[?]</span>
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const resolved = ids.map((id) => {
|
|
98
|
+
const entry = entries[id]
|
|
99
|
+
return entry ? { id, entry, kindMeta: getKindMeta(style, entry.kind) } : { id, missing: true }
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
if (resolved.length === 1 && resolved[0].missing) {
|
|
103
|
+
return (
|
|
104
|
+
<span className="xref xref--missing" title={`Missing label: ${resolved[0].id}`}>
|
|
105
|
+
[?{resolved[0].id}]
|
|
106
|
+
</span>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const allKinds = resolved.filter((r) => !r.missing).map((r) => r.entry.kind)
|
|
111
|
+
const sameKind = allKinds.every((k) => k === allKinds[0])
|
|
112
|
+
|
|
113
|
+
const locator = formatLocator(params)
|
|
114
|
+
|
|
115
|
+
if (!sameKind) {
|
|
116
|
+
if (typeof console !== 'undefined') {
|
|
117
|
+
// eslint-disable-next-line no-console
|
|
118
|
+
console.warn(
|
|
119
|
+
`[xref] mixed-kind cluster (${[...new Set(allKinds)].join(', ')}) — falling back to comma-separated rendering`,
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
const parts = resolved.map((r) =>
|
|
123
|
+
r.missing ? `[?${r.id}]` : renderEntry(r.entry, r.kindMeta),
|
|
124
|
+
)
|
|
125
|
+
return <span className="xref">{parts.join(', ')}{locator}</span>
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const onlyResolved = resolved.filter((r) => !r.missing)
|
|
129
|
+
const text = onlyResolved.length > 0
|
|
130
|
+
? renderGroupSameKind(
|
|
131
|
+
onlyResolved.map((r) => r.entry),
|
|
132
|
+
onlyResolved[0].kindMeta,
|
|
133
|
+
)
|
|
134
|
+
: ''
|
|
135
|
+
|
|
136
|
+
const missingTail = resolved.filter((r) => r.missing).map((r) => `[?${r.id}]`).join(', ')
|
|
137
|
+
const body = [text, missingTail].filter(Boolean).join(', ')
|
|
138
|
+
|
|
139
|
+
return <span className="xref">{body}{locator}</span>
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export default Ref
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @uniweb/kit/xref — cross-reference machinery for foundations that
|
|
3
|
+
* support `[#id]` markdown markers.
|
|
4
|
+
*
|
|
5
|
+
* Foundations using cross-references (academic, documentation, long-form)
|
|
6
|
+
* import:
|
|
7
|
+
* - `Ref` to register in `defaultInsets` so content-reader's
|
|
8
|
+
* `inset_ref { component: 'Ref' }` markers resolve.
|
|
9
|
+
* - `buildXrefRegistry` if they want to trigger registry construction
|
|
10
|
+
* manually (the runtime does it automatically when foundation
|
|
11
|
+
* declares `xref:`).
|
|
12
|
+
* - `getXrefRegistry` to read the registry from list sections (e.g.
|
|
13
|
+
* ListOfFigures).
|
|
14
|
+
*
|
|
15
|
+
* Foundations that do NOT use cross-references never import this
|
|
16
|
+
* subpath; kit's tree-shaking strips the entire xref module from their
|
|
17
|
+
* bundle.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export { buildXrefRegistry, getXrefRegistry } from './registry.js'
|
|
21
|
+
export {
|
|
22
|
+
XREF_STYLES,
|
|
23
|
+
DEFAULT_XREF_STYLE,
|
|
24
|
+
resolveXrefStyle,
|
|
25
|
+
getKindMeta,
|
|
26
|
+
} from './styles.js'
|
|
27
|
+
export { Ref, default as RefDefault } from './Ref.jsx'
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-reference registry — per-website lookup table from `{#id}` to
|
|
3
|
+
* counter / kind / metadata.
|
|
4
|
+
*
|
|
5
|
+
* The walker visits the parsed document tree (siteContent.pages[].
|
|
6
|
+
* sections[].content) once, finds every block-level element carrying a
|
|
7
|
+
* `{#id}` attribute, infers the element's kind from its node type, and
|
|
8
|
+
* records the entry with a per-kind counter. The registry is consumed
|
|
9
|
+
* by:
|
|
10
|
+
* - The framework's <Ref> component to render `[#id]` cross-references
|
|
11
|
+
* as "Figure 3" / "§3.2" / etc.
|
|
12
|
+
* - List sections (foundation-side: ListOfFigures / ListOfTables /
|
|
13
|
+
* TableOfContents) to enumerate captioned figures/tables/headings.
|
|
14
|
+
* - Press LaTeX adapters' \autoref helpers to filter unresolved ids.
|
|
15
|
+
*
|
|
16
|
+
* Built-in kinds:
|
|
17
|
+
* - heading → 'section' (hierarchical: 1, 1.1, 1.1.1, 2, …)
|
|
18
|
+
* - image → 'figure' (flat arabic counter)
|
|
19
|
+
* - math → 'equation' (flat arabic counter; display:true only)
|
|
20
|
+
* - table → 'table' (flat arabic counter)
|
|
21
|
+
*
|
|
22
|
+
* Foundation extensions: foundations that need additional kinds (e.g.
|
|
23
|
+
* theorem, lemma, definition for an academic foundation) declare them
|
|
24
|
+
* via `foundation.xref.kinds`. The id-collection pass uses each kind's
|
|
25
|
+
* `prefix` field (e.g. `thm` → `theorem`) to classify ids that don't
|
|
26
|
+
* land on a built-in element type.
|
|
27
|
+
*
|
|
28
|
+
* Storage: a WeakMap keyed by Website. The registry is NOT attached as
|
|
29
|
+
* a property on the Website object — keeping the persistent vanilla-JS
|
|
30
|
+
* object graph free of feature-specific properties is a deliberate
|
|
31
|
+
* Uniweb convention. Same pattern as cite-registry.js (book foundation)
|
|
32
|
+
* and Press's DocumentProvider registration store.
|
|
33
|
+
*
|
|
34
|
+
* Lifecycle: built once per website at runtime initialization (see
|
|
35
|
+
* runtime/src/setup.js / ssr-renderer.js's initPrerender — they detect
|
|
36
|
+
* `foundation.xref` and trigger the build). Consumers read it via
|
|
37
|
+
* `getXrefRegistry(website)`. When the website becomes unreachable
|
|
38
|
+
* (page navigation, editor preview swap), the WeakMap entry is GC-
|
|
39
|
+
* eligible automatically.
|
|
40
|
+
*
|
|
41
|
+
* Output shape per entry:
|
|
42
|
+
* {
|
|
43
|
+
* id, // the id itself
|
|
44
|
+
* kind, // 'figure' | 'equation' | 'section' | 'table' | <foundation-declared>
|
|
45
|
+
* counter, // number for flat kinds, dotted string for hierarchical
|
|
46
|
+
* counterText, // displayable counter ('3', '3.2')
|
|
47
|
+
* sourcePath, // page route the id was declared on (for back-refs)
|
|
48
|
+
* caption, // (figures, tables) caption attr, when set
|
|
49
|
+
* text, // (sections) heading's plain text content
|
|
50
|
+
* latex, // (equations) latex source for the equation
|
|
51
|
+
* }
|
|
52
|
+
*
|
|
53
|
+
* `caption`, `text`, and `latex` are populated only for the elements
|
|
54
|
+
* that carry them — undefined on other kinds.
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
const KIND_BY_TYPE = {
|
|
58
|
+
heading: 'section',
|
|
59
|
+
image: 'figure',
|
|
60
|
+
math: 'equation',
|
|
61
|
+
table: 'table',
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Per-website registry storage. Keyed by the Website instance (or any
|
|
66
|
+
* object — the WeakMap doesn't enforce a type). Entries are GC-eligible
|
|
67
|
+
* once the website becomes unreachable.
|
|
68
|
+
*/
|
|
69
|
+
const REGISTRIES = new WeakMap()
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Walk the website's parsed content tree to build the registry. Stores
|
|
73
|
+
* the result keyed by the website instance.
|
|
74
|
+
*
|
|
75
|
+
* @param {Object} website - The Website instance the registry belongs
|
|
76
|
+
* to. Used as the WeakMap key. Should expose its parsed content
|
|
77
|
+
* either at `website.rawContent` or `website.siteContent` depending
|
|
78
|
+
* on how the runtime exposes it; the walker accepts both.
|
|
79
|
+
* @param {Object} [options]
|
|
80
|
+
* @param {Object} [options.foundationKinds] - Map of kind → metadata
|
|
81
|
+
* the foundation declared via `foundation.xref.kinds`. Drives id-
|
|
82
|
+
* prefix classification for non-built-in kinds (e.g. thm-foo →
|
|
83
|
+
* theorem) and is referenced when an explicit `kind` attribute on a
|
|
84
|
+
* node points at a foundation-declared kind.
|
|
85
|
+
* @param {Function} [options.onWarn] - Per-warning callback. Default
|
|
86
|
+
* logs to console.
|
|
87
|
+
* @returns {{entries: Object}} The built registry.
|
|
88
|
+
*/
|
|
89
|
+
export function buildXrefRegistry(website, options = {}) {
|
|
90
|
+
const { foundationKinds = {}, onWarn = (msg) => console.warn(msg) } = options
|
|
91
|
+
|
|
92
|
+
const entries = {}
|
|
93
|
+
const flatCounters = {}
|
|
94
|
+
const sectionStack = []
|
|
95
|
+
|
|
96
|
+
const prefixToKind = {}
|
|
97
|
+
for (const [kind, meta] of Object.entries(foundationKinds)) {
|
|
98
|
+
if (meta?.prefix) prefixToKind[meta.prefix] = kind
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function nextFlat(kind) {
|
|
102
|
+
flatCounters[kind] = (flatCounters[kind] || 0) + 1
|
|
103
|
+
return flatCounters[kind]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let topLevel = null
|
|
107
|
+
|
|
108
|
+
function nextHierarchical(level) {
|
|
109
|
+
if (topLevel == null || level < topLevel) topLevel = level
|
|
110
|
+
const depth = level - topLevel + 1
|
|
111
|
+
while (sectionStack.length >= depth) sectionStack.pop()
|
|
112
|
+
while (sectionStack.length < depth - 1) sectionStack.push(1)
|
|
113
|
+
sectionStack.push((sectionStack[depth - 1] || 0) + 1)
|
|
114
|
+
sectionStack.length = depth
|
|
115
|
+
return sectionStack.slice().join('.')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function inferKind(el) {
|
|
119
|
+
// Built-in: element type matches a registered kind.
|
|
120
|
+
const builtin = KIND_BY_TYPE[el.type]
|
|
121
|
+
if (builtin) return builtin
|
|
122
|
+
// Explicit kind attribute on the element.
|
|
123
|
+
if (el.attrs?.kind && (KIND_BY_TYPE[el.attrs.kind] || foundationKinds[el.attrs.kind])) {
|
|
124
|
+
return el.attrs.kind
|
|
125
|
+
}
|
|
126
|
+
// Foundation-declared prefix on the id.
|
|
127
|
+
const id = readId(el)
|
|
128
|
+
if (id && id.includes('-')) {
|
|
129
|
+
const prefix = id.slice(0, id.indexOf('-'))
|
|
130
|
+
if (prefixToKind[prefix]) return prefixToKind[prefix]
|
|
131
|
+
}
|
|
132
|
+
return null
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Sequence elements expose `id` differently per element type. Most
|
|
136
|
+
// (heading, image, table) carry it under `attrs`; math promotes it
|
|
137
|
+
// to a top-level field (per @uniweb/semantic-parser's sequence
|
|
138
|
+
// builder). Read both.
|
|
139
|
+
function readId(el) {
|
|
140
|
+
return el.attrs?.id ?? el.id ?? null
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function visit(el, sourcePath) {
|
|
144
|
+
let counter = null
|
|
145
|
+
let counterText = null
|
|
146
|
+
if (el.type === 'heading') {
|
|
147
|
+
const level = Math.max(1, Math.min(6, el.level || el.attrs?.level || 1))
|
|
148
|
+
counterText = nextHierarchical(level)
|
|
149
|
+
counter = counterText
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const id = readId(el)
|
|
153
|
+
if (!id) return
|
|
154
|
+
|
|
155
|
+
const kind = inferKind(el)
|
|
156
|
+
if (!kind) {
|
|
157
|
+
onWarn(`[xref] {#${id}} on unrecognized element type "${el.type}" — ignored`)
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
if (entries[id]) {
|
|
161
|
+
onWarn(`[xref] duplicate id "${id}" — keeping first registration`)
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
if (counter == null) {
|
|
165
|
+
counter = nextFlat(kind)
|
|
166
|
+
counterText = String(counter)
|
|
167
|
+
}
|
|
168
|
+
const entry = { id, kind, counter, counterText, sourcePath: sourcePath || '' }
|
|
169
|
+
const caption = el.attrs?.caption
|
|
170
|
+
if ((kind === 'figure' || kind === 'table') && caption) {
|
|
171
|
+
entry.caption = String(caption)
|
|
172
|
+
}
|
|
173
|
+
if (el.type === 'heading' && el.text) {
|
|
174
|
+
entry.text = String(el.text)
|
|
175
|
+
}
|
|
176
|
+
if (el.type === 'math' && el.latex) {
|
|
177
|
+
entry.latex = String(el.latex)
|
|
178
|
+
}
|
|
179
|
+
entries[id] = entry
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Walk the Website's object graph: pages → bodyBlocks. Each Block's
|
|
183
|
+
// `parsedContent.sequence` is a flat document-order array of
|
|
184
|
+
// semantic elements (heading, paragraph, image, list, blockquote,
|
|
185
|
+
// codeBlock, table, math, divider, …). Iterating the sequence is
|
|
186
|
+
// enough — the registry's id-bearing kinds (heading, image,
|
|
187
|
+
// math (display:true), table) all live at sequence level.
|
|
188
|
+
//
|
|
189
|
+
// Website.pages, Page.bodyBlocks, and Block.parsedContent.sequence
|
|
190
|
+
// are framework invariants — always arrays. No defensive guards.
|
|
191
|
+
for (const page of website.pages) {
|
|
192
|
+
for (const block of page.bodyBlocks) {
|
|
193
|
+
for (const el of block.parsedContent.sequence) {
|
|
194
|
+
visit(el, page.route)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const registry = { entries }
|
|
200
|
+
REGISTRIES.set(website, registry)
|
|
201
|
+
return registry
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Read the registry previously built for a website. Returns null if no
|
|
206
|
+
* registry was built (e.g. the foundation didn't declare `xref` so
|
|
207
|
+
* runtime initialization didn't trigger `buildXrefRegistry`).
|
|
208
|
+
*
|
|
209
|
+
* Consumers (Ref renderer, ListOfFigures section, press LaTeX inset
|
|
210
|
+
* formatters) handle the null case gracefully — typically by treating
|
|
211
|
+
* it as an empty entries map, which renders cross-refs as "[?<id>]"
|
|
212
|
+
* placeholders.
|
|
213
|
+
*/
|
|
214
|
+
export function getXrefRegistry(website) {
|
|
215
|
+
if (!website || typeof website !== 'object') return null
|
|
216
|
+
return REGISTRIES.get(website) || null
|
|
217
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-reference style catalog.
|
|
3
|
+
*
|
|
4
|
+
* Cross-references are framework-managed (parallel to citestyle's nine
|
|
5
|
+
* citation styles, but framework-internal because there's no CSL-for-
|
|
6
|
+
* xrefs standard). Foundations pick a preset; documents may override
|
|
7
|
+
* per-kind.
|
|
8
|
+
*
|
|
9
|
+
* Each preset is a map from kind name to:
|
|
10
|
+
* {
|
|
11
|
+
* label, // singular label, e.g. "Figure"
|
|
12
|
+
* labelPlural, // plural label, e.g. "Figures"
|
|
13
|
+
* counter, // 'arabic' | 'hierarchical' | null (no counter)
|
|
14
|
+
* sep, // separator between label and counter ("" | " ")
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* The catalog ships four presets:
|
|
18
|
+
* - humanities (default — "Figure 3", "§3.2", "Equation 1")
|
|
19
|
+
* - engineering ("Fig. 3", "Sec. 3.2", "Eq. 1")
|
|
20
|
+
* - german ("Abb. 3", "Abschn. 3.2", "Gl. 1")
|
|
21
|
+
* - plain (counters only — "3", "3.2", "1")
|
|
22
|
+
*
|
|
23
|
+
* Foundations override the preset by declaring `xref.kinds` in
|
|
24
|
+
* foundation.js (additive: new kinds and overrides for existing kinds).
|
|
25
|
+
* Documents override per-kind via `book.xref.<kind>:` in document.yml.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const HUMANITIES = {
|
|
29
|
+
figure: { label: 'Figure', labelPlural: 'Figures', counter: 'arabic', sep: ' ' },
|
|
30
|
+
equation: { label: 'Equation', labelPlural: 'Equations', counter: 'arabic', sep: ' ' },
|
|
31
|
+
section: { label: '§', labelPlural: '§§', counter: 'hierarchical', sep: '' },
|
|
32
|
+
table: { label: 'Table', labelPlural: 'Tables', counter: 'arabic', sep: ' ' },
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const ENGINEERING = {
|
|
36
|
+
figure: { label: 'Fig.', labelPlural: 'Figs.', counter: 'arabic', sep: ' ' },
|
|
37
|
+
equation: { label: 'Eq.', labelPlural: 'Eqs.', counter: 'arabic', sep: ' ' },
|
|
38
|
+
section: { label: 'Sec.', labelPlural: 'Secs.', counter: 'hierarchical', sep: ' ' },
|
|
39
|
+
table: { label: 'Tab.', labelPlural: 'Tabs.', counter: 'arabic', sep: ' ' },
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const GERMAN = {
|
|
43
|
+
figure: { label: 'Abb.', labelPlural: 'Abb.', counter: 'arabic', sep: ' ' },
|
|
44
|
+
equation: { label: 'Gl.', labelPlural: 'Gl.', counter: 'arabic', sep: ' ' },
|
|
45
|
+
section: { label: 'Abschn.', labelPlural: 'Abschn.', counter: 'hierarchical', sep: ' ' },
|
|
46
|
+
table: { label: 'Tab.', labelPlural: 'Tab.', counter: 'arabic', sep: ' ' },
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const PLAIN = {
|
|
50
|
+
figure: { label: '', labelPlural: '', counter: 'arabic', sep: '' },
|
|
51
|
+
equation: { label: '', labelPlural: '', counter: 'arabic', sep: '' },
|
|
52
|
+
section: { label: '', labelPlural: '', counter: 'hierarchical', sep: '' },
|
|
53
|
+
table: { label: '', labelPlural: '', counter: 'arabic', sep: '' },
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const XREF_STYLES = {
|
|
57
|
+
humanities: HUMANITIES,
|
|
58
|
+
engineering: ENGINEERING,
|
|
59
|
+
german: GERMAN,
|
|
60
|
+
plain: PLAIN,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const DEFAULT_XREF_STYLE = 'humanities'
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resolve the active xref-style for a document. Reads:
|
|
67
|
+
* - presetName (book.xrefStyle: in document config).
|
|
68
|
+
* - `book.xref.<kind>:` per-kind overrides on top of the preset.
|
|
69
|
+
* - Foundation-declared kinds (`foundation.xref.kinds`) extend the
|
|
70
|
+
* preset with whatever the foundation provides.
|
|
71
|
+
*
|
|
72
|
+
* Returns a plain map keyed by kind name; per-kind values are merged
|
|
73
|
+
* objects (preset + foundation extensions + document overrides).
|
|
74
|
+
*/
|
|
75
|
+
export function resolveXrefStyle(presetName, config) {
|
|
76
|
+
const preset = XREF_STYLES[presetName] || XREF_STYLES[DEFAULT_XREF_STYLE]
|
|
77
|
+
const merged = { ...preset }
|
|
78
|
+
|
|
79
|
+
// Foundation extensions: kinds declared by the foundation (e.g.
|
|
80
|
+
// theorem, lemma, proof for a math foundation).
|
|
81
|
+
const foundationKinds = globalThis.uniweb?.foundationConfig?.xref?.kinds
|
|
82
|
+
if (foundationKinds && typeof foundationKinds === 'object') {
|
|
83
|
+
for (const [kind, meta] of Object.entries(foundationKinds)) {
|
|
84
|
+
merged[kind] = { ...merged[kind], ...meta }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Document overrides: `book.xref.<kind>:` granular tweaks.
|
|
89
|
+
const docOverrides = config?.book?.xref || config?.xref || null
|
|
90
|
+
if (docOverrides && typeof docOverrides === 'object') {
|
|
91
|
+
for (const [kind, meta] of Object.entries(docOverrides)) {
|
|
92
|
+
if (meta && typeof meta === 'object') {
|
|
93
|
+
merged[kind] = { ...merged[kind], ...meta }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return merged
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function getKindMeta(style, kind) {
|
|
102
|
+
return style?.[kind] || null
|
|
103
|
+
}
|