use pasta_dsl::parser::{ActorScope, FileItem, GlobalSceneScope, KeyWords, PastaFile};
use pasta_core::registry::SceneRegistry;
use super::code_gen::LuaCodeGenerator;
use super::code_gen::source_map::SourceMapSink;
use super::config::TranspilerConfig;
use super::context::TranspileContext;
use super::error::TranspileError;
use super::normalize::{LineShift, normalize_output_with_shift};
use std::io::Write;
pub struct LuaTranspiler {
config: TranspilerConfig,
}
impl Default for LuaTranspiler {
fn default() -> Self {
Self::new(TranspilerConfig::default())
}
}
impl LuaTranspiler {
pub fn new(config: TranspilerConfig) -> Self {
Self { config }
}
pub fn with_defaults() -> Self {
Self::default()
}
pub fn transpile<W: Write>(
&self,
file: &PastaFile,
writer: &mut W,
) -> Result<TranspileContext, TranspileError> {
self.transpile_with_sink(file, writer, None)
}
pub fn transpile_with_sink<W: Write>(
&self,
file: &PastaFile,
writer: &mut W,
sink: Option<&mut dyn SourceMapSink>,
) -> Result<TranspileContext, TranspileError> {
let (context, _shift) = self.transpile_with_source_map(file, writer, sink)?;
Ok(context)
}
pub fn transpile_with_source_map<W: Write>(
&self,
file: &PastaFile,
writer: &mut W,
sink: Option<&mut dyn SourceMapSink>,
) -> Result<(TranspileContext, LineShift), TranspileError> {
let mut context = TranspileContext::new();
let mut intermediate_buffer: Vec<u8> = Vec::new();
let mut codegen =
LuaCodeGenerator::with_line_ending(&mut intermediate_buffer, self.config.line_ending);
if let Some(sink) = sink {
codegen.set_source_map(sink);
}
codegen.write_header()?;
for item in &file.items {
match item {
FileItem::FileAttr(attr) => context.accumulate_file_attr(attr),
FileItem::GlobalWord(word) => {
Self::process_global_word(&mut context, &mut codegen, word)?;
}
FileItem::GlobalSceneScope(scene) => {
Self::process_global_scene(&mut context, &mut codegen, scene)?;
}
FileItem::ActorScope(actor) => {
Self::process_actor(&mut context, &mut codegen, actor)?;
}
}
}
let raw_output = String::from_utf8(intermediate_buffer)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
let (normalized_output, shift) = normalize_output_with_shift(&raw_output);
writer.write_all(normalized_output.as_bytes())?;
Ok((context, shift))
}
fn process_global_word<W: Write>(
context: &mut TranspileContext,
codegen: &mut LuaCodeGenerator<W>,
word: &KeyWords,
) -> Result<(), TranspileError> {
for name in &word.names {
context.word_registry.register_global(name, word.words.clone());
}
codegen.generate_global_word(word)?;
Ok(())
}
fn process_global_scene<W: Write>(
context: &mut TranspileContext,
codegen: &mut LuaCodeGenerator<W>,
scene: &GlobalSceneScope,
) -> Result<(), TranspileError> {
let (_, counter) = context.register_global_scene(scene);
let module_name = format!("{}{}", SceneRegistry::sanitize_name(&scene.name), counter);
for kw in &scene.words {
for name in &kw.names {
context
.word_registry
.register_local(&module_name, name, kw.words.clone());
}
}
let merged_attrs = context.merge_attrs(&scene.attrs);
codegen.generate_global_scene(scene, counter, context, &merged_attrs)?;
for (local_idx, local_scene) in scene.local_scenes.iter().enumerate() {
if local_scene.name.is_some() {
context.register_local_scene(
local_scene,
&scene.name,
counter,
local_idx + 1,
);
}
}
Ok(())
}
fn process_actor<W: Write>(
context: &mut TranspileContext,
codegen: &mut LuaCodeGenerator<W>,
actor: &ActorScope,
) -> Result<(), TranspileError> {
for word_def in &actor.words {
for name in &word_def.names {
context
.word_registry
.register_actor(&actor.name, name, word_def.words.clone());
}
}
codegen.generate_actor(actor)?;
Ok(())
}
pub fn config(&self) -> &TranspilerConfig {
&self.config
}
}
#[cfg(test)]
mod tests {
use super::*;
use pasta_dsl::parser::{ActorScope, GlobalSceneScope, KeyWords, LocalSceneScope, Span};
use std::path::PathBuf;
fn create_simple_actor(name: &str) -> ActorScope {
ActorScope {
name: name.to_string(),
attrs: vec![],
words: vec![KeyWords {
names: vec!["通常".to_string()],
words: vec!["\\s[0]".to_string()],
span: Span::default(),
}],
var_sets: vec![],
code_blocks: vec![],
span: Span::default(),
}
}
fn create_simple_scene(name: &str) -> GlobalSceneScope {
GlobalSceneScope {
name: name.to_string(),
is_continuation: false,
attrs: vec![],
words: vec![],
actors: vec![],
code_blocks: vec![],
local_scenes: vec![LocalSceneScope::start()],
span: Span::default(),
}
}
fn create_scene_with_words(
name: &str,
word_name: &str,
word_values: Vec<&str>,
) -> GlobalSceneScope {
GlobalSceneScope {
name: name.to_string(),
is_continuation: false,
attrs: vec![],
words: vec![KeyWords {
names: vec![word_name.to_string()],
words: word_values.iter().map(|s| s.to_string()).collect(),
span: Span::default(),
}],
actors: vec![],
code_blocks: vec![],
local_scenes: vec![LocalSceneScope::start()],
span: Span::default(),
}
}
fn create_scene_with_local(name: &str, local_name: &str) -> GlobalSceneScope {
GlobalSceneScope {
name: name.to_string(),
is_continuation: false,
attrs: vec![],
words: vec![],
actors: vec![],
code_blocks: vec![],
local_scenes: vec![
LocalSceneScope::start(),
LocalSceneScope::named(local_name.to_string()),
],
span: Span::default(),
}
}
fn create_pasta_file(actors: Vec<ActorScope>, scenes: Vec<GlobalSceneScope>) -> PastaFile {
let mut items: Vec<FileItem> = Vec::new();
for actor in actors {
items.push(FileItem::ActorScope(actor));
}
for scene in scenes {
items.push(FileItem::GlobalSceneScope(scene));
}
PastaFile {
path: PathBuf::from("test.pasta"),
items,
span: Span::default(),
}
}
#[test]
fn test_transpiler_default() {
let transpiler = LuaTranspiler::default();
assert!(transpiler.config().comment_mode);
}
#[test]
fn test_transpile_empty() {
let transpiler = LuaTranspiler::default();
let mut output = Vec::new();
let file = create_pasta_file(vec![], vec![]);
let result = transpiler.transpile(&file, &mut output);
assert!(result.is_ok());
let lua_code = String::from_utf8(output).unwrap();
assert!(lua_code.contains("local PASTA = require \"pasta\""));
}
#[test]
fn test_transpile_actor() {
let transpiler = LuaTranspiler::default();
let actors = vec![create_simple_actor("さくら")];
let file = create_pasta_file(actors, vec![]);
let mut output = Vec::new();
let result = transpiler.transpile(&file, &mut output);
assert!(result.is_ok());
let lua_code = String::from_utf8(output).unwrap();
assert!(lua_code.contains("PASTA.create_actor(\"さくら\")"));
assert!(lua_code.contains("ACTOR:create_word(\"通常\"):entry([=[\\s[0]]=])"));
}
#[test]
fn test_transpile_scene() {
let transpiler = LuaTranspiler::default();
let scenes = vec![create_simple_scene("メイン")];
let file = create_pasta_file(vec![], scenes);
let mut output = Vec::new();
let result = transpiler.transpile(&file, &mut output);
assert!(result.is_ok());
let lua_code = String::from_utf8(output).unwrap();
assert!(lua_code.contains("PASTA.create_scene(\"メイン\")"));
assert!(lua_code.contains("function SCENE.__start__(act, ...)"));
}
#[test]
fn test_transpile_multiple_scenes() {
let transpiler = LuaTranspiler::default();
let scenes = vec![
create_simple_scene("メイン"),
create_simple_scene("会話分岐"),
];
let file = create_pasta_file(vec![], scenes);
let mut output = Vec::new();
let result = transpiler.transpile(&file, &mut output);
assert!(result.is_ok());
let lua_code = String::from_utf8(output).unwrap();
assert!(lua_code.contains("PASTA.create_scene(\"メイン\")"));
assert!(lua_code.contains("PASTA.create_scene(\"会話分岐\")"));
}
#[test]
fn test_transpile_registers_global_scene() {
let transpiler = LuaTranspiler::default();
let scenes = vec![create_simple_scene("メイン")];
let file = create_pasta_file(vec![], scenes);
let mut output = Vec::new();
let context = transpiler.transpile(&file, &mut output).unwrap();
let registered_scenes = context.scene_registry.all_scenes();
assert_eq!(registered_scenes.len(), 1);
assert_eq!(registered_scenes[0].name, "メイン");
assert_eq!(registered_scenes[0].id, 1);
}
#[test]
fn test_transpile_registers_local_scene() {
let transpiler = LuaTranspiler::default();
let scenes = vec![create_scene_with_local("メイン", "自己紹介")];
let file = create_pasta_file(vec![], scenes);
let mut output = Vec::new();
let context = transpiler.transpile(&file, &mut output).unwrap();
let registered_scenes = context.scene_registry.all_scenes();
assert_eq!(registered_scenes.len(), 2); assert_eq!(registered_scenes[1].name, "自己紹介");
}
#[test]
fn test_transpile_registers_local_words() {
let transpiler = LuaTranspiler::default();
let scenes = vec![create_scene_with_words(
"メイン",
"場所",
vec!["東京", "大阪"],
)];
let file = create_pasta_file(vec![], scenes);
let mut output = Vec::new();
let context = transpiler.transpile(&file, &mut output).unwrap();
let entries = context.word_registry.all_entries();
assert_eq!(entries.len(), 1);
assert!(entries[0].key.contains("場所"));
}
#[test]
fn test_transpile_with_global_words() {
let transpiler = LuaTranspiler::default();
let global_words = KeyWords {
names: vec!["挨拶".to_string()],
words: vec!["こんにちは".to_string(), "やあ".to_string()],
span: Span::default(),
};
let scenes = vec![create_simple_scene("メイン")];
let file = PastaFile {
path: PathBuf::from("test.pasta"),
items: vec![
FileItem::GlobalWord(global_words),
FileItem::GlobalSceneScope(scenes.into_iter().next().unwrap()),
],
span: Span::default(),
};
let mut output = Vec::new();
let context = transpiler.transpile(&file, &mut output).unwrap();
let entries = context.word_registry.all_entries();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].key, "挨拶");
}
#[test]
fn test_transpile_registers_actor_words() {
let transpiler = LuaTranspiler::default();
let actor = ActorScope {
name: "さくら".to_string(),
attrs: vec![],
words: vec![
KeyWords {
names: vec!["通常".to_string()],
words: vec!["\\s[0]".to_string(), "\\s[1]".to_string()],
span: Span::default(),
},
KeyWords {
names: vec!["照れ".to_string()],
words: vec!["\\s[2]".to_string()],
span: Span::default(),
},
],
var_sets: vec![],
code_blocks: vec![],
span: Span::default(),
};
let file = create_pasta_file(vec![actor], vec![]);
let mut output = Vec::new();
let context = transpiler.transpile(&file, &mut output).unwrap();
let entries = context.word_registry.all_entries();
assert_eq!(entries.len(), 2, "Expected 2 actor word entries");
assert_eq!(entries[0].key, ":__actor_さくら__:通常");
assert_eq!(entries[1].key, ":__actor_さくら__:照れ");
assert_eq!(entries[0].values, vec!["\\s[0]", "\\s[1]"]);
assert_eq!(entries[1].values, vec!["\\s[2]"]);
}
#[test]
fn test_transpile_multi_key_global_word_registration() {
let transpiler = LuaTranspiler::default();
let global_words = KeyWords {
names: vec!["女性".to_string(), "水の妖精".to_string()],
words: vec!["水無灯里".to_string(), "アリス・キャロル".to_string()],
span: Span::default(),
};
let file = PastaFile {
path: PathBuf::from("test.pasta"),
items: vec![FileItem::GlobalWord(global_words)],
span: Span::default(),
};
let mut output = Vec::new();
let context = transpiler.transpile(&file, &mut output).unwrap();
let entries = context.word_registry.all_entries();
assert_eq!(entries.len(), 2, "複数キーでそれぞれ登録されること");
assert_eq!(entries[0].key, "女性");
assert_eq!(entries[1].key, "水の妖精");
assert_eq!(entries[0].values, vec!["水無灯里", "アリス・キャロル"]);
assert_eq!(entries[1].values, vec!["水無灯里", "アリス・キャロル"]);
}
#[test]
fn test_transpile_multi_key_actor_word_registration() {
let transpiler = LuaTranspiler::default();
let actor = ActorScope {
name: "さくら".to_string(),
attrs: vec![],
words: vec![KeyWords {
names: vec!["通常".to_string(), "普通".to_string()],
words: vec!["\\s[0]".to_string()],
span: Span::default(),
}],
var_sets: vec![],
code_blocks: vec![],
span: Span::default(),
};
let file = create_pasta_file(vec![actor], vec![]);
let mut output = Vec::new();
let context = transpiler.transpile(&file, &mut output).unwrap();
let entries = context.word_registry.all_entries();
assert_eq!(entries.len(), 2, "複数キーでそれぞれ登録されること");
assert_eq!(entries[0].key, ":__actor_さくら__:通常");
assert_eq!(entries[1].key, ":__actor_さくら__:普通");
}
#[test]
fn test_transpile_multi_key_global_word_lua_output() {
let transpiler = LuaTranspiler::default();
let global_words = KeyWords {
names: vec!["女性".to_string(), "水の妖精".to_string()],
words: vec!["水無灯里".to_string(), "アリス・キャロル".to_string()],
span: Span::default(),
};
let file = PastaFile {
path: PathBuf::from("test.pasta"),
items: vec![FileItem::GlobalWord(global_words)],
span: Span::default(),
};
let mut output = Vec::new();
transpiler.transpile(&file, &mut output).unwrap();
let lua_code = String::from_utf8(output).unwrap();
assert!(
lua_code.contains("PASTA.create_word(\"女性\"):entry(\"水無灯里\", \"アリス・キャロル\")"),
"女性キーの create_word が出力されること: {lua_code}"
);
assert!(
lua_code.contains("PASTA.create_word(\"水の妖精\"):entry(\"水無灯里\", \"アリス・キャロル\")"),
"水の妖精キーの create_word が出力されること: {lua_code}"
);
}
#[test]
fn test_transpile_multi_key_actor_word_lua_output() {
let transpiler = LuaTranspiler::default();
let actor = ActorScope {
name: "さくら".to_string(),
attrs: vec![],
words: vec![KeyWords {
names: vec!["通常".to_string(), "普通".to_string()],
words: vec!["\\s[0]".to_string()],
span: Span::default(),
}],
var_sets: vec![],
code_blocks: vec![],
span: Span::default(),
};
let file = create_pasta_file(vec![actor], vec![]);
let mut output = Vec::new();
transpiler.transpile(&file, &mut output).unwrap();
let lua_code = String::from_utf8(output).unwrap();
assert!(
lua_code.contains("ACTOR:create_word(\"通常\"):entry([=[\\s[0]]=])"),
"通常キーの create_word: {lua_code}"
);
assert!(
lua_code.contains("ACTOR:create_word(\"普通\"):entry([=[\\s[0]]=])"),
"普通キーの create_word: {lua_code}"
);
}
#[test]
fn test_with_defaults_matches_default() {
let transpiler = LuaTranspiler::with_defaults();
let default = TranspilerConfig::default();
assert_eq!(transpiler.config().comment_mode, default.comment_mode);
assert_eq!(transpiler.config().line_ending, default.line_ending);
}
#[test]
fn test_transpile_accumulates_file_attrs() {
use pasta_dsl::parser::{Attr, AttrValue};
let transpiler = LuaTranspiler::default();
let file = PastaFile {
path: PathBuf::from("test.pasta"),
items: vec![
FileItem::FileAttr(Attr {
key: "author".to_string(),
value: AttrValue::AttrString("Alice".to_string()),
span: Span::default(),
}),
FileItem::FileAttr(Attr {
key: "author".to_string(),
value: AttrValue::AttrString("Bob".to_string()),
span: Span::default(),
}),
FileItem::GlobalSceneScope(create_simple_scene("メイン")),
],
span: Span::default(),
};
let mut output = Vec::new();
let context = transpiler.transpile(&file, &mut output).unwrap();
let attrs = context.file_attrs();
assert_eq!(attrs.len(), 1);
assert_eq!(
attrs.get("author"),
Some(&AttrValue::AttrString("Bob".to_string())),
"later FileAttr must shadow the earlier one"
);
}
struct FailingWriter;
impl Write for FailingWriter {
fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
Err(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"simulated write failure",
))
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
#[test]
fn test_transpile_writer_failure_yields_io_error() {
let transpiler = LuaTranspiler::default();
let file = create_pasta_file(vec![], vec![create_simple_scene("メイン")]);
match transpiler.transpile(&file, &mut FailingWriter) {
Err(TranspileError::IoError(e)) => {
assert_eq!(e.kind(), std::io::ErrorKind::BrokenPipe);
}
Err(other) => panic!("expected IoError, got: {:?}", other),
Ok(_) => panic!("expected IoError, but transpile succeeded"),
}
}
#[test]
fn test_transpile_output_is_lf_only_even_with_crlf_config() {
use crate::config::LineEnding;
let config = TranspilerConfig::new().with_line_ending(LineEnding::CrLf);
let transpiler = LuaTranspiler::new(config);
let file = create_pasta_file(vec![], vec![create_simple_scene("メイン")]);
let mut output = Vec::new();
transpiler.transpile(&file, &mut output).unwrap();
let lua_code = String::from_utf8(output).unwrap();
assert!(
!lua_code.contains('\r'),
"normalized output must not contain CR"
);
assert!(lua_code.ends_with('\n'));
assert!(!lua_code.ends_with("\n\n"), "exactly one trailing newline");
}
#[test]
fn test_transpile_single_key_backward_compat_lua_output() {
let transpiler = LuaTranspiler::default();
let global_words = KeyWords {
names: vec!["挨拶".to_string()],
words: vec!["こんにちは".to_string()],
span: Span::default(),
};
let file = PastaFile {
path: PathBuf::from("test.pasta"),
items: vec![FileItem::GlobalWord(global_words)],
span: Span::default(),
};
let mut output = Vec::new();
transpiler.transpile(&file, &mut output).unwrap();
let lua_code = String::from_utf8(output).unwrap();
assert!(
lua_code.contains("PASTA.create_word(\"挨拶\"):entry(\"こんにちは\")"),
"単一キーの出力: {lua_code}"
);
}
}