summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock24
-rw-r--r--Cargo.toml14
-rw-r--r--gen/Cargo.toml7
-rw-r--r--gen/macros/Cargo.toml13
-rw-r--r--gen/macros/src/lib.rs572
-rw-r--r--gen/src/lib.rs1
-rw-r--r--src/lib.rs576
-rw-r--r--src/main.rs4
8 files changed, 579 insertions, 632 deletions
diff --git a/Cargo.lock b/Cargo.lock
index a8f9183..1d88bed 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -84,9 +84,10 @@ checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
name = "markdialog"
version = "0.1.0"
dependencies = [
- "markdialog_converter",
- "markdialog_player",
- "res_gen",
+ "proc-macro2",
+ "quote",
+ "sha2",
+ "syn",
]
[[package]]
@@ -158,23 +159,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c"
[[package]]
-name = "res_gen"
-version = "0.1.0"
-dependencies = [
- "res_gen_macros",
-]
-
-[[package]]
-name = "res_gen_macros"
-version = "0.1.0"
-dependencies = [
- "proc-macro2",
- "quote",
- "sha2",
- "syn",
-]
-
-[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 9d01bec..64a5913 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,20 +7,22 @@ description = "Write your story with Markdown!"
repository = "https://github.com/CatilGrass/MarkDialog"
license-file = "LICENSE-MIT"
readme = "README.md"
-keywords = ["dialog", "markdown", "avg_game"]
+keywords = ["dialog", "markdown"]
categories = ["game-development"]
publish = true
[workspace]
package.version = "0.1.0"
members = [
- "gen", # Generate Resource
- "gen/macros",
"player", # Dialog Player
"converter" # Markdown Converter
]
+[lib]
+proc-macro = true
+
[dependencies]
-res_gen = { path = "gen" }
-markdialog_player = { path = "player" }
-markdialog_converter = { path = "converter" }
+syn = { version = "2.0", features = ["full", "visit-mut"] }
+quote = "1.0"
+proc-macro2 = "1.0"
+sha2 = "0.10"
diff --git a/gen/Cargo.toml b/gen/Cargo.toml
deleted file mode 100644
index 52bba1c..0000000
--- a/gen/Cargo.toml
+++ /dev/null
@@ -1,7 +0,0 @@
-[package]
-name = "res_gen"
-edition = "2024"
-version.workspace = true
-
-[dependencies]
-res_gen_macros = { path = "macros" }
diff --git a/gen/macros/Cargo.toml b/gen/macros/Cargo.toml
deleted file mode 100644
index 59867f8..0000000
--- a/gen/macros/Cargo.toml
+++ /dev/null
@@ -1,13 +0,0 @@
-[package]
-name = "res_gen_macros"
-edition = "2024"
-version.workspace = true
-
-[lib]
-proc-macro = true
-
-[dependencies]
-syn = { version = "2.0", features = ["full", "visit-mut"] }
-quote = "1.0"
-proc-macro2 = "1.0"
-sha2 = "0.10"
diff --git a/gen/macros/src/lib.rs b/gen/macros/src/lib.rs
deleted file mode 100644
index f74f944..0000000
--- a/gen/macros/src/lib.rs
+++ /dev/null
@@ -1,572 +0,0 @@
-use proc_macro::TokenStream;
-use sha2::Digest;
-use std::fs;
-
-use syn::parse::{Parse, ParseStream, Result};
-use syn::{LitInt, LitStr, parse_macro_input};
-
-struct StepInput {
- s: String,
- n: i64,
-}
-
-impl Parse for StepInput {
- fn parse(input: ParseStream) -> Result<Self> {
- let s = input.parse::<LitStr>()?.value();
- let n = if input.is_empty() {
- 0
- } else {
- input.parse::<syn::Token![,]>()?;
- let lit = input.parse::<LitInt>()?;
- lit.base10_parse::<i64>()?
- };
- Ok(StepInput { s, n })
- }
-}
-
-struct MarkDialogInput {
- mod_name: syn::Ident,
- file_path: String,
-}
-
-impl Parse for MarkDialogInput {
- fn parse(input: ParseStream) -> Result<Self> {
- let mod_name = input.parse::<syn::Ident>()?;
- input.parse::<syn::Token![=]>()?;
- let file_path_lit = input.parse::<LitStr>()?;
- let file_path = file_path_lit.value();
-
- Ok(MarkDialogInput {
- mod_name,
- file_path,
- })
- }
-}
-
-/// Generates a StepId enum based on a string
-/// Will select the StepId imported or declared in the current module
-///
-/// ```
-/// # use res_gen_macros::step;
-/// #[allow(non_camel_case_types)]
-/// #[derive(Debug, PartialEq)]
-/// enum StepId {
-/// R_09B538D1_0, // Begin
-/// }
-///
-/// assert_eq!(step!("Begin"), StepId::R_09B538D1_0);
-/// ```
-#[proc_macro]
-pub fn step(input: TokenStream) -> TokenStream {
- let StepInput { s, n } = parse_macro_input!(input as StepInput);
-
- let hash = sha2::Sha256::digest(s.as_bytes());
- let hex = format!("{:x}", hash);
- let short = &hex[..8];
-
- let ident_str = format!("R_{}_{}", short.to_uppercase(), n);
- let ident = syn::Ident::new(&ident_str, proc_macro2::Span::call_site());
-
- let expanded = quote::quote! {
- StepId::#ident
- };
-
- expanded.into()
-}
-
-/// Generates a dialog module from a .dialog file
-///
-/// ```ignore
-/// use markdialog::generate::{markdialog, step};
-///
-/// // Define here
-/// markdialog!(my = "Chapter1.dialog");
-///
-/// fn main() {
-/// let my_step = my::get_step(step!("Begin")).unwrap();
-/// let sentence = my_step.sentences[0];
-///
-/// // Print my sentences
-/// println!(
-/// "{} said : \"{}\"",
-/// sentence.character.unwrap_or_default(),
-/// sentence
-/// .content_tokens
-/// .iter()
-/// .map(|token| match token {
-/// my::Token::Text(t) => t,
-/// my::Token::BoldText(t) => t,
-/// my::Token::ItalicText(t) => t,
-/// my::Token::BoldItalicText(t) => t,
-/// _ => "",
-/// }
-/// .to_string())
-/// .collect::<Vec<String>>()
-/// .join("")
-/// )
-/// }
-/// ```
-#[proc_macro]
-pub fn markdialog(input: TokenStream) -> TokenStream {
- let MarkDialogInput {
- mod_name,
- file_path,
- } = parse_macro_input!(input as MarkDialogInput);
-
- // Read file content
- let content = match fs::read_to_string(&file_path) {
- Ok(content) => content,
- Err(e) => {
- return syn::Error::new(
- proc_macro2::Span::call_site(),
- format!("Failed to read dialog file '{}': {}", file_path, e),
- )
- .to_compile_error()
- .into();
- }
- };
-
- // Parse dialog file
- let parsed_dialog = match parse_dialog_file(&content) {
- Ok(parsed) => parsed,
- Err(e) => {
- return syn::Error::new(
- proc_macro2::Span::call_site(),
- format!("Failed to parse dialog file: {}", e),
- )
- .to_compile_error()
- .into();
- }
- };
-
- // Generate code
- let expanded = generate_dialog_module(&mod_name, &parsed_dialog);
-
- expanded.into()
-}
-
-fn parse_dialog_file(content: &str) -> std::result::Result<DialogFile, String> {
- let mut steps = Vec::new();
- let mut current_step_id: Option<String> = None;
- let mut current_sentences: Vec<SentenceData> = Vec::new();
-
- for line in content.lines() {
- let line = line.trim();
-
- if line.is_empty() {
- continue;
- }
-
- // Check if it's a Step definition line
- if line.starts_with("@@@@@@@@@@") {
- // Save previous Step
- if let Some(step_id) = current_step_id.take() {
- steps.push(StepData {
- id: step_id,
- sentences: std::mem::take(&mut current_sentences),
- });
- }
-
- // Extract new Step ID
- let step_id = line["@@@@@@@@@@".len()..].trim().to_string();
- current_step_id = Some(step_id);
- } else if let Some(_step_id) = &current_step_id {
- // Parse Sentence line
- match parse_sentence_line(line) {
- Ok(sentence) => current_sentences.push(sentence),
- Err(e) => return Err(format!("Failed to parse sentence line '{}': {}", line, e)),
- }
- }
- }
-
- // Save the last Step
- if let Some(step_id) = current_step_id {
- steps.push(StepData {
- id: step_id,
- sentences: current_sentences,
- });
- }
-
- Ok(DialogFile { steps })
-}
-
-fn parse_sentence_line(line: &str) -> std::result::Result<SentenceData, String> {
- // Split character part and content part
- let parts: Vec<&str> = line.split("->").collect();
- if parts.len() != 2 {
- return Err(format!("Invalid sentence line format: {}", line));
- }
-
- let left_part = parts[0].trim();
- let right_part = parts[1].trim();
-
- // Parse character part
- let (character, silence_switch) = parse_character_part(left_part)?;
-
- // Parse content blocks
- let content_tokens = parse_content_blocks(left_part)?;
-
- // Parse jump block
- let next_step = parse_next_step(right_part)?;
-
- Ok(SentenceData {
- character,
- content_tokens,
- next_step,
- silence_switch,
- })
-}
-
-fn parse_character_part(line: &str) -> std::result::Result<(Option<String>, bool), String> {
- // Find the position of the first ']'
- let end_bracket = line
- .find(']')
- .ok_or_else(|| "No closing bracket found for character".to_string())?;
- let character_part = &line[..=end_bracket];
-
- if character_part == "[]" {
- return Ok((None, false));
- }
-
- if character_part == "[**]" {
- return Ok((None, true));
- }
-
- // Check if it's wrapped with *
- let has_stars = character_part.starts_with("[*") && character_part.ends_with("*]");
-
- let start = if has_stars { 2 } else { 1 };
- let end = character_part.len() - (if has_stars { 2 } else { 1 });
-
- let character_content = &character_part[start..end];
-
- // Handle Unicode escape sequences
- let decoded = decode_unicode_escapes(character_content)?;
-
- Ok((Some(decoded), has_stars))
-}
-
-fn parse_content_blocks(line: &str) -> std::result::Result<Vec<TokenData>, String> {
- let mut tokens = Vec::new();
- let mut current_pos;
-
- // Skip the character part
- let first_bracket = line
- .find(']')
- .ok_or_else(|| "No closing bracket found".to_string())?;
- current_pos = first_bracket + 1;
-
- while current_pos < line.len() {
- // Find the next '['
- if let Some(start) = line[current_pos..].find('[') {
- let start_pos = current_pos + start;
-
- // Find the matching ']'
- let mut bracket_count = 0;
- let mut end_pos = None;
-
- for (i, ch) in line[start_pos..].char_indices() {
- match ch {
- '[' => bracket_count += 1,
- ']' => {
- bracket_count -= 1;
- if bracket_count == 0 {
- end_pos = Some(start_pos + i);
- break;
- }
- }
- _ => {}
- }
- }
-
- if let Some(end) = end_pos {
- let block = &line[start_pos..=end];
-
- // Parse content block
- if let Ok(token) = parse_content_block(block) {
- tokens.push(token);
- }
-
- current_pos = end + 1;
- } else {
- break;
- }
- } else {
- break;
- }
- }
-
- Ok(tokens)
-}
-
-fn parse_content_block(block: &str) -> std::result::Result<TokenData, String> {
- // Format: [label:[content]]
- let inner_start = block
- .find(':')
- .ok_or_else(|| "No colon in content block".to_string())?;
- let label = &block[1..inner_start];
-
- // Find the start and end of content
- let content_start = inner_start + 2; // Skip ':['
- let content_end = block.len() - 2; // Skip the trailing ']]'
-
- if content_start >= content_end {
- return Err("Empty content in block".to_string());
- }
-
- let content = &block[content_start..content_end];
-
- // Handle Unicode escape sequences
- let decoded_content = decode_unicode_escapes(content)?;
-
- // Remove backticks if present
- let trimmed_content = decoded_content.trim_matches('`');
-
- match label {
- "text" => Ok(TokenData::Text(trimmed_content.to_string())),
- "bold" => Ok(TokenData::BoldText(trimmed_content.to_string())),
- "italic" => Ok(TokenData::ItalicText(trimmed_content.to_string())),
- "bold_italic" => Ok(TokenData::BoldItalicText(trimmed_content.to_string())),
- "code" => Ok(TokenData::Code(trimmed_content.to_string())),
- _ => Err(format!("Unknown label: {}", label)),
- }
-}
-
-fn parse_next_step(block: &str) -> std::result::Result<Option<String>, String> {
- if block == "[]" {
- return Ok(None);
- }
-
- if !block.starts_with("[#") || !block.ends_with(']') {
- return Err(format!("Invalid next step format: {}", block));
- }
-
- let step_id = &block[2..block.len() - 1];
- Ok(Some(step_id.to_string()))
-}
-
-fn decode_unicode_escapes(input: &str) -> std::result::Result<String, String> {
- let mut result = String::new();
- let mut chars = input.chars().peekable();
-
- while let Some(ch) = chars.next() {
- if ch == '\\' {
- if let Some(&next) = chars.peek() {
- if next == 'u' {
- chars.next(); // skip 'u'
-
- // read 4 hex digits
- let mut hex_str = String::new();
- for _ in 0..4 {
- if let Some(&hex_char) = chars.peek() {
- if hex_char.is_ascii_hexdigit() {
- hex_str.push(hex_char);
- chars.next();
- } else {
- return Err("Invalid Unicode escape sequence".to_string());
- }
- } else {
- return Err("Incomplete Unicode escape sequence".to_string());
- }
- }
-
- // parse hex number
- if let Ok(code_point) = u32::from_str_radix(&hex_str, 16) {
- if let Some(unicode_char) = char::from_u32(code_point) {
- result.push(unicode_char);
- } else {
- return Err(format!("Invalid Unicode code point: {}", code_point));
- }
- } else {
- return Err(format!("Invalid hex number: {}", hex_str));
- }
- } else {
- result.push(ch);
- result.push(next);
- chars.next();
- }
- } else {
- result.push(ch);
- }
- } else {
- result.push(ch);
- }
- }
-
- Ok(result)
-}
-
-fn generate_dialog_module(mod_name: &syn::Ident, dialog: &DialogFile) -> proc_macro2::TokenStream {
- // Generate StepId enum
- let step_id_variants: Vec<_> = dialog
- .steps
- .iter()
- .map(|step| {
- let id_upper = step.id.to_uppercase().replace('-', "_");
- let variant_name =
- syn::Ident::new(&format!("R_{}", id_upper), proc_macro2::Span::call_site());
- quote::quote! {
- #variant_name,
- }
- })
- .collect();
-
- let step_id_variants_formatted = if step_id_variants.is_empty() {
- quote::quote! {}
- } else {
- quote::quote! {
- #(#step_id_variants)*
- }
- };
-
- // Generate get_step function
- let match_arms: Vec<_> = dialog
- .steps
- .iter()
- .map(|step| {
- let id_upper = step.id.to_uppercase().replace('-', "_");
- let variant_name =
- syn::Ident::new(&format!("R_{}", id_upper), proc_macro2::Span::call_site());
-
- // Generate sentences
- let sentences: Vec<_> = step
- .sentences
- .iter()
- .map(|sentence| {
- // Generate Token array
- let tokens: Vec<_> = sentence
- .content_tokens
- .iter()
- .map(|token| match token {
- TokenData::Text(text) => {
- quote::quote! { &Token::Text(#text) }
- }
- TokenData::BoldText(text) => {
- quote::quote! { &Token::BoldText(#text) }
- }
- TokenData::ItalicText(text) => {
- quote::quote! { &Token::ItalicText(#text) }
- }
- TokenData::BoldItalicText(text) => {
- quote::quote! { &Token::BoldItalicText(#text) }
- }
- TokenData::Code(text) => {
- quote::quote! { &Token::Code(#text) }
- }
- })
- .collect();
-
- let character_expr = if let Some(ref char_name) = sentence.character {
- quote::quote! { Some(#char_name) }
- } else {
- quote::quote! { None }
- };
-
- let next_step_expr = if let Some(ref next_step_id) = sentence.next_step {
- let next_id_upper = next_step_id.to_uppercase().replace('-', "_");
- let next_variant_name = syn::Ident::new(
- &format!("R_{}", next_id_upper),
- proc_macro2::Span::call_site(),
- );
- quote::quote! { Some(StepId::#next_variant_name) }
- } else {
- quote::quote! { None }
- };
-
- let silence_switch = sentence.silence_switch;
- quote::quote! {
- &Sentence {
- character: #character_expr,
- content_tokens: &[#(#tokens),*],
- next_step: #next_step_expr,
- silence_switch: #silence_switch,
- }
- }
- })
- .collect();
-
- let sentences_formatted = if sentences.is_empty() {
- quote::quote! { &[] }
- } else {
- quote::quote! { &[#(#sentences),*] }
- };
-
- quote::quote! {
- StepId::#variant_name => Some(Step {
- sentences: #sentences_formatted,
- }),
- }
- })
- .collect();
-
- quote::quote! {
- pub use #mod_name::StepId;
- pub mod #mod_name {
- #[allow(non_camel_case_types)]
- pub enum StepId {
- #step_id_variants_formatted
- }
-
- pub struct Step<'a> {
- pub sentences: &'a [&'a Sentence<'a>],
- }
-
- pub struct Sentence<'a> {
- pub character: Option<&'a str>,
- pub content_tokens: &'a [&'a Token<'a>],
- pub next_step: Option<StepId>,
- pub silence_switch: bool,
- }
-
- pub enum Token<'a> {
- Text(&'a str),
- BoldText(&'a str),
- ItalicText(&'a str),
- BoldItalicText(&'a str),
- Code(&'a str),
- }
-
- impl<'a> std::fmt::Display for Token<'a> {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Token::Text(text) => write!(f, "{}", text),
- Token::BoldText(text) => write!(f, "**{}**", text),
- Token::ItalicText(text) => write!(f, "*{}*", text),
- Token::BoldItalicText(text) => write!(f, "***{}***", text),
- Token::Code(text) => write!(f, "`{}`", text),
- }
- }
- }
-
- pub fn get_step(id: StepId) -> Option<Step<'static>> {
- match id {
- #(#match_arms)*
- }
- }
- }
- }
-}
-
-struct DialogFile {
- steps: Vec<StepData>,
-}
-
-struct StepData {
- id: String,
- sentences: Vec<SentenceData>,
-}
-
-struct SentenceData {
- character: Option<String>,
- content_tokens: Vec<TokenData>,
- next_step: Option<String>,
- silence_switch: bool,
-}
-
-enum TokenData {
- Text(String),
- BoldText(String),
- ItalicText(String),
- BoldItalicText(String),
- Code(String),
-}
diff --git a/gen/src/lib.rs b/gen/src/lib.rs
deleted file mode 100644
index 833f39c..0000000
--- a/gen/src/lib.rs
+++ /dev/null
@@ -1 +0,0 @@
-pub use res_gen_macros::*;
diff --git a/src/lib.rs b/src/lib.rs
index 6d48a91..f74f944 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,14 +1,572 @@
-// markdialog::player
-pub mod player {
- pub use markdialog_player::*;
+use proc_macro::TokenStream;
+use sha2::Digest;
+use std::fs;
+
+use syn::parse::{Parse, ParseStream, Result};
+use syn::{LitInt, LitStr, parse_macro_input};
+
+struct StepInput {
+ s: String,
+ n: i64,
+}
+
+impl Parse for StepInput {
+ fn parse(input: ParseStream) -> Result<Self> {
+ let s = input.parse::<LitStr>()?.value();
+ let n = if input.is_empty() {
+ 0
+ } else {
+ input.parse::<syn::Token![,]>()?;
+ let lit = input.parse::<LitInt>()?;
+ lit.base10_parse::<i64>()?
+ };
+ Ok(StepInput { s, n })
+ }
+}
+
+struct MarkDialogInput {
+ mod_name: syn::Ident,
+ file_path: String,
+}
+
+impl Parse for MarkDialogInput {
+ fn parse(input: ParseStream) -> Result<Self> {
+ let mod_name = input.parse::<syn::Ident>()?;
+ input.parse::<syn::Token![=]>()?;
+ let file_path_lit = input.parse::<LitStr>()?;
+ let file_path = file_path_lit.value();
+
+ Ok(MarkDialogInput {
+ mod_name,
+ file_path,
+ })
+ }
+}
+
+/// Generates a StepId enum based on a string
+/// Will select the StepId imported or declared in the current module
+///
+/// ```
+/// # use res_gen_macros::step;
+/// #[allow(non_camel_case_types)]
+/// #[derive(Debug, PartialEq)]
+/// enum StepId {
+/// R_09B538D1_0, // Begin
+/// }
+///
+/// assert_eq!(step!("Begin"), StepId::R_09B538D1_0);
+/// ```
+#[proc_macro]
+pub fn step(input: TokenStream) -> TokenStream {
+ let StepInput { s, n } = parse_macro_input!(input as StepInput);
+
+ let hash = sha2::Sha256::digest(s.as_bytes());
+ let hex = format!("{:x}", hash);
+ let short = &hex[..8];
+
+ let ident_str = format!("R_{}_{}", short.to_uppercase(), n);
+ let ident = syn::Ident::new(&ident_str, proc_macro2::Span::call_site());
+
+ let expanded = quote::quote! {
+ StepId::#ident
+ };
+
+ expanded.into()
+}
+
+/// Generates a dialog module from a .dialog file
+///
+/// ```ignore
+/// use markdialog::generate::{markdialog, step};
+///
+/// // Define here
+/// markdialog!(my = "Chapter1.dialog");
+///
+/// fn main() {
+/// let my_step = my::get_step(step!("Begin")).unwrap();
+/// let sentence = my_step.sentences[0];
+///
+/// // Print my sentences
+/// println!(
+/// "{} said : \"{}\"",
+/// sentence.character.unwrap_or_default(),
+/// sentence
+/// .content_tokens
+/// .iter()
+/// .map(|token| match token {
+/// my::Token::Text(t) => t,
+/// my::Token::BoldText(t) => t,
+/// my::Token::ItalicText(t) => t,
+/// my::Token::BoldItalicText(t) => t,
+/// _ => "",
+/// }
+/// .to_string())
+/// .collect::<Vec<String>>()
+/// .join("")
+/// )
+/// }
+/// ```
+#[proc_macro]
+pub fn markdialog(input: TokenStream) -> TokenStream {
+ let MarkDialogInput {
+ mod_name,
+ file_path,
+ } = parse_macro_input!(input as MarkDialogInput);
+
+ // Read file content
+ let content = match fs::read_to_string(&file_path) {
+ Ok(content) => content,
+ Err(e) => {
+ return syn::Error::new(
+ proc_macro2::Span::call_site(),
+ format!("Failed to read dialog file '{}': {}", file_path, e),
+ )
+ .to_compile_error()
+ .into();
+ }
+ };
+
+ // Parse dialog file
+ let parsed_dialog = match parse_dialog_file(&content) {
+ Ok(parsed) => parsed,
+ Err(e) => {
+ return syn::Error::new(
+ proc_macro2::Span::call_site(),
+ format!("Failed to parse dialog file: {}", e),
+ )
+ .to_compile_error()
+ .into();
+ }
+ };
+
+ // Generate code
+ let expanded = generate_dialog_module(&mod_name, &parsed_dialog);
+
+ expanded.into()
+}
+
+fn parse_dialog_file(content: &str) -> std::result::Result<DialogFile, String> {
+ let mut steps = Vec::new();
+ let mut current_step_id: Option<String> = None;
+ let mut current_sentences: Vec<SentenceData> = Vec::new();
+
+ for line in content.lines() {
+ let line = line.trim();
+
+ if line.is_empty() {
+ continue;
+ }
+
+ // Check if it's a Step definition line
+ if line.starts_with("@@@@@@@@@@") {
+ // Save previous Step
+ if let Some(step_id) = current_step_id.take() {
+ steps.push(StepData {
+ id: step_id,
+ sentences: std::mem::take(&mut current_sentences),
+ });
+ }
+
+ // Extract new Step ID
+ let step_id = line["@@@@@@@@@@".len()..].trim().to_string();
+ current_step_id = Some(step_id);
+ } else if let Some(_step_id) = &current_step_id {
+ // Parse Sentence line
+ match parse_sentence_line(line) {
+ Ok(sentence) => current_sentences.push(sentence),
+ Err(e) => return Err(format!("Failed to parse sentence line '{}': {}", line, e)),
+ }
+ }
+ }
+
+ // Save the last Step
+ if let Some(step_id) = current_step_id {
+ steps.push(StepData {
+ id: step_id,
+ sentences: current_sentences,
+ });
+ }
+
+ Ok(DialogFile { steps })
+}
+
+fn parse_sentence_line(line: &str) -> std::result::Result<SentenceData, String> {
+ // Split character part and content part
+ let parts: Vec<&str> = line.split("->").collect();
+ if parts.len() != 2 {
+ return Err(format!("Invalid sentence line format: {}", line));
+ }
+
+ let left_part = parts[0].trim();
+ let right_part = parts[1].trim();
+
+ // Parse character part
+ let (character, silence_switch) = parse_character_part(left_part)?;
+
+ // Parse content blocks
+ let content_tokens = parse_content_blocks(left_part)?;
+
+ // Parse jump block
+ let next_step = parse_next_step(right_part)?;
+
+ Ok(SentenceData {
+ character,
+ content_tokens,
+ next_step,
+ silence_switch,
+ })
+}
+
+fn parse_character_part(line: &str) -> std::result::Result<(Option<String>, bool), String> {
+ // Find the position of the first ']'
+ let end_bracket = line
+ .find(']')
+ .ok_or_else(|| "No closing bracket found for character".to_string())?;
+ let character_part = &line[..=end_bracket];
+
+ if character_part == "[]" {
+ return Ok((None, false));
+ }
+
+ if character_part == "[**]" {
+ return Ok((None, true));
+ }
+
+ // Check if it's wrapped with *
+ let has_stars = character_part.starts_with("[*") && character_part.ends_with("*]");
+
+ let start = if has_stars { 2 } else { 1 };
+ let end = character_part.len() - (if has_stars { 2 } else { 1 });
+
+ let character_content = &character_part[start..end];
+
+ // Handle Unicode escape sequences
+ let decoded = decode_unicode_escapes(character_content)?;
+
+ Ok((Some(decoded), has_stars))
+}
+
+fn parse_content_blocks(line: &str) -> std::result::Result<Vec<TokenData>, String> {
+ let mut tokens = Vec::new();
+ let mut current_pos;
+
+ // Skip the character part
+ let first_bracket = line
+ .find(']')
+ .ok_or_else(|| "No closing bracket found".to_string())?;
+ current_pos = first_bracket + 1;
+
+ while current_pos < line.len() {
+ // Find the next '['
+ if let Some(start) = line[current_pos..].find('[') {
+ let start_pos = current_pos + start;
+
+ // Find the matching ']'
+ let mut bracket_count = 0;
+ let mut end_pos = None;
+
+ for (i, ch) in line[start_pos..].char_indices() {
+ match ch {
+ '[' => bracket_count += 1,
+ ']' => {
+ bracket_count -= 1;
+ if bracket_count == 0 {
+ end_pos = Some(start_pos + i);
+ break;
+ }
+ }
+ _ => {}
+ }
+ }
+
+ if let Some(end) = end_pos {
+ let block = &line[start_pos..=end];
+
+ // Parse content block
+ if let Ok(token) = parse_content_block(block) {
+ tokens.push(token);
+ }
+
+ current_pos = end + 1;
+ } else {
+ break;
+ }
+ } else {
+ break;
+ }
+ }
+
+ Ok(tokens)
+}
+
+fn parse_content_block(block: &str) -> std::result::Result<TokenData, String> {
+ // Format: [label:[content]]
+ let inner_start = block
+ .find(':')
+ .ok_or_else(|| "No colon in content block".to_string())?;
+ let label = &block[1..inner_start];
+
+ // Find the start and end of content
+ let content_start = inner_start + 2; // Skip ':['
+ let content_end = block.len() - 2; // Skip the trailing ']]'
+
+ if content_start >= content_end {
+ return Err("Empty content in block".to_string());
+ }
+
+ let content = &block[content_start..content_end];
+
+ // Handle Unicode escape sequences
+ let decoded_content = decode_unicode_escapes(content)?;
+
+ // Remove backticks if present
+ let trimmed_content = decoded_content.trim_matches('`');
+
+ match label {
+ "text" => Ok(TokenData::Text(trimmed_content.to_string())),
+ "bold" => Ok(TokenData::BoldText(trimmed_content.to_string())),
+ "italic" => Ok(TokenData::ItalicText(trimmed_content.to_string())),
+ "bold_italic" => Ok(TokenData::BoldItalicText(trimmed_content.to_string())),
+ "code" => Ok(TokenData::Code(trimmed_content.to_string())),
+ _ => Err(format!("Unknown label: {}", label)),
+ }
+}
+
+fn parse_next_step(block: &str) -> std::result::Result<Option<String>, String> {
+ if block == "[]" {
+ return Ok(None);
+ }
+
+ if !block.starts_with("[#") || !block.ends_with(']') {
+ return Err(format!("Invalid next step format: {}", block));
+ }
+
+ let step_id = &block[2..block.len() - 1];
+ Ok(Some(step_id.to_string()))
+}
+
+fn decode_unicode_escapes(input: &str) -> std::result::Result<String, String> {
+ let mut result = String::new();
+ let mut chars = input.chars().peekable();
+
+ while let Some(ch) = chars.next() {
+ if ch == '\\' {
+ if let Some(&next) = chars.peek() {
+ if next == 'u' {
+ chars.next(); // skip 'u'
+
+ // read 4 hex digits
+ let mut hex_str = String::new();
+ for _ in 0..4 {
+ if let Some(&hex_char) = chars.peek() {
+ if hex_char.is_ascii_hexdigit() {
+ hex_str.push(hex_char);
+ chars.next();
+ } else {
+ return Err("Invalid Unicode escape sequence".to_string());
+ }
+ } else {
+ return Err("Incomplete Unicode escape sequence".to_string());
+ }
+ }
+
+ // parse hex number
+ if let Ok(code_point) = u32::from_str_radix(&hex_str, 16) {
+ if let Some(unicode_char) = char::from_u32(code_point) {
+ result.push(unicode_char);
+ } else {
+ return Err(format!("Invalid Unicode code point: {}", code_point));
+ }
+ } else {
+ return Err(format!("Invalid hex number: {}", hex_str));
+ }
+ } else {
+ result.push(ch);
+ result.push(next);
+ chars.next();
+ }
+ } else {
+ result.push(ch);
+ }
+ } else {
+ result.push(ch);
+ }
+ }
+
+ Ok(result)
+}
+
+fn generate_dialog_module(mod_name: &syn::Ident, dialog: &DialogFile) -> proc_macro2::TokenStream {
+ // Generate StepId enum
+ let step_id_variants: Vec<_> = dialog
+ .steps
+ .iter()
+ .map(|step| {
+ let id_upper = step.id.to_uppercase().replace('-', "_");
+ let variant_name =
+ syn::Ident::new(&format!("R_{}", id_upper), proc_macro2::Span::call_site());
+ quote::quote! {
+ #variant_name,
+ }
+ })
+ .collect();
+
+ let step_id_variants_formatted = if step_id_variants.is_empty() {
+ quote::quote! {}
+ } else {
+ quote::quote! {
+ #(#step_id_variants)*
+ }
+ };
+
+ // Generate get_step function
+ let match_arms: Vec<_> = dialog
+ .steps
+ .iter()
+ .map(|step| {
+ let id_upper = step.id.to_uppercase().replace('-', "_");
+ let variant_name =
+ syn::Ident::new(&format!("R_{}", id_upper), proc_macro2::Span::call_site());
+
+ // Generate sentences
+ let sentences: Vec<_> = step
+ .sentences
+ .iter()
+ .map(|sentence| {
+ // Generate Token array
+ let tokens: Vec<_> = sentence
+ .content_tokens
+ .iter()
+ .map(|token| match token {
+ TokenData::Text(text) => {
+ quote::quote! { &Token::Text(#text) }
+ }
+ TokenData::BoldText(text) => {
+ quote::quote! { &Token::BoldText(#text) }
+ }
+ TokenData::ItalicText(text) => {
+ quote::quote! { &Token::ItalicText(#text) }
+ }
+ TokenData::BoldItalicText(text) => {
+ quote::quote! { &Token::BoldItalicText(#text) }
+ }
+ TokenData::Code(text) => {
+ quote::quote! { &Token::Code(#text) }
+ }
+ })
+ .collect();
+
+ let character_expr = if let Some(ref char_name) = sentence.character {
+ quote::quote! { Some(#char_name) }
+ } else {
+ quote::quote! { None }
+ };
+
+ let next_step_expr = if let Some(ref next_step_id) = sentence.next_step {
+ let next_id_upper = next_step_id.to_uppercase().replace('-', "_");
+ let next_variant_name = syn::Ident::new(
+ &format!("R_{}", next_id_upper),
+ proc_macro2::Span::call_site(),
+ );
+ quote::quote! { Some(StepId::#next_variant_name) }
+ } else {
+ quote::quote! { None }
+ };
+
+ let silence_switch = sentence.silence_switch;
+ quote::quote! {
+ &Sentence {
+ character: #character_expr,
+ content_tokens: &[#(#tokens),*],
+ next_step: #next_step_expr,
+ silence_switch: #silence_switch,
+ }
+ }
+ })
+ .collect();
+
+ let sentences_formatted = if sentences.is_empty() {
+ quote::quote! { &[] }
+ } else {
+ quote::quote! { &[#(#sentences),*] }
+ };
+
+ quote::quote! {
+ StepId::#variant_name => Some(Step {
+ sentences: #sentences_formatted,
+ }),
+ }
+ })
+ .collect();
+
+ quote::quote! {
+ pub use #mod_name::StepId;
+ pub mod #mod_name {
+ #[allow(non_camel_case_types)]
+ pub enum StepId {
+ #step_id_variants_formatted
+ }
+
+ pub struct Step<'a> {
+ pub sentences: &'a [&'a Sentence<'a>],
+ }
+
+ pub struct Sentence<'a> {
+ pub character: Option<&'a str>,
+ pub content_tokens: &'a [&'a Token<'a>],
+ pub next_step: Option<StepId>,
+ pub silence_switch: bool,
+ }
+
+ pub enum Token<'a> {
+ Text(&'a str),
+ BoldText(&'a str),
+ ItalicText(&'a str),
+ BoldItalicText(&'a str),
+ Code(&'a str),
+ }
+
+ impl<'a> std::fmt::Display for Token<'a> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Token::Text(text) => write!(f, "{}", text),
+ Token::BoldText(text) => write!(f, "**{}**", text),
+ Token::ItalicText(text) => write!(f, "*{}*", text),
+ Token::BoldItalicText(text) => write!(f, "***{}***", text),
+ Token::Code(text) => write!(f, "`{}`", text),
+ }
+ }
+ }
+
+ pub fn get_step(id: StepId) -> Option<Step<'static>> {
+ match id {
+ #(#match_arms)*
+ }
+ }
+ }
+ }
+}
+
+struct DialogFile {
+ steps: Vec<StepData>,
+}
+
+struct StepData {
+ id: String,
+ sentences: Vec<SentenceData>,
}
-// markdialog::converter
-pub mod converter {
- pub use markdialog_converter::*;
+struct SentenceData {
+ character: Option<String>,
+ content_tokens: Vec<TokenData>,
+ next_step: Option<String>,
+ silence_switch: bool,
}
-// markdialog::generate
-pub mod generate {
- pub use res_gen::*;
+enum TokenData {
+ Text(String),
+ BoldText(String),
+ ItalicText(String),
+ BoldItalicText(String),
+ Code(String),
}
diff --git a/src/main.rs b/src/main.rs
deleted file mode 100644
index e6f342c..0000000
--- a/src/main.rs
+++ /dev/null
@@ -1,4 +0,0 @@
-fn main() {
- // `cargo run` will invoke player
- markdialog_player::player_main();
-}