wowlab_parsers/parsers/spell_desc/renderer/
mod.rs1use wowlab_types::{
4 constants::{MS_PER_HOUR, MS_PER_MINUTE, MS_PER_SECOND},
5 spell_desc::{SpellDescFragment, SpellDescRenderResult},
6};
7
8use super::{
9 parser::parse,
10 resolver::{NullResolver, SpellDescResolver},
11 types::ParsedSpellDescription,
12};
13
14mod evaluate;
15mod render;
16#[cfg(test)]
17mod tests;
18
19const MAX_EMBED_DEPTH: u8 = 3;
20const FLOAT_EPSILON: f64 = 0.001;
21const BINARY_ARGS: usize = 2;
22const TERNARY_ARGS: usize = 3;
23
24pub fn render_with_resolver<R>(
26 ast: &ParsedSpellDescription,
27 self_spell_id: u32,
28 resolver: &R,
29 parse_errors: Vec<String>,
30) -> SpellDescRenderResult
31where
32 R: SpellDescResolver,
33{
34 let mut ctx = RenderContext::new(self_spell_id, resolver);
35 let fragments = ctx.render_nodes(&ast.nodes);
36 let warnings = ctx.warnings.into_iter().map(Into::into).collect();
37
38 SpellDescRenderResult {
39 fragments,
40 parse_errors,
41 warnings,
42 }
43}
44
45#[must_use]
47pub fn render_global_string(input: &str) -> String {
48 let parsed = parse(input);
49 let result = render_with_resolver(&parsed.ast, 0, &NullResolver, Vec::new());
50
51 result.to_plain_text()
52}
53
54struct RenderContext<'a, R>
55where
56 R: SpellDescResolver,
57{
58 self_spell_id: u32,
59 resolver: &'a R,
60 last_number: Option<f64>,
61 embed_depth: u8,
62 warnings: Vec<Box<str>>,
63}
64
65impl<'a, R> RenderContext<'a, R>
66where
67 R: SpellDescResolver,
68{
69 fn new(self_spell_id: u32, resolver: &'a R) -> Self {
70 Self {
71 self_spell_id,
72 resolver,
73 last_number: None,
74 embed_depth: 0,
75 warnings: Vec::new(),
76 }
77 }
78
79 fn warn(&mut self, message: String) {
80 self.warnings.push(message.into_boxed_str());
81 }
82}
83
84fn build_spell_level_key(var_type: &str, index: Option<u8>) -> String {
85 match index {
86 Some(idx) => format!("{var_type}{idx}"),
87 None => var_type.to_string(),
88 }
89}
90
91fn build_misc_key(var_name: &str, id: Option<u32>) -> String {
92 match id {
93 Some(id) => format!("{var_name}{id}"),
94 None => var_name.to_string(),
95 }
96}
97
98fn capitalize_if_needed(s: &str, capitalize: bool) -> String {
99 if capitalize && !s.is_empty() {
100 let mut chars = s.chars();
101
102 match chars.next() {
103 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
104 None => String::new(),
105 }
106 } else {
107 s.to_string()
108 }
109}
110
111fn parse_duration_ms(s: &str) -> Option<f64> {
112 let parts: Vec<&str> = s.split_whitespace().collect();
113
114 if parts.len() >= BINARY_ARGS {
115 if let Ok(num) = parts[0].parse::<f64>() {
116 let unit = parts[1].to_lowercase();
117
118 if unit.starts_with("sec") {
119 return Some(num * MS_PER_SECOND);
120 } else if unit.starts_with("min") {
121 return Some(num * MS_PER_MINUTE);
122 } else if unit.starts_with("hour") {
123 return Some(num * MS_PER_HOUR);
124 }
125 }
126 }
127
128 None
129}
130
131fn merge_text_fragments(fragments: &mut Vec<SpellDescFragment>) {
132 let mut i = 0;
133
134 while i + 1 < fragments.len() {
136 if let (SpellDescFragment::Text { value: a }, SpellDescFragment::Text { value: b }) =
137 (&fragments[i], &fragments[i + 1])
138 {
139 let merged = format!("{a}{b}");
140
141 fragments[i] = SpellDescFragment::Text { value: merged };
142 fragments.remove(i + 1);
143 } else {
144 i += 1;
145 }
146 }
147}