wowlab_parsers/parsers/spell_desc/
lexer.rs1use logos::{Lexer as LogosLexer, Logos};
4
5const DOLLAR_PREFIX_LEN: usize = 2;
6const GLOBAL_STRING_PREFIX_LEN: usize = 2;
7
8fn parse_expression_block<'a>(lex: &mut LogosLexer<'a, Token<'a>>) -> &'a str {
10 let remainder = lex.remainder();
11 let mut depth = 1;
12 let mut end = 0;
13
14 for (i, c) in remainder.char_indices() {
15 match c {
16 '{' => depth += 1,
17 '}' => {
18 depth -= 1;
19
20 if depth == 0 {
21 end = i;
22 break;
23 }
24 }
25 _ => {}
26 }
27 }
28
29 let after_brace = &remainder[end + 1..];
30 let format_len = if let Some(after_dot) = after_brace.strip_prefix('.') {
31 let digits: usize = after_dot.chars().take_while(char::is_ascii_digit).count();
32
33 if digits > 0 { 1 + digits } else { 0 }
34 } else {
35 0
36 };
37
38 lex.bump(end + 1 + format_len);
39
40 &remainder[..=(end + format_len)]
41}
42
43fn parse_pluralization<'a>(lex: &mut LogosLexer<'a, Token<'a>>) -> (&'a str, bool) {
45 let slice = lex.slice();
46 let capitalized = slice.chars().nth(1) == Some('L');
47 let content = &slice[DOLLAR_PREFIX_LEN..slice.len() - 1];
48
49 (content, capitalized)
50}
51
52fn parse_global_pluralization<'a>(lex: &mut LogosLexer<'a, Token<'a>>) -> (&'a str, bool) {
54 let slice = lex.slice();
55 let content = &slice[GLOBAL_STRING_PREFIX_LEN..slice.len() - 1];
56
57 (content, false)
58}
59
60fn parse_gender<'a>(lex: &mut LogosLexer<'a, Token<'a>>) -> (&'a str, bool) {
62 let slice = lex.slice();
63 let capitalized = slice.chars().nth(1) == Some('G');
64 let content = &slice[DOLLAR_PREFIX_LEN..slice.len() - 1];
65
66 (content, capitalized)
67}
68
69fn parse_cond_func<'a>(lex: &mut LogosLexer<'a, Token<'a>>) -> &'a str {
70 lex.slice()
71}
72
73#[derive(Clone, Debug, Logos, PartialEq)]
75pub(crate) enum Token<'a> {
76 #[regex(r"\$\{", parse_expression_block)]
78 ExpressionBlock(&'a str),
79
80 #[regex(r"\$<[a-zA-Z][a-zA-Z0-9]*>", priority = 10)]
81 CustomVariable(&'a str),
82
83 #[regex(r"\$@[a-zA-Z]+\d*", priority = 10)]
84 AtVariable(&'a str),
85
86 #[regex(r"\$(?:[sS]|m|M|o|t|a|A|e|w|W|x|bc|q|sw)[1-9]", priority = 10)]
87 EffectVariable(&'a str),
88
89 #[regex(r"\$(?:d[1-3]?|n|u|h|r|i|p\d?|z|c\d)", priority = 10)]
90 SpellLevelVariable(&'a str),
91
92 #[regex(r"\$(?:SP|sp|AP|ap|RAP|MHP|mhp|SPS|PL|pl|INT)", priority = 10)]
93 PlayerVariable(&'a str),
94
95 #[regex(r"\$(?:ec[12](?:s\d)?|ecix|ecim|ecd)", priority = 10)]
96 EnchantVariable(&'a str),
97
98 #[regex(r"\$(?:maxcast|pctD|W|B|ctrmax\d+|mws|mwb|MWS|MWB|ows|OWB|lpoint|mastery|mas|proccooldown|procrppm|pri|rolemult)", priority = 10)]
99 MiscVariable(&'a str),
100
101 #[token("$?", priority = 10)]
102 ConditionalStart,
103
104 #[regex(r"\$[lL][^:;]*:[^;]*;", parse_pluralization, priority = 10)]
105 #[regex(r"\|4[^:;]*:[^;]*;", parse_global_pluralization, priority = 10)]
106 Pluralization((&'a str, bool)),
107
108 #[regex(r"\$[gG][^:;]*:[^;]*;", parse_gender, priority = 10)]
109 Gender((&'a str, bool)),
110
111 #[regex(r"\$\d+[a-zA-Z]+\d*", priority = 10)]
112 CrossSpellRef(&'a str),
113
114 #[regex(r"\$[a-zA-Z_][a-zA-Z0-9_]*", priority = 1)]
115 SimpleVariable(&'a str),
116
117 #[token("$")]
118 Dollar,
119
120 #[regex(r"\|c[0-9a-fA-F]{8}|\|r", priority = 5)]
121 ColorCode(&'a str),
122
123 #[token("|")]
124 Pipe,
125
126 #[token("[")]
127 LBracket,
128
129 #[token("]")]
130 RBracket,
131
132 #[regex(r"\$[a-zA-Z]+\([^)]*\)", parse_cond_func, priority = 10)]
133 CondFuncCall(&'a str),
134
135 #[token("?")]
136 Question,
137
138 #[regex(r"[^$|\[\]?]+")]
139 Text(&'a str),
140}
141
142type Lexer<'a> = LogosLexer<'a, Token<'a>>;
144
145pub(crate) fn lex(input: &str) -> Lexer<'_> {
147 Token::lexer(input)
148}
149
150pub(crate) fn tokenize(input: &str) -> Vec<Token<'_>> {
152 lex(input).filter_map(Result::ok).collect()
153}
154
155#[derive(Clone, Debug, Logos, PartialEq)]
157#[logos(skip r"[ \t]+")]
158pub(crate) enum ExprToken<'a> {
159 #[regex(r"\.\d", |lex| lex.slice().chars().nth(1).and_then(|c| c.to_digit(10)).and_then(|digit| u8::try_from(digit).ok()))]
160 DecimalFormat(u8),
161
162 #[regex(r"\$<[a-zA-Z][a-zA-Z0-9]*>", priority = 10)]
163 CustomVar(&'a str),
164
165 #[regex(r"\$(?:[sS]|m|M|o|t|a|A|e|w|W|x|bc|q|sw)[1-9]", priority = 10)]
166 EffectVar(&'a str),
167
168 #[regex(r"\$(?:d[1-3]?|n|u|h|r|i|p\d?|z|c\d)", priority = 10)]
169 SpellLevelVar(&'a str),
170
171 #[regex(r"\$(?:SP|sp|AP|ap|RAP|MHP|mhp|SPS|PL|pl|INT)", priority = 10)]
172 PlayerVar(&'a str),
173
174 #[regex(r"\$(?:ec[12](?:s\d)?|ecix|ecim|ecd)", priority = 10)]
175 EnchantVar(&'a str),
176
177 #[regex(r"\$(?:maxcast|pctD|W|B|ctrmax\d+|mws|mwb|MWS|MWB|ows|OWB|lpoint|mastery|mas|proccooldown|procrppm|pri|rolemult)", priority = 10)]
178 MiscVar(&'a str),
179
180 #[regex(r"\$@[a-zA-Z]+\d*", priority = 10)]
181 AtVar(&'a str),
182
183 #[regex(r"\$\d+[a-zA-Z]+\d*", priority = 10)]
184 CrossSpellRef(&'a str),
185
186 #[regex(r"\$(?:cond|gte|gt|lte|lt|max|min|clamp|floor)", priority = 10)]
187 DollarFunc(&'a str),
188
189 #[regex(r"\$[a-zA-Z_][a-zA-Z0-9_]*", priority = 1)]
190 SimpleVar(&'a str),
191
192 #[regex(r"\d+(?:\.\d+)?", |lex| lex.slice().parse::<f64>().ok())]
193 Number(f64),
194
195 #[regex(r"[a-zA-Z_][a-zA-Z0-9_]*")]
196 Ident(&'a str),
197
198 #[token("(")]
199 LParen,
200
201 #[token(")")]
202 RParen,
203
204 #[token("+")]
205 Plus,
206
207 #[token("-")]
208 Minus,
209
210 #[token("*")]
211 Star,
212
213 #[token("/")]
214 Slash,
215
216 #[token(",")]
217 Comma,
218}
219
220pub(crate) fn lex_expr(input: &str) -> LogosLexer<'_, ExprToken<'_>> {
222 ExprToken::lexer(input)
223}
224
225#[cfg(feature = "wasm")]
226use wowlab_types::spell_desc::SpellDescFragment;
227
228#[cfg(feature = "wasm")]
229const COLOR_CODE_PREFIX_LEN: usize = 2;
230
231#[cfg(feature = "wasm")]
233fn token_to_fragment(
234 token: &Token<'_>,
235 input: &str,
236 span: std::ops::Range<usize>,
237) -> SpellDescFragment {
238 match token {
239 Token::ColorCode(code) => {
240 if code.to_lowercase() == "|r" {
241 SpellDescFragment::ColorEnd
242 } else {
243 SpellDescFragment::ColorStart {
244 color: code[COLOR_CODE_PREFIX_LEN..].to_uppercase(),
245 }
246 }
247 }
248
249 Token::ExpressionBlock(_) => SpellDescFragment::RawToken {
250 value: format!("${{{}}}", &input[span.start + 2..span.end]),
251 },
252
253 Token::Pluralization((content, cap)) => SpellDescFragment::RawToken {
254 value: format!("{}{};\u{200B}", if *cap { "$L" } else { "$l" }, content),
255 },
256 Token::Gender((content, cap)) => SpellDescFragment::RawToken {
257 value: format!("{}{};\u{200B}", if *cap { "$G" } else { "$g" }, content),
258 },
259
260 Token::CustomVariable(_)
261 | Token::AtVariable(_)
262 | Token::EffectVariable(_)
263 | Token::SpellLevelVariable(_)
264 | Token::PlayerVariable(_)
265 | Token::EnchantVariable(_)
266 | Token::MiscVariable(_)
267 | Token::CrossSpellRef(_)
268 | Token::SimpleVariable(_)
269 | Token::ConditionalStart
270 | Token::CondFuncCall(_) => SpellDescFragment::RawToken {
271 value: input[span].to_string(),
272 },
273
274 _ => SpellDescFragment::Text {
275 value: input[span].to_string(),
276 },
277 }
278}
279
280#[cfg(feature = "wasm")]
282pub(crate) fn tokenize_to_fragments(input: &str) -> Vec<SpellDescFragment> {
283 let mut lexer = lex(input);
284 let mut fragments = Vec::new();
285
286 while let Some(result) = lexer.next() {
287 if let Ok(token) = result {
288 let fragment = token_to_fragment(&token, input, lexer.span());
289
290 if let SpellDescFragment::Text { value } = &fragment {
292 if let Some(SpellDescFragment::Text { value: existing }) = fragments.last_mut() {
293 existing.push_str(value);
294 continue;
295 }
296 }
297
298 fragments.push(fragment);
299 }
300 }
301
302 fragments
303}