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