ck3_history_extractor/
jinja_env.rs

1use std::{fs, path::Path};
2
3use minijinja::{context, Environment, State, UndefinedBehavior, Value};
4use serde::{ser::SerializeStruct, Serialize};
5
6use super::{
7    display::ProceduralPath,
8    game_data::{GameData, Localize},
9    parser::GameRef,
10    structures::{FromGameObject, GameObjectDerived},
11    types::Wrapper,
12};
13
14#[cfg(feature = "internal")]
15mod internal_templates {
16    pub const INT_H_TEMPLATE: &str = include_str!("../templates/homeTemplate.html");
17    pub const INT_C_TEMPLATE: &str = include_str!("../templates/charTemplate.html");
18    pub const INT_CUL_TEMPLATE: &str = include_str!("../templates/cultureTemplate.html");
19    pub const INT_DYN_TEMPLATE: &str = include_str!("../templates/dynastyTemplate.html");
20    pub const INT_HOUSE_TEMPLATE: &str = include_str!("../templates/houseTemplate.html");
21    pub const INT_FAITH_TEMPLATE: &str = include_str!("../templates/faithTemplate.html");
22    pub const INT_TITLE_TEMPLATE: &str = include_str!("../templates/titleTemplate.html");
23    pub const INT_TIMELINE_TEMPLATE: &str = include_str!("../templates/timelineTemplate.html");
24    pub const INT_BASE_TEMPLATE: &str = include_str!("../templates/base.html");
25    pub const INT_REF_TEMPLATE: &str = include_str!("../templates/refTemplate.html");
26}
27
28pub const H_TEMPLATE_NAME: &str = "homeTemplate";
29pub const C_TEMPLATE_NAME: &str = "charTemplate";
30pub const CUL_TEMPLATE_NAME: &str = "cultureTemplate";
31pub const DYN_TEMPLATE_NAME: &str = "dynastyTemplate";
32pub const HOUSE_TEMPLATE_NAME: &str = "houseTemplate";
33pub const FAITH_TEMPLATE_NAME: &str = "faithTemplate";
34pub const TITLE_TEMPLATE_NAME: &str = "titleTemplate";
35pub const TIMELINE_TEMPLATE_NAME: &str = "timelineTemplate";
36pub const BASE_TEMPLATE_NAME: &str = "base";
37pub const REF_TEMPLATE_NAME: &str = "refTemplate";
38
39const TEMPLATE_NAMES: [&str; 10] = [
40    H_TEMPLATE_NAME,
41    C_TEMPLATE_NAME,
42    CUL_TEMPLATE_NAME,
43    DYN_TEMPLATE_NAME,
44    HOUSE_TEMPLATE_NAME,
45    FAITH_TEMPLATE_NAME,
46    TITLE_TEMPLATE_NAME,
47    TIMELINE_TEMPLATE_NAME,
48    BASE_TEMPLATE_NAME,
49    REF_TEMPLATE_NAME,
50];
51
52const LOCALIZATION_GLOBAL: &str = "localization";
53const LOCALIZATION_FUNC_NAME: &str = "localize";
54
55const DERIVED_REF_NAME_ATTR: &str = "name";
56const DERIVED_REF_SUBDIR_ATTR: &str = "subdir";
57const DERIVED_REF_ID_ATTR: &str = "id";
58
59impl<T: GameObjectDerived + FromGameObject + ProceduralPath> Serialize for GameRef<T> {
60    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
61    where
62        S: serde::Serializer,
63    {
64        let internal = self.get_internal();
65        if let Some(inner) = internal.inner() {
66            let mut state = serializer.serialize_struct("DerivedRef", 3)?;
67            state.serialize_field(DERIVED_REF_ID_ATTR, &internal.get_id())?;
68            state.serialize_field(DERIVED_REF_NAME_ATTR, &inner.get_name())?;
69            state.serialize_field(DERIVED_REF_SUBDIR_ATTR, T::get_subdir())?;
70            state.end()
71        } else {
72            serializer.serialize_none()
73        }
74    }
75}
76
77// MAYBE there's a better way of providing localization, however, I have yet to find it
78
79/* What we do here, is allow for all Value objects to act as localizer, and
80then embed the localizer in the environment. This is sort of bad. Performance
81wise at least */
82
83impl Localize<String> for Value {
84    fn lookup<K: AsRef<str>>(&self, key: K) -> Option<String> {
85        self.get_attr(key.as_ref())
86            .ok()
87            .map(|x| x.as_str().and_then(|x| Some(x.to_string())))
88            .flatten()
89    }
90
91    fn is_empty(&self) -> bool {
92        self.is_none()
93    }
94}
95
96/// # Environment creation
97///
98/// Create a new [Environment] with the filters and templates needed for the project.
99/// If the internal flag is set to true, it will use the internal templates, otherwise it will use the templates in the templates folder.
100/// If the templates folder does not exist, it will attempt use the internal templates regardless of the setting.
101///
102/// ## Env specifics
103///
104/// The environment will have no html escaping, and will not permit undefined chicanery.
105///
106/// ### Filters
107///
108/// The environment will have the following filters:
109/// - [render_ref] - renders a reference to another object
110/// - [localize] - localizes the provided string
111///
112/// ### Globals
113///
114/// The environment will have the following globals:
115/// - map_present - whether the map is present
116/// - no_vis - whether the visualizations are disabled
117///
118pub fn create_env<'a>(
119    internal: bool,
120    map_present: bool,
121    no_vis: bool,
122    data: &GameData,
123) -> Environment<'a> {
124    let mut env = Environment::new();
125    env.set_lstrip_blocks(true);
126    env.set_trim_blocks(true);
127    env.add_filter("render_ref", render_ref);
128    env.add_filter(LOCALIZATION_FUNC_NAME, localize);
129    env.add_function(LOCALIZATION_FUNC_NAME, localize);
130    env.add_global("map_present", map_present);
131    env.add_global("no_vis", no_vis);
132    env.add_global(
133        LOCALIZATION_GLOBAL,
134        Value::from_serialize(data.get_localizer()),
135    );
136    env.set_undefined_behavior(UndefinedBehavior::Strict);
137    let template_path = Path::new("./templates");
138    if internal || !template_path.exists() {
139        #[cfg(feature = "internal")]
140        {
141            use internal_templates::*;
142            env.add_template(H_TEMPLATE_NAME, INT_H_TEMPLATE).unwrap();
143            env.add_template(C_TEMPLATE_NAME, INT_C_TEMPLATE).unwrap();
144            env.add_template(CUL_TEMPLATE_NAME, INT_CUL_TEMPLATE)
145                .unwrap();
146            env.add_template(DYN_TEMPLATE_NAME, INT_DYN_TEMPLATE)
147                .unwrap();
148            env.add_template(HOUSE_TEMPLATE_NAME, INT_HOUSE_TEMPLATE)
149                .unwrap();
150            env.add_template(FAITH_TEMPLATE_NAME, INT_FAITH_TEMPLATE)
151                .unwrap();
152            env.add_template(TITLE_TEMPLATE_NAME, INT_TITLE_TEMPLATE)
153                .unwrap();
154            env.add_template(TIMELINE_TEMPLATE_NAME, INT_TIMELINE_TEMPLATE)
155                .unwrap();
156            env.add_template(BASE_TEMPLATE_NAME, INT_BASE_TEMPLATE)
157                .unwrap();
158            env.add_template(REF_TEMPLATE_NAME, INT_REF_TEMPLATE)
159                .unwrap();
160        }
161        #[cfg(not(feature = "internal"))]
162        {
163            panic!("Internal templates requested but not compiled in");
164        }
165    } else {
166        let template_dir = fs::read_dir(template_path).unwrap();
167        for read_result in template_dir {
168            match read_result {
169                Ok(entry) => {
170                    //it needs to be a template file
171                    let path = entry.path();
172                    if !path.is_file() {
173                        continue;
174                    }
175                    let name = TEMPLATE_NAMES
176                        .iter()
177                        .find(|&x| x == &path.file_stem().unwrap());
178                    if let Some(name) = name {
179                        env.add_template_owned(*name, fs::read_to_string(path).unwrap())
180                            .unwrap();
181                    }
182                }
183                Err(e) => eprintln!("Error reading template directory: {}", e),
184            }
185        }
186    }
187    env
188}
189
190/// A function that renders a reference.
191/// May be used in the templates as filter(using [Environment::add_filter]) or function(using [Environment::add_function]) to render a reference to another object.
192/// If the reference is shallow, it will render just the name, otherwise render it as a link.
193/// The function must be rendered without html escape.
194/// Calling this on an undefined reference will fail.
195fn render_ref(state: &State, reference: Value, root: Option<bool>) -> String {
196    if let Some(name) = reference
197        .get_attr(DERIVED_REF_NAME_ATTR)
198        .expect("Reference doesn't have attributes")
199        .as_str()
200    {
201        let subdir = reference.get_attr(DERIVED_REF_SUBDIR_ATTR).unwrap();
202        let id = reference.get_attr(DERIVED_REF_ID_ATTR).unwrap();
203        if state
204            .lookup("depth_map")
205            .unwrap()
206            .get_item(&subdir)
207            .unwrap()
208            .get_item(&id)
209            .ok()
210            .and_then(|i| i.as_i64())
211            .unwrap_or(0)
212            <= 0
213        {
214            name.to_string()
215        } else {
216            state
217                .env()
218                .get_template(REF_TEMPLATE_NAME)
219                .unwrap()
220                .render(context! {root=>root, ..reference})
221                .unwrap()
222        }
223    } else {
224        "".to_owned()
225    }
226}
227
228fn localize(state: &State, key: &str, value: Option<&str>, provider: Option<&str>) -> String {
229    let localizer = state.lookup(LOCALIZATION_GLOBAL).unwrap();
230    if let Some(value) = value {
231        if let Some(provider) = provider {
232            localizer.localize_provider(key, provider, value).unwrap()
233        } else {
234            localizer
235                .localize_query(key, |_| Some(value.to_string()))
236                .unwrap()
237        }
238    } else {
239        localizer.localize(key).unwrap()
240    }
241}