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#[must_use]
28pub fn strip_quotes(s: &str) -> &str {
29 s.trim_start_matches(['"', '\''])
30 .trim_end_matches(['"', '\''])
31}
32
33#[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 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 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 #[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 #[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 #[must_use]
96 pub const fn is_presence(&self) -> bool {
97 matches!(self.0, ValueKind::Presence)
98 }
99
100 #[must_use]
105 pub fn text(&self) -> Option<&str> {
106 self.stored_text().map(AsRef::as_ref)
107 }
108
109 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 #[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 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#[derive(Clone, Debug, PartialEq, Eq)]
287pub enum DocumentAttributeAssignment<'a> {
288 Set(DocumentAttributeValue<'a>),
290 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 #[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
368fn 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#[derive(Clone, Default)]
416struct EntryFilter {
417 lower: u64,
418 upper: u64,
419}
420
421impl EntryFilter {
422 fn slot(name: &str) -> usize {
423 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#[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 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 pub(crate) fn empty() -> Self {
624 Self {
625 entries: FxHashMap::default(),
626 base: None,
627 entry_filter: EntryFilter::default(),
628 }
629 }
630
631 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 #[must_use]
650 pub fn is_empty(&self) -> bool {
651 self.iter().next().is_none()
652 }
653
654 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 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 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 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 #[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 #[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 #[must_use]
910 pub fn assignment(&self, name: &str) -> Option<&DocumentAttributeAssignment<'_>> {
911 self.entry(name).map(|entry| &entry.assignment)
912 }
913
914 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 #[must_use]
957 pub fn contains_key(&self, name: &str) -> bool {
958 self.get(name).is_some()
959 }
960
961 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 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 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 #[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 #[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#[derive(Debug, Default, PartialEq, Clone)]
1532pub struct ElementAttributes<'a>(FxHashMap<AttributeName<'a>, AttributeValue<'a>>);
1533
1534impl<'a> ElementAttributes<'a> {
1535 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 #[must_use]
1546 pub fn is_empty(&self) -> bool {
1547 self.0.is_empty()
1548 }
1549
1550 pub fn insert(&mut self, name: AttributeName<'a>, value: AttributeValue<'a>) {
1552 self.0.entry(name).or_insert(value);
1553 }
1554
1555 pub fn set(&mut self, name: AttributeName<'a>, value: AttributeValue<'a>) {
1557 self.0.insert(name, value);
1558 }
1559
1560 #[must_use]
1562 pub fn get(&self, name: &str) -> Option<&AttributeValue<'a>> {
1563 self.0.get(name)
1564 }
1565
1566 #[must_use]
1568 pub fn contains_key(&self, name: &str) -> bool {
1569 self.0.contains_key(name)
1570 }
1571
1572 pub fn remove(&mut self, name: &str) -> Option<AttributeValue<'a>> {
1574 self.0.remove(name)
1575 }
1576
1577 pub fn merge(&mut self, other: Self) {
1579 for (key, value) in other.0 {
1580 self.insert(key, value);
1581 }
1582 }
1583
1584 #[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 #[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
1701pub type AttributeName<'a> = Cow<'a, str>;
1703
1704#[derive(Clone, Debug, PartialEq, Serialize)]
1708#[serde(untagged)]
1709#[non_exhaustive]
1710pub enum AttributeValue<'a> {
1711 String(Cow<'a, str>),
1713 Bool(bool),
1715 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}