Skip to main content

citum_schema_style/style/
model.rs

1/*
2SPDX-License-Identifier: MIT OR Apache-2.0
3SPDX-FileCopyrightText: © 2023-2026 Bruce D'Arcus and Citum contributors
4*/
5
6//! The Citum style model.
7
8use std::collections::HashMap;
9
10#[cfg(feature = "schema")]
11use schemars::JsonSchema;
12use serde::de::Error as _;
13use serde::{Deserialize, Serialize};
14
15#[allow(unused_imports, reason = "Referenced by intra-doc links.")]
16use crate::ResolutionError;
17use crate::style_base;
18use crate::{BibliographySpec, CitationSpec, Config, SchemaVersion, StyleInfo, Template};
19
20/// The new Citum Style model.
21///
22/// This is the target schema for Citum, featuring declarative options
23/// and simple template components instead of procedural conditionals.
24#[derive(Debug, Default, Deserialize, Serialize, Clone)]
25#[cfg_attr(feature = "schema", derive(JsonSchema))]
26#[serde(rename_all = "kebab-case")]
27pub struct Style {
28    /// Style schema version.
29    #[serde(default)]
30    pub version: SchemaVersion,
31    /// Style metadata.
32    #[serde(default)]
33    pub info: StyleInfo,
34    /// Named reusable templates.
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub templates: Option<HashMap<String, Template>>,
37    /// Global style options.
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub options: Option<Config>,
40    /// Citation specification.
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub citation: Option<CitationSpec>,
43    /// Bibliography specification.
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub bibliography: Option<BibliographySpec>,
46    /// Custom user-defined fields for extensions.
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub custom: Option<HashMap<String, serde_json::Value>>,
49    /// Extends a base style, with optional local overrides.
50    ///
51    /// When present, the base [`StyleReference`](style_base::StyleReference) is resolved and the local
52    /// overrides are merged before any further processing. Explicit `options`,
53    /// `citation`, and `bibliography` keys at the same document level take
54    /// precedence over the resolved base.
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub extends: Option<style_base::StyleReference>,
57    /// Optional content-addressed integrity pin for the parent style referenced
58    /// by [`extends`](Self::extends).
59    ///
60    /// When present, the resolver verifies that the SHA-256 of the fetched
61    /// parent matches this CIDv1 string before merging. Mismatches abort
62    /// resolution with [`ResolutionError::IntegrityFailure`]. Absent means
63    /// "no integrity check" — appropriate for `file://` parents under user
64    /// control or trusted local registries.
65    #[serde(rename = "extends-pin", skip_serializing_if = "Option::is_none")]
66    pub extends_pin: Option<String>,
67    /// Raw YAML captured when the style was loaded via [`Style::from_yaml_str`]
68    /// or [`Style::from_yaml_bytes`]. Used during style resolution for
69    /// null-aware overlay merging (e.g., `ibid: ~` correctly clears an
70    /// inherited preset value). Absent in programmatically-constructed styles.
71    #[cfg_attr(feature = "schema", schemars(skip))]
72    #[serde(skip, default)]
73    pub raw_yaml: Option<serde_yaml::Value>,
74    /// Forward-compat: captures unknown keys when an older engine reads a
75    /// style produced by a newer schema. Empty by default; treated as a
76    /// SoftDegrade signal. See `docs/specs/FORWARD_COMPATIBILITY.md`.
77    #[serde(
78        flatten,
79        default,
80        skip_serializing_if = "std::collections::BTreeMap::is_empty"
81    )]
82    #[cfg_attr(feature = "schema", schemars(skip))]
83    pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
84}
85
86impl Style {
87    /// Parse a Citum style from a YAML string, preserving raw YAML for
88    /// null-aware overlay merging during base resolution.
89    ///
90    /// Preferred over `serde_yaml::from_str` when the style extends a base,
91    /// so that `ibid: ~` and similar null overrides correctly clear inherited values.
92    ///
93    /// # Errors
94    ///
95    /// Returns a serde error if YAML parsing or deserialization fails.
96    pub fn from_yaml_str(s: &str) -> Result<Self, serde_yaml::Error> {
97        let raw: serde_yaml::Value = serde_yaml::from_str(s)?;
98        super::diagnostics::validate_raw_style(&raw).map_err(serde_yaml::Error::custom)?;
99        let mut style: Style = serde_yaml::from_value(raw.clone())?;
100        style.raw_yaml = Some(raw);
101        style
102            .validate_resource_limits()
103            .map_err(serde_yaml::Error::custom)?;
104        Ok(style)
105    }
106
107    /// Apply scoped citation and bibliography option overrides to this style.
108    ///
109    /// Translates typed option values (label mode, label wrap, repeated-author
110    /// rendering, date position, title terminator) into concrete template mutations.
111    /// Call this after mutating `bibliography.options` at runtime — e.g. after
112    /// applying per-document overrides — so that template state stays consistent
113    /// with the option values.
114    pub fn apply_scoped_options(&mut self) {
115        crate::options::scoped::apply_scoped_style_options(self);
116    }
117
118    /// Parse a Citum style from YAML bytes, preserving raw YAML for
119    /// null-aware overlay merging during preset resolution.
120    ///
121    /// # Errors
122    ///
123    /// Returns a serde error if YAML parsing or deserialization fails.
124    pub fn from_yaml_bytes(bytes: &[u8]) -> Result<Self, serde_yaml::Error> {
125        let raw: serde_yaml::Value = serde_yaml::from_slice(bytes)?;
126        super::diagnostics::validate_raw_style(&raw).map_err(serde_yaml::Error::custom)?;
127        let mut style: Style = serde_yaml::from_value(raw.clone())?;
128        style.raw_yaml = Some(raw);
129        style
130            .validate_resource_limits()
131            .map_err(serde_yaml::Error::custom)?;
132        Ok(style)
133    }
134}