Skip to main content

acdc_parser/model/
attributes.rs

1use std::{borrow::Cow, fmt, ops::Range, sync::Arc};
2
3use rustc_hash::{FxBuildHasher, FxHashMap};
4use serde::{
5    Serialize,
6    ser::{SerializeMap, Serializer},
7};
8
9use crate::{
10    Error, SourceLocation,
11    document_attribute::{
12        AssignmentDecision, AssignmentRequest, AssignmentState, AttributeLock, AttributeOrigin,
13        MAX_INCLUDE_DEPTH_ATTR, RawAttributeValue, assignment_decision, default_value,
14        is_intrinsic, processor_assignment_state, validate_assignment_value,
15    },
16};
17
18pub(crate) type RawAttributes<'a> = FxHashMap<AttributeName<'a>, AttributeValue<'a>>;
19
20pub const MAX_TOC_LEVELS: u8 = 5;
21pub const MAX_SECTION_LEVELS: u8 = 5;
22
23/// Strip surrounding single or double quotes from a string.
24///
25/// Attribute values in `AsciiDoc` can be quoted with either single or double quotes.
26/// This function strips the outermost matching quotes from both ends.
27#[must_use]
28pub fn strip_quotes(s: &str) -> &str {
29    s.trim_start_matches(['"', '\''])
30        .trim_end_matches(['"', '\''])
31}
32
33/// A defined document attribute, including its retained text representation.
34///
35/// Numeric values use attribute-specific validation. Text is not inferred to be
36/// numeric merely because it contains digits.
37#[derive(Clone, Debug, PartialEq, Eq)]
38pub struct DocumentAttributeValue<'a>(ValueKind<'a>);
39
40#[derive(Clone, Debug, PartialEq, Eq)]
41enum ValueKind<'a> {
42    Text(Cow<'a, str>),
43    // These fragments are already prepared for insertion, including through aliases.
44    Passthrough {
45        text: Cow<'a, str>,
46        ranges: Box<[Range<usize>]>,
47        fragments: Box<[AttributeInlineFragment]>,
48    },
49    Presence,
50    Integer {
51        value: i128,
52        source: Option<Cow<'a, str>>,
53    },
54}
55
56#[derive(Clone, Debug, PartialEq, Eq)]
57pub(crate) struct AttributeInlineFragment {
58    // Byte spans select grammar rules when retained source is inserted. They
59    // never store rendered markup or declaration-specific AST locations.
60    pub(crate) range: Range<usize>,
61    pub(crate) substitutions: Box<[crate::Substitution]>,
62}
63
64impl<'a> DocumentAttributeValue<'a> {
65    pub(crate) const fn presence() -> Self {
66        Self(ValueKind::Presence)
67    }
68
69    pub(crate) const fn integer(value: i128) -> Self {
70        Self(ValueKind::Integer {
71            value,
72            source: None,
73        })
74    }
75
76    /// Return text only when the semantic value is textual.
77    #[must_use]
78    pub fn as_str(&self) -> Option<&str> {
79        match &self.0 {
80            ValueKind::Text(value) | ValueKind::Passthrough { text: value, .. } => Some(value),
81            ValueKind::Presence | ValueKind::Integer { .. } => None,
82        }
83    }
84
85    /// Return the validated integer, without parsing a textual value.
86    #[must_use]
87    pub const fn as_integer(&self) -> Option<i128> {
88        match self.0 {
89            ValueKind::Integer { value, .. } => Some(value),
90            ValueKind::Text(_) | ValueKind::Passthrough { .. } | ValueKind::Presence => None,
91        }
92    }
93
94    /// Return whether the attribute was set without a semantic value.
95    #[must_use]
96    pub const fn is_presence(&self) -> bool {
97        matches!(self.0, ValueKind::Presence)
98    }
99
100    /// Return retained text, including the spelling of an explicit integer.
101    ///
102    /// Presence and generated integer defaults have no supplied text. This does
103    /// not remove surrounding quotes or undo definition-time substitution.
104    #[must_use]
105    pub fn text(&self) -> Option<&str> {
106        self.stored_text().map(AsRef::as_ref)
107    }
108
109    /// Write the attribute-reference representation without allocating.
110    ///
111    /// Presence writes nothing; explicit integers retain their spelling.
112    ///
113    /// # Errors
114    ///
115    /// Returns errors from the output buffer.
116    pub fn write_text<W: fmt::Write>(&self, output: &mut W) -> fmt::Result {
117        match &self.0 {
118            ValueKind::Text(value) | ValueKind::Passthrough { text: value, .. } => {
119                output.write_str(value)
120            }
121            ValueKind::Presence => Ok(()),
122            ValueKind::Integer { value, source } => match source {
123                Some(text) => output.write_str(text),
124                None => write!(output, "{value}"),
125            },
126        }
127    }
128
129    fn stored_text(&self) -> Option<&Cow<'a, str>> {
130        match &self.0 {
131            ValueKind::Text(value) | ValueKind::Passthrough { text: value, .. } => Some(value),
132            ValueKind::Integer { source, .. } => source.as_ref().filter(|text| !text.is_empty()),
133            ValueKind::Presence => None,
134        }
135    }
136
137    pub(crate) fn as_borrowed(&self) -> DocumentAttributeValue<'_> {
138        DocumentAttributeValue(match &self.0 {
139            ValueKind::Text(value) => ValueKind::Text(Cow::Borrowed(value)),
140            ValueKind::Passthrough {
141                text,
142                ranges,
143                fragments,
144            } => ValueKind::Passthrough {
145                text: Cow::Borrowed(text),
146                ranges: ranges.clone(),
147                fragments: fragments.clone(),
148            },
149            ValueKind::Presence => ValueKind::Presence,
150            ValueKind::Integer { value, source } => ValueKind::Integer {
151                value: *value,
152                source: source.as_deref().map(Cow::Borrowed),
153            },
154        })
155    }
156
157    /// Consume the value, retaining existing allocations for owned text.
158    #[must_use]
159    pub fn into_static(self) -> DocumentAttributeValue<'static> {
160        DocumentAttributeValue(match self.0 {
161            ValueKind::Text(value) => ValueKind::Text(Cow::Owned(value.into_owned())),
162            ValueKind::Passthrough {
163                text,
164                ranges,
165                fragments,
166            } => ValueKind::Passthrough {
167                text: Cow::Owned(text.into_owned()),
168                ranges,
169                fragments,
170            },
171            ValueKind::Presence => ValueKind::Presence,
172            ValueKind::Integer { value, source } => ValueKind::Integer {
173                value,
174                source: source.map(|text| Cow::Owned(text.into_owned())),
175            },
176        })
177    }
178
179    fn into_input(self) -> AttributeValue<'a> {
180        match self.0 {
181            ValueKind::Text(text) | ValueKind::Passthrough { text, .. } => {
182                AttributeValue::String(text)
183            }
184            ValueKind::Presence => AttributeValue::Bool(true),
185            ValueKind::Integer { value, source } => match source {
186                Some(text) if text.is_empty() => AttributeValue::Bool(true),
187                Some(text) => AttributeValue::String(text),
188                None => AttributeValue::String(value.to_string().into()),
189            },
190        }
191    }
192
193    fn serialized_value(&self, presence_as_empty: bool) -> SerializedDocumentAttributeValue<'_> {
194        match &self.0 {
195            ValueKind::Text(value) | ValueKind::Passthrough { text: value, .. } => {
196                SerializedDocumentAttributeValue::Text(value)
197            }
198            ValueKind::Presence if presence_as_empty => SerializedDocumentAttributeValue::Text(""),
199            ValueKind::Presence => SerializedDocumentAttributeValue::Bool(true),
200            ValueKind::Integer { value, source } => match source.as_deref() {
201                Some("") => SerializedDocumentAttributeValue::Bool(true),
202                Some(text) => SerializedDocumentAttributeValue::Text(text),
203                None => SerializedDocumentAttributeValue::Integer(*value),
204            },
205        }
206    }
207
208    pub(crate) fn passthrough_ranges(&self) -> &[Range<usize>] {
209        match &self.0 {
210            ValueKind::Passthrough { ranges, .. } => ranges,
211            ValueKind::Text(_) | ValueKind::Presence | ValueKind::Integer { .. } => &[],
212        }
213    }
214
215    pub(crate) fn with_passthrough_ranges(text: Cow<'a, str>, ranges: Vec<Range<usize>>) -> Self {
216        Self::with_inline_fragments(text, ranges, Vec::new())
217    }
218
219    pub(crate) fn inline_fragments(&self) -> &[AttributeInlineFragment] {
220        match &self.0 {
221            ValueKind::Passthrough { fragments, .. } => fragments,
222            ValueKind::Text(_) | ValueKind::Presence | ValueKind::Integer { .. } => &[],
223        }
224    }
225
226    pub(crate) fn with_inline_fragments(
227        text: Cow<'a, str>,
228        ranges: Vec<Range<usize>>,
229        mut fragments: Vec<AttributeInlineFragment>,
230    ) -> Self {
231        if !fragments.is_empty() {
232            // An alias may combine profiled text with an earlier raw value.
233            // Protect those raw spans from the surrounding use-site rules.
234            for range in &ranges {
235                if !fragments.iter().any(|fragment| {
236                    fragment.range.start <= range.start && fragment.range.end >= range.end
237                }) {
238                    fragments.push(AttributeInlineFragment {
239                        range: range.clone(),
240                        substitutions: Box::default(),
241                    });
242                }
243            }
244            fragments.sort_by_key(|fragment| fragment.range.start);
245        }
246        if ranges.is_empty() && fragments.is_empty() {
247            Self::from(text)
248        } else {
249            Self(ValueKind::Passthrough {
250                text,
251                ranges: ranges.into_boxed_slice(),
252                fragments: fragments.into_boxed_slice(),
253            })
254        }
255    }
256}
257
258impl<'a> From<Cow<'a, str>> for DocumentAttributeValue<'a> {
259    fn from(value: Cow<'a, str>) -> Self {
260        Self(ValueKind::Text(value))
261    }
262}
263
264impl<'a> From<&'a DocumentAttributeAssignment<'_>> for AttributeValue<'a> {
265    fn from(assignment: &'a DocumentAttributeAssignment<'_>) -> Self {
266        match assignment {
267            DocumentAttributeAssignment::Set(value) => value.as_borrowed().into_input(),
268            DocumentAttributeAssignment::Unset => Self::Bool(false),
269        }
270    }
271}
272
273impl<'a> From<&'a str> for DocumentAttributeValue<'a> {
274    fn from(value: &'a str) -> Self {
275        Self::from(Cow::Borrowed(value))
276    }
277}
278
279impl From<String> for DocumentAttributeValue<'_> {
280    fn from(value: String) -> Self {
281        Self::from(Cow::Owned(value))
282    }
283}
284
285/// An accepted set or unset document-attribute assignment.
286#[derive(Clone, Debug, PartialEq, Eq)]
287pub enum DocumentAttributeAssignment<'a> {
288    /// Replace the active value.
289    Set(DocumentAttributeValue<'a>),
290    /// Hide the active value, including an inherited value.
291    Unset,
292}
293
294impl<'a> DocumentAttributeAssignment<'a> {
295    pub(crate) fn new(mut assignment: Self, original_source_text: Option<Cow<'a, str>>) -> Self {
296        if let Self::Set(DocumentAttributeValue(ValueKind::Integer { source, .. })) =
297            &mut assignment
298        {
299            *source = original_source_text;
300        }
301        assignment
302    }
303
304    /// Borrow the assigned value, or return none for an unset.
305    #[must_use]
306    pub const fn value(&self) -> Option<&DocumentAttributeValue<'a>> {
307        match self {
308            Self::Set(value) => Some(value),
309            Self::Unset => None,
310        }
311    }
312
313    pub(crate) fn to_static(&self) -> DocumentAttributeAssignment<'static> {
314        match self {
315            Self::Set(value) => DocumentAttributeAssignment::Set(value.as_borrowed().into_static()),
316            Self::Unset => DocumentAttributeAssignment::Unset,
317        }
318    }
319
320    pub(crate) fn into_static(self) -> DocumentAttributeAssignment<'static> {
321        match self {
322            Self::Set(value) => DocumentAttributeAssignment::Set(value.into_static()),
323            Self::Unset => DocumentAttributeAssignment::Unset,
324        }
325    }
326
327    fn stored_text(&self) -> Option<&Cow<'a, str>> {
328        self.value().and_then(DocumentAttributeValue::stored_text)
329    }
330
331    pub(crate) fn serialized_value(
332        &self,
333        presence_as_empty: bool,
334    ) -> SerializedDocumentAttributeValue<'_> {
335        match self {
336            Self::Set(value) => value.serialized_value(presence_as_empty),
337            Self::Unset => SerializedDocumentAttributeValue::Bool(false),
338        }
339    }
340}
341
342pub(crate) enum SerializedDocumentAttributeValue<'a> {
343    Text(&'a str),
344    Bool(bool),
345    Integer(i128),
346}
347
348impl Serialize for SerializedDocumentAttributeValue<'_> {
349    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
350    where
351        S: Serializer,
352    {
353        match self {
354            Self::Text(value) => serializer.serialize_str(value),
355            Self::Bool(value) => serializer.serialize_bool(*value),
356            Self::Integer(value) => serializer.serialize_i128(*value),
357        }
358    }
359}
360
361#[derive(Clone, Debug, PartialEq, Eq)]
362pub(crate) enum DocumentAttributeStatus<'a> {
363    Set(&'a DocumentAttributeValue<'a>),
364    Unset,
365    Absent,
366}
367
368/// Validate bounded attributes and emit warnings for out-of-range values.
369///
370/// Some attributes like `sectnumlevels` and `toclevels` have valid ranges.
371/// This function emits a warning if the value is outside the valid range.
372fn validate_bounded_attribute(key: &str, value: &AttributeValue<'_>) {
373    let AttributeValue::String(s) = value else {
374        return;
375    };
376
377    match key {
378        "sectnumlevels" => {
379            if let Ok(level) = s.parse::<u8>()
380                && level > MAX_SECTION_LEVELS
381            {
382                tracing::warn!(
383                    attribute = "sectnumlevels",
384                    value = level,
385                    "sectnumlevels must be between 0 and {MAX_SECTION_LEVELS}, got {level}. \
386                         Values above {MAX_SECTION_LEVELS} will be treated as {MAX_SECTION_LEVELS}."
387                );
388            }
389        }
390        "toclevels" => {
391            if let Ok(level) = s.parse::<u8>()
392                && level > MAX_TOC_LEVELS
393            {
394                tracing::warn!(
395                    attribute = "toclevels",
396                    value = level,
397                    "toclevels must be between 0 and {MAX_TOC_LEVELS}, got {level}. \
398                         Values above {MAX_TOC_LEVELS} will be treated as {MAX_TOC_LEVELS}."
399                );
400            }
401        }
402        _ => {}
403    }
404}
405
406#[derive(Clone, Debug, PartialEq, Eq)]
407struct StoredAssignment<'a> {
408    assignment: DocumentAttributeAssignment<'a>,
409    state: AssignmentState,
410}
411
412// A 128-bit approximate membership filter for explicit attributes. `lower`
413// stores slots 0..63 and `upper` stores slots 64..127. A clear bit proves that
414// a name is absent; a set bit requires a map lookup because names can collide.
415#[derive(Clone, Default)]
416struct EntryFilter {
417    lower: u64,
418    upper: u64,
419}
420
421impl EntryFilter {
422    fn slot(name: &str) -> usize {
423        // Length and boundary bytes spread common attribute names across the
424        // slots without hashing the full string.
425        let bytes = name.as_bytes();
426        let length = bytes.len();
427        let first = usize::from(bytes.first().copied().unwrap_or_default());
428        let last = usize::from(bytes.last().copied().unwrap_or_default());
429        (length.wrapping_mul(13) ^ first.wrapping_mul(7) ^ last) & 127
430    }
431
432    fn may_contain(&self, name: &str) -> bool {
433        let slot = Self::slot(name);
434        if slot < 64 {
435            self.lower & (1_u64 << slot) != 0
436        } else {
437            self.upper & (1_u64 << (slot - 64)) != 0
438        }
439    }
440
441    fn insert(&mut self, name: &str) {
442        let slot = Self::slot(name);
443        if slot < 64 {
444            self.lower |= 1_u64 << slot;
445        } else {
446            self.upper |= 1_u64 << (slot - 64);
447        }
448    }
449}
450
451/// Validated document attributes with universal defaults.
452///
453/// Parsed documents expose their end-of-header snapshot through this read-only
454/// view. Use [`crate::Options::builder()`] to supply values.
455///
456/// Use `DocumentAttributes::default()` to get a map with universal defaults applied.
457///
458/// Parsed attributes cannot be changed through the read API:
459///
460/// ```compile_fail
461/// let mut attributes = acdc_parser::DocumentAttributes::default();
462/// attributes.set("name".into(), "value".into());
463/// ```
464#[derive(Clone)]
465pub struct DocumentAttributes<'a> {
466    entries: FxHashMap<AttributeName<'a>, StoredAssignment<'a>>,
467    base: Option<Arc<FxHashMap<AttributeName<'static>, DocumentAttributeValue<'static>>>>,
468    entry_filter: EntryFilter,
469}
470
471impl<'a> DocumentAttributes<'a> {
472    pub(crate) fn from_inputs(
473        inputs: RawAttributes<'a>,
474        state: AssignmentState,
475    ) -> Result<DocumentAttributes<'a>, Error> {
476        let mut attributes = DocumentAttributes::default();
477        attributes.entries.reserve(inputs.len());
478        for (name, value) in inputs {
479            if matches!(name.as_ref(), "outdir" | "outfile") {
480                continue;
481            }
482            validate_bounded_attribute(&name, &value);
483            let entry = validate_assignment_value(&name, RawAttributeValue::from(value))
484                .map_err(|invalid| invalid.into_error(&name, None))?;
485            attributes.set_entry(name, entry, state);
486        }
487        Ok(attributes)
488    }
489}
490
491impl<'a> DocumentAttributes<'a> {
492    fn into_input_map(self) -> RawAttributes<'a> {
493        let mut inputs = RawAttributes::default();
494        let defaults = default_document_attribute_values();
495        if let Some(base) = self.base
496            && !Arc::ptr_eq(&base, &defaults)
497        {
498            for (name, value) in base.iter() {
499                if defaults.get(name.as_ref()) != Some(value) {
500                    inputs.insert(name.clone(), value.clone().into_input());
501                }
502            }
503        }
504        for (name, stored) in self.entries {
505            let value = match stored.assignment {
506                DocumentAttributeAssignment::Unset => AttributeValue::None,
507                DocumentAttributeAssignment::Set(value) => value.into_input(),
508            };
509            inputs.insert(name, value);
510        }
511        inputs
512    }
513}
514
515pub(crate) fn default_document_attribute_values()
516-> Arc<FxHashMap<AttributeName<'static>, DocumentAttributeValue<'static>>> {
517    use std::sync::LazyLock;
518
519    static DEFAULTS: LazyLock<
520        Arc<FxHashMap<AttributeName<'static>, DocumentAttributeValue<'static>>>,
521    > = LazyLock::new(|| {
522        Arc::new(
523            crate::constants::DEFAULT_ATTRIBUTE_ENTRIES
524                .iter()
525                .filter_map(|(name, value)| {
526                    let value = match value {
527                        AttributeValue::String(value) => {
528                            DocumentAttributeValue::from(value.clone())
529                        }
530                        AttributeValue::Bool(true) => DocumentAttributeValue::presence(),
531                        AttributeValue::Bool(false) | AttributeValue::None => return None,
532                    };
533                    Some((name.clone(), value))
534                })
535                .collect(),
536        )
537    });
538    Arc::clone(&DEFAULTS)
539}
540
541impl fmt::Debug for DocumentAttributes<'_> {
542    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
543        f.debug_struct("DocumentAttributes")
544            .field("entries", &self.entries)
545            .field(
546                "base_entry_count",
547                &self.base.as_ref().map(|base| base.len()),
548            )
549            .finish()
550    }
551}
552
553impl PartialEq for DocumentAttributes<'_> {
554    fn eq(&self, other: &Self) -> bool {
555        self.entries == other.entries && self.base == other.base
556    }
557}
558
559impl Default for DocumentAttributes<'_> {
560    fn default() -> Self {
561        Self {
562            entries: FxHashMap::default(),
563            base: Some(default_document_attribute_values()),
564            entry_filter: EntryFilter::default(),
565        }
566    }
567}
568
569impl<'a> DocumentAttributes<'a> {
570    pub(crate) fn into_configuration(self) -> (RawAttributes<'a>, RawAttributes<'a>) {
571        let Self { entries, base, .. } = self;
572        let mut caller = RawAttributes::default();
573        let mut defaults = Self {
574            entries: FxHashMap::default(),
575            base,
576            entry_filter: EntryFilter::default(),
577        }
578        .into_input_map();
579        for (name, stored) in entries {
580            let value = match stored.assignment {
581                DocumentAttributeAssignment::Set(value) => value.into_input(),
582                DocumentAttributeAssignment::Unset => AttributeValue::None,
583            };
584            if stored.state.origin == AttributeOrigin::Caller {
585                caller.insert(name, value);
586            } else {
587                defaults.insert(name, value);
588            }
589        }
590        (caller, defaults)
591    }
592
593    /// Reuse explicit values as fresh configuration, retaining unsets and spelling.
594    ///
595    /// Universal defaults remain implicit. Rebuilding options determines the new
596    /// assignment precedence; parser-internal locks and origins are not exported.
597    pub fn into_inputs(self) -> impl Iterator<Item = (AttributeName<'a>, AttributeValue<'a>)> {
598        self.into_input_map().into_iter()
599    }
600
601    fn entry(&self, name: &str) -> Option<&StoredAssignment<'a>> {
602        if !self.entry_filter.may_contain(name) {
603            return None;
604        }
605        self.entries.get(name)
606    }
607
608    fn record_entry(&mut self, name: &str) {
609        self.entry_filter.insert(name);
610    }
611
612    fn rebuild_entry_filter(&mut self) {
613        let mut filter = EntryFilter::default();
614        for name in self.entries.keys() {
615            filter.insert(name);
616        }
617        self.entry_filter = filter;
618    }
619
620    /// Create an empty `DocumentAttributes` without default attributes.
621    /// Used for lightweight parsing contexts (e.g., quotes-only) where
622    /// document attributes aren't needed.
623    pub(crate) fn empty() -> Self {
624        Self {
625            entries: FxHashMap::default(),
626            base: None,
627            entry_filter: EntryFilter::default(),
628        }
629    }
630
631    /// Iterate over effective, referenceable attributes.
632    pub fn iter(&self) -> impl Iterator<Item = (&str, &DocumentAttributeValue<'_>)> {
633        let stored = self.entries.keys().map(AsRef::as_ref);
634        let base = self
635            .base
636            .iter()
637            .flat_map(|base| base.iter())
638            .filter(|(name, _)| !self.entries.contains_key(name.as_ref()))
639            .map(|(name, _)| name.as_ref());
640        let registry_defaults = std::iter::once(MAX_INCLUDE_DEPTH_ATTR)
641            .filter(|name| self.effective_default(name).is_some());
642        stored
643            .chain(base)
644            .chain(registry_defaults)
645            .filter_map(|name| self.get(name).map(|value| (name, value)))
646    }
647
648    /// Check whether the effective attribute view is empty.
649    #[must_use]
650    pub fn is_empty(&self) -> bool {
651        self.iter().next().is_none()
652    }
653
654    /// Only for derived text attributes, not typed caller inputs.
655    pub(crate) fn set_text(&mut self, name: AttributeName<'a>, value: Cow<'a, str>) {
656        debug_assert_ne!(name.as_ref(), MAX_INCLUDE_DEPTH_ATTR);
657        if matches!(name.as_ref(), "outdir" | "outfile") {
658            return;
659        }
660        self.set_entry(
661            name,
662            DocumentAttributeAssignment::new(
663                DocumentAttributeAssignment::Set(DocumentAttributeValue::from(value)),
664                None,
665            ),
666            AssignmentState::PROCESSOR,
667        );
668    }
669
670    pub(crate) fn insert_text(&mut self, name: AttributeName<'a>, value: Cow<'a, str>) {
671        if !self.entries.contains_key(name.as_ref())
672            && !self
673                .base
674                .as_ref()
675                .is_some_and(|base| base.contains_key(name.as_ref()))
676        {
677            self.set_text(name, value);
678        }
679    }
680
681    /// Refresh derived metadata without replacing caller overrides or document assignments.
682    pub(crate) fn set_derived_text(&mut self, name: AttributeName<'a>, value: Cow<'a, str>) {
683        match self.entry(&name) {
684            Some(entry) if entry.state.origin == AttributeOrigin::Processor => {
685                self.set_text(name, value);
686            }
687            None => self.insert_text(name, value),
688            Some(_) => {}
689        }
690    }
691
692    #[cfg(test)]
693    pub(crate) fn insert(
694        &mut self,
695        name: AttributeName<'a>,
696        value: AttributeValue<'a>,
697    ) -> Result<(), Error> {
698        if self.entries.contains_key(name.as_ref())
699            || self
700                .base
701                .as_ref()
702                .is_some_and(|base| base.contains_key(name.as_ref()))
703        {
704            return Ok(());
705        }
706        self.set(name, value)
707    }
708
709    #[cfg(test)]
710    pub(crate) fn set(
711        &mut self,
712        name: AttributeName<'a>,
713        value: AttributeValue<'a>,
714    ) -> Result<(), Error> {
715        if matches!(name.as_ref(), "outdir" | "outfile") {
716            return Ok(());
717        }
718        validate_bounded_attribute(&name, &value);
719        let entry = validate_assignment_value(&name, RawAttributeValue::from(value))
720            .map_err(|invalid| invalid.into_error(&name, None))?;
721        self.set_entry(name, entry, AssignmentState::PROCESSOR);
722        Ok(())
723    }
724
725    pub(crate) fn set_entry(
726        &mut self,
727        name: AttributeName<'a>,
728        assignment: DocumentAttributeAssignment<'a>,
729        state: AssignmentState,
730    ) {
731        self.record_entry(&name);
732        self.entries
733            .insert(name, StoredAssignment { assignment, state });
734    }
735
736    pub(crate) fn normalize_assignment(
737        &mut self,
738        name: AttributeName<'a>,
739        assignment: DocumentAttributeAssignment<'a>,
740    ) {
741        let state = self
742            .entry(&name)
743            .map_or(AssignmentState::PROCESSOR, |entry| entry.state);
744        self.set_entry(name, assignment, state);
745    }
746
747    pub(crate) fn assign_document_value(
748        &mut self,
749        name: AttributeName<'a>,
750        value: RawAttributeValue<'a>,
751        in_header: bool,
752        force_locked: bool,
753        source_location: Option<SourceLocation>,
754    ) -> Result<Option<DocumentAttributeAssignment<'a>>, Error> {
755        self.assign(
756            name,
757            AssignmentRequest {
758                raw: value,
759                state: AssignmentState::document(),
760                in_header,
761                force_locked,
762                source_location,
763            },
764        )
765    }
766
767    pub(crate) fn set_intrinsic(
768        &mut self,
769        name: AttributeName<'static>,
770        value: DocumentAttributeValue<'static>,
771    ) {
772        debug_assert!(is_intrinsic(&name));
773        if self.entries.remove(name.as_ref()).is_some() {
774            self.rebuild_entry_filter();
775        }
776        Arc::make_mut(self.base.get_or_insert_with(|| {
777            Arc::new(FxHashMap::with_capacity_and_hasher(16, FxBuildHasher))
778        }))
779        .insert(name, value);
780    }
781
782    pub(crate) fn set_base_intrinsic(
783        &mut self,
784        name: AttributeName<'static>,
785        value: DocumentAttributeValue<'static>,
786    ) {
787        debug_assert!(is_intrinsic(&name));
788        Arc::make_mut(self.base.get_or_insert_with(|| {
789            Arc::new(FxHashMap::with_capacity_and_hasher(16, FxBuildHasher))
790        }))
791        .insert(name, value);
792    }
793
794    pub(crate) fn set_base_map(
795        &mut self,
796        base: Arc<FxHashMap<AttributeName<'static>, DocumentAttributeValue<'static>>>,
797    ) {
798        let canonical = default_document_attribute_values();
799        if self
800            .base
801            .as_ref()
802            .is_some_and(|current| Arc::ptr_eq(current, &canonical))
803        {
804            self.base = Some(base);
805            return;
806        }
807
808        let current = Arc::make_mut(self.base.get_or_insert_with(|| {
809            Arc::new(FxHashMap::with_capacity_and_hasher(16, FxBuildHasher))
810        }));
811        for (name, value) in base.iter() {
812            if is_intrinsic(name) {
813                current.insert(name.clone(), value.clone());
814            }
815        }
816    }
817
818    /// Remove explicit assignments without changing shared base values.
819    pub(crate) fn remove_explicit_with_prefix(&mut self, prefix: &str) {
820        let names: Vec<_> = self
821            .entries
822            .iter()
823            .filter(|(name, _)| name.starts_with(prefix))
824            .map(|(name, _)| name)
825            .cloned()
826            .collect();
827        for name in names {
828            self.entries.remove(name.as_ref());
829        }
830        self.rebuild_entry_filter();
831    }
832
833    /// Remove one explicit assignment without changing shared base values.
834    pub(crate) fn remove_explicit(&mut self, name: &str) {
835        if self.entries.remove(name).is_some() {
836            self.rebuild_entry_filter();
837        }
838    }
839
840    fn assign(
841        &mut self,
842        name: AttributeName<'a>,
843        request: AssignmentRequest<'a>,
844    ) -> Result<Option<DocumentAttributeAssignment<'a>>, Error> {
845        let current = self.entries.get(name.as_ref()).map(|entry| {
846            (
847                entry.state,
848                matches!(entry.assignment, DocumentAttributeAssignment::Set(_)),
849            )
850        });
851        if assignment_decision(&name, current, &request) == AssignmentDecision::Reject {
852            return Ok(None);
853        }
854        let entry = validate_assignment_value(&name, request.raw)
855            .map_err(|invalid| invalid.into_error(&name, request.source_location))?;
856        self.set_entry(name, entry.clone(), request.state);
857        Ok(Some(entry))
858    }
859
860    /// Whether an explicit assignment exists, including an unset assignment.
861    #[must_use]
862    pub fn is_explicit(&self, name: &str) -> bool {
863        self.entry(name).is_some()
864    }
865
866    pub(crate) fn locks_nested_attribute(&self, name: &str) -> bool {
867        crate::document_attribute::nested_attribute_is_inherited(name)
868            && (self.contains_key(name)
869                || self
870                    .entry(name)
871                    .is_some_and(|entry| entry.state.lock == AttributeLock::Locked))
872    }
873
874    #[cfg(test)]
875    fn assignment_state(&self, name: &str) -> Option<AssignmentState> {
876        self.entries.get(name).map(|entry| entry.state)
877    }
878
879    pub(crate) fn processor_convenience_value(&self, prefix: &str) -> Option<String> {
880        self.entries
881            .iter()
882            .filter_map(|(name, entry)| {
883                (entry.state.origin == AttributeOrigin::Processor
884                    && entry.assignment.value().is_some())
885                .then(|| name.strip_prefix(prefix))
886                .flatten()
887                .filter(|suffix| !suffix.is_empty() && !suffix.contains('-'))
888            })
889            .min()
890            .map(str::to_string)
891    }
892
893    /// Borrow the effective value, including referenceable defaults.
894    #[must_use]
895    pub fn get(&self, name: &str) -> Option<&DocumentAttributeValue<'_>> {
896        if let Some(entry) = self.entry(name) {
897            return match &entry.assignment {
898                DocumentAttributeAssignment::Set(value) => Some(value),
899                DocumentAttributeAssignment::Unset => {
900                    self.base.as_ref()?;
901                    default_value(name)
902                }
903            };
904        }
905        self.resolve_base(name)
906    }
907
908    /// Borrow an explicit assignment, distinguishing unset from absent.
909    #[must_use]
910    pub fn assignment(&self, name: &str) -> Option<&DocumentAttributeAssignment<'_>> {
911        self.entry(name).map(|entry| &entry.assignment)
912    }
913
914    /// Iterate over explicit assignments, including unsets but not implicit defaults.
915    pub fn assignments(&self) -> impl Iterator<Item = (&str, &DocumentAttributeAssignment<'_>)> {
916        self.entries
917            .iter()
918            .map(|(name, entry)| (name.as_ref(), &entry.assignment))
919    }
920
921    pub(crate) fn status(&self, name: &str) -> DocumentAttributeStatus<'_> {
922        if let Some(entry) = self.entry(name) {
923            return match &entry.assignment {
924                DocumentAttributeAssignment::Set(value) => DocumentAttributeStatus::Set(value),
925                DocumentAttributeAssignment::Unset => DocumentAttributeStatus::Unset,
926            };
927        }
928        self.resolve_base(name).map_or(
929            DocumentAttributeStatus::Absent,
930            DocumentAttributeStatus::Set,
931        )
932    }
933
934    fn resolve_base(&self, name: &str) -> Option<&DocumentAttributeValue<'_>> {
935        let base = self.base.as_ref()?;
936        base.get(name).or_else(|| default_value(name))
937    }
938
939    pub(crate) fn text(&self, name: &str) -> Option<&str> {
940        self.get(name).and_then(DocumentAttributeValue::text)
941    }
942
943    pub(crate) fn is_document_value(&self, name: &str) -> bool {
944        self.entry(name)
945            .is_some_and(|entry| entry.state.origin == AttributeOrigin::Document)
946    }
947
948    pub(crate) fn stored_text(&self, name: &str) -> Option<&Cow<'a, str>> {
949        if let Some(entry) = self.entry(name) {
950            return entry.assignment.stored_text();
951        }
952        self.base.as_ref()?.get(name)?.stored_text()
953    }
954
955    /// Return whether the attribute has an effective value.
956    #[must_use]
957    pub fn contains_key(&self, name: &str) -> bool {
958        self.get(name).is_some()
959    }
960
961    /// Remove stored values for a name. A synthesized registry default may remain.
962    pub(crate) fn remove(&mut self, name: &str) -> Option<DocumentAttributeValue<'a>> {
963        let explicit = self.entries.remove(name);
964        let removed_explicit = explicit.is_some();
965        let explicit = explicit.and_then(|entry| match entry.assignment {
966            DocumentAttributeAssignment::Set(value) => Some(value),
967            DocumentAttributeAssignment::Unset => None,
968        });
969        if removed_explicit {
970            self.rebuild_entry_filter();
971        }
972        // Avoid cloning the shared base map when the requested value is absent.
973        let base = if self
974            .base
975            .as_ref()
976            .is_some_and(|base| base.contains_key(name))
977        {
978            self.base
979                .as_mut()
980                .and_then(|base| Arc::make_mut(base).remove(name))
981        } else {
982            None
983        };
984        explicit.or(base)
985    }
986
987    fn effective_default(&self, name: &str) -> Option<&'static DocumentAttributeValue<'static>> {
988        if self.base.is_none()
989            || self.entries.contains_key(name)
990            || self
991                .base
992                .as_ref()
993                .is_some_and(|base| base.contains_key(name))
994        {
995            return None;
996        }
997        default_value(name)
998    }
999
1000    pub(crate) fn serialization_is_empty(&self) -> bool {
1001        self.entries
1002            .values()
1003            .all(|entry| matches!(entry.assignment, DocumentAttributeAssignment::Unset))
1004    }
1005
1006    /// Merge processor defaults without replacing explicit input, except for
1007    /// protected processor attributes. Incoming assignments become processor values.
1008    pub(crate) fn merge(&mut self, other: Self) {
1009        let Self {
1010            entries: other_entries,
1011            base,
1012            entry_filter: _,
1013        } = other;
1014        if let Some(base) = base {
1015            if self.base.is_none() {
1016                self.base = Some(base);
1017            } else if !self
1018                .base
1019                .as_ref()
1020                .is_some_and(|current| Arc::ptr_eq(current, &base))
1021            {
1022                for (name, value) in base.iter() {
1023                    if !self.entries.contains_key(name.as_ref())
1024                        && !self
1025                            .base
1026                            .as_ref()
1027                            .is_some_and(|current| current.contains_key(name.as_ref()))
1028                    {
1029                        Arc::make_mut(self.base.get_or_insert_with(|| {
1030                            Arc::new(FxHashMap::with_capacity_and_hasher(16, FxBuildHasher))
1031                        }))
1032                        .insert(name.clone(), value.clone());
1033                    }
1034                }
1035            }
1036        }
1037        for (name, mut entry) in other_entries {
1038            let Some(state) =
1039                processor_assignment_state(&name, self.entries.contains_key(name.as_ref()))
1040            else {
1041                continue;
1042            };
1043            entry.state = state;
1044            self.entries.insert(name, entry);
1045        }
1046        self.rebuild_entry_filter();
1047    }
1048
1049    /// Clone the attributes into an independent `'static` copy. Used by
1050    /// converters that cache document attributes on a processor whose
1051    /// lifetime is independent of the document being rendered.
1052    #[must_use]
1053    pub fn to_static(&self) -> DocumentAttributes<'static> {
1054        DocumentAttributes {
1055            entries: self
1056                .entries
1057                .iter()
1058                .map(|(name, entry)| {
1059                    (
1060                        Cow::Owned(name.to_string()),
1061                        StoredAssignment {
1062                            assignment: entry.assignment.to_static(),
1063                            state: entry.state,
1064                        },
1065                    )
1066                })
1067                .collect(),
1068            base: self.base.clone(),
1069            entry_filter: self.entry_filter.clone(),
1070        }
1071    }
1072
1073    /// Consume the attributes, producing an independent `'static` copy.
1074    #[must_use]
1075    pub fn into_static(self) -> DocumentAttributes<'static> {
1076        let Self {
1077            entries,
1078            base,
1079            entry_filter: _,
1080        } = self;
1081        let entries = entries
1082            .into_iter()
1083            .map(|(name, entry)| {
1084                (
1085                    Cow::Owned(name.into_owned()),
1086                    StoredAssignment {
1087                        assignment: entry.assignment.into_static(),
1088                        state: entry.state,
1089                    },
1090                )
1091            })
1092            .collect();
1093        let mut attributes = DocumentAttributes {
1094            entries,
1095            base,
1096            entry_filter: EntryFilter::default(),
1097        };
1098        attributes.rebuild_entry_filter();
1099        attributes
1100    }
1101}
1102
1103impl Serialize for DocumentAttributes<'_> {
1104    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1105    where
1106        S: Serializer,
1107    {
1108        let mut entries: Vec<_> = self
1109            .entries
1110            .iter()
1111            .filter(|(_, entry)| matches!(entry.assignment, DocumentAttributeAssignment::Set(_)))
1112            .collect();
1113        entries.sort_by_key(|(name, _)| *name);
1114
1115        let mut state = serializer.serialize_map(Some(entries.len()))?;
1116        for (name, entry) in entries {
1117            state.serialize_entry(name, &entry.assignment.serialized_value(name == "toc"))?;
1118        }
1119        state.end()
1120    }
1121}
1122
1123#[cfg(test)]
1124mod document_attribute_tests {
1125    use super::*;
1126    use crate::document_attribute::{AttributeLock, AttributeOrigin, MAX_INCLUDE_DEPTH_ATTR};
1127    use serde_json::{Error as JsonError, json, to_value};
1128
1129    #[test]
1130    fn owned_attribute_text_and_names_survive_consuming_conversion_without_copying() {
1131        let name = String::from("owned-name");
1132        let value = String::from("owned attribute value");
1133        let name_pointer = name.as_ptr();
1134        let value_pointer = value.as_ptr();
1135        let mut attributes = DocumentAttributes::empty();
1136        assert!(
1137            attributes
1138                .set(Cow::Owned(name), AttributeValue::String(Cow::Owned(value)))
1139                .is_ok()
1140        );
1141        assert_eq!(
1142            attributes.text("owned-name").map(str::as_ptr),
1143            Some(value_pointer)
1144        );
1145        let attributes = attributes.into_static();
1146        assert_eq!(
1147            attributes.text("owned-name").map(str::as_ptr),
1148            Some(value_pointer)
1149        );
1150        assert_eq!(
1151            attributes
1152                .iter()
1153                .find(|(name, _)| *name == "owned-name")
1154                .map(|(name, _)| name.as_ptr()),
1155            Some(name_pointer)
1156        );
1157    }
1158
1159    #[test]
1160    fn captions_are_default_attributes_but_numbering_depths_are_not() {
1161        let attributes = DocumentAttributes::default();
1162
1163        assert_eq!(attributes.text("example-caption"), Some("Example"));
1164        assert_eq!(attributes.text("figure-caption"), Some("Figure"));
1165        assert_eq!(attributes.text("table-caption"), Some("Table"));
1166        assert_eq!(attributes.get("listing-caption"), None);
1167        assert_eq!(attributes.get("toclevels"), None);
1168        assert_eq!(attributes.get("sectnumlevels"), None);
1169    }
1170
1171    #[test]
1172    fn effective_defaults_obey_the_public_map_laws() -> Result<(), JsonError> {
1173        let attributes = DocumentAttributes::default();
1174
1175        assert_eq!(
1176            attributes
1177                .get(MAX_INCLUDE_DEPTH_ATTR)
1178                .and_then(DocumentAttributeValue::as_integer),
1179            Some(64)
1180        );
1181        assert!(attributes.contains_key(MAX_INCLUDE_DEPTH_ATTR));
1182        assert_eq!(
1183            attributes
1184                .iter()
1185                .filter(|(name, _)| *name == MAX_INCLUDE_DEPTH_ATTR)
1186                .count(),
1187            1
1188        );
1189        assert!(!attributes.is_empty());
1190        assert_eq!(to_value(&attributes)?, json!({}));
1191
1192        let static_attributes = attributes.into_static();
1193        assert_eq!(
1194            static_attributes
1195                .get(MAX_INCLUDE_DEPTH_ATTR)
1196                .and_then(DocumentAttributeValue::as_integer),
1197            Some(64)
1198        );
1199        Ok(())
1200    }
1201
1202    #[test]
1203    fn empty_document_attributes_do_not_synthesize_defaults() -> Result<(), JsonError> {
1204        let attributes = DocumentAttributes::empty();
1205
1206        assert_eq!(attributes.get(MAX_INCLUDE_DEPTH_ATTR), None);
1207        assert!(!attributes.contains_key(MAX_INCLUDE_DEPTH_ATTR));
1208        assert_eq!(attributes.iter().count(), 0);
1209        assert!(attributes.is_empty());
1210        assert_eq!(to_value(&attributes)?, json!({}));
1211
1212        let static_attributes = attributes.into_static();
1213        assert_eq!(static_attributes.get(MAX_INCLUDE_DEPTH_ATTR), None);
1214        Ok(())
1215    }
1216
1217    #[test]
1218    fn explicit_max_include_depth_uses_normal_map_semantics() -> Result<(), JsonError> {
1219        let mut attributes = DocumentAttributes::default();
1220        assert!(
1221            attributes
1222                .set(MAX_INCLUDE_DEPTH_ATTR.into(), "8".into())
1223                .is_ok()
1224        );
1225
1226        assert_eq!(attributes.text(MAX_INCLUDE_DEPTH_ATTR), Some("8"));
1227        assert!(attributes.contains_key(MAX_INCLUDE_DEPTH_ATTR));
1228        assert_eq!(
1229            attributes
1230                .iter()
1231                .find(|(name, _)| *name == MAX_INCLUDE_DEPTH_ATTR)
1232                .map(|(_, value)| value)
1233                .and_then(DocumentAttributeValue::as_integer),
1234            Some(8)
1235        );
1236        assert!(!attributes.is_empty());
1237        assert_eq!(to_value(&attributes)?, json!({ "max-include-depth": "8" }));
1238        Ok(())
1239    }
1240
1241    #[test]
1242    fn unset_max_include_depth_values_are_not_serialized() -> Result<(), JsonError> {
1243        for value in [AttributeValue::Bool(false), AttributeValue::None] {
1244            let mut attributes = DocumentAttributes::default();
1245            assert!(
1246                attributes
1247                    .set(MAX_INCLUDE_DEPTH_ATTR.into(), value.clone())
1248                    .is_ok()
1249            );
1250
1251            assert_eq!(
1252                attributes
1253                    .get(MAX_INCLUDE_DEPTH_ATTR)
1254                    .and_then(DocumentAttributeValue::as_integer),
1255                Some(64)
1256            );
1257            assert!(attributes.contains_key(MAX_INCLUDE_DEPTH_ATTR));
1258            assert_eq!(
1259                attributes
1260                    .iter()
1261                    .find(|(name, _)| *name == MAX_INCLUDE_DEPTH_ATTR)
1262                    .map(|(_, stored)| stored)
1263                    .and_then(DocumentAttributeValue::as_integer),
1264                Some(64)
1265            );
1266            assert!(!attributes.is_empty());
1267            assert_eq!(to_value(&attributes)?, json!({}));
1268        }
1269        Ok(())
1270    }
1271
1272    #[test]
1273    fn unset_values_are_excluded_from_effective_lookup_and_iteration() {
1274        let mut attributes = DocumentAttributes::empty();
1275        assert!(
1276            attributes
1277                .set("false".into(), AttributeValue::Bool(false))
1278                .is_ok()
1279        );
1280        assert!(attributes.set("none".into(), AttributeValue::None).is_ok());
1281
1282        for name in ["false", "none"] {
1283            assert_eq!(attributes.get(name), None);
1284            assert!(!attributes.contains_key(name));
1285            assert!(attributes.iter().all(|(candidate, _)| candidate != name));
1286            assert!(attributes.is_explicit(name));
1287        }
1288        assert!(attributes.is_empty());
1289        assert_eq!(
1290            attributes
1291                .entries
1292                .values()
1293                .filter(|entry| { matches!(entry.assignment, DocumentAttributeAssignment::Unset) })
1294                .count(),
1295            2
1296        );
1297    }
1298
1299    #[test]
1300    fn value_and_text_accessors_keep_distinct_values() {
1301        let mut attributes = DocumentAttributes::default();
1302        assert!(
1303            attributes
1304                .set(MAX_INCLUDE_DEPTH_ATTR.into(), "064".into())
1305                .is_ok()
1306        );
1307        assert!(
1308            attributes
1309                .set("present".into(), AttributeValue::Bool(true))
1310                .is_ok()
1311        );
1312        assert!(attributes.set("empty-text".into(), "".into()).is_ok());
1313        assert!(attributes.set("quoted".into(), "\"value\"".into()).is_ok());
1314
1315        assert_eq!(
1316            attributes
1317                .get(MAX_INCLUDE_DEPTH_ATTR)
1318                .and_then(DocumentAttributeValue::as_integer),
1319            Some(64)
1320        );
1321        assert_eq!(attributes.text(MAX_INCLUDE_DEPTH_ATTR), Some("064"));
1322        assert_eq!(attributes.text("quoted"), Some("\"value\""));
1323        assert!(
1324            attributes
1325                .get("present")
1326                .is_some_and(DocumentAttributeValue::is_presence)
1327        );
1328        assert_eq!(attributes.text("present"), None);
1329        assert_eq!(
1330            attributes
1331                .get("empty-text")
1332                .and_then(|value| value.as_str()),
1333            Some("")
1334        );
1335        assert!(attributes.contains_key("empty-text"));
1336
1337        assert!(
1338            attributes
1339                .set(MAX_INCLUDE_DEPTH_ATTR.into(), "0".into())
1340                .is_ok()
1341        );
1342        assert_eq!(
1343            attributes
1344                .get(MAX_INCLUDE_DEPTH_ATTR)
1345                .and_then(DocumentAttributeValue::as_integer),
1346            Some(0)
1347        );
1348        assert!(attributes.contains_key(MAX_INCLUDE_DEPTH_ATTR));
1349
1350        let default = DocumentAttributes::default();
1351        assert_eq!(
1352            default
1353                .get(MAX_INCLUDE_DEPTH_ATTR)
1354                .and_then(DocumentAttributeValue::as_integer),
1355            Some(64)
1356        );
1357        assert_eq!(default.text(MAX_INCLUDE_DEPTH_ATTR), None);
1358        let mut rendered = String::new();
1359        assert!(matches!(
1360            default
1361                .get(MAX_INCLUDE_DEPTH_ATTR)
1362                .map(|value| value.write_text(&mut rendered)),
1363            Some(Ok(()))
1364        ));
1365        assert_eq!(rendered, "64");
1366        rendered.clear();
1367        assert!(matches!(
1368            attributes
1369                .get("present")
1370                .map(|value| value.write_text(&mut rendered)),
1371            Some(Ok(()))
1372        ));
1373        assert_eq!(rendered, "");
1374    }
1375
1376    #[test]
1377    fn caller_and_document_assignments_retain_private_provenance() -> Result<(), Error> {
1378        let mut attributes = DocumentAttributes::default();
1379        assert!(attributes.set("name".into(), "processor".into()).is_ok());
1380        assert_eq!(
1381            attributes.assignment_state("name"),
1382            Some(AssignmentState::PROCESSOR)
1383        );
1384
1385        let mut attributes =
1386            crate::Options::with_attributes(attributes.into_inputs())?.document_attributes;
1387        assert_eq!(
1388            attributes
1389                .assignment_state("name")
1390                .map(|state| state.origin),
1391            Some(AttributeOrigin::Caller)
1392        );
1393        assert_eq!(
1394            attributes.assignment_state("name").map(|state| state.lock),
1395            Some(AttributeLock::Locked)
1396        );
1397
1398        assert!(
1399            attributes
1400                .assign_document_value(
1401                    "name".into(),
1402                    RawAttributeValue::Text("document".into()),
1403                    true,
1404                    false,
1405                    None,
1406                )?
1407                .is_none()
1408        );
1409        assert_eq!(attributes.text("name"), Some("processor"));
1410
1411        assert!(
1412            attributes
1413                .assign_document_value(
1414                    "other".into(),
1415                    RawAttributeValue::Text("document".into()),
1416                    false,
1417                    false,
1418                    None,
1419                )?
1420                .is_some()
1421        );
1422        assert_eq!(
1423            attributes
1424                .assignment_state("other")
1425                .map(|state| state.origin),
1426            Some(AttributeOrigin::Document)
1427        );
1428        assert_eq!(
1429            attributes.assignment_state("other").map(|state| state.lock),
1430            Some(AttributeLock::Unlocked)
1431        );
1432        Ok(())
1433    }
1434
1435    #[test]
1436    fn merge_preserves_processor_origin_and_numeric_value() {
1437        let mut processor = DocumentAttributes::default();
1438        assert!(
1439            processor
1440                .set(MAX_INCLUDE_DEPTH_ATTR.into(), "064".into())
1441                .is_ok()
1442        );
1443
1444        let mut attributes = DocumentAttributes::default();
1445        attributes.merge(processor);
1446
1447        assert_eq!(
1448            attributes.assignment_state(MAX_INCLUDE_DEPTH_ATTR),
1449            Some(AssignmentState::PROCESSOR)
1450        );
1451        assert_eq!(
1452            attributes
1453                .get(MAX_INCLUDE_DEPTH_ATTR)
1454                .and_then(DocumentAttributeValue::as_integer),
1455            Some(64)
1456        );
1457        assert_eq!(attributes.text(MAX_INCLUDE_DEPTH_ATTR), Some("064"));
1458    }
1459
1460    #[test]
1461    fn document_attributes_track_clones_merges_and_removed_assignments() {
1462        let mut attributes = DocumentAttributes::default();
1463        assert!(attributes.set("name".into(), "header".into()).is_ok());
1464        let header = attributes.clone();
1465
1466        assert!(attributes.set("name".into(), "body".into()).is_ok());
1467        assert_eq!(header.text("name"), Some("header"));
1468        assert_eq!(attributes.text("name"), Some("body"));
1469
1470        let mut processor = DocumentAttributes::empty();
1471        assert!(processor.set("merged".into(), "value".into()).is_ok());
1472        attributes.merge(processor);
1473        assert_eq!(attributes.text("merged"), Some("value"));
1474
1475        assert!(
1476            attributes
1477                .set("figure-caption".into(), false.into())
1478                .is_ok()
1479        );
1480        assert_eq!(attributes.text("figure-caption"), None);
1481        attributes.remove_explicit("figure-caption");
1482        assert_eq!(attributes.text("figure-caption"), Some("Figure"));
1483    }
1484
1485    #[test]
1486    fn processor_convenience_value_is_deterministic() {
1487        let mut attributes = DocumentAttributes::default();
1488        assert!(attributes.set("filetype-zeta".into(), true.into()).is_ok());
1489        assert!(attributes.set("filetype-alpha".into(), true.into()).is_ok());
1490
1491        assert_eq!(
1492            attributes.processor_convenience_value("filetype-"),
1493            Some("alpha".to_string())
1494        );
1495    }
1496
1497    #[test]
1498    fn configuration_validation_rejects_invalid_inputs() -> Result<(), Error> {
1499        let inputs =
1500            crate::Options::builder().with_default_attribute(MAX_INCLUDE_DEPTH_ATTR, "2junk");
1501        assert!(matches!(
1502            inputs.clone().build(),
1503            Err(Error::InvalidDocumentAttribute {
1504                ref name,
1505                ref value,
1506                expected: "a complete non-negative integer",
1507                location: None,
1508            }) if name == MAX_INCLUDE_DEPTH_ATTR && value == "2junk"
1509        ));
1510
1511        let attributes = inputs
1512            .with_default_attribute(MAX_INCLUDE_DEPTH_ATTR, "02")
1513            .build()?
1514            .into_document_attributes();
1515        assert_eq!(
1516            attributes
1517                .get(MAX_INCLUDE_DEPTH_ATTR)
1518                .and_then(DocumentAttributeValue::as_integer),
1519            Some(2)
1520        );
1521        assert_eq!(attributes.text(MAX_INCLUDE_DEPTH_ATTR), Some("02"));
1522        Ok(())
1523    }
1524}
1525
1526/// Element-level attributes (for blocks, sections, etc.).
1527///
1528/// These attributes are specific to individual elements and start empty.
1529///
1530/// Use `ElementAttributes::default()` to get an empty attribute map.
1531#[derive(Debug, Default, PartialEq, Clone)]
1532pub struct ElementAttributes<'a>(FxHashMap<AttributeName<'a>, AttributeValue<'a>>);
1533
1534impl<'a> ElementAttributes<'a> {
1535    /// Iterate over all attributes.
1536    pub fn iter(&self) -> impl Iterator<Item = (&AttributeName<'a>, &AttributeValue<'a>)> {
1537        self.0.iter()
1538    }
1539
1540    pub(crate) fn values_mut(&mut self) -> impl Iterator<Item = &mut AttributeValue<'a>> {
1541        self.0.values_mut()
1542    }
1543
1544    /// Check if the attribute map is empty.
1545    #[must_use]
1546    pub fn is_empty(&self) -> bool {
1547        self.0.is_empty()
1548    }
1549
1550    /// Insert an attribute without replacing an existing value.
1551    pub fn insert(&mut self, name: AttributeName<'a>, value: AttributeValue<'a>) {
1552        self.0.entry(name).or_insert(value);
1553    }
1554
1555    /// Set an attribute, overwriting any existing value.
1556    pub fn set(&mut self, name: AttributeName<'a>, value: AttributeValue<'a>) {
1557        self.0.insert(name, value);
1558    }
1559
1560    /// Get an attribute value by name.
1561    #[must_use]
1562    pub fn get(&self, name: &str) -> Option<&AttributeValue<'a>> {
1563        self.0.get(name)
1564    }
1565
1566    /// Check if an attribute exists.
1567    #[must_use]
1568    pub fn contains_key(&self, name: &str) -> bool {
1569        self.0.contains_key(name)
1570    }
1571
1572    /// Remove an attribute by name.
1573    pub fn remove(&mut self, name: &str) -> Option<AttributeValue<'a>> {
1574        self.0.remove(name)
1575    }
1576
1577    /// Merge attributes without replacing existing values.
1578    pub fn merge(&mut self, other: Self) {
1579        for (key, value) in other.0 {
1580            self.insert(key, value);
1581        }
1582    }
1583
1584    /// Convert all borrowed content to owned, producing `'static` lifetime attributes.
1585    #[must_use]
1586    pub fn into_static(self) -> ElementAttributes<'static> {
1587        ElementAttributes(
1588            self.0
1589                .into_iter()
1590                .map(|(k, v)| {
1591                    let key: AttributeName<'static> = Cow::Owned(k.into_owned());
1592                    let val = match v {
1593                        AttributeValue::String(s) => {
1594                            AttributeValue::String(Cow::Owned(s.into_owned()))
1595                        }
1596                        AttributeValue::Bool(b) => AttributeValue::Bool(b),
1597                        AttributeValue::None => AttributeValue::None,
1598                    };
1599                    (key, val)
1600                })
1601                .collect(),
1602        )
1603    }
1604
1605    /// Get a text attribute, borrowing its content when possible.
1606    ///
1607    /// Strips surrounding quotes from the value if present.
1608    #[must_use]
1609    pub fn get_string(&self, name: &str) -> Option<Cow<'a, str>> {
1610        self.get(name).and_then(|v| match v {
1611            AttributeValue::String(s) => Some(match s {
1612                Cow::Borrowed(b) => Cow::Borrowed(strip_quotes(b)),
1613                Cow::Owned(o) => Cow::Owned(strip_quotes(o).to_string()),
1614            }),
1615            AttributeValue::None | AttributeValue::Bool(_) => None,
1616        })
1617    }
1618}
1619
1620impl Serialize for ElementAttributes<'_> {
1621    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1622    where
1623        S: Serializer,
1624    {
1625        let mut entries: Vec<_> = self.0.iter().collect();
1626        entries.sort_by_key(|(key, _)| *key);
1627
1628        let mut state = serializer.serialize_map(Some(entries.len()))?;
1629        for (key, value) in entries {
1630            match value {
1631                AttributeValue::Bool(true) if key == "toc" => {
1632                    state.serialize_entry(key, "")?;
1633                }
1634                AttributeValue::Bool(true) => {
1635                    state.serialize_entry(key, &true)?;
1636                }
1637                AttributeValue::Bool(false) | AttributeValue::String(_) | AttributeValue::None => {
1638                    state.serialize_entry(key, value)?;
1639                }
1640            }
1641        }
1642        state.end()
1643    }
1644}
1645
1646#[cfg(test)]
1647mod element_attribute_tests {
1648    use super::{AttributeValue, ElementAttributes};
1649    use serde_json::{Error, json, to_value};
1650
1651    #[test]
1652    fn insert_set_and_merge_preserve_existing_value_rules() -> Result<(), Error> {
1653        let mut attributes = ElementAttributes::default();
1654        assert!(attributes.is_empty());
1655        attributes.insert("role".into(), "first".into());
1656        attributes.insert("role".into(), "ignored".into());
1657        assert_eq!(attributes.get_string("role").as_deref(), Some("first"));
1658
1659        attributes.set("role".into(), "replacement".into());
1660        let mut defaults = ElementAttributes::default();
1661        defaults.set("role".into(), "default".into());
1662        defaults.set("width".into(), "50%".into());
1663        attributes.merge(defaults);
1664        assert_eq!(attributes.iter().count(), 2);
1665        assert_eq!(
1666            to_value(&attributes)?,
1667            json!({"role": "replacement", "width": "50%"})
1668        );
1669
1670        let snapshot = attributes.clone().into_static();
1671        assert_eq!(snapshot, attributes);
1672        assert_eq!(attributes.remove("role"), Some("replacement".into()));
1673        assert!(!attributes.contains_key("role"));
1674        assert_eq!(attributes.remove("width"), Some("50%".into()));
1675        assert_eq!(attributes.remove("missing"), None);
1676        assert!(attributes.is_empty());
1677        assert_eq!(to_value(&attributes)?, json!({}));
1678        assert_eq!(snapshot.iter().count(), 2);
1679        Ok(())
1680    }
1681
1682    #[test]
1683    fn serialization_preserves_all_value_forms_and_sorted_names() -> Result<(), Error> {
1684        let mut attributes = ElementAttributes::default();
1685        for (name, value) in [
1686            ("text", "value".into()),
1687            ("toc", AttributeValue::Bool(true)),
1688            ("present", AttributeValue::Bool(true)),
1689            ("false", AttributeValue::Bool(false)),
1690            ("none", AttributeValue::None),
1691        ] {
1692            attributes.set(name.into(), value);
1693        }
1694        let expected = r#"{"false":false,"none":null,"present":true,"text":"value","toc":""}"#;
1695        assert_eq!(serde_json::to_string(&attributes)?, expected);
1696        assert_eq!(serde_json::to_string(&attributes.into_static())?, expected);
1697        Ok(())
1698    }
1699}
1700
1701/// An `AttributeName` represents the name of an attribute in a document.
1702pub type AttributeName<'a> = Cow<'a, str>;
1703
1704/// An `AttributeValue` represents the value of an attribute in a document.
1705///
1706/// An attribute value can be a string, a boolean, or nothing
1707#[derive(Clone, Debug, PartialEq, Serialize)]
1708#[serde(untagged)]
1709#[non_exhaustive]
1710pub enum AttributeValue<'a> {
1711    /// A string attribute value.
1712    String(Cow<'a, str>),
1713    /// A boolean attribute value. `false` means it is unset.
1714    Bool(bool),
1715    /// No value (or it was unset)
1716    None,
1717}
1718
1719impl std::fmt::Display for AttributeValue<'_> {
1720    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1721        match self {
1722            AttributeValue::String(value) => write!(f, "{value}"),
1723            AttributeValue::Bool(value) => write!(f, "{value}"),
1724            AttributeValue::None => write!(f, "null"),
1725        }
1726    }
1727}
1728
1729impl<'a> From<&'a str> for AttributeValue<'a> {
1730    fn from(value: &'a str) -> Self {
1731        AttributeValue::String(Cow::Borrowed(value))
1732    }
1733}
1734
1735impl From<String> for AttributeValue<'_> {
1736    fn from(value: String) -> Self {
1737        AttributeValue::String(Cow::Owned(value))
1738    }
1739}
1740
1741impl From<bool> for AttributeValue<'_> {
1742    fn from(value: bool) -> Self {
1743        AttributeValue::Bool(value)
1744    }
1745}
1746
1747impl From<()> for AttributeValue<'_> {
1748    fn from((): ()) -> Self {
1749        AttributeValue::None
1750    }
1751}