From 7eb3e834fa7bee08c5969363a45ca07e8ab204ca Mon Sep 17 00:00:00 2001 From: Jonathan Holmes Date: Mon, 8 Jan 2024 16:22:40 +1300 Subject: [PATCH] WIP TS codegen --- .../pack-these/TestDpi/L@1x.png | Bin 0 -> 1628 bytes .../pack-these/TestDpi/L@2x.png | Bin 0 -> 1628 bytes .../02-spritesheets/pack-these/index.d.ts | 25 ++ examples/02-spritesheets/pack-these/init.lua | 55 ++-- examples/02-spritesheets/tarmac-manifest.toml | 36 +- examples/02-spritesheets/tarmac.toml | 1 + src/codegen.rs | 311 +++++------------- src/data/config.rs | 4 + src/lua/codegen.rs | 170 ++++++++++ src/{ => lua}/lua_ast.rs | 0 src/lua/mod.rs | 2 + src/main.rs | 6 +- src/typescript/codegen.rs | 218 ++++++++++++ src/typescript/mod.rs | 2 + src/{ => typescript}/ts_ast.rs | 210 ++++++------ 15 files changed, 660 insertions(+), 380 deletions(-) create mode 100644 examples/02-spritesheets/pack-these/TestDpi/L@1x.png create mode 100644 examples/02-spritesheets/pack-these/TestDpi/L@2x.png create mode 100644 examples/02-spritesheets/pack-these/index.d.ts create mode 100644 src/lua/codegen.rs rename src/{ => lua}/lua_ast.rs (100%) create mode 100644 src/lua/mod.rs create mode 100644 src/typescript/codegen.rs create mode 100644 src/typescript/mod.rs rename src/{ => typescript}/ts_ast.rs (60%) diff --git a/examples/02-spritesheets/pack-these/TestDpi/L@1x.png b/examples/02-spritesheets/pack-these/TestDpi/L@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..7c35125478804ff67862fb60b031ad63e0ef75be GIT binary patch literal 1628 zcmd5*drVVz7%f2^qUbgU8W<)HmQ5Ds9z-040tR_F5rwiMEJb8O#Q_54p->zeCGr&I zA&NYv@@laIc`QZf&?#6E6fDT%Kwwospio+9q4eywa}Lb(pZ&96^2^P4&UeoDxc8pg zwbMyY*HD*4BI&s}JGhfbS_<{m(I$`@MqL;2(28((+Df|G_Wm#tED5xCwI`7((^n|{ zmJ*!rt{v2Euq46R0Q4`wbi?s|NDM^s_ec*z1`}zaND4w?0FL`1*by`{*z3W=6jc}T z@D8S>AkOGB2(F_v12I(Ctwfv`gaT9-!rL5I&moqEALx*XL0*Q&3WV=KXA4dpK+-{^ z1S2&BX@_t+92pTf6N$4?$c#o-46+U*`v`JkaV`!j73SxmRAO!pbF-LLKp}^G1~b!` zkwG>EsRYtVOif^N91~-h7=`#b#z!zVjL{)He~J+ihX26uAch9;tRGLGVDK^8nsB!P z_N&mq2Fn|_>kzgbxh$|V!7D|t5TZV`HsWz78aU`|2KPK#c=-7W0&Gxn8WlM>V252B zkarB1lkp1|uEr=kgO1y9SqCS5G*yDK9A9b66D{8*R=@m^^PUJ2X%%06wYDtvFeP5c zNf!ruY7Bj>ix!%<|Gg8t69={wH*bF1`?DfbX2-<=Y5pU-A#-VOKaww)Y)FzN*iky_ zyLov%9kG|4Ozy2DY~Os8(LNzOMYd01U0d)4{@65m{5qSmpAp^Oca7yYnVvn~AXXX6 zZ2XTrX-b->t+x5rXqnX6Qx!`-c zP9%_aVcLU_|LXPc5quXG_YnD(F7VK_ceHZx0DS5~g?eay-Qo-}d$Grh?Pw}4>{z4a zFVrq(|3&aWfr$097BBaokKlDJ77Ngmp#9eB^;S&zgg}sC;*`Fg9Ho?xjNG_!YRyM( z`Bss$Qi&w@%fR-0a{Ol^>K`b?(c~Y``Xz4c$Xwp_>Kj`98sD;{7@5wXoBwN zI)>HReRQY$g*D39$R&E-ce&jfKAzxLn{7=tB0vg9$DC4b!k7r##+Y;=hzeCGr&I zA&NYv@@laIc`QZf&?#6E6fDT%Kwwospio+9q4eywa}Lb(pZ&96^2^P4&UeoDxc8pg zwbMyY*HD*4BI&s}JGhfbS_<{m(I$`@MqL;2(28((+Df|G_Wm#tED5xCwI`7((^n|{ zmJ*!rt{v2Euq46R0Q4`wbi?s|NDM^s_ec*z1`}zaND4w?0FL`1*by`{*z3W=6jc}T z@D8S>AkOGB2(F_v12I(Ctwfv`gaT9-!rL5I&moqEALx*XL0*Q&3WV=KXA4dpK+-{^ z1S2&BX@_t+92pTf6N$4?$c#o-46+U*`v`JkaV`!j73SxmRAO!pbF-LLKp}^G1~b!` zkwG>EsRYtVOif^N91~-h7=`#b#z!zVjL{)He~J+ihX26uAch9;tRGLGVDK^8nsB!P z_N&mq2Fn|_>kzgbxh$|V!7D|t5TZV`HsWz78aU`|2KPK#c=-7W0&Gxn8WlM>V252B zkarB1lkp1|uEr=kgO1y9SqCS5G*yDK9A9b66D{8*R=@m^^PUJ2X%%06wYDtvFeP5c zNf!ruY7Bj>ix!%<|Gg8t69={wH*bF1`?DfbX2-<=Y5pU-A#-VOKaww)Y)FzN*iky_ zyLov%9kG|4Ozy2DY~Os8(LNzOMYd01U0d)4{@65m{5qSmpAp^Oca7yYnVvn~AXXX6 zZ2XTrX-b->t+x5rXqnX6Qx!`-c zP9%_aVcLU_|LXPc5quXG_YnD(F7VK_ceHZx0DS5~g?eay-Qo-}d$Grh?Pw}4>{z4a zFVrq(|3&aWfr$097BBaokKlDJ77Ngmp#9eB^;S&zgg}sC;*`Fg9Ho?xjNG_!YRyM( z`Bss$Qi&w@%fR-0a{Ol^>K`b?(c~Y``Xz4c$Xwp_>Kj`98sD;{7@5wXoBwN zI)>HReRQY$g*D39$R&E-ce&jfKAzxLn{7=tB0vg9$DC4b!k7r##+Y;=h ImageSlice; +} +interface Assets { + H: ImageSlice; + I: ImageSlice; + J: ImageSlice; + K: ImageSlice; + L: ImageSlice; + TestDpi: Folder_TestDpi; + a: ImageSlice; + b: ImageSlice; + c: ImageSlice; + d: ImageSlice; + e: ImageSlice; + f: ImageSlice; + g: ImageSlice; +} +export = Assets; diff --git a/examples/02-spritesheets/pack-these/init.lua b/examples/02-spritesheets/pack-these/init.lua index 754a7ad..8814c57 100644 --- a/examples/02-spritesheets/pack-these/init.lua +++ b/examples/02-spritesheets/pack-these/init.lua @@ -1,63 +1,80 @@ -- This file was @generated by Tarmac. It is not intended for manual editing. return { H = { - Image = "rbxassetid://6529931885", + Image = "rbxasset://.tarmac/02-spritesheets/spritesheet-1.png", ImageRectOffset = Vector2.new(0, 0), ImageRectSize = Vector2.new(512, 512), }, I = { - Image = "rbxassetid://6529931979", + Image = "rbxasset://.tarmac/02-spritesheets/spritesheet-2.png", ImageRectOffset = Vector2.new(0, 0), ImageRectSize = Vector2.new(512, 512), }, J = { - Image = "rbxassetid://6529931885", + Image = "rbxasset://.tarmac/02-spritesheets/spritesheet-1.png", ImageRectOffset = Vector2.new(513, 0), ImageRectSize = Vector2.new(505, 505), }, K = { - Image = "rbxassetid://6529931885", + Image = "rbxasset://.tarmac/02-spritesheets/spritesheet-1.png", ImageRectOffset = Vector2.new(0, 513), ImageRectSize = Vector2.new(505, 505), }, L = { - Image = "rbxassetid://6529931885", + Image = "rbxasset://.tarmac/02-spritesheets/spritesheet-1.png", ImageRectOffset = Vector2.new(513, 506), ImageRectSize = Vector2.new(505, 505), }, + TestDpi = { + L = function(dpiScale) + if dpiScale >= 2 then + return { + Image = "rbxasset://.tarmac/02-spritesheets/spritesheet-3.png", + ImageRectOffset = Vector2.new(0, 0), + ImageRectSize = Vector2.new(505, 505), + } + else + return { + Image = "rbxasset://.tarmac/02-spritesheets/spritesheet-2.png", + ImageRectOffset = Vector2.new(513, 0), + ImageRectSize = Vector2.new(505, 505), + } + end + end, + }, a = { - Image = "rbxassetid://6529931979", - ImageRectOffset = Vector2.new(513, 0), + Image = "rbxasset://.tarmac/02-spritesheets/spritesheet-2.png", + ImageRectOffset = Vector2.new(0, 513), ImageRectSize = Vector2.new(128, 128), }, b = { - Image = "rbxassetid://6529931979", - ImageRectOffset = Vector2.new(0, 513), + Image = "rbxasset://.tarmac/02-spritesheets/spritesheet-2.png", + ImageRectOffset = Vector2.new(513, 506), ImageRectSize = Vector2.new(128, 128), }, c = { - Image = "rbxassetid://6529931979", - ImageRectOffset = Vector2.new(642, 0), + Image = "rbxasset://.tarmac/02-spritesheets/spritesheet-2.png", + ImageRectOffset = Vector2.new(129, 513), ImageRectSize = Vector2.new(128, 128), }, d = { - Image = "rbxassetid://6529931979", - ImageRectOffset = Vector2.new(513, 129), + Image = "rbxasset://.tarmac/02-spritesheets/spritesheet-2.png", + ImageRectOffset = Vector2.new(0, 642), ImageRectSize = Vector2.new(128, 128), }, e = { - Image = "rbxassetid://6529931979", - ImageRectOffset = Vector2.new(129, 513), + Image = "rbxasset://.tarmac/02-spritesheets/spritesheet-2.png", + ImageRectOffset = Vector2.new(642, 506), ImageRectSize = Vector2.new(128, 128), }, f = { - Image = "rbxassetid://6529931979", - ImageRectOffset = Vector2.new(0, 642), + Image = "rbxasset://.tarmac/02-spritesheets/spritesheet-2.png", + ImageRectOffset = Vector2.new(513, 635), ImageRectSize = Vector2.new(128, 128), }, g = { - Image = "rbxassetid://6529931979", - ImageRectOffset = Vector2.new(771, 0), + Image = "rbxasset://.tarmac/02-spritesheets/spritesheet-2.png", + ImageRectOffset = Vector2.new(258, 513), ImageRectSize = Vector2.new(128, 128), }, } \ No newline at end of file diff --git a/examples/02-spritesheets/tarmac-manifest.toml b/examples/02-spritesheets/tarmac-manifest.toml index 7dd6b85..790e216 100644 --- a/examples/02-spritesheets/tarmac-manifest.toml +++ b/examples/02-spritesheets/tarmac-manifest.toml @@ -5,72 +5,70 @@ packable = false [inputs."pack-these/H.png"] hash = "643b283ec09f9ef9237c697ff2eaa57e2ff7520d8d05af0ba9131ce49fdf8d15" -id = 6529931885 slice = [[0, 0], [512, 512]] packable = true [inputs."pack-these/I.png"] hash = "8b45e473c554ab176bf983ff6169ab7777f1ba4901293f16619b9b91e801c5f0" -id = 6529931979 slice = [[0, 0], [512, 512]] packable = true [inputs."pack-these/J.png"] hash = "4d9997675cb7bfd11b911fb039e6b0ba0fd25f84f8ebbfa6efebb7475c5b8ee1" -id = 6529931885 slice = [[513, 0], [1018, 505]] packable = true [inputs."pack-these/K.png"] hash = "5903580d378c80727c7ebbaaac3e76c6906e433e3863229d76be43b4ac0ee83b" -id = 6529931885 slice = [[0, 513], [505, 1018]] packable = true [inputs."pack-these/L.png"] hash = "c7b6fc670f4621f775d8bffebaf560c416da27b9d870d245dcdd33039b767538" -id = 6529931885 slice = [[513, 506], [1018, 1011]] packable = true +[inputs."pack-these/TestDpi/L@1x.png"] +hash = "c7b6fc670f4621f775d8bffebaf560c416da27b9d870d245dcdd33039b767538" +slice = [[513, 0], [1018, 505]] +packable = true + +[inputs."pack-these/TestDpi/L@2x.png"] +hash = "c7b6fc670f4621f775d8bffebaf560c416da27b9d870d245dcdd33039b767538" +slice = [[0, 0], [505, 505]] +packable = true + [inputs."pack-these/a.png"] hash = "1944eaf325952605de9bfd06d8455bfea6dcbe9111e838d86ca9c619924d10bc" -id = 6529931979 -slice = [[513, 0], [641, 128]] +slice = [[0, 513], [128, 641]] packable = true [inputs."pack-these/b.png"] hash = "4ef1b9163f054aff8afa0b609dc300873ddb282a41bf18495ec55b792a4bc024" -id = 6529931979 -slice = [[0, 513], [128, 641]] +slice = [[513, 506], [641, 634]] packable = true [inputs."pack-these/c.png"] hash = "6cbc877353dc1ca4b3c5aec683e0a117e3c76e78bf230c5193a3c71b44dd83b2" -id = 6529931979 -slice = [[642, 0], [770, 128]] +slice = [[129, 513], [257, 641]] packable = true [inputs."pack-these/d.png"] hash = "7035a8cb15e325e4d1081cd81178b44f2e30dadc29696966fa7b95254bdc914b" -id = 6529931979 -slice = [[513, 129], [641, 257]] +slice = [[0, 642], [128, 770]] packable = true [inputs."pack-these/e.png"] hash = "a6a4d7869f3ec32b94cce8f4c2662827801629bb41e0350367638d8266f1c1f7" -id = 6529931979 -slice = [[129, 513], [257, 641]] +slice = [[642, 506], [770, 634]] packable = true [inputs."pack-these/f.png"] hash = "faae7bd7052c523477a815d007316abe73f46f1b1d4b8ba1c1b4d3907f8aaa90" -id = 6529931979 -slice = [[0, 642], [128, 770]] +slice = [[513, 635], [641, 763]] packable = true [inputs."pack-these/g.png"] hash = "3845d305bcbc0b4444790c76840ad5de8e138f63cd563dd26dee894a2f8fa440" -id = 6529931979 -slice = [[771, 0], [899, 128]] +slice = [[258, 513], [386, 641]] packable = true diff --git a/examples/02-spritesheets/tarmac.toml b/examples/02-spritesheets/tarmac.toml index 3f40e6f..dda8397 100644 --- a/examples/02-spritesheets/tarmac.toml +++ b/examples/02-spritesheets/tarmac.toml @@ -10,6 +10,7 @@ packable = true codegen = true codegen-path = "pack-these/init.lua" codegen-base-path = "pack-these" +codegen-typescript-declaration = true [[inputs]] glob = "dont-pack-these/**/*.png" diff --git a/src/codegen.rs b/src/codegen.rs index d8d87e1..f069735 100644 --- a/src/codegen.rs +++ b/src/codegen.rs @@ -1,35 +1,18 @@ -//! Defines how Tarmac generates Lua code for linking to assets. -//! -//! Tarmac uses a small Lua AST to build up generated code. +use std::{path::{Path, self}, io, collections::BTreeMap}; -use std::{ - collections::BTreeMap, - io::{self, Write}, - path::{self, Path}, -}; - -use fs_err::File; - -use crate::{ - data::ImageSlice, - data::{AssetId, SyncInput}, - lua_ast::{Block, Expression, Function, IfBlock, Statement, Table}, -}; - -const CODEGEN_HEADER: &str = - "-- This file was @generated by Tarmac. It is not intended for manual editing."; +use crate::{data::SyncInput, typescript, lua}; pub fn perform_codegen(output_path: Option<&Path>, inputs: &[&SyncInput]) -> io::Result<()> { - if let Some(path) = output_path { - codegen_grouped(path, inputs) - } else { - codegen_individual(inputs) - } + lua::codegen::perform_codegen(output_path, inputs)?; + typescript::codegen::perform_codegen(output_path, inputs)?; + + Ok(()) } /// Tree used to track and group inputs hierarchically, before turning them into /// Lua tables. -enum GroupedItem<'a> { +#[derive(Debug)] +pub enum GroupedItem<'a> { Folder { children_by_name: BTreeMap>, }, @@ -38,222 +21,86 @@ enum GroupedItem<'a> { }, } -/// Perform codegen for a group of inputs who have `codegen_path` defined. -/// -/// We'll build up a Lua file containing nested tables that match the structure -/// of the input's path with its base path stripped away. -fn codegen_grouped(output_path: &Path, inputs: &[&SyncInput]) -> io::Result<()> { - let mut root_folder: BTreeMap> = BTreeMap::new(); - - // First, collect all of the inputs and group them together into a tree - // according to their relative paths. - for &input in inputs { - // Not all inputs will be marked for codegen. We can eliminate those - // right away. - if !input.config.codegen { - continue; - } - - // The extension portion of the path is not useful for code generation. - // By stripping it off, we generate the names that users expect. - let mut path_without_extension = input.path_without_dpi_scale.clone(); - path_without_extension.set_extension(""); - - // If we can't construct a relative path, there isn't a sensible name - // that we can use to refer to this input. - let relative_path = path_without_extension - .strip_prefix(&input.config.codegen_base_path) - .expect("Input base path was not a base path for input"); +impl GroupedItem<'_> { + pub fn parse_root_folder<'a>(output_path: &Path, inputs: &'a [&SyncInput]) -> BTreeMap> { + let mut root_folder: BTreeMap> = BTreeMap::new(); - // Collapse `..` path segments so that we can map this path onto our - // tree of inputs. - let mut segments = Vec::new(); - for component in relative_path.components() { - match component { - path::Component::Prefix(_) - | path::Component::RootDir - | path::Component::Normal(_) => { - segments.push(component.as_os_str().to_str().unwrap()) - } - path::Component::CurDir => {} - path::Component::ParentDir => assert!(segments.pop().is_some()), + for &input in inputs { + // Not all inputs will be marked for codegen. We can eliminate those + // right away. + if !input.config.codegen { + continue; } - } - - // Navigate down the tree, creating any folder entries that don't exist - // yet. - let mut current_dir = &mut root_folder; - for (i, &segment) in segments.iter().enumerate() { - if i == segments.len() - 1 { - // We assume that the last segment of a path must be a file. - - let input_group = match current_dir.get_mut(segment) { - Some(existing) => existing, - None => { - let input_group = GroupedItem::InputGroup { - inputs_by_dpi_scale: BTreeMap::new(), - }; - current_dir.insert(segment.to_owned(), input_group); - current_dir.get_mut(segment).unwrap() + + // The extension portion of the path is not useful for code generation. + // By stripping it off, we generate the names that users expect. + let mut path_without_extension = input.path_without_dpi_scale.clone(); + path_without_extension.set_extension(""); + + // If we can't construct a relative path, there isn't a sensible name + // that we can use to refer to this input. + let relative_path = path_without_extension + .strip_prefix(&input.config.codegen_base_path) + .expect("Input base path was not a base path for input"); + + // Collapse `..` path segments so that we can map this path onto our + // tree of inputs. + let mut segments = Vec::new(); + for component in relative_path.components() { + match component { + path::Component::Prefix(_) + | path::Component::RootDir + | path::Component::Normal(_) => { + segments.push(component.as_os_str().to_str().unwrap()) } - }; - - if let GroupedItem::InputGroup { - inputs_by_dpi_scale, - } = input_group - { - inputs_by_dpi_scale.insert(input.dpi_scale, input); - } else { - unreachable!(); + path::Component::CurDir => {} + path::Component::ParentDir => assert!(segments.pop().is_some()), } - } else { - let next_entry = - current_dir - .entry(segment.to_owned()) - .or_insert_with(|| GroupedItem::Folder { - children_by_name: BTreeMap::new(), - }); - - if let GroupedItem::Folder { children_by_name } = next_entry { - current_dir = children_by_name; - } else { - unreachable!(); - } - } - } - } - - fn build_item(item: &GroupedItem<'_>) -> Option { - match item { - GroupedItem::Folder { children_by_name } => { - let entries = children_by_name - .iter() - .filter_map(|(name, child)| build_item(child).map(|item| (name.into(), item))) - .collect(); - - Some(Expression::table(entries)) } - GroupedItem::InputGroup { - inputs_by_dpi_scale, - } => { - if inputs_by_dpi_scale.len() == 1 { - // If there is exactly one input in this group, we can - // generate code knowing that there are no high DPI variants - // to choose from. - - let input = inputs_by_dpi_scale.values().next().unwrap(); - - match (&input.id, input.slice) { - (Some(id), Some(slice)) => Some(codegen_url_and_slice(id, slice)), - (Some(id), None) => Some(codegen_just_asset_url(id)), - _ => None, + + // Navigate down the tree, creating any folder entries that don't exist + // yet. + let mut current_dir = &mut root_folder; + for (i, &segment) in segments.iter().enumerate() { + if i == segments.len() - 1 { + // We assume that the last segment of a path must be a file. + + let input_group = match current_dir.get_mut(segment) { + Some(existing) => existing, + None => { + let input_group = GroupedItem::InputGroup { + inputs_by_dpi_scale: BTreeMap::new(), + }; + current_dir.insert(segment.to_owned(), input_group); + current_dir.get_mut(segment).unwrap() + } + }; + + if let GroupedItem::InputGroup { + inputs_by_dpi_scale, + } = input_group + { + inputs_by_dpi_scale.insert(input.dpi_scale, input); + } else { + unreachable!(); } } else { - // In this case, we have the same asset in multiple - // different DPI scales. We can generate code to pick - // between them at runtime. - Some(codegen_with_high_dpi_options(inputs_by_dpi_scale)) + let next_entry = + current_dir + .entry(segment.to_owned()) + .or_insert_with(|| GroupedItem::Folder { + children_by_name: BTreeMap::new(), + }); + + if let GroupedItem::Folder { children_by_name } = next_entry { + current_dir = children_by_name; + } else { + unreachable!(); + } } } } - } - let root_item = build_item(&GroupedItem::Folder { - children_by_name: root_folder, - }) - .unwrap(); - let ast = Statement::Return(root_item); - - let mut file = File::create(output_path)?; - writeln!(file, "{}", CODEGEN_HEADER)?; - write!(file, "{}", ast)?; - - Ok(()) -} - -/// Perform codegen for a group of inputs that don't have `codegen_path` -/// defined, and so generate individual files. -fn codegen_individual(inputs: &[&SyncInput]) -> io::Result<()> { - for input in inputs { - let expression = match (&input.id, input.slice) { - (Some(id), Some(slice)) => codegen_url_and_slice(id, slice), - (Some(id), None) => codegen_just_asset_url(id), - _ => continue, - }; - - let ast = Statement::Return(expression); - - let path = input.path.with_extension("lua"); - - let mut file = File::create(path)?; - writeln!(file, "{}", CODEGEN_HEADER)?; - write!(file, "{}", ast)?; + root_folder } - - Ok(()) -} - -fn codegen_url_and_slice(id: &AssetId, slice: ImageSlice) -> Expression { - let offset = slice.min(); - let size = slice.size(); - - let mut table = Table::new(); - table.add_entry("Image", id.to_string()); - table.add_entry( - "ImageRectOffset", - Expression::Raw(format!("Vector2.new({}, {})", offset.0, offset.1)), - ); - - table.add_entry( - "ImageRectSize", - Expression::Raw(format!("Vector2.new({}, {})", size.0, size.1)), - ); - - Expression::Table(table) -} - -fn codegen_just_asset_url(id: &AssetId) -> Expression { - Expression::String(id.to_string()) -} - -fn codegen_dpi_option(input: &SyncInput) -> (Expression, Block) { - let condition = Expression::Raw(format!("dpiScale >= {}", input.dpi_scale)); - - // FIXME: We should probably pull data out of SyncInput at the start of - // codegen so that we can handle invariants like this. - let id = input.id.as_ref().unwrap(); - - let value = match input.slice { - Some(slice) => codegen_url_and_slice(id, slice), - None => codegen_just_asset_url(id), - }; - - let body = Statement::Return(value); - - (condition, body.into()) -} - -fn codegen_with_high_dpi_options(inputs: &BTreeMap) -> Expression { - let args = "dpiScale".to_owned(); - - let mut options_high_to_low = inputs.values().rev().peekable(); - - let highest_dpi_option = options_high_to_low.next().unwrap(); - let (highest_cond, highest_body) = codegen_dpi_option(highest_dpi_option); - - let mut if_block = IfBlock::new(highest_cond, highest_body); - - while let Some(dpi_option) = options_high_to_low.next() { - let (cond, body) = codegen_dpi_option(dpi_option); - - if options_high_to_low.peek().is_some() { - if_block.else_if_blocks.push((cond, body)); - } else { - if_block.else_block = Some(body); - } - } - - let statements = vec![Statement::If(if_block)]; - - Expression::Function(Function::new(args, statements)) -} +} \ No newline at end of file diff --git a/src/data/config.rs b/src/data/config.rs index 123c2e4..e3da228 100644 --- a/src/data/config.rs +++ b/src/data/config.rs @@ -162,6 +162,10 @@ pub struct InputConfig { /// instances. #[serde(default)] pub packable: bool, + + /// Generate a .d.ts file alongside the codegen (for roblox-ts users) + #[serde(default)] + pub codegen_typescript_declaration: bool, } #[derive(Debug, Error)] diff --git a/src/lua/codegen.rs b/src/lua/codegen.rs new file mode 100644 index 0000000..b07c920 --- /dev/null +++ b/src/lua/codegen.rs @@ -0,0 +1,170 @@ +//! Defines how Tarmac generates Lua code for linking to assets. +//! +//! Tarmac uses a small Lua AST to build up generated code. + +use std::{ + collections::BTreeMap, + io::{self, Write}, + path::Path, +}; + +use fs_err::File; + +use crate::{ + data::ImageSlice, + data::{AssetId, SyncInput}, + lua::lua_ast::{Block, Expression, Function, IfBlock, Statement, Table}, codegen::GroupedItem, +}; + +const CODEGEN_HEADER: &str = + "-- This file was @generated by Tarmac. It is not intended for manual editing."; + +pub fn perform_codegen(output_path: Option<&Path>, inputs: &[&SyncInput]) -> io::Result<()> { + if let Some(path) = output_path { + codegen_grouped(path, inputs) + } else { + codegen_individual(inputs) + } +} + +/// Perform codegen for a group of inputs who have `codegen_path` defined. +/// +/// We'll build up a Lua file containing nested tables that match the structure +/// of the input's path with its base path stripped away. +fn codegen_grouped(output_path: &Path, inputs: &[&SyncInput]) -> io::Result<()> { + let root_folder = GroupedItem::parse_root_folder(output_path, inputs); + + fn build_item(item: &GroupedItem<'_>) -> Option { + match item { + GroupedItem::Folder { children_by_name } => { + let entries = children_by_name + .iter() + .filter_map(|(name, child)| build_item(child).map(|item| (name.into(), item))) + .collect(); + + Some(Expression::table(entries)) + } + GroupedItem::InputGroup { + inputs_by_dpi_scale, + } => { + if inputs_by_dpi_scale.len() == 1 { + // If there is exactly one input in this group, we can + // generate code knowing that there are no high DPI variants + // to choose from. + + let input = inputs_by_dpi_scale.values().next().unwrap(); + + match (&input.id, input.slice) { + (Some(id), Some(slice)) => Some(codegen_url_and_slice(id, slice)), + (Some(id), None) => Some(codegen_just_asset_url(id)), + _ => None, + } + } else { + // In this case, we have the same asset in multiple + // different DPI scales. We can generate code to pick + // between them at runtime. + Some(codegen_with_high_dpi_options(inputs_by_dpi_scale)) + } + } + } + } + + let root_item = build_item(&GroupedItem::Folder { + children_by_name: root_folder, + }) + .unwrap(); + let ast = Statement::Return(root_item); + + let mut file = File::create(output_path)?; + writeln!(file, "{}", CODEGEN_HEADER)?; + write!(file, "{}", ast)?; + + Ok(()) +} + +/// Perform codegen for a group of inputs that don't have `codegen_path` +/// defined, and so generate individual files. +fn codegen_individual(inputs: &[&SyncInput]) -> io::Result<()> { + for input in inputs { + let expression = match (&input.id, input.slice) { + (Some(id), Some(slice)) => codegen_url_and_slice(id, slice), + (Some(id), None) => codegen_just_asset_url(id), + _ => continue, + }; + + let ast = Statement::Return(expression); + + let path = input.path.with_extension("lua"); + + let mut file = File::create(path)?; + writeln!(file, "{}", CODEGEN_HEADER)?; + write!(file, "{}", ast)?; + } + + Ok(()) +} + +fn codegen_url_and_slice(id: &AssetId, slice: ImageSlice) -> Expression { + let offset = slice.min(); + let size = slice.size(); + + let mut table = Table::new(); + table.add_entry("Image", id.to_string()); + table.add_entry( + "ImageRectOffset", + Expression::Raw(format!("Vector2.new({}, {})", offset.0, offset.1)), + ); + + table.add_entry( + "ImageRectSize", + Expression::Raw(format!("Vector2.new({}, {})", size.0, size.1)), + ); + + Expression::Table(table) +} + +fn codegen_just_asset_url(id: &AssetId) -> Expression { + Expression::String(id.to_string()) +} + +fn codegen_dpi_option(input: &SyncInput) -> (Expression, Block) { + let condition = Expression::Raw(format!("dpiScale >= {}", input.dpi_scale)); + + // FIXME: We should probably pull data out of SyncInput at the start of + // codegen so that we can handle invariants like this. + let id = input.id.as_ref().unwrap(); + + let value = match input.slice { + Some(slice) => codegen_url_and_slice(id, slice), + None => codegen_just_asset_url(id), + }; + + let body = Statement::Return(value); + + (condition, body.into()) +} + +fn codegen_with_high_dpi_options(inputs: &BTreeMap) -> Expression { + let args = "dpiScale".to_owned(); + + let mut options_high_to_low = inputs.values().rev().peekable(); + + let highest_dpi_option = options_high_to_low.next().unwrap(); + let (highest_cond, highest_body) = codegen_dpi_option(highest_dpi_option); + + let mut if_block = IfBlock::new(highest_cond, highest_body); + + while let Some(dpi_option) = options_high_to_low.next() { + let (cond, body) = codegen_dpi_option(dpi_option); + + if options_high_to_low.peek().is_some() { + if_block.else_if_blocks.push((cond, body)); + } else { + if_block.else_block = Some(body); + } + } + + let statements = vec![Statement::If(if_block)]; + + Expression::Function(Function::new(args, statements)) +} diff --git a/src/lua_ast.rs b/src/lua/lua_ast.rs similarity index 100% rename from src/lua_ast.rs rename to src/lua/lua_ast.rs diff --git a/src/lua/mod.rs b/src/lua/mod.rs new file mode 100644 index 0000000..3a00196 --- /dev/null +++ b/src/lua/mod.rs @@ -0,0 +1,2 @@ +pub mod codegen; +pub mod lua_ast; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 54ae5f1..a681a20 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,16 @@ mod alpha_bleed; mod asset_name; mod auth_cookie; -mod codegen; mod commands; mod data; mod dpi_scale; mod glob; -mod lua_ast; mod options; mod roblox_web_api; mod sync_backend; -mod ts_ast; +mod lua; +mod typescript; +mod codegen; use std::{env, panic, process}; diff --git a/src/typescript/codegen.rs b/src/typescript/codegen.rs new file mode 100644 index 0000000..0d9a0fc --- /dev/null +++ b/src/typescript/codegen.rs @@ -0,0 +1,218 @@ +use std::{ + collections::BTreeMap, + io::{self, Write}, + path::Path, +}; + +use fs_err::File; + +use crate::{ + codegen::GroupedItem, + data::{AssetId, SyncInput}, + lua::lua_ast::Function, + typescript::ts_ast::Expression, +}; + +use super::ts_ast::{ + FunctionType, InterfaceDeclaration, ModifierToken, Parameter, PropertySignature, Statement, + TypeReference, +}; + +const CODEGEN_HEADER: &str = + "/** This file was @generated by Tarmac. It is not intended for manual editing. */"; +const CODEGEN_IMAGE_SLICE_INTERFACE: &str = "ImageSlice"; + +pub fn perform_codegen(output_path: Option<&Path>, inputs: &[&SyncInput]) -> io::Result<()> { + if let Some(path) = output_path { + codegen_grouped(path, inputs) + } else { + codegen_individual(inputs) + } +} + +struct Prereqs { + prereq: Vec, + statement: T, +} +impl Prereqs { + pub fn new(value: T) -> Prereqs { + Prereqs { + prereq: vec![], + statement: value, + } + } +} + +type PrereqExpression = Prereqs; + +fn codegen_with_high_dpi_options(inputs: &BTreeMap) -> Expression { + let mut options_high_to_low = inputs.values().rev().peekable(); + let highest_dpi_option = options_high_to_low.next().unwrap(); + + let expression = match (&highest_dpi_option.id, highest_dpi_option.slice) { + (.., Some(..)) => Some(Expression::Identifier(CODEGEN_IMAGE_SLICE_INTERFACE.into())), + (.., None) => Some(Expression::Identifier("string".into())), + }; + + let mut dpi_values = vec![]; + for (size, ..) in inputs { + dpi_values.push(TypeReference::num(size.clone() as i32)); + } + + Expression::FunctionType(FunctionType::new( + vec![Parameter::new( + "dpiScale".into(), + if dpi_values.len() > 0 { + TypeReference::union(dpi_values) + } else { + TypeReference::id("number".into()) + }, + )], + expression.unwrap(), + )) +} + +fn get_properties(item: &GroupedItem) -> (Vec, Vec) { + let mut prereqs: Vec = Vec::new(); + let mut fields: Vec = Vec::new(); + + match item { + GroupedItem::Folder { children_by_name } => { + for (name, child) in children_by_name { + match child { + GroupedItem::Folder { .. } => { + let (properties, inner_prereqs) = get_properties(child); + for prereq in inner_prereqs { + prereqs.push(prereq); + } + + let sanitized_name: String = + name.chars().filter(|c| c.is_alphanumeric()).collect(); + let interface_name = format!("Folder_{}", sanitized_name); + let assets_interface = + InterfaceDeclaration::new(interface_name.clone(), None, properties); + prereqs.push(Statement::InterfaceDeclaration(assets_interface)); + + fields.push(PropertySignature::new( + sanitized_name.clone(), + None, + Expression::Identifier(interface_name), + )); + } + GroupedItem::InputGroup { + inputs_by_dpi_scale, + } => { + if inputs_by_dpi_scale.len() == 1 { + let input = inputs_by_dpi_scale.values().next().unwrap(); + + let expression = match (&input.id, input.slice) { + (.., Some(..)) => Some(Expression::Identifier( + CODEGEN_IMAGE_SLICE_INTERFACE.into(), + )), + (.., None) => Some(Expression::Identifier("string".into())), + }; + + fields.push(PropertySignature::new( + name.clone(), + None, + expression.unwrap(), + )); + } else { + fields.push(PropertySignature::new( + name.clone(), + None, + codegen_with_high_dpi_options(inputs_by_dpi_scale), + )); + } + } + } + } + } + GroupedItem::InputGroup { + inputs_by_dpi_scale, + } => {} + } + + (fields, prereqs) +} + +fn get_sprite_interface() -> Statement { + Statement::InterfaceDeclaration(InterfaceDeclaration::new( + CODEGEN_IMAGE_SLICE_INTERFACE.into(), + None, + vec![ + PropertySignature::new( + "Image".into(), + Some(vec![ModifierToken::Readonly]), + Expression::Identifier("string".into()), + ), + PropertySignature::new( + "ImageRectOffset".into(), + Some(vec![ModifierToken::Readonly]), + Expression::Identifier("Vector2".into()), + ), + PropertySignature::new( + "ImageRectSize".into(), + Some(vec![ModifierToken::Readonly]), + Expression::Identifier("Vector2".into()), + ), + ], + )) +} + +/// Perform codegen for a group of inputs who have `codegen_path` defined. +/// +/// We'll build up a Lua file containing nested tables that match the structure +/// of the input's path with its base path stripped away. +fn codegen_grouped(output_path: &Path, inputs: &[&SyncInput]) -> io::Result<()> { + let root_folder = GroupedItem::parse_root_folder(output_path, inputs); + + let root = &GroupedItem::Folder { + children_by_name: root_folder, + }; + + let mut file = File::create(output_path.parent().unwrap().join("index.d.ts"))?; + writeln!(file, "{}", CODEGEN_HEADER)?; + + let (properties, prereqs) = get_properties(&root); + + write!(file, "{}", get_sprite_interface())?; + + for prereq in prereqs { + write!(file, "{}", prereq)?; + } + + let assets_interface = InterfaceDeclaration::new("Assets".into(), None, properties); + let export_assignment = Statement::export_assignment(Expression::Identifier("Assets".into())); + + write!( + file, + "{}", + Statement::InterfaceDeclaration(assets_interface) + )?; + write!(file, "{}", export_assignment)?; + + Ok(()) +} + +/// Perform codegen for a group of inputs that don't have `codegen_path` +/// defined, and so generate individual files. +fn codegen_individual(inputs: &[&SyncInput]) -> io::Result<()> { + for input in inputs { + // let expression = match (&input.id, input.slice) { + // (Some(id), Some(slice)) => codegen_url_and_slice(id, slice), + // (Some(id), None) => codegen_just_asset_url(id), + // _ => continue, + // }; + + // let ast = Statement::Return(expression); + + let path = input.path.with_extension("d.ts"); + + let mut file = File::create(path)?; + writeln!(file, "{}", CODEGEN_HEADER)?; + //write!(file, "{}", ast)?; + } + + Ok(()) +} diff --git a/src/typescript/mod.rs b/src/typescript/mod.rs new file mode 100644 index 0000000..3920368 --- /dev/null +++ b/src/typescript/mod.rs @@ -0,0 +1,2 @@ +pub mod ts_ast; +pub mod codegen; \ No newline at end of file diff --git a/src/ts_ast.rs b/src/typescript/ts_ast.rs similarity index 60% rename from src/ts_ast.rs rename to src/typescript/ts_ast.rs index 04fa41e..ee4113c 100644 --- a/src/ts_ast.rs +++ b/src/typescript/ts_ast.rs @@ -37,6 +37,14 @@ pub(crate) struct FunctionType { parameters: Vec, return_type: Box, } +impl FunctionType { + pub fn new(parameters: Vec, return_type: Expression) -> FunctionType { + FunctionType { + parameters, + return_type: Box::new(return_type), + } + } +} impl FmtTS for FunctionType { fn fmt_ts(&self, output: &mut TSStream) -> fmt::Result { write!(output, "(")?; @@ -56,6 +64,15 @@ pub(crate) struct PropertySignature { modifiers: Option>, expression: Expression, } +impl PropertySignature { + pub fn new(name: String, modifiers: Option>, expression: Expression) -> PropertySignature { + PropertySignature { + name, + modifiers, + expression, + } + } +} impl FmtTS for PropertySignature { fn fmt_ts(&self, output: &mut TSStream) -> fmt::Result { if let Some(modifiers) = &self.modifiers { @@ -63,30 +80,83 @@ impl FmtTS for PropertySignature { write!(output, "{} ", modifier.as_str())?; } } - writeln!(output, "{}: {};", self.name, self.expression) + + if self.name.chars().all(char::is_alphanumeric) && self.name.chars().nth(0).unwrap().is_alphabetic() { + writeln!(output, "{}: {};", self.name, self.expression) + } else { + writeln!(output, "[\"{}\"]: {};", self.name, self.expression) + } + } +} + +pub(crate) enum TypeReference { + Expression(Expression), + Union(Vec), +} +impl FmtTS for TypeReference { + fn fmt_ts(&self, output: &mut TSStream) -> fmt::Result { + match self { + TypeReference::Expression(inner) => { + write!(output, "{}", inner)?; + }, + TypeReference::Union(types) => { + let count = types.len(); + let mut iter = 0; + + for parameter in types { + iter += 1; + + parameter.fmt_ts(output)?; + + if iter < count { + write!(output, " | ")?; + } + } + }, + } + + Ok(()) + } +} +impl TypeReference { + pub fn id(id: String) -> TypeReference { + TypeReference::Expression(Expression::Identifier(id)) + } + + pub fn num(value: i32) -> TypeReference { + TypeReference::Expression(Expression::NumericLiteral(value)) + } + + pub fn union(inner: Vec) -> TypeReference { + TypeReference::Union(inner) } } pub(crate) struct Parameter { name: String, - // modifiers: Option>, - expression: Expression, + param_type: TypeReference, +} + +impl Parameter { + pub fn new(name: String, param_type: TypeReference) -> Parameter { + Parameter { + name, + param_type, + } + } } + impl FmtTS for Vec { fn fmt_ts(&self, output: &mut TSStream) -> fmt::Result { let count = self.len(); let mut iter = 0; for parameter in self { - // if let Some(modifiers) = ¶meter.modifiers { - // for modifier in modifiers { - // write!(output, "{} ", modifier.as_str())?; - // } - // } - iter += 1; - write!(output, "{}: {}", parameter.name, parameter.expression)?; + write!(output, "{}: ", parameter.name)?; + parameter.param_type.fmt_ts(output)?; + if iter < count { write!(output, ", ")?; } @@ -147,6 +217,19 @@ pub(crate) struct InterfaceDeclaration { modifiers: Option>, members: Vec, } +impl InterfaceDeclaration { + pub fn new( + name: String, + modifiers: Option>, + members: Vec, + ) -> InterfaceDeclaration { + InterfaceDeclaration { + name, + modifiers, + members, + } + } +} impl FmtTS for InterfaceDeclaration { fn fmt_ts(&self, output: &mut TSStream) -> fmt::Result { if let Some(mod_tokens) = &self.modifiers { @@ -170,6 +253,7 @@ impl FmtTS for InterfaceDeclaration { pub(crate) enum Expression { Identifier(String), StringLiteral(String), + NumericLiteral(i32), TypeLiteral(Vec), FunctionType(FunctionType), } @@ -182,6 +266,9 @@ impl FmtTS for Expression { Self::StringLiteral(literal) => { write!(output, "\"{}\"", literal) } + Self::NumericLiteral(literal) => { + write!(output, "{}", literal) + } Self::TypeLiteral(literal) => { writeln!(output, "{{")?; @@ -206,6 +293,12 @@ pub(crate) enum Statement { ExportAssignment(ExportAssignment), } +impl Statement { + pub fn export_assignment(expression: Expression) -> Statement { + Statement::ExportAssignment(ExportAssignment { expression }) + } +} + impl FmtTS for Statement { fn fmt_ts(&self, output: &mut TSStream) -> fmt::Result { match self { @@ -274,100 +367,3 @@ impl fmt::Write for TSStream<'_> { Ok(()) } } - -mod test { - use super::*; - - #[test] - fn test() { - println!( - "{}", - Statement::InterfaceDeclaration(InterfaceDeclaration { - name: "Sprite".into(), - modifiers: None, - members: vec![ - PropertySignature { - name: "Image".into(), - modifiers: Some(vec![ModifierToken::Readonly]), - expression: Expression::Identifier("string".into()) - }, - PropertySignature { - name: "ImageRectOffset".into(), - modifiers: Some(vec![ModifierToken::Readonly]), - expression: Expression::Identifier("Vector2".into()) - }, - PropertySignature { - name: "ImageRectSize".into(), - modifiers: Some(vec![ModifierToken::Readonly]), - expression: Expression::Identifier("Vector2".into()) - } - ] - }) - ); - - println!( - "{}", - Statement::InterfaceDeclaration(InterfaceDeclaration { - name: "Assets".into(), - modifiers: Some(vec![ModifierToken::Declare]), - members: vec![ - PropertySignature { - name: "AssetName1".into(), - modifiers: Some(vec![ModifierToken::Readonly]), - expression: Expression::Identifier("Sprite".into()) - }, - PropertySignature { - name: "AssetName2".into(), - modifiers: Some(vec![ModifierToken::Readonly]), - expression: Expression::TypeLiteral(vec![ - PropertySignature { - name: "Image".into(), - modifiers: Some(vec![ModifierToken::Readonly]), - expression: Expression::Identifier("string".into()) - }, - PropertySignature { - name: "ImageRectOffset".into(), - modifiers: Some(vec![ModifierToken::Readonly]), - expression: Expression::Identifier("Vector2".into()) - }, - PropertySignature { - name: "ImageRectSize".into(), - modifiers: Some(vec![ModifierToken::Readonly]), - expression: Expression::Identifier("Vector2".into()) - } - ]) - }, - PropertySignature { - name: "FunctionTypedAsset".into(), - modifiers: Some(vec![ModifierToken::Readonly]), - expression: Expression::FunctionType(FunctionType { - parameters: vec![Parameter { - name: "dpiScale".into(), - expression: Expression::Identifier("number".into()) - }], - return_type: Box::new(Expression::Identifier("test".into())) - }) - } - ] - }) - ); - - println!( - "{}", - Statement::VariableDeclaration(VariableDeclaration { - name: "Assets".into(), - kind: VariableKind::Const, - type_expression: Some(Expression::Identifier("Assets".into())), - modifiers: Some(vec![ModifierToken::Declare]), - expression: Some(Expression::Identifier("Assets".into())) - }) - ); - - println!( - "{}", - Statement::ExportAssignment(ExportAssignment { - expression: Expression::Identifier("Assets".into()) - }) - ); - } -}