From be1d9c97eaeb205ca95e4b29958ba2eea4eee8ca Mon Sep 17 00:00:00 2001 From: Adrien Burgun Date: Wed, 27 Jul 2022 16:24:25 +0200 Subject: [PATCH] :sparkles: Working mustache-based generator --- .gitignore | 2 + Cargo.toml | 12 ++ species/blobfox/assets/base.svg | 161 +++++++++++++++++++++ species/blobfox/species.toml | 1 + species/blobfox/templates/base.mustache | 7 + species/blobfox/templates/eyes.mustache | 4 + species/blobfox/templates/footer.mustache | 1 + species/blobfox/templates/header.mustache | 2 + species/blobfox/templates/mouth-w.mustache | 1 + species/blobfox/templates/nose.mustache | 4 + species/blobfox/variants/base.mustache | 6 + src/main.rs | 16 ++ src/parse.rs | 94 ++++++++++++ src/template.rs | 158 ++++++++++++++++++++ vector/blobfox.svg | 28 ++-- 15 files changed, 483 insertions(+), 14 deletions(-) create mode 100644 Cargo.toml create mode 100644 species/blobfox/assets/base.svg create mode 100644 species/blobfox/species.toml create mode 100644 species/blobfox/templates/base.mustache create mode 100644 species/blobfox/templates/eyes.mustache create mode 100644 species/blobfox/templates/footer.mustache create mode 100644 species/blobfox/templates/header.mustache create mode 100644 species/blobfox/templates/mouth-w.mustache create mode 100644 species/blobfox/templates/nose.mustache create mode 100644 species/blobfox/variants/base.mustache create mode 100644 src/main.rs create mode 100644 src/parse.rs create mode 100644 src/template.rs diff --git a/.gitignore b/.gitignore index b9fb92a..9d605e3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ original/ output/ +Cargo.lock +target/ diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..8febedd --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "blobfox-template" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = { version = "1.0.140", features = ["derive"] } +toml = "0.5.9" +xmltree = "0.10.3" +mustache = { git = "https://git.shadamethyst.xyz/adri326/rust-mustache.git" } diff --git a/species/blobfox/assets/base.svg b/species/blobfox/assets/base.svg new file mode 100644 index 0000000..e0bc79d --- /dev/null +++ b/species/blobfox/assets/base.svg @@ -0,0 +1,161 @@ + + + + + blobfox + + + + + + + + + + + + + + + + + + + + + + + + + Blobfox team (https://git.shadamethyst.xyz/adri326/blobfox), licensed under the Apache 2.0 License + + + blobfox + + + Feuerfuchs + + + https://git.shadamethyst.xyz/adri326/blobfox + + + Shad Amethyst + + + + + + diff --git a/species/blobfox/species.toml b/species/blobfox/species.toml new file mode 100644 index 0000000..f13fa38 --- /dev/null +++ b/species/blobfox/species.toml @@ -0,0 +1 @@ +# Add options in here as needs be diff --git a/species/blobfox/templates/base.mustache b/species/blobfox/templates/base.mustache new file mode 100644 index 0000000..cbeaa72 --- /dev/null +++ b/species/blobfox/templates/base.mustache @@ -0,0 +1,7 @@ + + {{#base}}body{{/base}} + {{#base}}left-ear{{/base}} + {{#base}}hair{{/base}} + {{#base}}right-ear{{/base}} + {{#base}}right-ear-fluff{{/base}} + diff --git a/species/blobfox/templates/eyes.mustache b/species/blobfox/templates/eyes.mustache new file mode 100644 index 0000000..0581ed4 --- /dev/null +++ b/species/blobfox/templates/eyes.mustache @@ -0,0 +1,4 @@ + + {{#base}}left-eye{{/base}} + {{#base}}right-eye{{/base}} + diff --git a/species/blobfox/templates/footer.mustache b/species/blobfox/templates/footer.mustache new file mode 100644 index 0000000..b590cc4 --- /dev/null +++ b/species/blobfox/templates/footer.mustache @@ -0,0 +1 @@ + diff --git a/species/blobfox/templates/header.mustache b/species/blobfox/templates/header.mustache new file mode 100644 index 0000000..05199ce --- /dev/null +++ b/species/blobfox/templates/header.mustache @@ -0,0 +1,2 @@ + + {{variant_name}} diff --git a/species/blobfox/templates/mouth-w.mustache b/species/blobfox/templates/mouth-w.mustache new file mode 100644 index 0000000..c0e10bc --- /dev/null +++ b/species/blobfox/templates/mouth-w.mustache @@ -0,0 +1 @@ +{{#base}}mouth{{/base}} diff --git a/species/blobfox/templates/nose.mustache b/species/blobfox/templates/nose.mustache new file mode 100644 index 0000000..815ce1e --- /dev/null +++ b/species/blobfox/templates/nose.mustache @@ -0,0 +1,4 @@ + + {{#base}}nose-outline{{/base}} + {{#base}}nose{{/base}} + diff --git a/species/blobfox/variants/base.mustache b/species/blobfox/variants/base.mustache new file mode 100644 index 0000000..30488d9 --- /dev/null +++ b/species/blobfox/variants/base.mustache @@ -0,0 +1,6 @@ +{{>header}} + {{>base}} + {{>eyes}} + {{>nose}} + {{>mouth-w}} +{{>footer}} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..9b82bfa --- /dev/null +++ b/src/main.rs @@ -0,0 +1,16 @@ +use std::sync::Arc; + +pub mod parse; +use parse::*; + +pub mod template; +use template::*; + +fn main() { + let species = Arc::new(dbg!(load_species("species/blobfox")).unwrap()); + let context = RenderingContext::new(species); + let template = context.compile("species/blobfox/variants/base.svg").unwrap(); + let rendered = template.render_data_to_string(&context.get_data()).unwrap(); + println!("{}", rendered); + std::fs::write("./test.svg", rendered).unwrap(); +} diff --git a/src/parse.rs b/src/parse.rs new file mode 100644 index 0000000..ea2214b --- /dev/null +++ b/src/parse.rs @@ -0,0 +1,94 @@ +use xmltree::{XMLNode, Element}; +use serde::{Serialize, Deserialize}; +use std::path::{PathBuf, Path}; +use std::collections::HashMap; + +/// Error returned upon failing to parse something +#[derive(Debug)] +pub enum ParseError { + Io(PathBuf, std::io::Error), + XmlParse(xmltree::ParseError), + Toml(toml::de::Error), +} + +impl From for ParseError { + fn from(err: xmltree::ParseError) -> Self { + Self::XmlParse(err) + } +} + +impl From for ParseError { + fn from(err: toml::de::Error) -> Self { + Self::Toml(err) + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct SpeciesDecl { + /// Imports xml and svg files from this folder if they aren't found + pub base: Option, + + #[serde(skip)] + pub templates: HashMap, + + #[serde(skip)] + pub variants: HashMap, + + #[serde(skip)] + pub assets: HashMap, +} + +/// Loads the given file as an XML tree +pub fn load_xml(path: impl AsRef) -> Result { + let file = std::fs::File::open(path.as_ref()).map_err(|err| { + ParseError::Io(path.as_ref().to_path_buf(), err) + })?; + + Ok(Element::parse(file)?) +} + +/// Loads the basic description of a SpeciesDecl +pub fn load_species(path: impl AsRef) -> Result { + let declaration_path = path.as_ref().join("species.toml"); + let declaration = std::fs::read_to_string(&declaration_path).map_err(|err| { + ParseError::Io(declaration_path, err) + })?; + + let mut res: SpeciesDecl = toml::from_str(&declaration)?; + + // Read the `templates` directory and populate the `templates` field; + // on error, ignore the directory. + res.templates = read_dir_xml(path.as_ref().join("templates")); + + // Read the `variants` directory + res.variants = read_dir_xml(path.as_ref().join("variants")); + + // Read the `assets` directory + res.assets = read_dir_xml(path.as_ref().join("assets")); + + Ok(res) +} + +fn read_dir_xml(path: impl AsRef) -> HashMap { + let mut res = HashMap::new(); + + if let Ok(iter) = std::fs::read_dir(path) { + for entry in iter.filter_map(|x| x.ok()) { + match (entry.path().file_stem(), entry.path().extension()) { + (Some(name), Some(ext)) => { + if matches!(ext.to_str(), Some("xml") | Some("svg") | Some("mustache")) { + if let Some(name) = name.to_str() { + res.insert( + name.to_string(), + entry.path().to_path_buf() + ); + } + } + } + _ => {} + } + } + } + + res +} diff --git a/src/template.rs b/src/template.rs new file mode 100644 index 0000000..4bf21e1 --- /dev/null +++ b/src/template.rs @@ -0,0 +1,158 @@ +use mustache::{ + Context, + PartialLoader, + Template, + MapBuilder, + Data, +}; +use super::*; +use std::collections::HashMap; +use std::path::Path; +use std::sync::{Arc, Mutex}; +use xmltree::{XMLNode, Element}; + +#[derive(Debug, Clone)] +pub struct RenderingContext { + species: Arc, + + rendered_variants: Arc>>, + + loaded_assets: Arc>>, +} + +impl RenderingContext { + pub fn new(species: Arc) -> Self { + Self { + species, + rendered_variants: Arc::new(Mutex::new(HashMap::new())), + loaded_assets: Arc::new(Mutex::new(HashMap::new())), + } + } + + pub fn compile(&self, path: impl AsRef) -> Result, mustache::Error> { + let template = std::fs::read_to_string(path)?; + Context::with_loader(self.clone()).compile(template.chars()) + } + + pub fn get_data(&self) -> Data { + let mut builder = MapBuilder::new(); + + builder = builder.insert_map("variant", |mut builder| { + for variant_name in self.species.variants.keys() { + let this = self.clone(); + let variant_name = variant_name.to_string(); + builder = builder.insert_fn(variant_name.clone(), move |selector| { + let svg = this.get_variant(&variant_name); + if let Some(svg) = svg { + if let Some(element) = query_selector(svg, &selector) { + if let Some(string) = xml_to_string(element) { + return string + } + } + } + + String::new() + }) + } + builder + }); + + for asset_name in self.species.assets.keys() { + let this = self.clone(); + let asset_name = asset_name.to_string(); + + builder = builder.insert_fn(asset_name.clone(), move |selector| { + let svg = this.get_asset(&asset_name); + if let Some(svg) = svg { + if let Some(element) = query_selector(svg, &selector) { + if let Some(string) = xml_to_string(element) { + return string + } + } + } + + String::new() + }); + } + + builder.build() + } + + pub fn get_variant(&self, name: &String) -> Option { + let rendered = self.rendered_variants.lock().unwrap().get(name).cloned(); + if let Some(rendered) = rendered { + Some(rendered) + } else if let Some(path) = self.species.variants.get(name) { + // TODO: log error + let template = self.compile(path).ok()?; + let data = self.get_data(); + let rendered = template.render_data_to_string(&data).ok()?; + + let parsed = Element::parse(rendered.as_bytes()).ok()?; + self.rendered_variants.lock().unwrap().insert(name.clone(), parsed.clone()); + + Some(parsed) + } else { + None + } + } + + pub fn get_asset(&self, name: &String) -> Option { + let loaded = self.loaded_assets.lock().unwrap().get(name).cloned(); + if let Some(loaded) = loaded { + Some(loaded) + } else if let Some(path) = self.species.assets.get(name) { + let string = std::fs::read_to_string(path).ok()?; + let parsed = Element::parse(string.as_bytes()).ok()?; + self.loaded_assets.lock().unwrap().insert(name.clone(), parsed.clone()); + + Some(parsed) + } else { + None + } + } +} + +impl PartialLoader for RenderingContext { + fn load(&self, name: impl AsRef) -> Result { + let name = name.as_ref().to_str().ok_or(mustache::Error::InvalidStr)?; + + if let Some(path) = self.species.templates.get(name) { + Ok(std::fs::read_to_string(path)?) + } else { + eprintln!("No template named {}", name); + Err(mustache::Error::NoFilename) + } + } +} + +pub fn query_selector(svg: Element, pattern: &str) -> Option { + if pattern == "" { + return Some(svg); + } + + for child in svg.children { + if let XMLNode::Element(child) = child { + if child.attributes.get("id").map(|id| id == pattern).unwrap_or(false) { + return Some(child); + } else if child.children.len() > 0 { + if let Some(res) = query_selector(child, pattern) { + return Some(res); + } + } + } + } + + None +} + +pub fn xml_to_string(element: Element) -> Option { + let mut s: Vec = Vec::new(); + let mut config = xmltree::EmitterConfig::default(); + config.perform_indent = true; + config.write_document_declaration = false; + + element.write_with_config(&mut s, config); + + String::from_utf8(s).ok() +} diff --git a/vector/blobfox.svg b/vector/blobfox.svg index 48c9a9d..e0bc79d 100644 --- a/vector/blobfox.svg +++ b/vector/blobfox.svg @@ -29,15 +29,15 @@ inkscape:pagecheckerboard="1" inkscape:document-units="mm" showgrid="false" - inkscape:zoom="4.1184431" - inkscape:cx="-8.4983571" - inkscape:cy="56.331967" + inkscape:zoom="10.154753" + inkscape:cx="59.381059" + inkscape:cy="57.214585" inkscape:window-width="1536" inkscape:window-height="779" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" - inkscape:current-layer="layer1" + inkscape:current-layer="layer3" units="px" inkscape:showpageshadow="2" inkscape:deskcolor="#505050"> @@ -69,7 +69,7 @@ id="layer1" style="display:inline"> @@ -105,31 +105,31 @@