Skip to main content

acdc_parser/model/
metadata.rs

1//! Block metadata types for `AsciiDoc` documents.
2
3use serde::Serialize;
4
5use super::anchor::Anchor;
6use super::attributes::{AttributeValue, ElementAttributes};
7use super::attribution::{Attribution, CiteTitle};
8use super::location::Location;
9use super::substitution::SubstitutionSpec;
10
11pub type Role<'a> = &'a str;
12
13/// A `BlockMetadata` represents the metadata of a block in a document.
14#[derive(Clone, Debug, Default, PartialEq, Serialize)]
15#[non_exhaustive]
16pub struct BlockMetadata<'a> {
17    #[serde(default, skip_serializing_if = "ElementAttributes::is_empty")]
18    pub attributes: ElementAttributes<'a>,
19    /// Parser intermediate state: positional attrs from `[foo,bar,baz]` that
20    /// haven't yet been routed to named slots (style/width/height/…) or
21    /// merged into `attributes`. Grammar rules drain this before the block
22    /// is finalised; external consumers never see non-empty values.
23    #[serde(default, skip_serializing)]
24    pub(crate) positional_attributes: Vec<&'a str>,
25    #[serde(default, skip_serializing_if = "Vec::is_empty")]
26    pub roles: Vec<Role<'a>>,
27    #[serde(default, skip_serializing_if = "Vec::is_empty")]
28    pub options: Vec<&'a str>,
29    #[serde(default, skip_serializing_if = "Option::is_none")]
30    pub style: Option<&'a str>,
31    #[serde(default, skip_serializing_if = "Option::is_none")]
32    pub id: Option<Anchor<'a>>,
33    #[serde(default, skip_serializing_if = "Vec::is_empty")]
34    pub anchors: Vec<Anchor<'a>>,
35    /// Substitutions to apply to block content.
36    ///
37    /// - `None`: Use block-type defaults (VERBATIM for listing/literal, NORMAL for paragraphs)
38    /// - `Some(Explicit([]))`: No substitutions (equivalent to `subs=none`)
39    /// - `Some(Explicit(list))`: Use the explicit list of substitutions
40    /// - `Some(Modifiers(ops))`: Apply modifier operations to block-type defaults
41    #[serde(default, skip_serializing_if = "Option::is_none")]
42    pub substitutions: Option<SubstitutionSpec>,
43    #[serde(default, skip_serializing_if = "Option::is_none")]
44    pub attribution: Option<Attribution<'a>>,
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub citetitle: Option<CiteTitle<'a>>,
47    #[serde(skip)]
48    pub location: Option<Location>,
49}
50
51impl<'a> BlockMetadata<'a> {
52    /// Create a new block metadata with default values.
53    #[must_use]
54    pub fn new() -> Self {
55        Self::default()
56    }
57
58    /// Set the attributes.
59    #[must_use]
60    pub fn with_attributes(mut self, attributes: ElementAttributes<'a>) -> Self {
61        self.attributes = attributes;
62        self
63    }
64
65    /// Set the options.
66    #[must_use]
67    pub fn with_options(mut self, options: Vec<&'a str>) -> Self {
68        self.options = options;
69        self
70    }
71
72    /// Set the roles.
73    #[must_use]
74    pub fn with_roles(mut self, roles: Vec<Role<'a>>) -> Self {
75        self.roles = roles;
76        self
77    }
78
79    /// Set the style.
80    #[must_use]
81    pub fn with_style(mut self, style: Option<&'a str>) -> Self {
82        self.style = style;
83        self
84    }
85
86    /// Set the ID.
87    #[must_use]
88    pub fn with_id(mut self, id: Option<Anchor<'a>>) -> Self {
89        self.id = id;
90        self
91    }
92
93    pub(crate) fn move_positional_attributes_to_attributes(&mut self) {
94        for positional_attribute in self.positional_attributes.drain(..) {
95            self.attributes.insert(
96                std::borrow::Cow::Borrowed(positional_attribute),
97                AttributeValue::None,
98            );
99        }
100    }
101
102    #[must_use]
103    pub fn is_default(&self) -> bool {
104        self.roles.is_empty()
105            && self.options.is_empty()
106            && self.style.is_none()
107            && self.id.is_none()
108            && self.anchors.is_empty()
109            && self.attributes.is_empty()
110            && self.positional_attributes.is_empty()
111            && self.substitutions.is_none()
112            && self.attribution.is_none()
113            && self.citetitle.is_none()
114    }
115
116    #[tracing::instrument(level = "debug")]
117    pub(crate) fn merge(&mut self, other: &BlockMetadata<'a>) {
118        self.attributes.merge(other.attributes.clone());
119        self.positional_attributes
120            .extend(other.positional_attributes.clone());
121        self.roles.extend(other.roles.clone());
122        self.options.extend(other.options.clone());
123        if self.style.is_none() {
124            self.style.clone_from(&other.style);
125        }
126        if self.id.is_none() {
127            self.id.clone_from(&other.id);
128        }
129        self.anchors.extend(other.anchors.clone());
130        if self.substitutions.is_none() {
131            self.substitutions.clone_from(&other.substitutions);
132        }
133        if self.attribution.is_none() {
134            self.attribution.clone_from(&other.attribution);
135        }
136        if self.citetitle.is_none() {
137            self.citetitle.clone_from(&other.citetitle);
138        }
139    }
140}