Skip to main content

wow_sharedmedia/
template.rs

1//! Addon template management for `loader.lua` and `.toc`.
2//!
3//! Template sources live in `templates/` and are embedded into the crate with
4//! `include_str!`. Rust is responsible only for version and interface
5//! substitution plus writing the final files to disk.
6//!
7//! The `.toc` file name is derived from the addon directory name (e.g.
8//! `TestAddon.toc` for a folder named `TestAddon`).
9//!
10//! `TOC_INTERFACE` is set at compile time by `build.rs`, which extracts the
11//! latest WoW Interface version from the vendor's LibSharedMedia-3.0.toc.
12
13use std::path::Path;
14
15use crate::Error;
16
17const TOC_INTERFACE: &str = env!("TOC_INTERFACE");
18const LOADER_TEMPLATE: &str = include_str!("../templates/loader.lua");
19const TOC_TEMPLATE: &str = include_str!("../templates/template.toc");
20
21const LIBSTUB_LUA: &str = include_str!("../vendor/libsharedmedia-3.0/LibStub/LibStub.lua");
22const CALLBACKHANDLER_LUA: &str =
23	include_str!("../vendor/libsharedmedia-3.0/CallbackHandler-1.0/CallbackHandler-1.0.lua");
24const LSM_LUA: &str = include_str!("../vendor/libsharedmedia-3.0/LibSharedMedia-3.0/LibSharedMedia-3.0.lua");
25const INNER_LIB_XML: &str = include_str!("../vendor/libsharedmedia-3.0/LibSharedMedia-3.0/lib.xml");
26
27fn generate_loader(version: &str) -> String {
28	LOADER_TEMPLATE.replace("__VERSION__", version)
29}
30
31fn generate_toc(version: &str, addon_name: &str) -> String {
32	let title = crate::addon_title(addon_name);
33	TOC_TEMPLATE
34		.replace("__VERSION__", version)
35		.replace("__INTERFACE__", TOC_INTERFACE)
36		.replace("__TITLE__", title)
37}
38
39/// Write template files (`loader.lua`, `{addon_name}.toc`) to the addon directory.
40///
41/// The `.toc` file name matches the addon directory name. For example, a folder
42/// named `TestAddon` produces `TestAddon.toc` with `## Title: TestAddon`.
43///
44/// `data.lua` is intentionally excluded because it is managed independently by
45/// the registry writer.
46pub fn deploy_templates(addon_dir: &Path) -> Result<(), Error> {
47	let version = env!("CARGO_PKG_VERSION");
48	let name = crate::addon_name(addon_dir);
49
50	write_file(addon_dir, "loader.lua", &generate_loader(version))?;
51	write_file(addon_dir, &format!("{name}.toc"), &generate_toc(version, name))?;
52
53	write_file(addon_dir, "libraries/LibStub/LibStub.lua", LIBSTUB_LUA)?;
54	write_file(
55		addon_dir,
56		"libraries/CallbackHandler-1.0/CallbackHandler-1.0.lua",
57		CALLBACKHANDLER_LUA,
58	)?;
59	write_file(addon_dir, "libraries/LibSharedMedia-3.0/lib.xml", INNER_LIB_XML)?;
60	write_file(
61		addon_dir,
62		"libraries/LibSharedMedia-3.0/LibSharedMedia-3.0.lua",
63		LSM_LUA,
64	)?;
65
66	Ok(())
67}
68
69fn write_file(dir: &Path, filename: &str, content: &str) -> Result<(), Error> {
70	let path = dir.join(filename);
71	if let Some(parent) = path.parent() {
72		std::fs::create_dir_all(parent).map_err(|e| Error::Io {
73			source: e,
74			path: parent.to_path_buf(),
75		})?;
76	}
77	std::fs::write(&path, content).map_err(|e| Error::Io {
78		source: e,
79		path: path.clone(),
80	})
81}
82
83#[cfg(test)]
84mod tests {
85	use super::*;
86	use std::sync::{Arc, Mutex};
87
88	use mlua::{Lua, Value, Variadic};
89	use tempfile::TempDir;
90
91	type Registration = (String, String, String, Option<i64>);
92
93	/// Create a named subdirectory inside a TempDir for testing.
94	fn named_addon_dir(dir: &TempDir, name: &str) -> std::path::PathBuf {
95		let p = dir.path().join(name);
96		std::fs::create_dir_all(&p).unwrap();
97		p
98	}
99
100	#[test]
101	fn test_deploy_creates_files() {
102		let dir = TempDir::new().unwrap();
103		let addon_dir = named_addon_dir(&dir, "TestAddon");
104		deploy_templates(&addon_dir).unwrap();
105
106		assert!(addon_dir.join("loader.lua").exists());
107		assert!(addon_dir.join("TestAddon.toc").exists());
108
109		let loader = std::fs::read_to_string(addon_dir.join("loader.lua")).unwrap();
110		assert!(loader.contains("Media registration loader"));
111		assert!(loader.contains("local ADDON_NAME, addon = ..."));
112		assert!(loader.contains("BASE_PATH"));
113		assert!(loader.contains("ADDON_NAME"));
114		assert!(loader.contains(&format!("Version: {}", env!("CARGO_PKG_VERSION"))));
115
116		let toc = std::fs::read_to_string(addon_dir.join("TestAddon.toc")).unwrap();
117		assert!(toc.contains("data.lua"));
118		assert!(toc.contains("loader.lua"));
119		assert!(toc.contains("## Title: TestAddon"));
120		assert!(toc.contains("## Notes: Provides textures, sounds, and other media for LibSharedMedia addons."));
121		assert!(!toc.contains("## Author:"));
122		assert!(!toc.contains("!!!WindMedia"));
123	}
124
125	#[test]
126	fn test_deploy_creates_vendor_files() {
127		let dir = TempDir::new().unwrap();
128		let addon_dir = named_addon_dir(&dir, "TestAddon");
129		deploy_templates(&addon_dir).unwrap();
130
131		assert!(addon_dir.join("libraries/LibStub/LibStub.lua").exists());
132		assert!(
133			addon_dir
134				.join("libraries/CallbackHandler-1.0/CallbackHandler-1.0.lua")
135				.exists()
136		);
137		assert!(
138			addon_dir
139				.join("libraries/LibSharedMedia-3.0/LibSharedMedia-3.0.lua")
140				.exists()
141		);
142		assert!(addon_dir.join("libraries/LibSharedMedia-3.0/lib.xml").exists());
143	}
144
145	#[test]
146	fn test_deploy_overwrites() {
147		let dir = TempDir::new().unwrap();
148		let addon_dir = named_addon_dir(&dir, "TestAddon");
149		deploy_templates(&addon_dir).unwrap();
150
151		std::fs::write(addon_dir.join("loader.lua"), "corrupted").unwrap();
152
153		deploy_templates(&addon_dir).unwrap();
154		let loader = std::fs::read_to_string(addon_dir.join("loader.lua")).unwrap();
155		assert!(loader.contains("Media registration loader"));
156		assert!(loader.contains("Version: "));
157		assert!(!loader.contains("DO NOT EDIT MANUALLY"));
158		assert!(!loader.contains("Reads the data table"));
159	}
160
161	#[test]
162	fn test_toc_contains_interface_version() {
163		let dir = TempDir::new().unwrap();
164		let addon_dir = named_addon_dir(&dir, "TestAddon");
165		deploy_templates(&addon_dir).unwrap();
166
167		let toc = std::fs::read_to_string(addon_dir.join("TestAddon.toc")).unwrap();
168		assert!(toc.contains(&format!("## Interface: {}", TOC_INTERFACE)));
169		assert!(toc.contains(&format!("## Version: {}", env!("CARGO_PKG_VERSION"))));
170		assert!(toc.contains("## Title: TestAddon"));
171		assert!(toc.contains("## Notes: Provides textures, sounds, and other media for LibSharedMedia addons."));
172		assert!(toc.contains("## DefaultState: enabled"));
173		assert!(!toc.contains("## Author:"));
174		assert!(!toc.contains("!!!WindMedia"));
175		assert!(toc.contains("libraries\\LibStub\\LibStub.lua"));
176		assert!(toc.contains("LibSharedMedia-3.0\\lib.xml"));
177	}
178
179	#[test]
180	fn test_toc_orders_libraries_before_runtime_files() {
181		let dir = TempDir::new().unwrap();
182		let addon_dir = named_addon_dir(&dir, "TestAddon");
183		deploy_templates(&addon_dir).unwrap();
184
185		let toc = std::fs::read_to_string(addon_dir.join("TestAddon.toc")).unwrap();
186		let lines: Vec<&str> = toc.lines().collect();
187
188		let libstub = lines
189			.iter()
190			.position(|line| *line == "libraries\\LibStub\\LibStub.lua")
191			.unwrap();
192		let callbackhandler = lines
193			.iter()
194			.position(|line| *line == "libraries\\CallbackHandler-1.0\\CallbackHandler-1.0.lua")
195			.unwrap();
196		let lsm = lines
197			.iter()
198			.position(|line| *line == "libraries\\LibSharedMedia-3.0\\lib.xml")
199			.unwrap();
200		let data = lines.iter().position(|line| *line == "data.lua").unwrap();
201		let loader = lines.iter().position(|line| *line == "loader.lua").unwrap();
202
203		assert!(libstub < callbackhandler);
204		assert!(callbackhandler < lsm);
205		assert!(lsm < data);
206		assert!(data < loader);
207	}
208
209	#[test]
210	fn test_toc_skips_data_lua() {
211		let dir = TempDir::new().unwrap();
212		let addon_dir = named_addon_dir(&dir, "TestAddon");
213		deploy_templates(&addon_dir).unwrap();
214		assert!(!addon_dir.join("data.lua").exists());
215	}
216
217	#[test]
218	fn test_loader_uses_dynamic_addon_name() {
219		let dir = TempDir::new().unwrap();
220		let addon_dir = named_addon_dir(&dir, "TestAddon");
221		deploy_templates(&addon_dir).unwrap();
222
223		let loader = std::fs::read_to_string(addon_dir.join("loader.lua")).unwrap();
224		assert!(loader.contains("Media registration loader"));
225		assert!(loader.contains("local ADDON_NAME, addon = ..."));
226		assert!(loader.contains(r#"Interface\\AddOns\\"#));
227		assert!(loader.contains("ADDON_NAME"));
228		assert!(loader.contains("data.entries"));
229		assert!(!loader.contains("DO NOT EDIT MANUALLY"));
230	}
231
232	#[test]
233	fn test_generate_loader_reflects_version_changes() {
234		let v1 = generate_loader("1.2.3");
235		let v2 = generate_loader("9.9.9");
236
237		assert!(v1.contains("Version: 1.2.3"));
238		assert!(v2.contains("Version: 9.9.9"));
239		assert_ne!(v1, v2);
240	}
241
242	#[test]
243	fn test_generate_toc_reflects_version_changes() {
244		let v1 = generate_toc("0.1.0", "TestAddon");
245		let v2 = generate_toc("0.2.0", "TestAddon");
246
247		assert!(v1.contains("## Version: 0.1.0"));
248		assert!(v2.contains("## Version: 0.2.0"));
249		assert_ne!(v1, v2);
250	}
251
252	#[test]
253	fn test_toc_strips_bangs_from_title() {
254		let dir = TempDir::new().unwrap();
255		let addon_dir = named_addon_dir(&dir, "!!!WindMedia");
256		deploy_templates(&addon_dir).unwrap();
257
258		assert!(addon_dir.join("!!!WindMedia.toc").exists());
259		let toc = std::fs::read_to_string(addon_dir.join("!!!WindMedia.toc")).unwrap();
260		assert!(toc.contains("## Title: WindMedia"));
261		assert!(!toc.contains("## Title: !!!"));
262	}
263
264	#[test]
265	fn test_loader_executes_in_lua51_style_runtime() {
266		let lua = Lua::new();
267		let registrations: Arc<Mutex<Vec<Registration>>> = Arc::new(Mutex::new(Vec::new()));
268
269		let lsm = lua.create_table().unwrap();
270		lsm.set("LOCALE_BIT_koKR", 1).unwrap();
271		lsm.set("LOCALE_BIT_ruRU", 2).unwrap();
272		lsm.set("LOCALE_BIT_zhCN", 4).unwrap();
273		lsm.set("LOCALE_BIT_zhTW", 8).unwrap();
274		lsm.set("LOCALE_BIT_western", 16).unwrap();
275
276		let regs = registrations.clone();
277		let register = lua
278			.create_function_mut(move |_, args: Variadic<Value>| {
279				let kind = match &args[1] {
280					Value::String(s) => s.to_str()?.to_string(),
281					other => panic!("unexpected type arg: {other:?}"),
282				};
283				let key = match &args[2] {
284					Value::String(s) => s.to_str()?.to_string(),
285					other => panic!("unexpected key arg: {other:?}"),
286				};
287				let file = match &args[3] {
288					Value::String(s) => s.to_str()?.to_string(),
289					other => panic!("unexpected file arg: {other:?}"),
290				};
291				let mask = args.get(4).and_then(|v| match v {
292					Value::Integer(i) => Some(*i),
293					_ => None,
294				});
295				regs.lock().unwrap().push((kind, key, file, mask));
296				Ok(())
297			})
298			.unwrap();
299		lsm.set("Register", register).unwrap();
300
301		let globals = lua.globals();
302		let libstub_lsm = lsm.clone();
303		let libstub = lua
304			.create_function(move |_, (_name, _silent): (String, bool)| Ok(libstub_lsm.clone()))
305			.unwrap();
306		globals.set("LibStub", libstub).unwrap();
307
308		let addon = lua.create_table().unwrap();
309		let data = lua.create_table().unwrap();
310		let entries = lua.create_table().unwrap();
311
312		let font = lua.create_table().unwrap();
313		font.set("type", "font").unwrap();
314		font.set("key", "Body Font").unwrap();
315		font.set("file", "media/font/body.ttf").unwrap();
316		let metadata = lua.create_table().unwrap();
317		let locales = lua.create_table().unwrap();
318		locales.set(1, "western").unwrap();
319		locales.set(2, "zhCN").unwrap();
320		metadata.set("locales", locales).unwrap();
321		font.set("metadata", metadata).unwrap();
322
323		let statusbar = lua.create_table().unwrap();
324		statusbar.set("type", "statusbar").unwrap();
325		statusbar.set("key", "Smooth").unwrap();
326		statusbar.set("file", "media/statusbar/smooth.tga").unwrap();
327
328		entries.set(1, font).unwrap();
329		entries.set(2, statusbar).unwrap();
330		data.set("entries", entries).unwrap();
331		addon.set("data", data).unwrap();
332
333		let loader = generate_loader("1.2.3");
334		let wrapped = format!("return function(...)\n{}\nend", loader);
335		let func: mlua::Function = lua.load(&wrapped).eval().unwrap();
336		func.call::<()>(("TestAddon".to_string(), addon)).unwrap();
337
338		let regs = registrations.lock().unwrap();
339		assert_eq!(regs.len(), 2);
340		assert_eq!(regs[0].0, "font");
341		assert_eq!(regs[0].1, "Body Font");
342		assert_eq!(regs[0].2, r#"Interface\AddOns\TestAddon\media/font/body.ttf"#);
343		assert_eq!(regs[0].3, Some(20));
344		assert_eq!(regs[1].0, "statusbar");
345		assert_eq!(regs[1].1, "Smooth");
346		assert_eq!(regs[1].2, r#"Interface\AddOns\TestAddon\media/statusbar/smooth.tga"#);
347		assert_eq!(regs[1].3, None);
348	}
349}