Skip to main content

wowlab_parsers/parsers/spell_desc/renderer/
mod.rs

1// #t(file: rust_unchecked_indexing) renderer indexes into bounds-checked argument vectors
2
3use 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
24/// Render a parsed spell description to structured fragments.
25pub 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/// Render a raw `WoW` global-string value to plain display text without spell data.
46#[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    // #t(block: rust_unchecked_indexing, rust_alloc_in_loop) bounds checked by while guard; in-place merge requires indexed access
135    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}