Skip to main content

acdc_parser/model/
attributes.rs

1use std::borrow::Cow;
2
3use rustc_hash::FxHashMap;
4use serde::{
5    Serialize,
6    ser::{SerializeMap, Serializer},
7};
8
9pub const MAX_TOC_LEVELS: u8 = 5;
10pub const MAX_SECTION_LEVELS: u8 = 5;
11
12/// Strip surrounding single or double quotes from a string.
13///
14/// Attribute values in `AsciiDoc` can be quoted with either single or double quotes.
15/// This function strips the outermost matching quotes from both ends.
16#[must_use]
17pub fn strip_quotes(s: &str) -> &str {
18    s.trim_start_matches(['"', '\''])
19        .trim_end_matches(['"', '\''])
20}
21
22/// Internal shared implementation for both document and element attributes.
23///
24/// This type is not exported directly. Use `DocumentAttributes` for document-level
25/// attributes or `ElementAttributes` for element-level attributes.
26#[derive(Debug, PartialEq, Clone)]
27struct AttributeMap<'a> {
28    /// All attributes including defaults
29    all: FxHashMap<AttributeName<'a>, AttributeValue<'a>>,
30    /// Only explicitly set attributes (not defaults) - used for serialization
31    explicit: FxHashMap<AttributeName<'a>, AttributeValue<'a>>,
32}
33
34impl Default for AttributeMap<'_> {
35    fn default() -> Self {
36        use std::sync::LazyLock;
37        // Cache the built map so each `default()` call pays only a hashmap
38        // clone (pre-sized buckets, trivial `Cow::Borrowed` copies) instead
39        // of re-hashing the ~80 entries every time. The `FxHashMap` type
40        // is deliberately confined to this file — `constants.rs` only
41        // exposes the raw entry slice.
42        static DEFAULTS: LazyLock<FxHashMap<AttributeName<'static>, AttributeValue<'static>>> =
43            LazyLock::new(|| {
44                crate::constants::DEFAULT_ATTRIBUTE_ENTRIES
45                    .iter()
46                    .cloned()
47                    .collect()
48            });
49        AttributeMap {
50            all: DEFAULTS.clone(),
51            explicit: FxHashMap::default(), // Defaults are not explicit
52        }
53    }
54}
55
56impl<'a> AttributeMap<'a> {
57    fn empty() -> Self {
58        AttributeMap {
59            all: FxHashMap::default(),
60            explicit: FxHashMap::default(),
61        }
62    }
63
64    fn iter(&self) -> impl Iterator<Item = (&AttributeName<'a>, &AttributeValue<'a>)> {
65        self.all.iter()
66    }
67
68    fn is_empty(&self) -> bool {
69        // We only consider explicit attributes for emptiness because defaults are always
70        // present.
71        self.explicit.is_empty()
72    }
73
74    fn insert(&mut self, name: AttributeName<'a>, value: AttributeValue<'a>) {
75        if !self.contains_key(&name) {
76            self.all.insert(name.clone(), value.clone());
77            self.explicit.insert(name, value); // Track as explicit
78        }
79    }
80
81    fn set(&mut self, name: AttributeName<'a>, value: AttributeValue<'a>) {
82        self.all.insert(name.clone(), value.clone());
83        self.explicit.insert(name, value); // Track as explicit
84    }
85
86    fn get(&self, name: &str) -> Option<&AttributeValue<'a>> {
87        self.all.get(name)
88    }
89
90    fn contains_key(&self, name: &str) -> bool {
91        self.all.contains_key(name)
92    }
93
94    fn remove(&mut self, name: &str) -> Option<AttributeValue<'a>> {
95        self.explicit.remove(name);
96        self.all.remove(name)
97    }
98
99    fn merge(&mut self, other: AttributeMap<'a>) {
100        for (key, value) in other.all {
101            self.insert(key, value);
102        }
103    }
104}
105
106impl Serialize for AttributeMap<'_> {
107    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
108    where
109        S: Serializer,
110    {
111        // Only serialize explicitly set attributes, not defaults
112        let mut sorted_keys: Vec<_> = self.explicit.keys().collect();
113        sorted_keys.sort();
114
115        let mut state = serializer.serialize_map(Some(self.explicit.len()))?;
116        for key in sorted_keys {
117            if let Some(value) = &self.explicit.get(key) {
118                match value {
119                    AttributeValue::Bool(true) => {
120                        if key == "toc" {
121                            state.serialize_entry(key, "")?;
122                        } else {
123                            state.serialize_entry(key, &true)?;
124                        }
125                    }
126                    value @ (AttributeValue::Bool(false)
127                    | AttributeValue::String(_)
128                    | AttributeValue::None) => {
129                        state.serialize_entry(key, value)?;
130                    }
131                }
132            }
133        }
134        state.end()
135    }
136}
137
138/// Validate bounded attributes and emit warnings for out-of-range values.
139///
140/// Some attributes like `sectnumlevels` and `toclevels` have valid ranges.
141/// This function emits a warning if the value is outside the valid range.
142fn validate_bounded_attribute(key: &str, value: &AttributeValue<'_>) {
143    let AttributeValue::String(s) = value else {
144        return;
145    };
146
147    match key {
148        "sectnumlevels" => {
149            if let Ok(level) = s.parse::<u8>()
150                && level > MAX_SECTION_LEVELS
151            {
152                tracing::warn!(
153                    attribute = "sectnumlevels",
154                    value = level,
155                    "sectnumlevels must be between 0 and {MAX_SECTION_LEVELS}, got {level}. \
156                         Values above {MAX_SECTION_LEVELS} will be treated as {MAX_SECTION_LEVELS}."
157                );
158            }
159        }
160        "toclevels" => {
161            if let Ok(level) = s.parse::<u8>()
162                && level > MAX_TOC_LEVELS
163            {
164                tracing::warn!(
165                    attribute = "toclevels",
166                    value = level,
167                    "toclevels must be between 0 and {MAX_TOC_LEVELS}, got {level}. \
168                         Values above {MAX_TOC_LEVELS} will be treated as {MAX_TOC_LEVELS}."
169                );
170            }
171        }
172        _ => {}
173    }
174}
175
176/// Document-level attributes with universal defaults.
177///
178/// These attributes apply to the entire document and include defaults for
179/// admonition captions, TOC settings, structural settings, etc.
180///
181/// Use `DocumentAttributes::default()` to get a map with universal defaults applied.
182#[derive(Debug, PartialEq, Clone, Default)]
183pub struct DocumentAttributes<'a>(AttributeMap<'a>);
184
185impl<'a> DocumentAttributes<'a> {
186    /// Create an empty `DocumentAttributes` without default attributes.
187    /// Used for lightweight parsing contexts (e.g., quotes-only) where
188    /// document attributes aren't needed.
189    pub(crate) fn empty() -> Self {
190        Self(AttributeMap::empty())
191    }
192
193    /// Iterate over all attributes.
194    pub fn iter(&self) -> impl Iterator<Item = (&AttributeName<'a>, &AttributeValue<'a>)> {
195        self.0.iter()
196    }
197
198    /// Check if the attribute map is empty.
199    #[must_use]
200    pub fn is_empty(&self) -> bool {
201        self.0.is_empty()
202    }
203
204    /// Insert a new attribute.
205    ///
206    /// NOTE: This will *NOT* overwrite an existing attribute with the same name.
207    pub fn insert(&mut self, name: AttributeName<'a>, value: AttributeValue<'a>) {
208        validate_bounded_attribute(&name, &value);
209        self.0.insert(name, value);
210    }
211
212    /// Set an attribute, overwriting any existing value.
213    pub fn set(&mut self, name: AttributeName<'a>, value: AttributeValue<'a>) {
214        validate_bounded_attribute(&name, &value);
215        self.0.set(name, value);
216    }
217
218    /// Get an attribute value by name.
219    #[must_use]
220    pub fn get(&self, name: &str) -> Option<&AttributeValue<'a>> {
221        self.0.get(name)
222    }
223
224    /// Check if an attribute exists.
225    #[must_use]
226    pub fn contains_key(&self, name: &str) -> bool {
227        self.0.contains_key(name)
228    }
229
230    /// Remove an attribute by name.
231    pub fn remove(&mut self, name: &str) -> Option<AttributeValue<'a>> {
232        self.0.remove(name)
233    }
234
235    /// Merge another attribute map into this one.
236    pub fn merge(&mut self, other: Self) {
237        self.0.merge(other.0);
238    }
239
240    /// Helper to get a string value.
241    ///
242    /// Strips surrounding quotes from the value if present (parser quirk workaround).
243    #[must_use]
244    pub fn get_string(&self, name: &str) -> Option<Cow<'a, str>> {
245        self.get(name).and_then(|v| match v {
246            AttributeValue::String(s) => Some(match s {
247                Cow::Borrowed(b) => Cow::Borrowed(strip_quotes(b)),
248                Cow::Owned(o) => Cow::Owned(strip_quotes(o).to_string()),
249            }),
250            AttributeValue::None | AttributeValue::Bool(_) => None,
251        })
252    }
253
254    /// Clone the attributes into an independent `'static` copy. Used by
255    /// converters that cache document attributes on a processor whose
256    /// lifetime is independent of the document being rendered.
257    #[must_use]
258    pub fn to_static(&self) -> DocumentAttributes<'static> {
259        self.clone().into_static()
260    }
261
262    /// Consume the attributes, producing an independent `'static` copy.
263    #[must_use]
264    pub fn into_static(self) -> DocumentAttributes<'static> {
265        let convert_map = |map: FxHashMap<AttributeName<'a>, AttributeValue<'a>>| -> FxHashMap<AttributeName<'static>, AttributeValue<'static>> {
266            map.into_iter()
267                .map(|(k, v)| {
268                    let key: AttributeName<'static> = Cow::Owned(k.into_owned());
269                    let val = match v {
270                        AttributeValue::String(s) => AttributeValue::String(Cow::Owned(s.into_owned())),
271                        AttributeValue::Bool(b) => AttributeValue::Bool(b),
272                        AttributeValue::None => AttributeValue::None,
273                    };
274                    (key, val)
275                })
276                .collect()
277        };
278        DocumentAttributes(AttributeMap {
279            all: convert_map(self.0.all),
280            explicit: convert_map(self.0.explicit),
281        })
282    }
283}
284
285impl Serialize for DocumentAttributes<'_> {
286    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
287    where
288        S: Serializer,
289    {
290        self.0.serialize(serializer)
291    }
292}
293
294/// Element-level attributes (for blocks, sections, etc.).
295///
296/// These attributes are specific to individual elements and start empty.
297///
298/// Use `ElementAttributes::default()` to get an empty attribute map.
299#[derive(Debug, PartialEq, Clone)]
300pub struct ElementAttributes<'a>(AttributeMap<'a>);
301
302impl Default for ElementAttributes<'_> {
303    fn default() -> Self {
304        ElementAttributes(AttributeMap::empty())
305    }
306}
307
308impl<'a> ElementAttributes<'a> {
309    /// Iterate over all attributes.
310    pub fn iter(&self) -> impl Iterator<Item = (&AttributeName<'a>, &AttributeValue<'a>)> {
311        self.0.iter()
312    }
313
314    /// Check if the attribute map is empty.
315    #[must_use]
316    pub fn is_empty(&self) -> bool {
317        self.0.is_empty()
318    }
319
320    /// Insert a new attribute.
321    ///
322    /// NOTE: This will *NOT* overwrite an existing attribute with the same name.
323    pub fn insert(&mut self, name: AttributeName<'a>, value: AttributeValue<'a>) {
324        self.0.insert(name, value);
325    }
326
327    /// Set an attribute, overwriting any existing value.
328    pub fn set(&mut self, name: AttributeName<'a>, value: AttributeValue<'a>) {
329        self.0.set(name, value);
330    }
331
332    /// Get an attribute value by name.
333    #[must_use]
334    pub fn get(&self, name: &str) -> Option<&AttributeValue<'a>> {
335        self.0.get(name)
336    }
337
338    /// Check if an attribute exists.
339    #[must_use]
340    pub fn contains_key(&self, name: &str) -> bool {
341        self.0.contains_key(name)
342    }
343
344    /// Remove an attribute by name.
345    pub fn remove(&mut self, name: &str) -> Option<AttributeValue<'a>> {
346        self.0.remove(name)
347    }
348
349    /// Merge another attribute map into this one.
350    pub fn merge(&mut self, other: Self) {
351        self.0.merge(other.0);
352    }
353
354    /// Convert all borrowed content to owned, producing `'static` lifetime attributes.
355    #[must_use]
356    pub fn into_static(self) -> ElementAttributes<'static> {
357        let convert_map = |map: FxHashMap<AttributeName<'a>, AttributeValue<'a>>| -> FxHashMap<AttributeName<'static>, AttributeValue<'static>> {
358            map.into_iter()
359                .map(|(k, v)| {
360                    let key: AttributeName<'static> = Cow::Owned(k.into_owned());
361                    let val = match v {
362                        AttributeValue::String(s) => AttributeValue::String(Cow::Owned(s.into_owned())),
363                        AttributeValue::Bool(b) => AttributeValue::Bool(b),
364                        AttributeValue::None => AttributeValue::None,
365                    };
366                    (key, val)
367                })
368                .collect()
369        };
370        ElementAttributes(AttributeMap {
371            all: convert_map(self.0.all),
372            explicit: convert_map(self.0.explicit),
373        })
374    }
375
376    /// Get a string attribute value as an owned `String`.
377    ///
378    /// Strips surrounding quotes from the value if present.
379    #[must_use]
380    pub fn get_string(&self, name: &str) -> Option<Cow<'a, str>> {
381        self.get(name).and_then(|v| match v {
382            AttributeValue::String(s) => Some(match s {
383                Cow::Borrowed(b) => Cow::Borrowed(strip_quotes(b)),
384                Cow::Owned(o) => Cow::Owned(strip_quotes(o).to_string()),
385            }),
386            AttributeValue::None | AttributeValue::Bool(_) => None,
387        })
388    }
389}
390
391impl Serialize for ElementAttributes<'_> {
392    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
393    where
394        S: Serializer,
395    {
396        self.0.serialize(serializer)
397    }
398}
399
400/// An `AttributeName` represents the name of an attribute in a document.
401pub type AttributeName<'a> = Cow<'a, str>;
402
403/// An `AttributeValue` represents the value of an attribute in a document.
404///
405/// An attribute value can be a string, a boolean, or nothing
406#[derive(Clone, Debug, PartialEq, Serialize)]
407#[serde(untagged)]
408#[non_exhaustive]
409pub enum AttributeValue<'a> {
410    /// A string attribute value.
411    String(Cow<'a, str>),
412    /// A boolean attribute value. `false` means it is unset.
413    Bool(bool),
414    /// No value (or it was unset)
415    None,
416}
417
418impl std::fmt::Display for AttributeValue<'_> {
419    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
420        match self {
421            AttributeValue::String(value) => write!(f, "{value}"),
422            AttributeValue::Bool(value) => write!(f, "{value}"),
423            AttributeValue::None => write!(f, "null"),
424        }
425    }
426}
427
428impl<'a> From<&'a str> for AttributeValue<'a> {
429    fn from(value: &'a str) -> Self {
430        AttributeValue::String(Cow::Borrowed(value))
431    }
432}
433
434impl From<String> for AttributeValue<'_> {
435    fn from(value: String) -> Self {
436        AttributeValue::String(Cow::Owned(value))
437    }
438}
439
440impl From<bool> for AttributeValue<'_> {
441    fn from(value: bool) -> Self {
442        AttributeValue::Bool(value)
443    }
444}
445
446impl From<()> for AttributeValue<'_> {
447    fn from((): ()) -> Self {
448        AttributeValue::None
449    }
450}