ck3_history_extractor/
jinja_env.rs

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