Skip to main content

acdc_parser/model/
attributes.rs

1use rustc_hash::FxHashMap;
2use serde::{
3    Serialize,
4    ser::{SerializeMap, Serializer},
5};
6
7pub const MAX_TOC_LEVELS: u8 = 5;
8pub const MAX_SECTION_LEVELS: u8 = 5;
9
10/// Internal shared implementation for both document and element attributes.
11///
12/// This type is not exported directly. Use `DocumentAttributes` for document-level
13/// attributes or `ElementAttributes` for element-level attributes.
14#[derive(Debug, PartialEq, Clone)]
15struct AttributeMap {
16    /// All attributes including defaults
17    all: FxHashMap<AttributeName, AttributeValue>,
18    /// Only explicitly set attributes (not defaults) - used for serialization
19    explicit: FxHashMap<AttributeName, AttributeValue>,
20}
21
22impl Default for AttributeMap {
23    fn default() -> Self {
24        AttributeMap {
25            all: crate::constants::default_attributes(),
26            explicit: FxHashMap::default(), // Defaults are not explicit
27        }
28    }
29}
30
31impl AttributeMap {
32    fn empty() -> Self {
33        AttributeMap {
34            all: FxHashMap::default(),
35            explicit: FxHashMap::default(),
36        }
37    }
38
39    fn iter(&self) -> impl Iterator<Item = (&AttributeName, &AttributeValue)> {
40        self.all.iter()
41    }
42
43    fn is_empty(&self) -> bool {
44        // We only consider explicit attributes for emptiness because defaults are always
45        // present.
46        self.explicit.is_empty()
47    }
48
49    fn insert(&mut self, name: AttributeName, value: AttributeValue) {
50        if !self.contains_key(&name) {
51            self.all.insert(name.clone(), value.clone());
52            self.explicit.insert(name, value); // Track as explicit
53        }
54    }
55
56    fn set(&mut self, name: AttributeName, value: AttributeValue) {
57        self.all.insert(name.clone(), value.clone());
58        self.explicit.insert(name, value); // Track as explicit
59    }
60
61    fn get(&self, name: &str) -> Option<&AttributeValue> {
62        self.all.get(name)
63    }
64
65    fn contains_key(&self, name: &str) -> bool {
66        self.all.contains_key(name)
67    }
68
69    fn remove(&mut self, name: &str) -> Option<AttributeValue> {
70        self.explicit.remove(name);
71        self.all.remove(name)
72    }
73
74    fn merge(&mut self, other: AttributeMap) {
75        for (key, value) in other.all {
76            self.insert(key, value);
77        }
78    }
79}
80
81impl Serialize for AttributeMap {
82    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
83    where
84        S: Serializer,
85    {
86        // Only serialize explicitly set attributes, not defaults
87        let mut sorted_keys: Vec<_> = self.explicit.keys().collect();
88        sorted_keys.sort();
89
90        let mut state = serializer.serialize_map(Some(self.explicit.len()))?;
91        for key in sorted_keys {
92            if let Some(value) = &self.explicit.get(key) {
93                match value {
94                    AttributeValue::Bool(true) => {
95                        if key == "toc" {
96                            state.serialize_entry(key, "")?;
97                        } else {
98                            state.serialize_entry(key, &true)?;
99                        }
100                    }
101                    value @ (AttributeValue::Bool(false)
102                    | AttributeValue::String(_)
103                    | AttributeValue::None) => {
104                        state.serialize_entry(key, value)?;
105                    }
106                }
107            }
108        }
109        state.end()
110    }
111}
112
113/// Validate bounded attributes and emit warnings for out-of-range values.
114///
115/// Some attributes like `sectnumlevels` and `toclevels` have valid ranges.
116/// This function emits a warning if the value is outside the valid range.
117fn validate_bounded_attribute(key: &str, value: &AttributeValue) {
118    let AttributeValue::String(s) = value else {
119        return;
120    };
121
122    match key {
123        "sectnumlevels" => {
124            if let Ok(level) = s.parse::<u8>()
125                && level > MAX_SECTION_LEVELS
126            {
127                tracing::warn!(
128                    attribute = "sectnumlevels",
129                    value = level,
130                    "sectnumlevels must be between 0 and {MAX_SECTION_LEVELS}, got {level}. \
131                         Values above {MAX_SECTION_LEVELS} will be treated as {MAX_SECTION_LEVELS}."
132                );
133            }
134        }
135        "toclevels" => {
136            if let Ok(level) = s.parse::<u8>()
137                && level > MAX_TOC_LEVELS
138            {
139                tracing::warn!(
140                    attribute = "toclevels",
141                    value = level,
142                    "toclevels must be between 0 and {MAX_TOC_LEVELS}, got {level}. \
143                         Values above {MAX_TOC_LEVELS} will be treated as {MAX_TOC_LEVELS}."
144                );
145            }
146        }
147        _ => {}
148    }
149}
150
151/// Document-level attributes with universal defaults.
152///
153/// These attributes apply to the entire document and include defaults for
154/// admonition captions, TOC settings, structural settings, etc.
155///
156/// Use `DocumentAttributes::default()` to get a map with universal defaults applied.
157#[derive(Debug, PartialEq, Clone, Default)]
158pub struct DocumentAttributes(AttributeMap);
159
160impl DocumentAttributes {
161    /// Iterate over all attributes.
162    pub fn iter(&self) -> impl Iterator<Item = (&AttributeName, &AttributeValue)> {
163        self.0.iter()
164    }
165
166    /// Check if the attribute map is empty.
167    #[must_use]
168    pub fn is_empty(&self) -> bool {
169        self.0.is_empty()
170    }
171
172    /// Insert a new attribute.
173    ///
174    /// NOTE: This will *NOT* overwrite an existing attribute with the same name.
175    pub fn insert(&mut self, name: AttributeName, value: AttributeValue) {
176        validate_bounded_attribute(&name, &value);
177        self.0.insert(name, value);
178    }
179
180    /// Set an attribute, overwriting any existing value.
181    pub fn set(&mut self, name: AttributeName, value: AttributeValue) {
182        validate_bounded_attribute(&name, &value);
183        self.0.set(name, value);
184    }
185
186    /// Get an attribute value by name.
187    #[must_use]
188    pub fn get(&self, name: &str) -> Option<&AttributeValue> {
189        self.0.get(name)
190    }
191
192    /// Check if an attribute exists.
193    #[must_use]
194    pub fn contains_key(&self, name: &str) -> bool {
195        self.0.contains_key(name)
196    }
197
198    /// Remove an attribute by name.
199    pub fn remove(&mut self, name: &str) -> Option<AttributeValue> {
200        self.0.remove(name)
201    }
202
203    /// Merge another attribute map into this one.
204    pub fn merge(&mut self, other: Self) {
205        self.0.merge(other.0);
206    }
207
208    /// Helper to get a string value.
209    ///
210    /// Strips surrounding quotes from the value if present (parser quirk workaround).
211    #[must_use]
212    pub fn get_string(&self, name: &str) -> Option<String> {
213        self.get(name).and_then(|v| match v {
214            AttributeValue::String(s) => {
215                // Strip surrounding quotes if present (parser includes them for quoted values)
216                let trimmed = s.trim_matches('"');
217                Some(trimmed.to_string())
218            }
219            AttributeValue::None | AttributeValue::Bool(_) => None,
220        })
221    }
222}
223
224impl Serialize for DocumentAttributes {
225    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
226    where
227        S: Serializer,
228    {
229        self.0.serialize(serializer)
230    }
231}
232
233/// Element-level attributes (for blocks, sections, etc.).
234///
235/// These attributes are specific to individual elements and start empty.
236///
237/// Use `ElementAttributes::default()` to get an empty attribute map.
238#[derive(Debug, PartialEq, Clone)]
239pub struct ElementAttributes(AttributeMap);
240
241impl Default for ElementAttributes {
242    fn default() -> Self {
243        ElementAttributes(AttributeMap::empty())
244    }
245}
246
247impl ElementAttributes {
248    /// Iterate over all attributes.
249    pub fn iter(&self) -> impl Iterator<Item = (&AttributeName, &AttributeValue)> {
250        self.0.iter()
251    }
252
253    /// Check if the attribute map is empty.
254    #[must_use]
255    pub fn is_empty(&self) -> bool {
256        self.0.is_empty()
257    }
258
259    /// Insert a new attribute.
260    ///
261    /// NOTE: This will *NOT* overwrite an existing attribute with the same name.
262    pub fn insert(&mut self, name: AttributeName, value: AttributeValue) {
263        self.0.insert(name, value);
264    }
265
266    /// Set an attribute, overwriting any existing value.
267    pub fn set(&mut self, name: AttributeName, value: AttributeValue) {
268        self.0.set(name, value);
269    }
270
271    /// Get an attribute value by name.
272    #[must_use]
273    pub fn get(&self, name: &str) -> Option<&AttributeValue> {
274        self.0.get(name)
275    }
276
277    /// Check if an attribute exists.
278    #[must_use]
279    pub fn contains_key(&self, name: &str) -> bool {
280        self.0.contains_key(name)
281    }
282
283    /// Remove an attribute by name.
284    pub fn remove(&mut self, name: &str) -> Option<AttributeValue> {
285        self.0.remove(name)
286    }
287
288    /// Merge another attribute map into this one.
289    pub fn merge(&mut self, other: Self) {
290        self.0.merge(other.0);
291    }
292
293    /// Get a string attribute value as an owned `String`.
294    ///
295    /// Strips surrounding quotes from the value if present.
296    #[must_use]
297    pub fn get_string(&self, name: &str) -> Option<String> {
298        self.get(name).and_then(|v| match v {
299            AttributeValue::String(s) => {
300                // Strip surrounding quotes if present (parser includes them for quoted values)
301                let trimmed = s.trim_matches('"');
302                Some(trimmed.to_string())
303            }
304            AttributeValue::None | AttributeValue::Bool(_) => None,
305        })
306    }
307}
308
309impl Serialize for ElementAttributes {
310    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
311    where
312        S: Serializer,
313    {
314        self.0.serialize(serializer)
315    }
316}
317
318/// An `AttributeName` represents the name of an attribute in a document.
319pub type AttributeName = String;
320
321/// An `AttributeValue` represents the value of an attribute in a document.
322///
323/// An attribute value can be a string, a boolean, or nothing
324#[derive(Clone, Debug, PartialEq, Serialize)]
325#[serde(untagged)]
326#[non_exhaustive]
327pub enum AttributeValue {
328    /// A string attribute value.
329    String(String),
330    /// A boolean attribute value. `false` means it is unset.
331    Bool(bool),
332    /// No value (or it was unset)
333    None,
334}
335
336impl std::fmt::Display for AttributeValue {
337    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
338        match self {
339            AttributeValue::String(value) => write!(f, "{value}"),
340            AttributeValue::Bool(value) => write!(f, "{value}"),
341            AttributeValue::None => write!(f, "null"),
342        }
343    }
344}
345
346impl From<&str> for AttributeValue {
347    fn from(value: &str) -> Self {
348        AttributeValue::String(value.to_string())
349    }
350}
351
352impl From<String> for AttributeValue {
353    fn from(value: String) -> Self {
354        AttributeValue::String(value)
355    }
356}
357
358impl From<bool> for AttributeValue {
359    fn from(value: bool) -> Self {
360        AttributeValue::Bool(value)
361    }
362}
363
364impl From<()> for AttributeValue {
365    fn from((): ()) -> Self {
366        AttributeValue::None
367    }
368}