1use std::collections::HashMap;
12
13use crate::ast::{ArgumentValue, Ast, ContentMode, GroupKind, Node, NodeId, Slot};
14use crate::rewrite::helpers::mandatory_content_slot;
15
16mod generated {
17 include!(concat!(env!("OUT_DIR"), "/lower_attributes_generated.rs"));
18}
19
20use generated::{CommandRef, DeclarativeEntry, ModeTarget, PrefixEntry};
21
22#[derive(Clone, Debug, Default, PartialEq, Eq)]
33pub struct LowerAttributesReport {
34 pub attributes: HashMap<AttributeSet, AttributeStat>,
36 pub eliminated_empty_segments: usize,
39}
40
41#[derive(Clone, Debug, Default, PartialEq, Eq)]
42pub struct AttributeStat {
43 pub consumed: AttributeFormCounts,
45 pub redundant: AttributeFormCounts,
50 pub emitted: AttributeFormCounts,
52}
53
54#[derive(Clone, Debug, Default, PartialEq, Eq)]
56pub struct AttributeFormCounts {
57 pub declaratives: usize,
58 pub prefixes: usize,
59}
60
61#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
63pub enum Attr {
64 MathFont,
65 MathSize,
66 MathStyle,
67 TextFamily,
68 TextSeries,
69 TextShape,
70 TextSize,
71}
72
73#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
75pub enum AttrValue {
76 MathFont(MathFontValue),
77 Size(SizeValue),
78 Style(StyleValue),
79 TextFamily(TextFamily),
80 TextSeries(TextSeries),
81 TextShape(TextShape),
82}
83
84#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
85pub struct MathFontValue(pub &'static str);
86
87#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
90pub struct SizeValue(pub i32);
91
92#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
93pub struct StyleValue {
94 pub letter: &'static str,
95 pub display: bool,
96 pub level: u8,
97}
98
99#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
100pub enum TextFamily {
101 Roman,
102 SansSerif,
103 Typewriter,
104 Calligraphic,
105 Italic,
106 Oldstyle,
107}
108
109#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
110pub enum TextSeries {
111 Medium,
112 Bold,
113}
114
115#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
116pub enum TextShape {
117 Upright,
118 Italic,
119 Slanted,
120 SmallCaps,
121}
122
123#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
124pub struct AttributeSet {
125 attr: Attr,
126 value: AttrValue,
127}
128
129impl AttributeSet {
130 pub const fn new(attr: Attr, value: AttrValue) -> Self {
131 Self { attr, value }
132 }
133
134 pub const fn attr(self) -> Attr {
135 self.attr
136 }
137
138 pub const fn value(self) -> AttrValue {
139 self.value
140 }
141}
142
143impl LowerAttributesReport {
144 fn stat_mut(&mut self, set: AttributeSet) -> &mut AttributeStat {
145 self.attributes.entry(set).or_default()
146 }
147
148 fn record_consumed_declarative(&mut self, set: AttributeSet) {
149 self.stat_mut(set).consumed.declaratives += 1;
150 }
151
152 fn record_consumed_prefix(&mut self, set: AttributeSet) {
153 self.stat_mut(set).consumed.prefixes += 1;
154 }
155
156 fn record_redundant_declarative(&mut self, set: AttributeSet) {
157 self.stat_mut(set).redundant.declaratives += 1;
158 }
159
160 fn record_redundant_prefix(&mut self, set: AttributeSet) {
161 self.stat_mut(set).redundant.prefixes += 1;
162 }
163
164 fn record_emitted_declarative(&mut self, set: AttributeSet) {
165 self.stat_mut(set).emitted.declaratives += 1;
166 }
167
168 fn record_emitted_prefix(&mut self, set: AttributeSet) {
169 self.stat_mut(set).emitted.prefixes += 1;
170 }
171}
172
173#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
178struct AttributeState {
179 math_font: Option<AttrValue>,
180 math_size: Option<AttrValue>,
181 math_style: Option<AttrValue>,
182 text_family: Option<AttrValue>,
183 text_series: Option<AttrValue>,
184 text_shape: Option<AttrValue>,
185 text_size: Option<AttrValue>,
186}
187
188#[derive(Clone, Copy, Debug, PartialEq, Eq)]
189struct Pair {
190 state: AttributeState,
191 node: NodeId,
192}
193
194#[derive(Clone, Debug, PartialEq, Eq)]
195struct CollectResult {
196 pairs: Vec<Pair>,
197 final_state: AttributeState,
198}
199
200impl AttributeState {
201 fn get(self, attr: Attr) -> Option<AttrValue> {
202 match attr {
203 Attr::MathFont => self.math_font,
204 Attr::MathSize => self.math_size,
205 Attr::MathStyle => self.math_style,
206 Attr::TextFamily => self.text_family,
207 Attr::TextSeries => self.text_series,
208 Attr::TextShape => self.text_shape,
209 Attr::TextSize => self.text_size,
210 }
211 }
212
213 #[allow(dead_code)]
214 fn with(mut self, set: AttributeSet) -> Self {
215 self.set(set);
216 self
217 }
218
219 fn set(&mut self, set: AttributeSet) -> bool {
222 let slot = self.slot_mut(set.attr);
223 if *slot == Some(set.value) {
224 return false;
225 }
226 *slot = Some(set.value);
227 true
228 }
229
230 fn diff_axes(self, inherited: Self, mode: ContentMode) -> Vec<Attr> {
231 Self::axis_order(mode)
232 .iter()
233 .copied()
234 .filter(|attr| self.get(*attr) != inherited.get(*attr))
235 .collect()
236 }
237
238 fn with_mode_reset(mut self, mode: ContentMode) -> Self {
239 for &attr in Self::axis_order(mode) {
240 *self.slot_mut(attr) = None;
241 }
242 self
243 }
244
245 fn slot_mut(&mut self, attr: Attr) -> &mut Option<AttrValue> {
246 match attr {
247 Attr::MathFont => &mut self.math_font,
248 Attr::MathSize => &mut self.math_size,
249 Attr::MathStyle => &mut self.math_style,
250 Attr::TextFamily => &mut self.text_family,
251 Attr::TextSeries => &mut self.text_series,
252 Attr::TextShape => &mut self.text_shape,
253 Attr::TextSize => &mut self.text_size,
254 }
255 }
256
257 fn axis_order(mode: ContentMode) -> &'static [Attr] {
258 match mode {
259 ContentMode::Math => &[Attr::MathFont, Attr::MathSize, Attr::MathStyle],
260 ContentMode::Text => &[
261 Attr::TextShape,
262 Attr::TextSeries,
263 Attr::TextFamily,
264 Attr::TextSize,
265 ],
266 }
267 }
268}
269
270#[derive(Clone, Copy, Debug, PartialEq, Eq)]
275pub struct LowerAttributesConfig {
276 pub enabled: bool,
277}
278
279impl LowerAttributesConfig {
280 pub const ENABLED: Self = Self { enabled: true };
281 pub const DISABLED: Self = Self { enabled: false };
282 pub const DEFAULTS: Self = Self::ENABLED;
283}
284
285pub fn run(ast: &mut Ast, _config: &LowerAttributesConfig, report: &mut LowerAttributesReport) {
286 canonicalize_subtree(
287 ast,
288 ast.root(),
289 AttributeState::default(),
290 ContentMode::Math,
291 report,
292 );
293}
294
295fn canonicalize_subtree(
300 ast: &mut Ast,
301 node_id: NodeId,
302 inherited: AttributeState,
303 mode: ContentMode,
304 report: &mut LowerAttributesReport,
305) {
306 let container_mode = match ast.node(node_id) {
307 Node::Root { mode, .. } | Node::Group { mode, .. } => Some(*mode),
308 _ => None,
309 };
310
311 if let Some(container_mode) = container_mode {
312 process_container(ast, node_id, inherited, container_mode, report);
313 } else {
314 canonicalize_content_slots(ast, node_id, inherited, mode, report);
315 }
316}
317
318fn canonicalize_content_slots(
319 ast: &mut Ast,
320 parent: NodeId,
321 inherited: AttributeState,
322 parent_mode: ContentMode,
323 report: &mut LowerAttributesReport,
324) {
325 let edges = ast.edges(parent);
326 for (child, slot) in edges {
327 let Some(child_mode) = content_slot_mode(ast, parent, slot) else {
328 continue;
329 };
330 let child_inherited = inherited_for_child_mode(inherited, parent_mode, child_mode);
331 let placeholder = empty_implicit_group(ast, child_mode);
332 ast.replace_content_child(child, placeholder);
333
334 let collected =
335 collect_single_detached_node(ast, child, child_inherited, child_mode, report);
336 let rebuilt = segment_and_emit(ast, collected.pairs, child_inherited, child_mode, report);
337 let replacement = single_content_replacement(ast, rebuilt, child_mode);
338 ast.replace_content_child(placeholder, replacement);
339 ast.remove_detached(placeholder);
340 }
341}
342
343fn content_slot_mode(ast: &Ast, parent: NodeId, slot: Slot) -> Option<ContentMode> {
344 match slot {
345 Slot::Argument(index) => argument_content_mode(ast, parent, index),
346 Slot::EnvBody | Slot::ScriptBase | Slot::ScriptSub | Slot::ScriptSup => {
347 Some(ContentMode::Math)
348 }
349 _ => None,
350 }
351}
352
353fn argument_content_mode(ast: &Ast, parent: NodeId, index: usize) -> Option<ContentMode> {
354 ast.arg_slots(parent)
355 .get(index)
356 .and_then(Option::as_ref)
357 .and_then(|argument| match argument.value {
358 ArgumentValue::MathContent(_) => Some(ContentMode::Math),
359 ArgumentValue::TextContent(_) => Some(ContentMode::Text),
360 _ => None,
361 })
362}
363
364fn inherited_for_child_mode(
365 inherited: AttributeState,
366 parent_mode: ContentMode,
367 child_mode: ContentMode,
368) -> AttributeState {
369 if child_mode == parent_mode {
370 inherited
371 } else {
372 inherited.with_mode_reset(child_mode)
373 }
374}
375
376fn single_content_replacement(ast: &mut Ast, mut nodes: Vec<NodeId>, mode: ContentMode) -> NodeId {
377 if nodes.len() == 1 {
378 return nodes.pop().expect("single content node should exist");
379 }
380
381 ast.new_node(Node::Group {
382 children: nodes,
383 kind: GroupKind::Implicit,
384 mode,
385 })
386}
387
388fn process_container(
393 ast: &mut Ast,
394 container: NodeId,
395 inherited: AttributeState,
396 mode: ContentMode,
397 report: &mut LowerAttributesReport,
398) {
399 let len = ast.children(container).len();
400 if len == 0 {
401 return;
402 }
403
404 let detached = ast.detach_children_range(container, 0..len);
405 let collected = collect_detached_children(ast, detached, inherited, mode, report);
406 record_trailing_empty_segment(
407 &collected.pairs,
408 collected.final_state,
409 inherited,
410 mode,
411 report,
412 );
413 let rebuilt = segment_and_emit(ast, collected.pairs, inherited, mode, report);
414 let removed = ast.replace_children(container, rebuilt);
415 debug_assert!(removed.is_empty());
416}
417
418fn collect_detached_children(
419 ast: &mut Ast,
420 children: Vec<NodeId>,
421 inherited: AttributeState,
422 mode: ContentMode,
423 report: &mut LowerAttributesReport,
424) -> CollectResult {
425 let mut pairs = Vec::new();
426 let mut state = inherited;
427
428 for child in children {
429 collect_detached_child(ast, child, &mut state, mode, report, &mut pairs);
430 }
431
432 CollectResult {
433 pairs,
434 final_state: state,
435 }
436}
437
438fn collect_detached_child(
439 ast: &mut Ast,
440 child: NodeId,
441 state: &mut AttributeState,
442 mode: ContentMode,
443 report: &mut LowerAttributesReport,
444 pairs: &mut Vec<Pair>,
445) {
446 if let Some(entry) = lookup_declarative_at(ast, child, mode) {
447 consume_declarative(ast, child, state, entry, report);
448 return;
449 }
450
451 if let Some(entry) = lookup_prefix_at(ast, child, mode)
452 && mandatory_content_child(ast, child).is_some()
453 {
454 let previous = *state;
455 let body_pairs = collect_prefix_body(ast, child, previous, entry, mode, report);
456 if prefix_is_fully_absorbed(previous, entry.set, &body_pairs) {
457 report.record_redundant_prefix(entry.set);
458 }
459 pairs.extend(body_pairs);
460 ast.remove_detached(child);
461 return;
462 }
463
464 if is_explicit_group(ast, child) {
465 pairs.extend(collect_explicit_group(ast, child, *state, mode, report));
466 return;
467 }
468
469 canonicalize_subtree(ast, child, *state, mode, report);
470 pairs.push(Pair {
471 state: *state,
472 node: child,
473 });
474}
475
476fn consume_declarative(
477 ast: &mut Ast,
478 node: NodeId,
479 state: &mut AttributeState,
480 entry: &'static DeclarativeEntry,
481 report: &mut LowerAttributesReport,
482) {
483 report.record_consumed_declarative(entry.set);
484 if !state.set(entry.set) {
485 report.record_redundant_declarative(entry.set);
486 }
487 ast.remove_detached(node);
488}
489
490fn collect_prefix_body(
491 ast: &mut Ast,
492 prefix: NodeId,
493 previous: AttributeState,
494 entry: &'static PrefixEntry,
495 mode: ContentMode,
496 report: &mut LowerAttributesReport,
497) -> Vec<Pair> {
498 report.record_consumed_prefix(entry.set);
499 let body_state = previous.with(entry.set);
500 let body = mandatory_content_child(ast, prefix).expect("registered prefix should have a body");
501
502 match ast.node(body) {
503 Node::Group {
504 kind: GroupKind::Implicit,
505 ..
506 } => {
507 let len = ast.children(body).len();
508 let detached = ast.detach_children_range(body, 0..len);
509 detach_body_from_prefix(ast, body, mode);
510 ast.remove_detached(body);
511 let collected = collect_detached_children(ast, detached, body_state, mode, report);
512 record_trailing_empty_segment(
513 &collected.pairs,
514 collected.final_state,
515 body_state,
516 mode,
517 report,
518 );
519 collected.pairs
520 }
521 Node::Group {
522 kind: GroupKind::Explicit,
523 ..
524 } => {
525 detach_body_from_prefix(ast, body, mode);
526 collect_explicit_group(ast, body, body_state, mode, report)
527 }
528 _ => {
529 detach_body_from_prefix(ast, body, mode);
530 collect_single_detached_node(ast, body, body_state, mode, report).pairs
531 }
532 }
533}
534
535fn detach_body_from_prefix(ast: &mut Ast, body: NodeId, mode: ContentMode) {
536 let placeholder = empty_implicit_group(ast, mode);
537 ast.replace_content_child(body, placeholder);
538}
539
540fn collect_explicit_group(
541 ast: &mut Ast,
542 group: NodeId,
543 inherited: AttributeState,
544 mode: ContentMode,
545 report: &mut LowerAttributesReport,
546) -> Vec<Pair> {
547 if !has_direct_declarative_marker(ast, group, mode) {
548 canonicalize_subtree(ast, group, inherited, mode, report);
549 return vec![Pair {
550 state: inherited,
551 node: group,
552 }];
553 }
554
555 let len = ast.children(group).len();
556 let detached = ast.detach_children_range(group, 0..len);
557 let inner = collect_detached_children(ast, detached, inherited, mode, report);
558 record_trailing_empty_segment(&inner.pairs, inner.final_state, inherited, mode, report);
559
560 if !inner.pairs.is_empty() && inner.pairs.iter().any(|pair| pair.state != inherited) {
561 ast.remove_detached(group);
562 return inner.pairs;
563 }
564
565 let nodes = inner.pairs.into_iter().map(|pair| pair.node).collect();
566 let removed = ast.replace_children(group, nodes);
567 debug_assert!(removed.is_empty());
568 vec![Pair {
569 state: inherited,
570 node: group,
571 }]
572}
573
574fn collect_single_detached_node(
575 ast: &mut Ast,
576 node: NodeId,
577 inherited: AttributeState,
578 mode: ContentMode,
579 report: &mut LowerAttributesReport,
580) -> CollectResult {
581 let mut pairs = Vec::new();
582 let mut state = inherited;
583 collect_detached_child(ast, node, &mut state, mode, report, &mut pairs);
584 record_trailing_empty_segment(&pairs, state, inherited, mode, report);
585 CollectResult {
586 pairs,
587 final_state: state,
588 }
589}
590
591fn record_trailing_empty_segment(
592 pairs: &[Pair],
593 final_state: AttributeState,
594 inherited: AttributeState,
595 mode: ContentMode,
596 report: &mut LowerAttributesReport,
597) {
598 let segment_state = pairs.last().map_or(inherited, |pair| pair.state);
599 if !final_state.diff_axes(segment_state, mode).is_empty() {
600 report.eliminated_empty_segments += 1;
601 }
602}
603
604fn has_direct_declarative_marker(ast: &Ast, group: NodeId, mode: ContentMode) -> bool {
605 ast.children(group)
606 .iter()
607 .any(|child| lookup_declarative_at(ast, *child, mode).is_some())
608}
609
610fn is_explicit_group(ast: &Ast, node: NodeId) -> bool {
611 matches!(
612 ast.node(node),
613 Node::Group {
614 kind: GroupKind::Explicit,
615 ..
616 }
617 )
618}
619
620fn mandatory_content_child(ast: &Ast, node: NodeId) -> Option<NodeId> {
621 let mut found = None;
622 for argument in ast.arg_slots(node).iter().flatten() {
623 if !matches!(argument.kind, crate::ast::ArgumentKind::Mandatory) {
624 continue;
625 }
626 let child = match argument.value {
627 ArgumentValue::MathContent(child) | ArgumentValue::TextContent(child) => child,
628 _ => continue,
629 };
630 if found.replace(child).is_some() {
631 return None;
632 }
633 }
634 found
635}
636
637fn empty_implicit_group(ast: &mut Ast, mode: ContentMode) -> NodeId {
638 ast.new_node(Node::Group {
639 children: Vec::new(),
640 kind: GroupKind::Implicit,
641 mode,
642 })
643}
644
645fn prefix_is_fully_absorbed(
646 previous: AttributeState,
647 set: AttributeSet,
648 body_pairs: &[Pair],
649) -> bool {
650 let previous_value = previous.get(set.attr);
651 if previous_value == Some(set.value) {
652 return true;
653 }
654
655 body_pairs.is_empty()
656 || body_pairs
657 .iter()
658 .all(|pair| pair.state.get(set.attr) != Some(set.value))
659}
660
661fn segment_and_emit(
662 ast: &mut Ast,
663 pairs: Vec<Pair>,
664 inherited: AttributeState,
665 mode: ContentMode,
666 report: &mut LowerAttributesReport,
667) -> Vec<NodeId> {
668 let mut rebuilt = Vec::new();
669 let mut iter = pairs.into_iter().peekable();
670
671 while let Some(first) = iter.next() {
672 let segment_state = first.state;
673 let mut segment = vec![first.node];
674
675 while let Some(next) = iter.peek() {
676 if next.state != segment_state {
677 break;
678 }
679 segment.push(iter.next().expect("peeked segment pair should exist").node);
680 }
681
682 rebuilt.extend(wrap_with_canonical(
683 ast,
684 segment,
685 segment_state,
686 inherited,
687 mode,
688 report,
689 ));
690 }
691
692 rebuilt
693}
694
695fn wrap_with_canonical(
700 ast: &mut Ast,
701 mut children: Vec<NodeId>,
702 state: AttributeState,
703 inherited: AttributeState,
704 mode: ContentMode,
705 report: &mut LowerAttributesReport,
706) -> Vec<NodeId> {
707 debug_assert!(
708 !children.is_empty(),
709 "segment_and_emit must not call wrap_with_canonical with an empty segment"
710 );
711
712 for attr in emit_axis_order(state, inherited, mode) {
713 let Some(value) = state.get(attr) else {
714 continue;
715 };
716 let Some(target) = lookup_target(attr, value, mode) else {
717 continue;
718 };
719
720 if let Some(prefix) = target.prefix {
721 let group = ast.new_node(Node::Group {
722 children,
723 kind: GroupKind::Implicit,
724 mode,
725 });
726 let command = ast.new_node(Node::Command {
727 name: prefix.name.to_string(),
728 args: vec![mandatory_content_slot(group, mode)],
729 known: true,
730 });
731 report.record_emitted_prefix(AttributeSet::new(attr, value));
732 children = vec![command];
733 } else {
734 children.insert(0, new_declarative_node(ast, target.declarative));
735 report.record_emitted_declarative(AttributeSet::new(attr, value));
736 }
737 }
738
739 children
740}
741
742fn emit_axis_order(
743 state: AttributeState,
744 inherited: AttributeState,
745 mode: ContentMode,
746) -> Vec<Attr> {
747 let mut axes = state.diff_axes(inherited, mode);
748 if matches!(mode, ContentMode::Math) && axes == [Attr::MathFont, Attr::MathSize] {
749 axes.swap(0, 1);
750 }
751 axes
752}
753
754fn lookup_declarative_at(
759 ast: &Ast,
760 node_id: NodeId,
761 mode: ContentMode,
762) -> Option<&'static DeclarativeEntry> {
763 let Node::Declarative { name, args } = ast.node(node_id) else {
764 return None;
765 };
766 if !args.is_empty() {
767 return None;
768 }
769 lookup_declarative(mode, name)
770}
771
772fn lookup_declarative(mode: ContentMode, name: &str) -> Option<&'static DeclarativeEntry> {
773 generated::DECLARATIVES
774 .iter()
775 .find(|entry| entry.allowed_mode == mode && entry.name == name)
776}
777
778fn lookup_prefix_at(ast: &Ast, node_id: NodeId, mode: ContentMode) -> Option<&'static PrefixEntry> {
779 let Node::Command { name, .. } = ast.node(node_id) else {
780 return None;
781 };
782 generated::PREFIXES
783 .iter()
784 .find(|entry| entry.allowed_mode == mode && entry.name == name)
785}
786
787fn lookup_target(attr: Attr, value: AttrValue, mode: ContentMode) -> Option<&'static ModeTarget> {
788 generated::ATTRIBUTE_TARGETS
789 .iter()
790 .find(|entry| entry.attr == attr && entry.value == value)
791 .and_then(|entry| match mode {
792 ContentMode::Math => entry.math.as_ref(),
793 ContentMode::Text => entry.text.as_ref(),
794 })
795}
796
797fn new_declarative_node(ast: &mut Ast, command: CommandRef) -> NodeId {
798 ast.new_node(Node::Declarative {
799 name: command.name.to_string(),
800 args: Vec::new(),
801 })
802}
803
804#[cfg(test)]
805mod tests {
806 use super::{
807 Attr, AttrValue, AttributeSet, AttributeState, MathFontValue, SizeValue, TextFamily,
808 TextSeries, TextShape,
809 };
810 use crate::ast::ContentMode;
811
812 #[test]
813 fn attribute_state_set_returns_false_on_repeat() {
814 let mut state = AttributeState::default();
815 let bold = AttributeSet {
816 attr: Attr::MathFont,
817 value: AttrValue::MathFont(MathFontValue("VARIANT.BOLD")),
818 };
819
820 assert!(state.set(bold));
821 assert!(!state.set(bold));
822 assert_eq!(state.get(Attr::MathFont), Some(bold.value));
823 }
824
825 #[test]
826 fn attribute_state_diff_axes_uses_mode_specific_order() {
827 let inherited = AttributeState::default();
828 let state = AttributeState::default()
829 .with(AttributeSet {
830 attr: Attr::MathStyle,
831 value: AttrValue::Style(super::StyleValue {
832 letter: "D",
833 display: true,
834 level: 0,
835 }),
836 })
837 .with(AttributeSet {
838 attr: Attr::MathFont,
839 value: AttrValue::MathFont(MathFontValue("VARIANT.BOLD")),
840 })
841 .with(AttributeSet {
842 attr: Attr::TextFamily,
843 value: AttrValue::TextFamily(TextFamily::Roman),
844 })
845 .with(AttributeSet {
846 attr: Attr::TextShape,
847 value: AttrValue::TextShape(TextShape::Italic),
848 })
849 .with(AttributeSet {
850 attr: Attr::TextSeries,
851 value: AttrValue::TextSeries(TextSeries::Bold),
852 });
853
854 assert_eq!(
855 state.diff_axes(inherited, ContentMode::Math),
856 vec![Attr::MathFont, Attr::MathStyle]
857 );
858 assert_eq!(
859 state.diff_axes(inherited, ContentMode::Text),
860 vec![Attr::TextShape, Attr::TextSeries, Attr::TextFamily]
861 );
862 }
863
864 #[test]
865 fn attribute_state_mode_reset_preserves_other_mode() {
866 let state = AttributeState::default()
867 .with(AttributeSet {
868 attr: Attr::MathSize,
869 value: AttrValue::Size(SizeValue(120)),
870 })
871 .with(AttributeSet {
872 attr: Attr::TextSize,
873 value: AttrValue::Size(SizeValue(85)),
874 })
875 .with(AttributeSet {
876 attr: Attr::TextShape,
877 value: AttrValue::TextShape(TextShape::Italic),
878 });
879
880 let math_reset = state.with_mode_reset(ContentMode::Math);
881 assert_eq!(math_reset.get(Attr::MathSize), None);
882 assert_eq!(
883 math_reset.get(Attr::TextSize),
884 Some(AttrValue::Size(SizeValue(85)))
885 );
886 assert_eq!(
887 math_reset.get(Attr::TextShape),
888 Some(AttrValue::TextShape(TextShape::Italic))
889 );
890
891 let text_reset = state.with_mode_reset(ContentMode::Text);
892 assert_eq!(
893 text_reset.get(Attr::MathSize),
894 Some(AttrValue::Size(SizeValue(120)))
895 );
896 assert_eq!(text_reset.get(Attr::TextSize), None);
897 assert_eq!(text_reset.get(Attr::TextShape), None);
898 }
899}