Skip to main content

texform_core/
target_counter.rs

1//! Target occurrence counting for parsed TeXForm syntax trees.
2//!
3//! Counts occurrences of command, environment, and character targets by
4//! `(kind, content mode, name)`. Command-like nodes may contribute both command
5//! and character counts when the knowledge base exposes the same name in both
6//! categories.
7
8use std::collections::HashMap;
9
10use texform_interface::syntax_node::{Argument, ArgumentValue, ContentMode, SyntaxNode};
11use texform_knowledge::builtin::ALL_PACKAGES;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub enum TargetKind {
15    Cmd,
16    Env,
17    Char,
18}
19
20impl TargetKind {
21    pub fn as_str(self) -> &'static str {
22        match self {
23            TargetKind::Cmd => "cmd",
24            TargetKind::Env => "env",
25            TargetKind::Char => "char",
26        }
27    }
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Hash)]
31pub struct TargetCounterKey {
32    pub kind: TargetKind,
33    pub mode: ContentMode,
34    pub name: String,
35}
36
37#[derive(Debug, Default)]
38pub struct TargetCounter {
39    pub counts: HashMap<TargetCounterKey, u32>,
40}
41
42impl TargetCounter {
43    pub fn is_empty(&self) -> bool {
44        self.counts.is_empty()
45    }
46
47    pub fn logical_counts(&self) -> HashMap<String, u32> {
48        let mut out = HashMap::new();
49        for (key, count) in &self.counts {
50            let logical = format!("{}:{}", key.kind.as_str(), key.name);
51            *out.entry(logical).or_insert(0) += *count;
52        }
53        out
54    }
55
56    pub fn bump(&mut self, kind: TargetKind, mode: ContentMode, name: &str) {
57        let key = TargetCounterKey {
58            kind,
59            mode,
60            name: name.to_string(),
61        };
62        *self.counts.entry(key).or_insert(0) += 1;
63    }
64}
65
66/// Walk a `SyntaxNode` and accumulate target counts into `out`.
67pub fn count_node(node: &SyntaxNode, out: &mut TargetCounter) {
68    count_node_in_mode(node, ContentMode::Math, out);
69}
70
71fn count_node_in_mode(node: &SyntaxNode, inherited_mode: ContentMode, out: &mut TargetCounter) {
72    match node {
73        SyntaxNode::Root { mode, children } | SyntaxNode::Group { mode, children, .. } => {
74            for child in children {
75                count_node_in_mode(child, *mode, out);
76            }
77        }
78        SyntaxNode::Command { name, args, .. } => {
79            bump_cmd_like(out, inherited_mode, name);
80            count_args(args, out);
81        }
82        SyntaxNode::Infix {
83            name,
84            args,
85            left,
86            right,
87        } => {
88            bump_cmd_like(out, inherited_mode, name);
89            count_args(args, out);
90            count_node_in_mode(left, inherited_mode, out);
91            count_node_in_mode(right, inherited_mode, out);
92        }
93        SyntaxNode::Declarative { name, args } => {
94            bump_cmd_like(out, inherited_mode, name);
95            count_args(args, out);
96        }
97        SyntaxNode::Environment {
98            name, args, body, ..
99        } => {
100            out.bump(TargetKind::Env, inherited_mode, name);
101            count_args(args, out);
102            count_node_in_mode(body, inherited_mode, out);
103        }
104        SyntaxNode::Scripted {
105            base,
106            subscript,
107            superscript,
108        } => {
109            count_node_in_mode(base, inherited_mode, out);
110            if let Some(sub) = subscript {
111                count_node_in_mode(sub, inherited_mode, out);
112            }
113            if let Some(sup) = superscript {
114                count_node_in_mode(sup, inherited_mode, out);
115            }
116        }
117        SyntaxNode::Prime { .. }
118        | SyntaxNode::Text(_)
119        | SyntaxNode::Char(_)
120        | SyntaxNode::ActiveSpace
121        | SyntaxNode::Error { .. } => {}
122    }
123}
124
125fn bump_cmd_like(out: &mut TargetCounter, mode: ContentMode, name: &str) {
126    let has_cmd = ALL_PACKAGES
127        .iter()
128        .any(|pkg| pkg.commands.iter().any(|record| record.name == name));
129    let has_char = ALL_PACKAGES
130        .iter()
131        .any(|pkg| pkg.characters.iter().any(|record| record.name == name));
132
133    if has_cmd || !has_char {
134        out.bump(TargetKind::Cmd, mode, name);
135    }
136    if has_char {
137        out.bump(TargetKind::Char, mode, name);
138    }
139}
140
141fn count_args(args: &[Option<Argument>], out: &mut TargetCounter) {
142    for slot in args {
143        let Some(arg) = slot else { continue };
144        match &arg.value {
145            ArgumentValue::MathContent(node) => {
146                count_node_in_mode(node, ContentMode::Math, out);
147            }
148            ArgumentValue::TextContent(node) => {
149                count_node_in_mode(node, ContentMode::Text, out);
150            }
151            ArgumentValue::Delimiter(_)
152            | ArgumentValue::CSName(_)
153            | ArgumentValue::Dimension(_)
154            | ArgumentValue::Integer(_)
155            | ArgumentValue::KeyVal(_)
156            | ArgumentValue::Column(_)
157            | ArgumentValue::Boolean(_) => {}
158        }
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use std::collections::HashMap;
165
166    use super::*;
167    use crate::parse::{ParseConfig, ParseContext};
168
169    fn count(src: &str) -> HashMap<String, u32> {
170        let output = ParseContext::shared().parse(src, &ParseConfig::default());
171        let document = output.document().expect("parse result");
172        let mut counter = TargetCounter::default();
173        count_node(&document.to_syntax(), &mut counter);
174        counter.logical_counts()
175    }
176
177    #[test]
178    fn counts_commands_envs_and_character_aliases() {
179        let counts = count(r"\begin{matrix}\frac{a}{b} & x \le y\end{matrix}");
180
181        assert_eq!(counts.get("env:matrix"), Some(&1));
182        assert_eq!(counts.get("cmd:frac"), Some(&1));
183        assert_eq!(counts.get("char:le"), Some(&1));
184    }
185
186    #[test]
187    fn counts_text_mode_command_arguments() {
188        let counts = count(r"\text{A \mkern 1em B}");
189
190        assert_eq!(counts.get("cmd:text"), Some(&1));
191        assert_eq!(counts.get("cmd:mkern"), Some(&1));
192    }
193}