1use 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
66pub 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}