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
/*
SPDX-License-Identifier: MIT OR Apache-2.0
SPDX-FileCopyrightText: © 2023-2026 Bruce D'Arcus and Citum contributors
*/
//! The Citum style model.
use std::collections::HashMap;
#[cfg(feature = "schema")]
use schemars::JsonSchema;
use serde::de::Error as _;
use serde::{Deserialize, Serialize};
#[allow(unused_imports, reason = "Referenced by intra-doc links.")]
use crate::ResolutionError;
use crate::style_base;
use crate::{BibliographySpec, CitationSpec, Config, SchemaVersion, StyleInfo, Template};
/// The new Citum Style model.
///
/// This is the target schema for Citum, featuring declarative options
/// and simple template components instead of procedural conditionals.
#[derive(Debug, Default, Deserialize, Serialize, Clone)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub struct Style {
/// Style schema version.
#[serde(default)]
pub version: SchemaVersion,
/// Style metadata.
#[serde(default)]
pub info: StyleInfo,
/// Named reusable templates.
#[serde(skip_serializing_if = "Option::is_none")]
pub templates: Option<HashMap<String, Template>>,
/// Global style options.
#[serde(skip_serializing_if = "Option::is_none")]
pub options: Option<Config>,
/// Citation specification.
#[serde(skip_serializing_if = "Option::is_none")]
pub citation: Option<CitationSpec>,
/// Bibliography specification.
#[serde(skip_serializing_if = "Option::is_none")]
pub bibliography: Option<BibliographySpec>,
/// Custom user-defined fields for extensions.
#[serde(skip_serializing_if = "Option::is_none")]
pub custom: Option<HashMap<String, serde_json::Value>>,
/// Extends a base style, with optional local overrides.
///
/// When present, the base [`StyleReference`](style_base::StyleReference) is resolved and the local
/// overrides are merged before any further processing. Explicit `options`,
/// `citation`, and `bibliography` keys at the same document level take
/// precedence over the resolved base.
#[serde(skip_serializing_if = "Option::is_none")]
pub extends: Option<style_base::StyleReference>,
/// Optional content-addressed integrity pin for the parent style referenced
/// by [`extends`](Self::extends).
///
/// When present, the resolver verifies that the SHA-256 of the fetched
/// parent matches this CIDv1 string before merging. Mismatches abort
/// resolution with [`ResolutionError::IntegrityFailure`]. Absent means
/// "no integrity check" — appropriate for `file://` parents under user
/// control or trusted local registries.
#[serde(rename = "extends-pin", skip_serializing_if = "Option::is_none")]
pub extends_pin: Option<String>,
/// Raw YAML captured when the style was loaded via [`Style::from_yaml_str`]
/// or [`Style::from_yaml_bytes`]. Used during style resolution for
/// null-aware overlay merging (e.g., `ibid: ~` correctly clears an
/// inherited preset value). Absent in programmatically-constructed styles.
#[cfg_attr(feature = "schema", schemars(skip))]
#[serde(skip, default)]
pub raw_yaml: Option<serde_yaml::Value>,
/// Forward-compat: captures unknown keys when an older engine reads a
/// style produced by a newer schema. Empty by default; treated as a
/// SoftDegrade signal. See `docs/specs/FORWARD_COMPATIBILITY.md`.
#[serde(
flatten,
default,
skip_serializing_if = "std::collections::BTreeMap::is_empty"
)]
#[cfg_attr(feature = "schema", schemars(skip))]
pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
}
impl Style {
/// Parse a Citum style from a YAML string, preserving raw YAML for
/// null-aware overlay merging during base resolution.
///
/// Preferred over `serde_yaml::from_str` when the style extends a base,
/// so that `ibid: ~` and similar null overrides correctly clear inherited values.
///
/// # Errors
///
/// Returns a serde error if YAML parsing or deserialization fails.
pub fn from_yaml_str(s: &str) -> Result<Self, serde_yaml::Error> {
let raw: serde_yaml::Value = serde_yaml::from_str(s)?;
super::diagnostics::validate_raw_style(&raw).map_err(serde_yaml::Error::custom)?;
let mut style: Style = serde_yaml::from_value(raw.clone())?;
style.raw_yaml = Some(raw);
style
.validate_resource_limits()
.map_err(serde_yaml::Error::custom)?;
Ok(style)
}
/// Apply scoped citation and bibliography option overrides to this style.
///
/// Translates typed option values (label mode, label wrap, repeated-author
/// rendering, date position, title terminator) into concrete template mutations.
/// Call this after mutating `bibliography.options` at runtime — e.g. after
/// applying per-document overrides — so that template state stays consistent
/// with the option values.
pub fn apply_scoped_options(&mut self) {
crate::options::scoped::apply_scoped_style_options(self);
}
/// Merge a partial overlay style over this style in place; overlay fields win.
///
/// Overlay merging is typed and matches `extends` inheritance for the fields it supports:
/// - `info`, `templates`, `options`, and `custom` are merged (overlay wins for `Some` fields / keys).
/// - `citation` / `bibliography` are deep-merged; explicit YAML `~` can clear inherited fields when
/// `overlay.raw_yaml` is populated (e.g. via `Style::from_yaml_bytes`).
///
/// The caller is responsible for calling [`apply_scoped_options`](Self::apply_scoped_options)
/// afterwards if scoped-option side-effects (label-wrap, date-position, etc.) are needed.
pub fn apply_overlay(&mut self, overlay: &Style) {
super::overlay::merge_style_overlay(self, overlay);
}
/// Parse a Citum style from YAML bytes, preserving raw YAML for
/// null-aware overlay merging during preset resolution.
///
/// # Errors
///
/// Returns a serde error if YAML parsing or deserialization fails.
pub fn from_yaml_bytes(bytes: &[u8]) -> Result<Self, serde_yaml::Error> {
let raw: serde_yaml::Value = serde_yaml::from_slice(bytes)?;
super::diagnostics::validate_raw_style(&raw).map_err(serde_yaml::Error::custom)?;
let mut style: Style = serde_yaml::from_value(raw.clone())?;
style.raw_yaml = Some(raw);
style
.validate_resource_limits()
.map_err(serde_yaml::Error::custom)?;
Ok(style)
}
}