Skip to main content

texform_transform/rewrite/
helpers.rs

1//! Convenience constructors for building AST argument slots and nodes.
2//!
3//! Transform rules frequently need to assemble replacement [`Node`] trees.
4//! The helpers here eliminate the boilerplate of constructing [`Argument`]
5//! wrappers by hand, keeping rule implementations focused on semantics.
6
7use texform_knowledge::builtin::base;
8use texform_knowledge::specs::BuiltinCommandRecord;
9
10use crate::ast::{
11    Argument, ArgumentKind, ArgumentSlot, ArgumentValue, ContentMode, Delimiter, Node, NodeId,
12};
13
14/// Creates a mandatory content argument slot wrapping the subtree rooted at `node_id`.
15pub fn mandatory_content_slot(node_id: NodeId, mode: ContentMode) -> ArgumentSlot {
16    let value = match mode {
17        ContentMode::Math => ArgumentValue::MathContent(node_id),
18        ContentMode::Text => ArgumentValue::TextContent(node_id),
19    };
20    Some(Argument {
21        kind: ArgumentKind::Mandatory,
22        value,
23    })
24}
25
26/// Creates a mandatory delimiter argument slot.
27pub fn delimiter_slot(delimiter: Delimiter) -> ArgumentSlot {
28    Some(Argument {
29        kind: ArgumentKind::Mandatory,
30        value: ArgumentValue::Delimiter(delimiter),
31    })
32}
33
34/// Creates a mandatory dimension argument slot.
35pub fn dimension_slot(value: impl Into<String>) -> ArgumentSlot {
36    Some(Argument {
37        kind: ArgumentKind::Mandatory,
38        value: ArgumentValue::Dimension(value.into()),
39    })
40}
41
42/// Creates a mandatory integer argument slot.
43pub fn integer_slot(value: impl Into<String>) -> ArgumentSlot {
44    Some(Argument {
45        kind: ArgumentKind::Mandatory,
46        value: ArgumentValue::Integer(value.into()),
47    })
48}
49
50/// Creates the two mandatory content arguments used when converting an infix node to a prefix command.
51pub fn infix_prefix_args(left: NodeId, right: NodeId, mode: ContentMode) -> Vec<ArgumentSlot> {
52    vec![
53        mandatory_content_slot(left, mode),
54        mandatory_content_slot(right, mode),
55    ]
56}
57
58/// Creates a star (boolean) argument slot, representing a `*` modifier on a command.
59pub fn star_slot(value: bool) -> ArgumentSlot {
60    Some(Argument {
61        kind: ArgumentKind::Star,
62        value: ArgumentValue::Boolean(value),
63    })
64}
65
66/// Creates a known command node with no arguments.
67pub fn bare_command_node(name: &str) -> Node {
68    Node::Command {
69        name: name.to_string(),
70        args: Vec::new(),
71        known: true,
72    }
73}
74
75/// Creates a prefix [`Node::Command`] from a builtin command record and a list of argument slots.
76pub fn prefix_command_node(record: &'static BuiltinCommandRecord, args: Vec<ArgumentSlot>) -> Node {
77    Node::Command {
78        name: record.name.to_string(),
79        args,
80        known: true,
81    }
82}
83
84/// Creates the parser-shaped linebreak command.
85pub fn linebreak_command_node() -> Node {
86    prefix_command_node(&base::cmd::_BACKSLASH, vec![star_slot(false), None])
87}
88
89#[derive(Clone, Copy)]
90pub enum FenceToken {
91    Char(char),
92    Control(&'static str),
93}
94
95impl FenceToken {
96    pub fn node(self) -> Node {
97        match self {
98            Self::Char(ch) => Node::Char(ch),
99            Self::Control(name) => bare_command_node(name),
100        }
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn constructs_common_argument_slots() {
110        let mut ast = crate::ast::Ast::new();
111        let node_id = ast.new_node(Node::Char('x'));
112
113        let mandatory = mandatory_content_slot(node_id, ContentMode::Math);
114        assert!(matches!(
115            mandatory,
116            Some(Argument {
117                kind: ArgumentKind::Mandatory,
118                value: ArgumentValue::MathContent(id),
119            }) if id == node_id
120        ));
121
122        let star = star_slot(true);
123        assert!(matches!(
124            star,
125            Some(Argument {
126                kind: ArgumentKind::Star,
127                value: ArgumentValue::Boolean(true),
128            })
129        ));
130
131        let linebreak = linebreak_command_node();
132        assert!(matches!(
133            linebreak,
134            Node::Command { name, args, known }
135                if name == base::cmd::_BACKSLASH.name
136                    && known
137                    && matches!(
138                        args.as_slice(),
139                        [
140                            Some(Argument {
141                                kind: ArgumentKind::Star,
142                                value: ArgumentValue::Boolean(false),
143                            }),
144                            None,
145                        ]
146                    )
147        ));
148    }
149}