csl-legacy 0.80.0

Legacy CSL 1.0 XML and CSL-JSON parsers — infrastructure for the Citum migration pipeline
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
/*
SPDX-License-Identifier: MIT OR Apache-2.0
SPDX-FileCopyrightText: © 2023-2026 Bruce D'Arcus and Citum contributors
*/

use serde::{Deserialize, Serialize};

/// A parsed CSL 1.0 style.
///
/// Corresponds to the root `<style>` element in a `.csl` XML file.
/// After parsing, this struct is the primary IR handed to the migration pipeline.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Style {
    /// CSL specification version declared in the style (e.g., `"1.0"`).
    pub version: String,
    /// XML namespace URI (typically `"http://purl.org/net/xbiblio/csl"`).
    pub xmlns: String,
    /// Citation class: `"in-text"` or `"note"`.
    pub class: String,
    /// The default locale for this style (e.g., `"en-US"`, `"de-DE"`).
    pub default_locale: Option<String>,
    /// Style-level name formatting options (inherited by all names unless overridden).
    pub initialize_with: Option<String>,
    /// Whether to include a hyphen when initialising given names.
    pub initialize_with_hyphen: Option<bool>,
    /// Delimiter inserted between rendered names.
    pub names_delimiter: Option<String>,
    /// Which names are rendered in sort order (`"first"` or `"all"`).
    pub name_as_sort_order: Option<String>,
    /// String placed between family name and given name in sort order (default `", "`).
    pub sort_separator: Option<String>,
    /// When the Oxford comma is inserted before the last name delimiter.
    pub delimiter_precedes_last: Option<String>,
    /// When the name delimiter precedes "et al." truncation.
    pub delimiter_precedes_et_al: Option<String>,
    /// Controls sorting/display of non-dropping particles (e.g., `"display-and-sort"`).
    pub demote_non_dropping_particle: Option<String>,
    /// Conjunction word inserted before the last name (`"text"` → locale term, `"symbol"` → `&`).
    pub and: Option<String>,
    /// Page range formatting algorithm (`"expanded"`, `"minimal"`, `"chicago"`, `"chicago-16"`).
    pub page_range_format: Option<String>,
    /// Style metadata (`<info>` element).
    pub info: Info,
    /// In-style locale overrides (`<locale>` elements).
    pub locale: Vec<Locale>,
    /// Named macro definitions (`<macro>` elements).
    pub macros: Vec<Macro>,
    /// Citation rendering configuration (`<citation>` element).
    pub citation: Citation,
    /// Bibliography rendering configuration (`<bibliography>` element), if present.
    pub bibliography: Option<Bibliography>,
}

/// Metadata block for a CSL style (`<info>` element).
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
pub struct Info {
    /// Human-readable style title.
    pub title: String,
    /// Canonical URI uniquely identifying this style.
    pub id: String,
    /// ISO 8601 date-time string of the last update.
    pub updated: String,
    /// Subject-area categories (`field=` attribute values; `generic-base` is silently ignored).
    pub fields: Vec<String>,
    /// Optional short description of the style.
    pub summary: Option<String>,
    /// Links to related resources (self-link, template, documentation, etc.).
    pub links: Vec<InfoLink>,
    /// Style authors.
    pub authors: Vec<InfoPerson>,
    /// Style contributors.
    pub contributors: Vec<InfoPerson>,
    /// License URI from `<rights license="…">` or the element's text content.
    pub rights: Option<String>,
}

/// A hyperlink entry inside `<info>` (`<link href="…" rel="…"/>`).
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
pub struct InfoLink {
    /// Absolute URI of the linked resource.
    pub href: String,
    /// Link relation type (e.g., `"self"`, `"template"`, `"documentation"`).
    pub rel: Option<String>,
}

/// A person credited in `<info>` as an `<author>` or `<contributor>`.
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
pub struct InfoPerson {
    /// Display name of the person.
    pub name: Option<String>,
    /// Contact email address.
    pub email: Option<String>,
    /// Personal or institutional URI.
    pub uri: Option<String>,
}

/// A locale block inside the style (`<locale xml:lang="…">`).
///
/// Locale blocks supply style-specific term overrides for a given language.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Locale {
    /// BCP 47 language tag (e.g., `"en-US"`); `None` applies to all locales.
    pub lang: Option<String>,
    /// Term definitions provided by this locale block.
    pub terms: Vec<Term>,
}

/// A term definition inside a `<locale>` block (`<term name="…">`).
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Term {
    /// CSL term name (e.g., `"editor"`, `"page"`).
    pub name: String,
    /// Term form (`"long"`, `"short"`, `"verb"`, `"verb-short"`, `"symbol"`).
    pub form: Option<String>,
    /// Text value for terms without plural/singular distinction.
    pub value: String,
    /// Singular form (used when the term has `<single>` / `<multiple>` children).
    pub single: Option<String>,
    /// Plural form (used when the term has `<single>` / `<multiple>` children).
    pub multiple: Option<String>,
}

/// A named macro (`<macro name="…">`).
///
/// Macros group reusable rendering logic and are referenced by `<text macro="…">`.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Macro {
    /// The macro's name as it appears in `macro=` references.
    pub name: String,
    /// Top-level CSL nodes that make up the macro body.
    pub children: Vec<CslNode>,
}

/// Citation configuration (`<citation>` element).
///
/// Controls how in-text or note citations are rendered and sorted.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Citation {
    /// The `<layout>` block that renders each individual citation.
    pub layout: Layout,
    /// Ordered CSL-M layouts, including the final unscoped fallback.
    ///
    /// This is empty for conventional CSL with zero or one unscoped layout.
    #[serde(default)]
    pub localized_layouts: Vec<LocalizedLayout>,
    /// Optional sort order applied to cites within a single citation cluster.
    pub sort: Option<Sort>,
    /// Collapse mode for grouped citations (for example, `"citation-number"`).
    pub collapse: Option<String>,
    /// Minimum number of names before et-al truncation kicks in.
    pub et_al_min: Option<usize>,
    /// Number of names to show before the et-al term.
    pub et_al_use_first: Option<usize>,
    /// If `true`, a year-suffix is appended to disambiguate ambiguous cites.
    pub disambiguate_add_year_suffix: Option<bool>,
    /// If `true`, additional names are expanded for disambiguation.
    pub disambiguate_add_names: Option<bool>,
    /// If `true`, given names are expanded for disambiguation.
    pub disambiguate_add_givenname: Option<bool>,
    /// Rule controlling which author positions receive given-name expansion.
    pub disambiguate_givenname_rule: Option<String>,
}

/// Bibliography configuration (`<bibliography>` element).
///
/// Controls how the reference list is rendered and sorted.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Bibliography {
    /// The `<layout>` block that renders each bibliography entry.
    pub layout: Layout,
    /// Ordered CSL-M layouts, including the final unscoped fallback.
    ///
    /// This is empty for conventional CSL with zero or one unscoped layout.
    #[serde(default)]
    pub localized_layouts: Vec<LocalizedLayout>,
    /// Optional sort order applied to bibliography entries.
    pub sort: Option<Sort>,
    /// Minimum number of names before et-al truncation kicks in.
    pub et_al_min: Option<usize>,
    /// Number of names to show before the et-al term.
    pub et_al_use_first: Option<usize>,
    /// If `true`, a hanging indent is applied to each entry.
    pub hanging_indent: Option<bool>,
    /// String substituted for repeated subsequent authors (e.g., `"---"`).
    pub subsequent_author_substitute: Option<String>,
    /// Rule controlling when `subsequent_author_substitute` is applied.
    pub subsequent_author_substitute_rule: Option<String>,
}

/// A layout block (`<layout prefix="…" suffix="…" delimiter="…">`).
///
/// Wraps a sequence of CSL nodes that are rendered as a unit.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Layout {
    /// String prepended before the rendered output.
    pub prefix: Option<String>,
    /// String appended after the rendered output.
    pub suffix: Option<String>,
    /// String inserted between consecutive rendered children.
    pub delimiter: Option<String>,
    /// The rendering nodes contained in this layout.
    pub children: Vec<CslNode>,
}

/// A locale-scoped CSL-M layout in source order.
///
/// An empty `locales` list identifies the required final fallback layout.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct LocalizedLayout {
    /// Whitespace-separated language tags from the CSL-M `locale` attribute.
    pub locales: Vec<String>,
    /// Rendering layout selected for those locales.
    pub layout: Layout,
}

/// A sort specification (`<sort>`).
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Sort {
    /// Ordered list of sort keys.
    pub keys: Vec<SortKey>,
}

/// A single sort key (`<key variable="…">` or `<key macro="…">`).
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SortKey {
    /// Variable name to sort by (mutually exclusive with `macro_name`).
    pub variable: Option<String>,
    /// Macro name to sort by (mutually exclusive with `variable`).
    pub macro_name: Option<String>,
    /// Sort direction: `"ascending"` (default) or `"descending"`.
    pub sort: Option<String>,
}

/// A CSL rendering node — one of the ten node types defined by CSL 1.0.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum CslNode {
    /// Renders a text value, variable, macro, or term.
    Text(Text),
    /// Renders a date variable.
    Date(Date),
    /// Renders a localised label for a variable.
    Label(Label),
    /// Renders a name list variable.
    Names(Names),
    /// Groups child nodes; suppressed when all children render empty.
    Group(Group),
    /// Conditional branching.
    Choose(Choose),
    /// Renders a numeric variable with optional formatting.
    Number(Number),
    /// Configures name formatting within a `<names>` block.
    Name(Name),
    /// Customises the "et al." term within a `<names>` block.
    EtAl(EtAl),
    /// Fallback rendering when the primary name variable is empty.
    Substitute(Substitute),
}

/// A `<text>` node — renders a literal value, variable, macro reference, or term.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Text {
    /// Literal string value (`value="…"`).
    pub value: Option<String>,
    /// Name of the reference variable to render.
    pub variable: Option<String>,
    /// Name of the macro to call.
    pub macro_name: Option<String>,
    /// Name of the locale term to render.
    pub term: Option<String>,
    /// Term form (`"long"`, `"short"`, etc.).
    pub form: Option<String>,
    /// String prepended before the rendered output.
    pub prefix: Option<String>,
    /// String appended after the rendered output.
    pub suffix: Option<String>,
    /// If `true`, the rendered text is wrapped in locale-defined quotation marks.
    pub quotes: Option<bool>,
    /// Text case transformation (`"uppercase"`, `"lowercase"`, `"capitalize-first"`, etc.).
    pub text_case: Option<String>,
    /// If `true`, trailing periods are stripped from the output.
    pub strip_periods: Option<bool>,
    /// Plural behaviour for term rendering (`"always"`, `"never"`, `"contextual"`).
    pub plural: Option<String>,
    /// Internal: position of this node in the macro call order (set during post-processing).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub macro_call_order: Option<usize>,
    /// Inline formatting attributes.
    #[serde(flatten)]
    pub formatting: Formatting,
}

/// A `<name>` node — controls how individual names in a `<names>` list are rendered.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Name {
    /// Conjunction placed before the last name (`"text"` or `"symbol"`).
    pub and: Option<String>,
    /// Delimiter inserted between names.
    pub delimiter: Option<String>,
    /// Which names are rendered in sort order (`"first"` or `"all"`).
    pub name_as_sort_order: Option<String>,
    /// String placed between family and given name in sort order.
    pub sort_separator: Option<String>,
    /// String appended to each given-name initial (e.g., `"."`).
    pub initialize_with: Option<String>,
    /// Whether to include a hyphen when initialising hyphenated given names.
    pub initialize_with_hyphen: Option<bool>,
    /// Name form: `"long"` (given + family) or `"short"` (family only).
    pub form: Option<String>,
    /// When the Oxford comma is inserted before the last name.
    pub delimiter_precedes_last: Option<String>,
    /// When the name delimiter precedes "et al." truncation.
    pub delimiter_precedes_et_al: Option<String>,
    /// Minimum number of names before et-al truncation.
    pub et_al_min: Option<usize>,
    /// Number of names shown before the et-al term.
    pub et_al_use_first: Option<usize>,
    /// Minimum number of names for et-al truncation on subsequent cites.
    pub et_al_subsequent_min: Option<usize>,
    /// Names shown before et-al on subsequent cites.
    pub et_al_subsequent_use_first: Option<usize>,
    /// String prepended before the rendered output.
    pub prefix: Option<String>,
    /// String appended after the rendered output.
    pub suffix: Option<String>,
    /// Inline formatting attributes.
    #[serde(flatten)]
    pub formatting: Formatting,
}

/// A `<et-al>` node — customises the et-al term rendered inside `<names>`.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct EtAl {
    /// Locale term to use instead of the default `"et-al"`.
    pub term: Option<String>,
}

/// Inline text formatting attributes shared by many CSL node types.
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
pub struct Formatting {
    /// Font style: `"italic"`, `"oblique"`, or `"normal"`.
    pub font_style: Option<String>,
    /// Font variant: `"small-caps"` or `"normal"`.
    pub font_variant: Option<String>,
    /// Font weight: `"bold"`, `"light"`, or `"normal"`.
    pub font_weight: Option<String>,
    /// Text decoration: `"underline"` or `"none"`.
    pub text_decoration: Option<String>,
    /// Vertical alignment: `"sup"`, `"sub"`, or `"baseline"`.
    pub vertical_align: Option<String>,
    /// Display mode (primarily for bibliography entries): `"block"`, `"left-margin"`, `"right-inline"`, `"indent"`.
    pub display: Option<String>,
}

/// A `<substitute>` node — provides fallback rendering when the primary `<names>` variable is empty.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Substitute {
    /// Alternative rendering nodes tried in order until one produces output.
    pub children: Vec<CslNode>,
}

/// A `<date>` node — renders a date variable using a built-in or custom format.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Date {
    /// The date variable to render (e.g., `"issued"`, `"accessed"`).
    pub variable: String,
    /// Built-in date form: `"text"`, `"numeric"`, or `"ordinal"`.
    pub form: Option<String>,
    /// String prepended before the rendered output.
    pub prefix: Option<String>,
    /// String appended after the rendered output.
    pub suffix: Option<String>,
    /// Delimiter inserted between date parts.
    pub delimiter: Option<String>,
    /// Which date parts to render: `"year-month-day"`, `"year-month"`, or `"year"`.
    pub date_parts: Option<String>,
    /// Text case transformation applied to the rendered date.
    pub text_case: Option<String>,
    /// Custom `<date-part>` nodes that override the built-in form.
    pub parts: Vec<DatePart>,
    /// Internal: position in the macro call order (set during post-processing).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub macro_call_order: Option<usize>,
    /// Inline formatting attributes.
    #[serde(flatten)]
    pub formatting: Formatting,
}

/// A `<date-part>` node — configures rendering of one component of a date.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DatePart {
    /// The date component: `"day"`, `"month"`, or `"year"`.
    pub name: String,
    /// Rendering form for the component (e.g., `"numeric"`, `"long"`, `"short"`, `"ordinal"`).
    pub form: Option<String>,
    /// String prepended before this date part.
    pub prefix: Option<String>,
    /// String appended after this date part.
    pub suffix: Option<String>,
}

/// A `<label>` node — renders a localised term that describes a variable.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Label {
    /// The variable whose label is rendered (e.g., `"page"`, `"locator"`).
    pub variable: Option<String>,
    /// Term form (`"long"`, `"short"`, `"verb"`, `"verb-short"`, `"symbol"`).
    pub form: Option<String>,
    /// String prepended before the rendered output.
    pub prefix: Option<String>,
    /// String appended after the rendered output.
    pub suffix: Option<String>,
    /// Text case transformation applied to the label.
    pub text_case: Option<String>,
    /// If `true`, trailing periods are stripped from the label.
    pub strip_periods: Option<bool>,
    /// Plural behaviour: `"always"`, `"never"`, or `"contextual"`.
    pub plural: Option<String>,
    /// Internal: position in the macro call order (set during post-processing).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub macro_call_order: Option<usize>,
    /// Inline formatting attributes.
    #[serde(flatten)]
    pub formatting: Formatting,
}

/// A `<names>` node — renders one or more name-list variables.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Names {
    /// Space-separated list of name variables to render (e.g., `"author editor"`).
    pub variable: String,
    /// Delimiter inserted between names.
    pub delimiter: Option<String>,
    /// When the name delimiter precedes "et al." truncation.
    pub delimiter_precedes_et_al: Option<String>,
    /// Minimum number of names before et-al truncation.
    pub et_al_min: Option<usize>,
    /// Number of names rendered before the et-al term.
    pub et_al_use_first: Option<usize>,
    /// Et-al minimum for subsequent cites.
    pub et_al_subsequent_min: Option<usize>,
    /// Et-al use-first for subsequent cites.
    pub et_al_subsequent_use_first: Option<usize>,
    /// String prepended before the rendered output.
    pub prefix: Option<String>,
    /// String appended after the rendered output.
    pub suffix: Option<String>,
    /// Child nodes: `<name>`, `<label>`, `<substitute>`, `<et-al>`.
    pub children: Vec<CslNode>,
    /// Internal: position in the macro call order (set during post-processing).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub macro_call_order: Option<usize>,
    /// Inline formatting attributes.
    #[serde(flatten)]
    pub formatting: Formatting,
}

/// A `<group>` node — renders child nodes as a unit, suppressed when all children are empty.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Group {
    /// Delimiter inserted between non-empty children.
    pub delimiter: Option<String>,
    /// String prepended before the rendered group.
    pub prefix: Option<String>,
    /// String appended after the rendered group.
    pub suffix: Option<String>,
    /// Child rendering nodes.
    pub children: Vec<CslNode>,
    /// Internal: position in the macro call order (set during post-processing).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub macro_call_order: Option<usize>,
    /// Inline formatting attributes.
    #[serde(flatten)]
    pub formatting: Formatting,
}

/// A `<choose>` node — conditional rendering based on reference attributes.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Choose {
    /// The mandatory `<if>` branch.
    pub if_branch: ChooseBranch,
    /// Zero or more `<else-if>` branches evaluated in order.
    pub else_if_branches: Vec<ChooseBranch>,
    /// Optional `<else>` branch rendered when all preceding conditions fail.
    pub else_branch: Option<Vec<CslNode>>,
}

/// A conditional branch inside `<choose>` (`<if>` or `<else-if>`).
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ChooseBranch {
    /// How multiple test attributes are combined: `"all"` (default), `"any"`, or `"none"`.
    pub match_mode: Option<String>,
    /// Reference type test (e.g., `"book article-journal"`).
    pub type_: Option<String>,
    /// Variable presence test — branch fires when the variable is non-empty.
    pub variable: Option<String>,
    /// Numeric test — branch fires when the variable value is numeric.
    pub is_numeric: Option<String>,
    /// Uncertain-date test.
    pub is_uncertain_date: Option<String>,
    /// Locator type test.
    pub locator: Option<String>,
    /// Citation position test (`"first"`, `"subsequent"`, `"ibid"`, `"ibid-with-locator"`).
    pub position: Option<String>,
    /// Nodes rendered when this branch matches.
    pub children: Vec<CslNode>,
}

/// A `<number>` node — renders a numeric variable with optional form and formatting.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Number {
    /// The numeric variable to render (e.g., `"volume"`, `"issue"`, `"edition"`).
    pub variable: String,
    /// Rendering form: `"numeric"` (default), `"ordinal"`, `"long-ordinal"`, or `"roman"`.
    pub form: Option<String>,
    /// String prepended before the rendered output.
    pub prefix: Option<String>,
    /// String appended after the rendered output.
    pub suffix: Option<String>,
    /// Text case transformation.
    pub text_case: Option<String>,
    /// Internal: position in the macro call order (set during post-processing).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub macro_call_order: Option<usize>,
    /// Inline formatting attributes.
    #[serde(flatten)]
    pub formatting: Formatting,
}