Skip to main content

wowlab_parsers/parsers/spell_desc/parser/
variables.rs

1// #t(file: rust_unchecked_indexing) parser slices token strings from regex-matched logos output with guaranteed structure
2
3use super::super::types::{
4    AtVariableNode, AuraConditionNode, CrossSpellReferenceNode, CustomVariableNode,
5    EffectVariableNode, EnchantVariableNode, GenderNode, MiscVariableNode, PlayerConditionNode,
6    PlayerVariableNode, PluralizationNode, SingleConditionNode, SpecializationConditionNode,
7    SpellKnownConditionNode, SpellLevelVariableNode, VariableNode,
8};
9
10const DECIMAL_RADIX: u32 = 10;
11const CONTENT_SPLIT_PARTS: usize = 2;
12const DOLLAR_TWO_CHAR_PREFIX: usize = 2;
13
14#[rustfmt::skip]
15const EFFECT_VAR_TYPES: &[&str] = &[
16    "s",
17    "m",
18    "o",
19    "t",
20    "a",
21    "e",
22    "w",
23    "x",
24    "bc",
25    "q",
26    "sw",
27];
28
29pub(super) fn parse_effect_var(s: &str) -> VariableNode {
30    let body = &s[1..];
31    let index = body
32        .chars()
33        .last()
34        .and_then(|c| c.to_digit(DECIMAL_RADIX))
35        .and_then(|digit| u8::try_from(digit).ok())
36        .unwrap_or(1);
37    let var_type = &body[..body.len() - 1];
38
39    // No normalization: case matters ($s1 vs $S1, $w1 vs $W1 are distinct).
40
41    VariableNode::Effect(EffectVariableNode {
42        var_type: var_type.to_string(),
43        effect_index: index,
44    })
45}
46
47pub(super) fn parse_spell_level_var(s: &str) -> VariableNode {
48    let body = &s[1..];
49
50    let last_char = body.chars().last();
51    let has_index = last_char.is_some_and(|c| c.is_ascii_digit()) && body.len() > 1;
52
53    let (var_type, index) = if has_index {
54        let idx = last_char
55            .and_then(|c| c.to_digit(DECIMAL_RADIX))
56            .and_then(|digit| u8::try_from(digit).ok());
57
58        (&body[..body.len() - 1], idx)
59    } else {
60        (body, None)
61    };
62
63    VariableNode::SpellLevel(SpellLevelVariableNode {
64        var_type: var_type.to_string(),
65        index,
66    })
67}
68
69pub(super) fn parse_player_var(s: &str) -> VariableNode {
70    VariableNode::Player(PlayerVariableNode::new(s[1..].to_string()))
71}
72
73pub(super) fn parse_enchant_var(s: &str) -> VariableNode {
74    VariableNode::Enchant(EnchantVariableNode::new(s[1..].to_string()))
75}
76
77pub(super) fn parse_misc_var(s: &str) -> VariableNode {
78    let body = &s[1..];
79
80    let digits: String = body
81        .chars()
82        .rev()
83        .take_while(char::is_ascii_digit)
84        .collect();
85    let id = if digits.is_empty() {
86        None
87    } else {
88        digits.chars().rev().collect::<String>().parse().ok()
89    };
90
91    let var_name = if id.is_some() {
92        &body[..body.len() - digits.len()]
93    } else {
94        body
95    };
96
97    VariableNode::Misc(MiscVariableNode {
98        var_name: var_name.to_string(),
99        id,
100    })
101}
102
103pub(super) fn parse_custom_var(s: &str) -> VariableNode {
104    let var_name = &s[DOLLAR_TWO_CHAR_PREFIX..s.len() - 1];
105
106    VariableNode::Custom(CustomVariableNode::new(var_name.to_string()))
107}
108
109pub(super) fn parse_at_var(s: &str) -> VariableNode {
110    let body = &s[DOLLAR_TWO_CHAR_PREFIX..];
111
112    let digits: String = body
113        .chars()
114        .rev()
115        .take_while(char::is_ascii_digit)
116        .collect();
117    let spell_id = if digits.is_empty() {
118        None
119    } else {
120        digits.chars().rev().collect::<String>().parse().ok()
121    };
122
123    let var_type = if spell_id.is_some() {
124        &body[..body.len() - digits.len()]
125    } else {
126        body
127    };
128
129    VariableNode::At(AtVariableNode {
130        var_type: var_type.to_string(),
131        spell_id,
132    })
133}
134
135pub(super) fn parse_cross_spell_ref(s: &str) -> VariableNode {
136    let body = &s[1..];
137
138    let id_digits: String = body.chars().take_while(char::is_ascii_digit).collect();
139    let spell_id: u32 = id_digits.parse().unwrap_or(0);
140
141    let remainder = &body[id_digits.len()..];
142
143    let trail_digits: String = remainder
144        .chars()
145        .rev()
146        .take_while(char::is_ascii_digit)
147        .collect();
148    let mut effect_index = if trail_digits.is_empty() {
149        None
150    } else {
151        trail_digits.chars().rev().collect::<String>().parse().ok()
152    };
153
154    let var_type = if effect_index.is_some() {
155        &remainder[..remainder.len() - trail_digits.len()]
156    } else {
157        remainder
158    };
159
160    let var_type_lower = var_type.to_lowercase();
161
162    if effect_index.is_none() && EFFECT_VAR_TYPES.contains(&var_type_lower.as_str()) {
163        effect_index = Some(1);
164    }
165
166    VariableNode::CrossSpell(CrossSpellReferenceNode {
167        spell_id,
168        var_type: var_type.to_string(),
169        effect_index,
170    })
171}
172
173pub(super) fn parse_pluralization(content: &str, capitalized: bool) -> PluralizationNode {
174    let parts: Vec<&str> = content.splitn(CONTENT_SPLIT_PARTS, ':').collect();
175    let singular = parts.first().unwrap_or(&"").to_string();
176    let plural = parts.get(1).unwrap_or(&"").to_string();
177
178    PluralizationNode {
179        singular,
180        plural,
181        capitalized,
182    }
183}
184
185pub(super) fn parse_gender(content: &str, capitalized: bool) -> GenderNode {
186    let parts: Vec<&str> = content.splitn(CONTENT_SPLIT_PARTS, ':').collect();
187    let male = parts.first().unwrap_or(&"").to_string();
188    let female = parts.get(1).unwrap_or(&"").to_string();
189
190    GenderNode {
191        male,
192        female,
193        capitalized,
194    }
195}
196
197// #t(rust_cyclomatic_complexity) condition prefix dispatch: s/a/c/pc patterns
198pub(super) fn parse_condition_type(s: &str) -> Option<SingleConditionNode> {
199    let s = s.trim();
200
201    if s.is_empty() {
202        return None;
203    }
204
205    if let Some(rest) = s.strip_prefix('s') {
206        if let Ok(id) = rest.parse::<u32>() {
207            return Some(SingleConditionNode::SpellKnown(
208                SpellKnownConditionNode::new(id),
209            ));
210        }
211    }
212
213    if let Some(rest) = s.strip_prefix('a') {
214        if let Ok(id) = rest.parse::<u32>() {
215            return Some(SingleConditionNode::Aura(AuraConditionNode::new(id)));
216        }
217    }
218
219    if let Some(rest) = s.strip_prefix('c') {
220        if let Ok(spec_index) = rest.parse::<u8>() {
221            return Some(SingleConditionNode::Specialization(
222                SpecializationConditionNode::new(spec_index),
223            ));
224        }
225    }
226
227    if let Some(rest) = s.strip_prefix("pc") {
228        if let Ok(id) = rest.parse::<u32>() {
229            return Some(SingleConditionNode::PlayerCondition(
230                PlayerConditionNode::new(id),
231            ));
232        }
233    }
234
235    None
236}