Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tarmac-Styled Code Generation Support #33

Merged
merged 15 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ asset_dir = "test/"
write_dir = "output/"
typescript = true
luau = true
tarmac = true

[creator]
type = "user"
Expand All @@ -60,6 +61,8 @@ id = 583095803
- Generate a Typescript definition file.
- `luau`: boolean (optional)
- Use the `luau` file extension.
- `tarmac`: boolean (optional)
- Use tarmac-styled code generation.
- `output_name`: string (optional)
- The name for the generated files. Defaults to `assets`.
- `existing`: map<string, ExistingAsset> (optional)
Expand Down
234 changes: 234 additions & 0 deletions src/ast.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
use std::fmt::{self, Write};

macro_rules! proxy_display {
( $target: ty ) => {
impl fmt::Display for $target {
fn fmt(&self, output: &mut fmt::Formatter) -> fmt::Result {
let mut stream = ASTStream::new(output, &self.1);
ASTFormat::fmt_ast(self, &mut stream)
}
}
};
}

trait ASTFormat {
fn fmt_ast(&self, output: &mut ASTStream) -> fmt::Result;
fn fmt_key(&self, output: &mut ASTStream<'_, '_>) -> fmt::Result {
write!(output, "[")?;
self.fmt_ast(output)?;
write!(output, "]")
}
}

#[derive(Debug)]
pub(crate) enum ASTTarget {
Lua,
Typescript { output_dir: String },
}

pub(crate) struct ASTStream<'a, 'b> {
number_of_spaces: usize,
indents: usize,
is_start_of_line: bool,
writer: &'a mut (dyn Write),
target: &'b ASTTarget,
}

impl<'a, 'b> ASTStream<'a, 'b> {
pub fn new(writer: &'a mut (dyn fmt::Write + 'a), target: &'b ASTTarget) -> Self {
Self {
number_of_spaces: 4,
indents: 0,
is_start_of_line: true,
writer,
target,
}
}

fn indent(&mut self) {
self.indents += 1
}

fn unindent(&mut self) {
if self.indents > 0 {
self.indents -= 1
}
}

fn begin_line(&mut self) -> fmt::Result {
self.is_start_of_line = true;
self.writer.write_char('\n')
}
}

impl Write for ASTStream<'_, '_> {
fn write_str(&mut self, value: &str) -> fmt::Result {
let mut is_first_line = true;

for line in value.split('\n') {
if is_first_line {
is_first_line = false;
} else {
self.begin_line()?;
}

if !line.is_empty() {
if self.is_start_of_line {
self.is_start_of_line = false;
self.writer.write_str(&format!(
"{: >1$}",
"",
self.number_of_spaces * self.indents
))?;
}

self.writer.write_str(line)?;
}
}

Ok(())
}
}

proxy_display!(ReturnStatement);

#[derive(Debug)]
pub(crate) struct ReturnStatement(pub Expression, pub ASTTarget);

impl ASTFormat for ReturnStatement {
fn fmt_ast(&self, output: &mut ASTStream) -> fmt::Result {
match output.target {
ASTTarget::Lua => {
write!(output, "return ")
}
ASTTarget::Typescript { output_dir } => {
write!(output, "declare const {output_dir}: ")
}
}
.expect("Failed to write to output");
let result = self.0.fmt_ast(output);
if let ASTTarget::Typescript { output_dir } = output.target {
write!(output, "\nexport = {output_dir}")?
}
result
}
}

#[derive(Debug)]
pub(crate) enum Expression {
String(String),
Table(Table),
}

impl Expression {
pub fn table(expressions: Vec<(Expression, Expression)>) -> Self {
Self::Table(Table { expressions })
}
}

impl ASTFormat for Expression {
fn fmt_ast(&self, output: &mut ASTStream) -> fmt::Result {
match self {
Self::Table(val) => val.fmt_ast(output),
Self::String(val) => val.fmt_ast(output),
}
}

fn fmt_key(&self, output: &mut ASTStream<'_, '_>) -> fmt::Result {
match self {
Self::Table(val) => val.fmt_key(output),
Self::String(val) => val.fmt_key(output),
}
}
}

#[derive(Debug)]
pub(crate) struct Table {
pub expressions: Vec<(Expression, Expression)>,
}

impl ASTFormat for Table {
fn fmt_ast(&self, output: &mut ASTStream<'_, '_>) -> fmt::Result {
let assignment = match output.target {
ASTTarget::Lua => " = ",
ASTTarget::Typescript { .. } => ": ",
};

writeln!(output, "{{")?;
output.indent();

for (key, value) in &self.expressions {
key.fmt_key(output)?;
write!(output, "{assignment}")?;
value.fmt_ast(output)?;
writeln!(output, ",")?;
}

output.unindent();
write!(output, "}}")
}
}

impl ASTFormat for String {
fn fmt_ast(&self, output: &mut ASTStream) -> fmt::Result {
write!(output, "\"{}\"", self)
}

fn fmt_key(&self, output: &mut ASTStream<'_, '_>) -> fmt::Result {
if is_valid_identifier(self) {
write!(output, "{}", self)
} else {
match output.target {
ASTTarget::Lua => write!(output, "[\"{}\"]", self),
ASTTarget::Typescript { .. } => write!(output, "\"{}\"", self),
}
}
}
}
bibi-reden marked this conversation as resolved.
Show resolved Hide resolved

impl From<String> for Expression {
fn from(value: String) -> Self {
Self::String(value)
}
}

impl From<&'_ String> for Expression {
fn from(value: &String) -> Self {
Self::String(value.clone())
}
}

impl From<&'_ str> for Expression {
fn from(value: &str) -> Self {
Self::String(value.to_owned())
}
}

impl From<Table> for Expression {
fn from(value: Table) -> Self {
Self::Table(value)
}
}

fn is_valid_ident_char_start(value: char) -> bool {
value.is_ascii_alphabetic() || value == '_'
}

fn is_valid_ident_char(value: char) -> bool {
value.is_ascii_alphanumeric() || value == '_'
}

fn is_valid_identifier(value: &str) -> bool {
let mut chars = value.chars();

match chars.next() {
Some(first) => {
if !is_valid_ident_char_start(first) {
return false;
}
}
None => return false,
}

chars.all(is_valid_ident_char)
}
23 changes: 21 additions & 2 deletions src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ pub fn generate_ts(
mod tests {
use std::collections::BTreeMap;

use crate::{FileEntry, LockFile};
use crate::{tarmac, FileEntry, LockFile};

fn test_lockfile() -> LockFile {
let mut entries = BTreeMap::new();
Expand All @@ -70,7 +70,6 @@ mod tests {
hash: "b".to_string(),
},
);

LockFile { entries }
}

Expand All @@ -89,4 +88,24 @@ mod tests {
let ts = super::generate_ts(&lockfile, "assets", "assets").unwrap();
assert_eq!(ts, "declare const assets: {\n\t\"/bar/baz.png\": string,\n\t\"/foo.png\": string\n}\nexport = assets");
}

#[test]
fn generate_lua_tarmac() {
let lockfile = test_lockfile();

let lua = tarmac::generate_lua(&lockfile, "assets").unwrap();
assert_eq!(
lua,
"return {\n bar = {\n [\"baz.png\"] = \"rbxassetid://2\",\n },\n [\"foo.png\"] = \"rbxassetid://1\",\n}");
}

#[test]
fn generate_ts_tarmac() {
let lockfile = test_lockfile();

let ts = tarmac::generate_ts(&lockfile, "assets", "assets").unwrap();
assert_eq!(
ts,
"declare const assets: {\n bar: {\n \"baz.png\": \"rbxassetid://2\",\n },\n \"foo.png\": \"rbxassetid://1\",\n}\nexport = assets");
}
}
1 change: 1 addition & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ pub struct Config {
pub output_name: Option<String>,
pub typescript: Option<bool>,
pub luau: Option<bool>,
pub tarmac: Option<bool>,

pub existing: Option<HashMap<String, ExistingAsset>>,
}
14 changes: 12 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ use upload::upload_asset;
use crate::config::Config;

pub mod args;
mod ast;
mod codegen;
pub mod config;
pub mod lockfile;
pub mod state;
mod svg;
mod tarmac;
mod upload;

fn fix_path(path: &str) -> String {
Expand Down Expand Up @@ -178,15 +180,23 @@ async fn main() -> anyhow::Result<()> {
}));

let lua_filename = format!("{}.{}", state.output_name, state.lua_extension);
let lua_output = generate_lua(&state.new_lockfile, asset_dir_str);
let lua_output = if state.tarmac {
tarmac::generate_lua
} else {
generate_lua
}(&state.new_lockfile, asset_dir_str);

write(Path::new(&state.write_dir).join(lua_filename), lua_output?)
.await
.context("Failed to write output Lua file")?;

if state.typescript {
let ts_filename = format!("{}.d.ts", state.output_name);
let ts_output = generate_ts(
let ts_output = if state.tarmac {
tarmac::generate_ts
} else {
generate_ts
}(
&state.new_lockfile,
asset_dir_str,
state.output_name.as_str(),
Expand Down
4 changes: 4 additions & 0 deletions src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ pub struct State {
pub output_name: String,
pub lua_extension: String,

pub tarmac: bool,

pub font_db: Database,

pub existing_lockfile: LockFile,
Expand Down Expand Up @@ -72,6 +74,7 @@ impl State {
.to_string();

let typescript = config.typescript.unwrap_or(false);
let tarmac = config.tarmac.unwrap_or(false);

let lua_extension = String::from(if config.luau.unwrap_or(false) {
"luau"
Expand Down Expand Up @@ -101,6 +104,7 @@ impl State {
typescript,
output_name,
lua_extension,
tarmac,
font_db,
existing_lockfile,
new_lockfile,
Expand Down
Loading
Loading