summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-02-10 04:07:12 +0800
committer魏曹先生 <1992414357@qq.com>2026-02-10 04:07:12 +0800
commit275084f025b81da78f2a6c5cb23bc4a846a7b909 (patch)
treef8899947ad53534318c7d531f1b0df2506620d9f
parentade7980b250d0d679355d9583edd03deed871ff2 (diff)
Refactor converter and replace built_res with resource generator
-rw-r--r--Cargo.lock64
-rw-r--r--Cargo.toml7
-rw-r--r--README_zh_CN.md2
-rw-r--r--built_res/Cargo.toml6
-rw-r--r--built_res/src/lib.rs2
-rw-r--r--built_res/src/res_sentences.rs51
-rw-r--r--built_res/src/structs.rs1
-rw-r--r--built_res/src/structs/sentence.rs12
-rw-r--r--converter/Cargo.toml2
-rw-r--r--converter/src/parse.rs1970
-rw-r--r--converter/src/syntax_checker.rs20
-rw-r--r--gen/Cargo.toml7
-rw-r--r--gen/macros/Cargo.toml13
-rw-r--r--gen/macros/src/lib.rs584
-rw-r--r--gen/src/lib.rs1
-rw-r--r--player/Cargo.toml2
-rw-r--r--player/src/lib.rs4
-rw-r--r--src/lib.rs12
-rw-r--r--src/main.rs4
19 files changed, 1608 insertions, 1156 deletions
diff --git a/Cargo.lock b/Cargo.lock
index d2f6e4e..be758b3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -21,10 +21,6 @@ dependencies = [
]
[[package]]
-name = "built_res"
-version = "0.0.0"
-
-[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -86,16 +82,16 @@ checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
[[package]]
name = "markdialog"
-version = "0.1.0"
+version = "0.0.0"
dependencies = [
- "built_res",
"markdialog_converter",
"markdialog_player",
+ "res_gen",
]
[[package]]
name = "markdialog_converter"
-version = "0.0.0"
+version = "0.1.0"
dependencies = [
"colored",
"regex",
@@ -106,7 +102,7 @@ dependencies = [
[[package]]
name = "markdialog_player"
-version = "0.0.0"
+version = "0.1.0"
[[package]]
name = "memchr"
@@ -115,6 +111,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
name = "regex"
version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -144,6 +158,23 @@ 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"
@@ -164,12 +195,29 @@ dependencies = [
]
[[package]]
+name = "syn"
+version = "2.0.114"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
name = "typenum"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]]
+name = "unicode-ident"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e"
+
+[[package]]
name = "unicode-width"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index a6ce65a..9b8d785 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,16 +1,17 @@
[package]
name = "markdialog"
-version = "0.1.0"
edition = "2024"
[workspace]
+package.version = "0.1.0"
members = [
- "built_res", # Built Resources
+ "gen", # Generate Resource
+ "gen/macros",
"player", # Dialog Player
"converter" # Markdown Converter
]
[dependencies]
-built_res = { path = "built_res" }
+res_gen = { path = "gen" }
markdialog_player = { path = "player" }
markdialog_converter = { path = "converter" }
diff --git a/README_zh_CN.md b/README_zh_CN.md
index e096ac3..c7210b2 100644
--- a/README_zh_CN.md
+++ b/README_zh_CN.md
@@ -102,6 +102,8 @@
+
+
## 开源协议
哈哈,我采用 MIT License,放心玩去吧!
diff --git a/built_res/Cargo.toml b/built_res/Cargo.toml
deleted file mode 100644
index b88a569..0000000
--- a/built_res/Cargo.toml
+++ /dev/null
@@ -1,6 +0,0 @@
-[package]
-name = "built_res"
-workspaces.version = true
-edition = "2024"
-
-[dependencies]
diff --git a/built_res/src/lib.rs b/built_res/src/lib.rs
deleted file mode 100644
index bef5b44..0000000
--- a/built_res/src/lib.rs
+++ /dev/null
@@ -1,2 +0,0 @@
-pub mod res_sentences;
-pub mod structs;
diff --git a/built_res/src/res_sentences.rs b/built_res/src/res_sentences.rs
deleted file mode 100644
index 8354ded..0000000
--- a/built_res/src/res_sentences.rs
+++ /dev/null
@@ -1,51 +0,0 @@
-use crate::structs::sentence::{Sentence, Token};
-
-#[derive(Hash, PartialEq, Eq)]
-pub enum SentenceId {
- // 在此处确认所有跳转点
- Main0,
- Main1,
- Main2,
- Ok1,
-}
-
-pub fn get_sentence(id: SentenceId) -> Option<Sentence<'static>> {
- match id {
- SentenceId::Main0 => Some(Sentence {
- content_tokens: &[
- &Token::Text("你好我是"),
- &Token::Command("red"),
- &Token::Text("猫尾草"),
- &Token::Command("/"),
- ],
- next_sentence: Some(SentenceId::Main1),
- }),
- SentenceId::Main1 => Some(Sentence {
- content_tokens: &[
- &Token::Text("你好我是"),
- &Token::Command("red"),
- &Token::Text("猫尾草"),
- &Token::Command("/"),
- ],
- next_sentence: Some(SentenceId::Main2),
- }),
- SentenceId::Main2 => Some(Sentence {
- content_tokens: &[
- &Token::Text("你好我是"),
- &Token::Command("red"),
- &Token::Text("猫尾草"),
- &Token::Command("/"),
- ],
- next_sentence: Some(SentenceId::Ok1),
- }),
- SentenceId::Ok1 => Some(Sentence {
- content_tokens: &[
- &Token::Text("你好我是"),
- &Token::Command("red"),
- &Token::Text("猫尾草"),
- &Token::Command("/"),
- ],
- next_sentence: None,
- }),
- }
-}
diff --git a/built_res/src/structs.rs b/built_res/src/structs.rs
deleted file mode 100644
index b7bb9ef..0000000
--- a/built_res/src/structs.rs
+++ /dev/null
@@ -1 +0,0 @@
-pub mod sentence;
diff --git a/built_res/src/structs/sentence.rs b/built_res/src/structs/sentence.rs
deleted file mode 100644
index 270d5f7..0000000
--- a/built_res/src/structs/sentence.rs
+++ /dev/null
@@ -1,12 +0,0 @@
-pub struct Sentence<'a> {
- pub content_tokens: &'a [&'static Token],
- pub next_sentence: Option<crate::res_sentences::SentenceId>,
-}
-
-pub enum Token {
- Text(&'static str),
- BoldText(&'static str),
- ItalicText(&'static str),
- BoldItalicText(&'static str),
- Command(&'static str),
-}
diff --git a/converter/Cargo.toml b/converter/Cargo.toml
index 982d18a..ce44af9 100644
--- a/converter/Cargo.toml
+++ b/converter/Cargo.toml
@@ -1,7 +1,7 @@
[package]
name = "markdialog_converter"
-workspaces.version = true
edition = "2024"
+version.workspace = true
[dependencies]
colored = "3.0"
diff --git a/converter/src/parse.rs b/converter/src/parse.rs
index c480c3a..02119d3 100644
--- a/converter/src/parse.rs
+++ b/converter/src/parse.rs
@@ -1,789 +1,671 @@
-use std::path::{Path, PathBuf};
+use std::path::PathBuf;
-use regex::Regex;
-use sha2::{Digest, Sha256};
-
-use crate::{error::Exit, syntax_checker::check_markdown_syntax, utils::path_fmt::format_path};
+use crate::{
+ error::Exit,
+ syntax_checker::{check_duplicate_marker, check_markdown_syntax},
+};
pub fn parse(input: PathBuf, ir_output: PathBuf) -> Result<(), Exit> {
let result = std::fs::read_to_string(&input)?;
check_markdown_syntax(&result)?;
- let result = unwrap_includes(result, input)?;
+ let result = unwrap_includes::proc(result, input)?;
check_duplicate_marker(&result)?;
- let result = clean_markdown(result)?;
- let result = fix_mark_jump(result)?;
- let result = replace_marker_name(result)?;
- let result = convert_to_step_sentence_structure(result)?;
- let result = strip_invalid_jump(result)?;
- let result = convert_image_to_code(result)?;
- let result = apply_code_lines(result)?;
- let result = split_sentence_and_encode(result)?;
+ let result = markdown_cleanup::proc(result)?;
+ let result = markdown_jump_fix::proc(result)?;
+ let result = markdown_marker_rename::proc(result)?;
+ let result = markdown_struct_build::proc(result)?;
+ let result = markdown_strip_invalid_jump::proc(result)?;
+ let result = markdown_convert_image::proc(result)?;
+ let result = markdown_apply_codes::proc(result)?;
+ let result = markdown_split_and_encode::proc(result)?;
std::fs::write(&ir_output, result)?;
Ok(())
}
-/// Expand text includes of [[Dialog.md]]
-pub fn unwrap_includes(input: String, self_path: PathBuf) -> Result<String, Exit> {
- let mut stack = Vec::<PathBuf>::new();
- expand_recursive(input, &self_path, &mut stack)
-}
-
-fn expand_recursive(
- content: String,
- current_path: &Path,
- stack: &mut Vec<PathBuf>,
-) -> Result<String, Exit> {
- let mut output = String::new();
- let mut in_code_block = false;
+pub mod unwrap_includes {
+ use crate::{error::Exit, utils::path_fmt::format_path};
+ use std::path::{Path, PathBuf};
- let current_norm = format_path(current_path)?;
-
- if stack.contains(&current_norm) {
- return Err(Exit::CycleDependency(current_norm));
+ /// Expand text includes of [[Dialog.md]]
+ pub fn proc(input: String, self_path: PathBuf) -> Result<String, Exit> {
+ let mut stack = Vec::<PathBuf>::new();
+ expand_recursive(input, &self_path, &mut stack)
}
- stack.push(current_norm.clone());
+ fn expand_recursive(
+ content: String,
+ current_path: &Path,
+ stack: &mut Vec<PathBuf>,
+ ) -> Result<String, Exit> {
+ let mut output = String::new();
+ let mut in_code_block = false;
- for line in content.lines() {
- if line.trim().starts_with("```") {
- in_code_block = !in_code_block;
- output.push_str(line);
- output.push('\n');
- continue;
- }
+ let current_norm = format_path(current_path)?;
- if in_code_block {
- output.push_str(line);
- output.push('\n');
- continue;
+ if stack.contains(&current_norm) {
+ return Err(Exit::CycleDependency(current_norm));
}
- if let Some(include_path) = extract_include(line) {
- let include_abs = format_path(&current_path.parent().unwrap().join(include_path))?;
- let include_content = std::fs::read_to_string(&include_abs).map_err(|e| {
- if e.kind() == std::io::ErrorKind::NotFound {
- Exit::FileNotFound(include_abs.clone())
- } else {
- Exit::IoError(e)
- }
- })?;
-
- let expanded = expand_recursive(include_content, &include_abs, stack)?;
- output.push_str(&expanded);
- } else {
- output.push_str(line);
- output.push('\n');
- }
- }
+ stack.push(current_norm.clone());
- stack.pop();
-
- Ok(output)
-}
+ for line in content.lines() {
+ if line.trim().starts_with("```") {
+ in_code_block = !in_code_block;
+ output.push_str(line);
+ output.push('\n');
+ continue;
+ }
-fn extract_include(line: &str) -> Option<&str> {
- line.trim()
- .strip_prefix("[[")
- .and_then(|s| s.strip_suffix("]]"))
-}
+ if in_code_block {
+ output.push_str(line);
+ output.push('\n');
+ continue;
+ }
-/// Check for duplicate markers
-pub fn check_duplicate_marker(input: &String) -> Result<(), Exit> {
- let mut seen = std::collections::HashSet::new();
- let heading_re = Regex::new(r"^(#{1,5})\s+(.+)$").unwrap();
+ if let Some(include_path) = extract_include(line) {
+ let include_abs = format_path(&current_path.parent().unwrap().join(include_path))?;
+ let include_content = std::fs::read_to_string(&include_abs).map_err(|e| {
+ if e.kind() == std::io::ErrorKind::NotFound {
+ Exit::FileNotFound(include_abs.clone())
+ } else {
+ Exit::IoError(e)
+ }
+ })?;
- for line in input.lines() {
- if let Some(caps) = heading_re.captures(line) {
- let heading_text = caps[2].trim().to_string();
- if seen.contains(&heading_text) {
- return Err(Exit::DuplicateMarker(heading_text));
+ let expanded = expand_recursive(include_content, &include_abs, stack)?;
+ output.push_str(&expanded);
+ } else {
+ output.push_str(line);
+ output.push('\n');
}
- seen.insert(heading_text);
}
+
+ stack.pop();
+
+ Ok(output)
}
- Ok(())
+ fn extract_include(line: &str) -> Option<&str> {
+ line.trim()
+ .strip_prefix("[[")
+ .and_then(|s| s.strip_suffix("]]"))
+ }
}
-/// Clean Markdown
-/// 1. Remove blockquotes
-/// 2. Remove empty lines
-/// 3. Trim each line
-pub fn clean_markdown(i: String) -> Result<String, Exit> {
- let lines = i.lines();
- let mut cleaned = Vec::new();
-
- for line in lines {
- if line.starts_with('>') {
- continue;
- }
- let trimmed = line.trim();
- if trimmed.is_empty() {
- continue;
+pub mod markdown_cleanup {
+ use crate::error::Exit;
+
+ /// Clean Markdown
+ /// 1. Remove blockquotes
+ /// 2. Remove empty lines
+ /// 3. Trim each line
+ pub fn proc(i: String) -> Result<String, Exit> {
+ let lines = i.lines();
+ let mut cleaned = Vec::new();
+
+ for line in lines {
+ if line.starts_with('>') {
+ continue;
+ }
+ let trimmed = line.trim();
+ if trimmed.is_empty() {
+ continue;
+ }
+ cleaned.push(trimmed.to_string());
}
- cleaned.push(trimmed.to_string());
+
+ Ok(cleaned.join("\n"))
}
- Ok(cleaned.join("\n"))
-}
+ #[cfg(test)]
+ mod test_clean_markdown {
+ use super::*;
-#[cfg(test)]
-mod test_clean_markdown {
- use super::*;
-
- #[test]
- fn test_clean_markdown_removes_blockquotes() {
- let input = "> This is a blockquote\nNormal text\n> Another blockquote".to_string();
- let expected = "Normal text".to_string();
- let Ok(result) = clean_markdown(input) else {
- panic!("Parse error!");
- };
- assert_eq!(result, expected);
- }
+ #[test]
+ fn test_clean_markdown_removes_blockquotes() {
+ let input = "> This is a blockquote\nNormal text\n> Another blockquote".to_string();
+ let expected = "Normal text".to_string();
+ let Ok(result) = proc(input) else {
+ panic!("Parse error!");
+ };
+ assert_eq!(result, expected);
+ }
- #[test]
- fn test_clean_markdown_removes_empty_lines() {
- let input = "Line 1\n\n\nLine 2\n\n".to_string();
- let expected = "Line 1\nLine 2".to_string();
- let Ok(result) = clean_markdown(input) else {
- panic!("Parse error!");
- };
- assert_eq!(result, expected);
- }
+ #[test]
+ fn test_clean_markdown_removes_empty_lines() {
+ let input = "Line 1\n\n\nLine 2\n\n".to_string();
+ let expected = "Line 1\nLine 2".to_string();
+ let Ok(result) = proc(input) else {
+ panic!("Parse error!");
+ };
+ assert_eq!(result, expected);
+ }
- #[test]
- fn test_clean_markdown_trims_lines() {
- let input = " Line 1 \n\tLine 2\t\n".to_string();
- let expected = "Line 1\nLine 2".to_string();
- let Ok(result) = clean_markdown(input) else {
- panic!("Parse error!");
- };
- assert_eq!(result, expected);
- }
+ #[test]
+ fn test_clean_markdown_trims_lines() {
+ let input = " Line 1 \n\tLine 2\t\n".to_string();
+ let expected = "Line 1\nLine 2".to_string();
+ let Ok(result) = proc(input) else {
+ panic!("Parse error!");
+ };
+ assert_eq!(result, expected);
+ }
- #[test]
- fn test_clean_markdown_combined() {
- let input = "> Blockquote\n\n Line 1 \n> Another\n\nLine 2\n\n".to_string();
- let expected = "Line 1\nLine 2".to_string();
- let Ok(result) = clean_markdown(input) else {
- panic!("Parse error!");
- };
- assert_eq!(result, expected);
- }
+ #[test]
+ fn test_clean_markdown_combined() {
+ let input = "> Blockquote\n\n Line 1 \n> Another\n\nLine 2\n\n".to_string();
+ let expected = "Line 1\nLine 2".to_string();
+ let Ok(result) = proc(input) else {
+ panic!("Parse error!");
+ };
+ assert_eq!(result, expected);
+ }
- #[test]
- fn test_clean_markdown_empty_input() {
- let input = "".to_string();
- let expected = "".to_string();
- let Ok(result) = clean_markdown(input) else {
- panic!("Parse error!");
- };
- assert_eq!(result, expected);
- }
+ #[test]
+ fn test_clean_markdown_empty_input() {
+ let input = "".to_string();
+ let expected = "".to_string();
+ let Ok(result) = proc(input) else {
+ panic!("Parse error!");
+ };
+ assert_eq!(result, expected);
+ }
- #[test]
- fn test_clean_markdown_only_blockquotes() {
- let input = "> Quote 1\n> Quote 2".to_string();
- let expected = "".to_string();
- let Ok(result) = clean_markdown(input) else {
- panic!("Parse error!");
- };
- assert_eq!(result, expected);
- }
+ #[test]
+ fn test_clean_markdown_only_blockquotes() {
+ let input = "> Quote 1\n> Quote 2".to_string();
+ let expected = "".to_string();
+ let Ok(result) = proc(input) else {
+ panic!("Parse error!");
+ };
+ assert_eq!(result, expected);
+ }
- #[test]
- fn test_clean_markdown_only_whitespace() {
- let input = " \n\t\n ".to_string();
- let expected = "".to_string();
- let Ok(result) = clean_markdown(input) else {
- panic!("Parse error!");
- };
- assert_eq!(result, expected);
+ #[test]
+ fn test_clean_markdown_only_whitespace() {
+ let input = " \n\t\n ".to_string();
+ let expected = "".to_string();
+ let Ok(result) = proc(input) else {
+ panic!("Parse error!");
+ };
+ assert_eq!(result, expected);
+ }
}
}
-/// Fix jump syntax in each line
-/// 1. Correct the following syntax
-/// ```ignore
-/// - It's [Item](#Mark)
-/// > corrected to
-/// - It's Item [](#Mark)
-/// ```
-///
-/// 2. If there are multiple options, take the first one
-/// ```ignore
-/// - There might be two options: [A](#A) and [B](#B)!
-/// > corrected to
-/// - There might be two options: A and B! [](#A)
-/// ```
-pub fn fix_mark_jump(i: String) -> Result<String, Exit> {
- let mut result = String::new();
-
- for line in i.lines() {
- let (processed_content, first_link_dest) = helper_process_line_content(line);
- let processed_line = helper_format_line_with_link(processed_content, first_link_dest);
- let final_line = helper_convert_ordered_list_marker(processed_line);
-
- result.push_str(&final_line);
- result.push('\n');
- }
-
- if result.ends_with('\n') {
- result.pop();
- }
+pub mod markdown_jump_fix {
+ use crate::error::Exit;
+
+ /// Fix jump syntax in each line
+ /// 1. Correct the following syntax
+ /// ```ignore
+ /// - It's [Item](#Mark)
+ /// > corrected to
+ /// - It's Item [](#Mark)
+ /// ```
+ ///
+ /// 2. If there are multiple options, take the first one
+ /// ```ignore
+ /// - There might be two options: [A](#A) and [B](#B)!
+ /// > corrected to
+ /// - There might be two options: A and B! [](#A)
+ /// ```
+ pub fn proc(i: String) -> Result<String, Exit> {
+ let mut result = String::new();
+
+ for line in i.lines() {
+ let (processed_content, first_link_dest) = process_line_content(line);
+ let processed_line = format_line_with_link(processed_content, first_link_dest);
+ let final_line = convert_ordered_list_marker(processed_line);
+
+ result.push_str(&final_line);
+ result.push('\n');
+ }
- Ok(result)
-}
+ if result.ends_with('\n') {
+ result.pop();
+ }
-/// Process line content, extract link text and return the first link target
-///
-/// # Examples
-///
-/// ```
-/// use markdialog_parser::parse::helper_process_line_content;
-///
-/// // Single link
-/// let (content, dest) = helper_process_line_content("This is a [Link](#target) Example");
-/// assert_eq!(content, "This is a Link Example");
-/// assert_eq!(dest, Some("target".to_string()));
-///
-/// // Extract the first link
-/// let (content, dest) = helper_process_line_content("First [link1](#target1) and second [link2](#target2)");
-/// assert_eq!(content, "First link1 and second link2");
-/// assert_eq!(dest, Some("target1".to_string()));
-///
-/// // No link
-/// let (content, dest) = helper_process_line_content("Text without link");
-/// assert_eq!(content, "Text without link");
-/// assert_eq!(dest, None);
-///
-/// // Invalid link
-/// let (content, dest) = helper_process_line_content("Invalid [link format");
-/// assert_eq!(content, "Invalid [");
-/// assert_eq!(dest, None);
-///
-/// // Empty
-/// let (content, dest) = helper_process_line_content("");
-/// assert_eq!(content, "");
-/// assert_eq!(dest, None);
-///
-/// // Link target contains spaces and extra # symbols
-/// let (content, dest) = helper_process_line_content("Link[text](# target#)");
-/// assert_eq!(content, "Linktext");
-/// assert_eq!(dest, Some("target".to_string()));
-/// ```
-pub fn helper_process_line_content(line: &str) -> (String, Option<String>) {
- // Check if line is an image line (starts with "![")
- if line.starts_with("![") {
- // Return the original line unchanged with no link destination
- return (line.to_string(), None);
+ Ok(result)
}
- let mut processed = String::new();
- let mut chars = line.chars().peekable();
- let mut first_link_dest = None;
- let mut has_link = false;
-
- while let Some(ch) = chars.next() {
- if ch == '[' {
- if let Some((link_text, link_dest, remaining_chars)) = helper_parse_link(&mut chars) {
- processed.push_str(&link_text);
- if !has_link {
- first_link_dest = Some(link_dest);
- has_link = true;
+ pub fn process_line_content(line: &str) -> (String, Option<String>) {
+ // Check if line is an image line (starts with "![")
+ if line.starts_with("![") {
+ // Return the original line unchanged with no link destination
+ return (line.to_string(), None);
+ }
+
+ let mut processed = String::new();
+ let mut chars = line.chars().peekable();
+ let mut first_link_dest = None;
+ let mut has_link = false;
+
+ while let Some(ch) = chars.next() {
+ if ch == '[' {
+ if let Some((link_text, link_dest, remaining_chars)) = helper_parse_link(&mut chars)
+ {
+ processed.push_str(&link_text);
+ if !has_link {
+ first_link_dest = Some(link_dest);
+ has_link = true;
+ }
+ chars = remaining_chars;
+ continue;
+ } else {
+ // Invalid
+ processed.push(ch);
}
- chars = remaining_chars;
- continue;
} else {
- // Invalid
processed.push(ch);
}
- } else {
- processed.push(ch);
}
+
+ (processed, first_link_dest)
}
- (processed, first_link_dest)
-}
+ pub fn helper_parse_link<'a>(
+ chars: &mut std::iter::Peekable<std::str::Chars<'a>>,
+ ) -> Option<(String, String, std::iter::Peekable<std::str::Chars<'a>>)> {
+ let mut link_text = String::new();
-/// Parse possible Markdown links, return (link text, link target, remaining character iterator)
-///
-/// # Examples
-///
-/// ```
-/// use markdialog_parser::parse::helper_parse_link;
-///
-/// // Standard Link
-/// let mut chars = "[Link](#target)".chars().peekable();
-/// chars.next(); // Skip '['
-/// let result = helper_parse_link(&mut chars);
-/// assert!(result.is_some());
-/// let (text, dest, _) = result.unwrap();
-/// assert_eq!(text, "Link");
-/// assert_eq!(dest, "target");
-///
-/// // Link text contains spaces
-/// let mut chars = "[Link text](#target)".chars().peekable();
-/// chars.next();
-/// let result = helper_parse_link(&mut chars);
-/// assert!(result.is_some());
-/// let (text, dest, _) = result.unwrap();
-/// assert_eq!(text, "Link text");
-/// assert_eq!(dest, "target");
-///
-/// // Link target contains spaces and extra # symbols
-/// let mut chars = "[text](# target#)".chars().peekable();
-/// chars.next();
-/// let result = helper_parse_link(&mut chars);
-/// assert!(result.is_some());
-/// let (text, dest, _) = result.unwrap();
-/// assert_eq!(text, "text");
-/// assert_eq!(dest, "target");
-///
-/// // Invalid format: missing ']'
-/// let mut chars = "[Link(#target)".chars().peekable();
-/// chars.next();
-/// let result = helper_parse_link(&mut chars);
-/// assert!(result.is_none());
-///
-/// // Invalid format: missing '(#'
-/// let mut chars = "[Link]target)".chars().peekable();
-/// chars.next();
-/// let result = helper_parse_link(&mut chars);
-/// assert!(result.is_none());
-///
-/// // Invalid format: missing ')'
-/// let mut chars = "[Link](#target".chars().peekable();
-/// chars.next();
-/// let result = helper_parse_link(&mut chars);
-/// assert!(result.is_some());
-/// let (text, dest, _) = result.unwrap();
-/// assert_eq!(text, "Link");
-/// assert_eq!(dest, "target");
-/// ```
-pub fn helper_parse_link<'a>(
- chars: &mut std::iter::Peekable<std::str::Chars<'a>>,
-) -> Option<(String, String, std::iter::Peekable<std::str::Chars<'a>>)> {
- let mut link_text = String::new();
-
- while let Some(&ch) = chars.peek() {
- chars.next();
- if ch == ']' {
- break;
+ while let Some(&ch) = chars.peek() {
+ chars.next();
+ if ch == ']' {
+ break;
+ }
+ link_text.push(ch);
}
- link_text.push(ch);
- }
- if chars.next() != Some('(') || chars.next() != Some('#') {
- return None;
- }
+ if chars.next() != Some('(') || chars.next() != Some('#') {
+ return None;
+ }
- let mut link_dest = String::new();
- while let Some(ch) = chars.next() {
- if ch == ')' {
- break;
+ let mut link_dest = String::new();
+ while let Some(ch) = chars.next() {
+ if ch == ')' {
+ break;
+ }
+ link_dest.push(ch);
}
- link_dest.push(ch);
- }
- let cleaned_dest = link_dest.trim().replace(' ', "").replace('#', "");
+ let cleaned_dest = link_dest.trim().replace(' ', "").replace('#', "");
- Some((link_text, cleaned_dest, chars.clone()))
-}
+ Some((link_text, cleaned_dest, chars.clone()))
+ }
-/// If there is a link dest, add a jump marker at the end of the line
-///
-/// # Examples
-///
-/// ```
-/// use markdialog_parser::parse::helper_format_line_with_link;
-///
-/// // With a link dest
-/// let content = "Some content".to_string();
-/// let link_dest = Some("target".to_string());
-/// let result = helper_format_line_with_link(content, link_dest);
-/// assert_eq!(result, "Some content [](#target)");
-///
-/// // With empty content and a link dest
-/// let content = "".to_string();
-/// let link_dest = Some("target".to_string());
-/// let result = helper_format_line_with_link(content, link_dest);
-/// assert_eq!(result, "[](#target)");
-///
-/// // With trailing spaces in content
-/// let content = "Content with spaces ".to_string();
-/// let link_dest = Some("target".to_string());
-/// let result = helper_format_line_with_link(content, link_dest);
-/// assert_eq!(result, "Content with spaces [](#target)");
-///
-/// // Without a link dest
-/// let content = "Some content".to_string();
-/// let link_dest = None;
-/// let result = helper_format_line_with_link(content, link_dest);
-/// assert_eq!(result, "Some content");
-///
-/// // With an empty link dest
-/// let content = "Some content".to_string();
-/// let link_dest = Some("".to_string());
-/// let result = helper_format_line_with_link(content, link_dest);
-/// assert_eq!(result, "Some content");
-///
-/// // With whitespace-only link dest
-/// let content = "Some content".to_string();
-/// let link_dest = Some(" ".to_string());
-/// let result = helper_format_line_with_link(content, link_dest);
-/// assert_eq!(result, "Some content");
-/// ```
-pub fn helper_format_line_with_link(content: String, link_dest: Option<String>) -> String {
- match link_dest {
- Some(dest) if !dest.trim().is_empty() => {
- format!("{} [](#{})", content.trim_end(), dest.trim())
- .trim()
- .to_string()
+ pub fn format_line_with_link(content: String, link_dest: Option<String>) -> String {
+ match link_dest {
+ Some(dest) if !dest.trim().is_empty() => {
+ format!("{} [](#{})", content.trim_end(), dest.trim())
+ .trim()
+ .to_string()
+ }
+ _ => content,
}
- _ => content,
}
-}
-/// Convert ordered list markers to unordered list markers
-///
-/// # Examples
-///
-/// ```
-/// use markdialog_parser::parse::helper_convert_ordered_list_marker;
-///
-/// // Basic conversion
-/// let input = "1. First item".to_string();
-/// let result = helper_convert_ordered_list_marker(input);
-/// assert_eq!(result, "- First item");
-///
-/// // Multi-digit numbers
-/// let input = "10. Tenth item".to_string();
-/// let result = helper_convert_ordered_list_marker(input);
-/// assert_eq!(result, "- Tenth item");
-///
-/// // With leading spaces
-/// let input = " 2. Second item".to_string();
-/// let result = helper_convert_ordered_list_marker(input);
-/// assert_eq!(result, "- Second item");
-///
-/// // Not an ordered list marker (no dot and space)
-/// let input = "1.Not a list".to_string();
-/// let result = helper_convert_ordered_list_marker(input);
-/// assert_eq!(result, "1.Not a list");
-///
-/// // Not an ordered list marker (different spacing)
-/// let input = "1. Extra space".to_string();
-/// let result = helper_convert_ordered_list_marker(input);
-/// assert_eq!(result, "- Extra space");
-///
-/// // Already unordered list
-/// let input = "- Already unordered".to_string();
-/// let result = helper_convert_ordered_list_marker(input);
-/// assert_eq!(result, "- Already unordered");
-///
-/// // Regular text
-/// let input = "This is not a list".to_string();
-/// let result = helper_convert_ordered_list_marker(input);
-/// assert_eq!(result, "This is not a list");
-///
-/// // Empty string
-/// let input = "".to_string();
-/// let result = helper_convert_ordered_list_marker(input);
-/// assert_eq!(result, "");
-///
-/// // Only whitespace
-/// let input = " ".to_string();
-/// let result = helper_convert_ordered_list_marker(input);
-/// assert_eq!(result, " ");
-/// ```
-pub fn helper_convert_ordered_list_marker(line: String) -> String {
- let trimmed = line.trim_start();
-
- if let Some(_rest) = trimmed.strip_prefix(|c: char| c.is_ascii_digit()) {
- let mut chars = trimmed.chars();
- let mut digit_count = 0;
-
- while let Some(c) = chars.next() {
- if c.is_ascii_digit() {
- digit_count += 1;
- } else {
- break;
+ pub fn convert_ordered_list_marker(line: String) -> String {
+ let trimmed = line.trim_start();
+
+ if let Some(_rest) = trimmed.strip_prefix(|c: char| c.is_ascii_digit()) {
+ let mut chars = trimmed.chars();
+ let mut digit_count = 0;
+
+ while let Some(c) = chars.next() {
+ if c.is_ascii_digit() {
+ digit_count += 1;
+ } else {
+ break;
+ }
}
- }
- if digit_count > 0 {
- let rest_after_digits = &trimmed[digit_count..];
- if let Some(content) = rest_after_digits.strip_prefix(". ") {
- return format!("- {}", content);
+ if digit_count > 0 {
+ let rest_after_digits = &trimmed[digit_count..];
+ if let Some(content) = rest_after_digits.strip_prefix(". ") {
+ return format!("- {}", content);
+ }
}
}
+
+ line
}
- line
-}
+ #[cfg(test)]
+ mod test_fix_mark_jump {
+ use super::*;
-#[cfg(test)]
-mod test_fix_mark_jump {
- use super::*;
-
- #[test]
- fn test_fix_mark_jump_single_link() {
- let input = "- It's [Item](#Mark)".to_string();
- let expected = "- It's Item [](#Mark)".to_string();
- let Ok(result) = fix_mark_jump(input) else {
- panic!("Parse error!");
- };
- assert_eq!(result, expected);
- }
+ #[test]
+ fn test_fix_mark_jump_single_link() {
+ let input = "- It's [Item](#Mark)".to_string();
+ let expected = "- It's Item [](#Mark)".to_string();
+ let Ok(result) = proc(input) else {
+ panic!("Parse error!");
+ };
+ assert_eq!(result, expected);
+ }
- #[test]
- fn test_fix_mark_jump_multiple_links_takes_first() {
- let input = "- There might be two options: [A](#A) and [B](#B)!".to_string();
- let expected = "- There might be two options: A and B! [](#A)".to_string();
- let Ok(result) = fix_mark_jump(input) else {
- panic!("Parse error!");
- };
- assert_eq!(result, expected);
- }
+ #[test]
+ fn test_fix_mark_jump_multiple_links_takes_first() {
+ let input = "- There might be two options: [A](#A) and [B](#B)!".to_string();
+ let expected = "- There might be two options: A and B! [](#A)".to_string();
+ let Ok(result) = proc(input) else {
+ panic!("Parse error!");
+ };
+ assert_eq!(result, expected);
+ }
- #[test]
- fn test_fix_mark_jump_no_link() {
- let input = "- Just a normal line".to_string();
- let expected = "- Just a normal line".to_string();
- let Ok(result) = fix_mark_jump(input) else {
- panic!("Parse error!");
- };
- assert_eq!(result, expected);
- }
+ #[test]
+ fn test_fix_mark_jump_no_link() {
+ let input = "- Just a normal line".to_string();
+ let expected = "- Just a normal line".to_string();
+ let Ok(result) = proc(input) else {
+ panic!("Parse error!");
+ };
+ assert_eq!(result, expected);
+ }
- #[test]
- fn test_fix_mark_jump_empty_line() {
- let input = "".to_string();
- let expected = "".to_string();
- let Ok(result) = fix_mark_jump(input) else {
- panic!("Parse error!");
- };
- assert_eq!(result, expected);
- }
+ #[test]
+ fn test_fix_mark_jump_empty_line() {
+ let input = "".to_string();
+ let expected = "".to_string();
+ let Ok(result) = proc(input) else {
+ panic!("Parse error!");
+ };
+ assert_eq!(result, expected);
+ }
- #[test]
- fn test_fix_mark_jump_multiple_lines() {
- let input = "- First [Item](#First)\n- Second [Item](#Second)".to_string();
- let expected = "- First Item [](#First)\n- Second Item [](#Second)".to_string();
- let Ok(result) = fix_mark_jump(input) else {
- panic!("Parse error!");
- };
- assert_eq!(result, expected);
- }
+ #[test]
+ fn test_fix_mark_jump_multiple_lines() {
+ let input = "- First [Item](#First)\n- Second [Item](#Second)".to_string();
+ let expected = "- First Item [](#First)\n- Second Item [](#Second)".to_string();
+ let Ok(result) = proc(input) else {
+ panic!("Parse error!");
+ };
+ assert_eq!(result, expected);
+ }
- #[test]
- fn test_fix_mark_jump_link_at_end() {
- let input = "- End with [link](#target)".to_string();
- let expected = "- End with link [](#target)".to_string();
- let Ok(result) = fix_mark_jump(input) else {
- panic!("Parse error!");
- };
- assert_eq!(result, expected);
- }
+ #[test]
+ fn test_fix_mark_jump_link_at_end() {
+ let input = "- End with [link](#target)".to_string();
+ let expected = "- End with link [](#target)".to_string();
+ let Ok(result) = proc(input) else {
+ panic!("Parse error!");
+ };
+ assert_eq!(result, expected);
+ }
- #[test]
- fn test_fix_mark_jump_link_at_beginning() {
- let input = "- [Start](#target) with link".to_string();
- let expected = "- Start with link [](#target)".to_string();
- let Ok(result) = fix_mark_jump(input) else {
- panic!("Parse error!");
- };
- assert_eq!(result, expected);
- }
+ #[test]
+ fn test_fix_mark_jump_link_at_beginning() {
+ let input = "- [Start](#target) with link".to_string();
+ let expected = "- Start with link [](#target)".to_string();
+ let Ok(result) = proc(input) else {
+ panic!("Parse error!");
+ };
+ assert_eq!(result, expected);
+ }
- #[test]
- fn test_fix_mark_jump_link_in_middle() {
- let input = "- Text [middle](#target) text".to_string();
- let expected = "- Text middle text [](#target)".to_string();
- let Ok(result) = fix_mark_jump(input) else {
- panic!("Parse error!");
- };
- assert_eq!(result, expected);
- }
+ #[test]
+ fn test_fix_mark_jump_link_in_middle() {
+ let input = "- Text [middle](#target) text".to_string();
+ let expected = "- Text middle text [](#target)".to_string();
+ let Ok(result) = proc(input) else {
+ panic!("Parse error!");
+ };
+ assert_eq!(result, expected);
+ }
- #[test]
- fn test_fix_mark_jump_ordered_list_conversion() {
- let input = "1. [Item](#target)".to_string();
- let expected = "- Item [](#target)".to_string();
- let Ok(result) = fix_mark_jump(input) else {
- panic!("Parse error!");
- };
- assert_eq!(result, expected);
- }
+ #[test]
+ fn test_fix_mark_jump_ordered_list_conversion() {
+ let input = "1. [Item](#target)".to_string();
+ let expected = "- Item [](#target)".to_string();
+ let Ok(result) = proc(input) else {
+ panic!("Parse error!");
+ };
+ assert_eq!(result, expected);
+ }
- #[test]
- fn test_fix_mark_jump_ordered_list_multiple_digits() {
- let input = "10. [Tenth](#target) item".to_string();
- let expected = "- Tenth item [](#target)".to_string();
- let Ok(result) = fix_mark_jump(input) else {
- panic!("Parse error!");
- };
- assert_eq!(result, expected);
- }
+ #[test]
+ fn test_fix_mark_jump_ordered_list_multiple_digits() {
+ let input = "10. [Tenth](#target) item".to_string();
+ let expected = "- Tenth item [](#target)".to_string();
+ let Ok(result) = proc(input) else {
+ panic!("Parse error!");
+ };
+ assert_eq!(result, expected);
+ }
- #[test]
- fn test_fix_mark_jump_mixed_ordered_and_unordered() {
- let input = "1. [First](#first)\n- [Second](#second)\n2. [Third](#third)".to_string();
- let expected = "- First [](#first)\n- Second [](#second)\n- Third [](#third)".to_string();
- let Ok(result) = fix_mark_jump(input) else {
- panic!("Parse error!");
- };
- assert_eq!(result, expected);
- }
+ #[test]
+ fn test_fix_mark_jump_mixed_ordered_and_unordered() {
+ let input = "1. [First](#first)\n- [Second](#second)\n2. [Third](#third)".to_string();
+ let expected =
+ "- First [](#first)\n- Second [](#second)\n- Third [](#third)".to_string();
+ let Ok(result) = proc(input) else {
+ panic!("Parse error!");
+ };
+ assert_eq!(result, expected);
+ }
- #[test]
- fn test_fix_mark_jump_invalid_link_format() {
- let input = "- Invalid [link format".to_string();
- let expected = "- Invalid [".to_string();
- let Ok(result) = fix_mark_jump(input) else {
- panic!("Parse error!");
- };
- assert_eq!(result, expected);
- }
+ #[test]
+ fn test_fix_mark_jump_invalid_link_format() {
+ let input = "- Invalid [link format".to_string();
+ let expected = "- Invalid [".to_string();
+ let Ok(result) = proc(input) else {
+ panic!("Parse error!");
+ };
+ assert_eq!(result, expected);
+ }
- #[test]
- fn test_fix_mark_jump_link_with_spaces_in_target() {
- let input = "- Link [text](# target#)".to_string();
- let expected = "- Link text [](#target)".to_string();
- let Ok(result) = fix_mark_jump(input) else {
- panic!("Parse error!");
- };
- assert_eq!(result, expected);
- }
+ #[test]
+ fn test_fix_mark_jump_link_with_spaces_in_target() {
+ let input = "- Link [text](# target#)".to_string();
+ let expected = "- Link text [](#target)".to_string();
+ let Ok(result) = proc(input) else {
+ panic!("Parse error!");
+ };
+ assert_eq!(result, expected);
+ }
- #[test]
- fn test_fix_mark_jump_empty_link_text() {
- let input = "- [](#target)".to_string();
- let expected = "- [](#target)".to_string();
- let Ok(result) = fix_mark_jump(input) else {
- panic!("Parse error!");
- };
- assert_eq!(result, expected);
- }
+ #[test]
+ fn test_fix_mark_jump_empty_link_text() {
+ let input = "- [](#target)".to_string();
+ let expected = "- [](#target)".to_string();
+ let Ok(result) = proc(input) else {
+ panic!("Parse error!");
+ };
+ assert_eq!(result, expected);
+ }
- #[test]
- fn test_fix_mark_jump_only_whitespace() {
- let input = " ".to_string();
- let expected = " ".to_string();
- let Ok(result) = fix_mark_jump(input) else {
- panic!("Parse error!");
- };
- assert_eq!(result, expected);
- }
+ #[test]
+ fn test_fix_mark_jump_only_whitespace() {
+ let input = " ".to_string();
+ let expected = " ".to_string();
+ let Ok(result) = proc(input) else {
+ panic!("Parse error!");
+ };
+ assert_eq!(result, expected);
+ }
- #[test]
- fn test_fix_mark_jump_complex_multiple_links() {
- let input = "- Choose [A](#A), [B](#B), or [C](#C)!".to_string();
- let expected = "- Choose A, B, or C! [](#A)".to_string();
- let Ok(result) = fix_mark_jump(input) else {
- panic!("Parse error!");
- };
- assert_eq!(result, expected);
+ #[test]
+ fn test_fix_mark_jump_complex_multiple_links() {
+ let input = "- Choose [A](#A), [B](#B), or [C](#C)!".to_string();
+ let expected = "- Choose A, B, or C! [](#A)".to_string();
+ let Ok(result) = proc(input) else {
+ panic!("Parse error!");
+ };
+ assert_eq!(result, expected);
+ }
}
}
-/// Replace marker names: replace heading text and link anchors with corresponding SHA256
-///
-/// Example:
-/// ```ignore
-/// # Original text
-/// # Chapter Title
-/// - Jump to [Chapter Title](#Chapter Title)
-///
-/// # After processing
-/// # a1b2c3d4
-/// - Jump to [](#a1b2c3d4)
-/// ```
-pub fn replace_marker_name(i: String) -> Result<String, Exit> {
- let mut result = i;
-
- let heading_re = Regex::new(r"^(#{1,5})\s+(.+)$").unwrap();
- let mut heading_map = std::collections::HashMap::new();
-
- for line in result.lines() {
- if let Some(caps) = heading_re.captures(line) {
- let heading_text = caps[2].trim().to_string();
- let hash = format!("{:x}", Sha256::digest(heading_text.as_bytes()));
- let short_hash = &hash[..8];
- heading_map.insert(heading_text, short_hash.to_string());
+pub mod markdown_marker_rename {
+ use regex::Regex;
+ use sha2::{Digest, Sha256};
+
+ use crate::error::Exit;
+
+ /// Replace marker names: replace heading text and link anchors with corresponding SHA256
+ ///
+ /// Example:
+ /// ```ignore
+ /// # Original text
+ /// # Chapter Title
+ /// - Jump to [Chapter Title](#Chapter Title)
+ ///
+ /// # After processing
+ /// # a1b2c3d4
+ /// - Jump to [](#a1b2c3d4)
+ /// ```
+ pub fn proc(i: String) -> Result<String, Exit> {
+ let mut result = i;
+
+ let heading_re = Regex::new(r"^(#{1,5})\s+(.+)$").unwrap();
+ let mut heading_map = std::collections::HashMap::new();
+
+ for line in result.lines() {
+ if let Some(caps) = heading_re.captures(line) {
+ let heading_text = caps[2].trim().to_string();
+ let hash = format!("{:x}", Sha256::digest(heading_text.as_bytes()));
+ let short_hash = &hash[..8];
+ heading_map.insert(heading_text, short_hash.to_string());
+ }
}
- }
- let mut lines: Vec<String> = Vec::new();
- for line in result.lines() {
- if let Some(caps) = heading_re.captures(line) {
- let level = &caps[1];
- let heading_text = caps[2].trim();
+ let mut lines: Vec<String> = Vec::new();
+ for line in result.lines() {
+ if let Some(caps) = heading_re.captures(line) {
+ let level = &caps[1];
+ let heading_text = caps[2].trim();
- if let Some(hash) = heading_map.get(heading_text) {
- lines.push(format!("{} {}", level, hash));
+ if let Some(hash) = heading_map.get(heading_text) {
+ lines.push(format!("{} {}", level, hash));
+ } else {
+ lines.push(line.to_string());
+ }
} else {
lines.push(line.to_string());
}
- } else {
- lines.push(line.to_string());
}
+ result = lines.join("\n");
+
+ let link_re = Regex::new(r"\[\]\(#([^)]+)\)").unwrap();
+ result = link_re
+ .replace_all(&result, |caps: &regex::Captures| {
+ let anchor_name = &caps[1];
+ if let Some(hash) = heading_map.get(anchor_name) {
+ format!("[](#{})", hash)
+ } else {
+ let hash = format!("{:x}", Sha256::digest(anchor_name.as_bytes()));
+ let short_hash = &hash[..8];
+ format!("[](#{})", short_hash)
+ }
+ })
+ .to_string();
+
+ Ok(result)
}
- result = lines.join("\n");
-
- let link_re = Regex::new(r"\[\]\(#([^)]+)\)").unwrap();
- result = link_re
- .replace_all(&result, |caps: &regex::Captures| {
- let anchor_name = &caps[1];
- if let Some(hash) = heading_map.get(anchor_name) {
- format!("[](#{})", hash)
- } else {
- let hash = format!("{:x}", Sha256::digest(anchor_name.as_bytes()));
- let short_hash = &hash[..8];
- format!("[](#{})", short_hash)
+}
+
+pub mod markdown_struct_build {
+ use regex::Regex;
+
+ use crate::error::Exit;
+
+ /// Split content into Step + Sentence structure
+ pub fn proc(input: String) -> Result<String, Exit> {
+ let mut result = String::new();
+ let mut current_marker = String::new();
+ let mut current_step_id = 0;
+ let mut current_character = String::new();
+ let mut has_no_switch_flag = false;
+
+ let mut code_record_mode = false;
+ let mut option_record_mode = false;
+
+ let mut sentences_buffer = String::new();
+ for line in input.split("\n") {
+ // Record code
+ if code_record_mode {
+ // If code block marker is found again, end code recording
+ if line.starts_with("```") && code_record_mode {
+ sentences_buffer.push_str("\n");
+ code_record_mode = false;
+ continue;
+ }
+ sentences_buffer.push_str(format!("`{}`", line).as_str());
+ continue;
}
- })
- .to_string();
- Ok(result)
-}
+ // Record options
+ if option_record_mode {
+ // Still an option, continue appending
+ if line.starts_with("- ") {
+ let (sentence, next) = get_jump_from_line(line);
+ let next = if let Some(next) = next {
+ format!("->[#{}_0]", next)
+ } else {
+ next_flag(current_marker.as_str(), current_step_id)
+ };
+ let option_line = format!(
+ "{}[{}]{}",
+ character(&current_character, has_no_switch_flag),
+ sentence,
+ next
+ );
+ sentences_buffer.push_str(option_line.as_str());
+ sentences_buffer.push('\n');
+ continue;
+ } else {
+ // When ending option recording, create and advance one Step
+ result.push_str(step_line(current_marker.as_str(), current_step_id).as_str());
+ result.push('\n');
+ result.push_str(sentences_buffer.as_str());
+ sentences_buffer.clear();
+ current_step_id += 1;
+ // Clean "Has no switch flag"
+ has_no_switch_flag = false;
+ // Close option mode
+ option_record_mode = false;
+ // Do not continue here, proceed to process subsequent content
+ }
+ }
-/// Split content into Step + Sentence structure
-pub fn convert_to_step_sentence_structure(input: String) -> Result<String, Exit> {
- let mut result = String::new();
- let mut current_marker = String::new();
- let mut current_step_id = 0;
- let mut current_character = String::new();
- let mut has_no_switch_flag = false;
-
- let mut code_record_mode = false;
- let mut option_record_mode = false;
-
- let mut sentences_buffer = String::new();
- for line in input.split("\n") {
- // Record code
- if code_record_mode {
- // If code block marker is found again, end code recording
- if line.starts_with("```") && code_record_mode {
- sentences_buffer.push_str("\n");
- code_record_mode = false;
+ // Refresh heading
+ if is_marker(line) {
+ current_marker = read_maker(line).to_string();
+ current_step_id = 0;
+ continue;
+ }
+
+ // Refresh character
+ if is_character(line) {
+ let (character, no_switch_flag) = read_character(line);
+ current_character = character.to_string();
+ has_no_switch_flag = no_switch_flag;
+ continue;
+ }
+
+ // Image recording
+ if line.starts_with('!') {
+ sentences_buffer.push_str(line);
+ sentences_buffer.push('\n');
+ continue;
+ }
+
+ // Start code recording
+ if line.starts_with("```") && !code_record_mode {
+ code_record_mode = true;
continue;
}
- sentences_buffer.push_str(format!("`{}`", line).as_str());
- continue;
- }
- // Record options
- if option_record_mode {
- // Still an option, continue appending
+ // Option recording
if line.starts_with("- ") {
- let (sentence, next) = helper_get_jump_from_line(line);
+ let (sentence, next) = get_jump_from_line(line);
let next = if let Some(next) = next {
format!("->[#{}_0]", next)
} else {
@@ -797,482 +679,442 @@ pub fn convert_to_step_sentence_structure(input: String) -> Result<String, Exit>
);
sentences_buffer.push_str(option_line.as_str());
sentences_buffer.push('\n');
+
+ // Start option recording mode
+ if !option_record_mode {
+ option_record_mode = true;
+ }
continue;
- } else {
- // When ending option recording, create and advance one Step
- result.push_str(step_line(current_marker.as_str(), current_step_id).as_str());
- result.push('\n');
- result.push_str(sentences_buffer.as_str());
- sentences_buffer.clear();
- current_step_id += 1;
- // Clean "Has no switch flag"
- has_no_switch_flag = false;
- // Close option mode
- option_record_mode = false;
- // Do not continue here, proceed to process subsequent content
}
- }
- // Refresh heading
- if helper_is_marker(line) {
- current_marker = helper_read_maker(line).to_string();
- current_step_id = 0;
- continue;
- }
-
- // Refresh character
- if helper_is_character(line) {
- let (character, no_switch_flag) = helper_read_character(line);
- current_character = character.to_string();
- has_no_switch_flag = no_switch_flag;
- continue;
- }
-
- // Image recording
- if line.starts_with('!') {
- sentences_buffer.push_str(line);
- sentences_buffer.push('\n');
- continue;
- }
-
- // Start code recording
- if line.starts_with("```") && !code_record_mode {
- code_record_mode = true;
- continue;
- }
-
- // Option recording
- if line.starts_with("- ") {
- let (sentence, next) = helper_get_jump_from_line(line);
+ // Normal sentence
+ let (sentence, next) = get_jump_from_line(line);
let next = if let Some(next) = next {
format!("->[#{}_0]", next)
} else {
next_flag(current_marker.as_str(), current_step_id)
};
- let option_line = format!(
+ let sentence_line = format!(
"{}[{}]{}",
character(&current_character, has_no_switch_flag),
sentence,
next
);
- sentences_buffer.push_str(option_line.as_str());
- sentences_buffer.push('\n');
+ has_no_switch_flag = false;
- // Start option recording mode
- if !option_record_mode {
- option_record_mode = true;
- }
- continue;
+ // Create and advance one Step
+ result.push_str(step_line(current_marker.as_str(), current_step_id).as_str());
+ result.push('\n');
+ result.push_str(sentences_buffer.as_str());
+ sentences_buffer.clear();
+ result.push_str(sentence_line.as_str());
+ result.push('\n');
+ current_step_id += 1;
}
- // Normal sentence
- let (sentence, next) = helper_get_jump_from_line(line);
- let next = if let Some(next) = next {
- format!("->[#{}_0]", next)
- } else {
- next_flag(current_marker.as_str(), current_step_id)
- };
- let sentence_line = format!(
- "{}[{}]{}",
- character(&current_character, has_no_switch_flag),
- sentence,
- next
- );
- has_no_switch_flag = false;
-
- // Create and advance one Step
- result.push_str(step_line(current_marker.as_str(), current_step_id).as_str());
- result.push('\n');
- result.push_str(sentences_buffer.as_str());
- sentences_buffer.clear();
- result.push_str(sentence_line.as_str());
- result.push('\n');
- current_step_id += 1;
+ Ok(result)
}
- Ok(result)
-}
+ pub fn character(character: &str, has_no_switch_flag: bool) -> String {
+ let flag = if has_no_switch_flag { "*" } else { "" };
+ format!("[{}{}{}]:", &flag, character, &flag)
+ }
-pub fn character(character: &str, has_no_switch_flag: bool) -> String {
- let flag = if has_no_switch_flag { "*" } else { "" };
- format!("[{}{}{}]:", &flag, character, &flag)
-}
+ pub fn step_name(marker: &str, current_id: i64) -> String {
+ format!("{}_{}", marker, current_id)
+ }
-pub fn step_name(marker: &str, current_id: i64) -> String {
- format!("{}_{}", marker, current_id)
-}
+ pub fn step_line(marker: &str, current_id: i64) -> String {
+ format!("@@@@@@@@@@ {}_{}", marker, current_id)
+ }
-pub fn step_line(marker: &str, current_id: i64) -> String {
- format!("@@@@@@@@@@ {}_{}", marker, current_id)
-}
+ pub fn next_flag(marker: &str, current_id: i64) -> String {
+ format!("->[#{}_{}]", marker, current_id + 1)
+ }
-pub fn next_flag(marker: &str, current_id: i64) -> String {
- format!("->[#{}_{}]", marker, current_id + 1)
-}
+ pub fn is_marker(line: &str) -> bool {
+ line.starts_with("# ")
+ || line.starts_with("## ")
+ || line.starts_with("### ")
+ || line.starts_with("#### ")
+ || line.starts_with("##### ")
+ }
-pub fn helper_is_marker(line: &str) -> bool {
- line.starts_with("# ")
- || line.starts_with("## ")
- || line.starts_with("### ")
- || line.starts_with("#### ")
- || line.starts_with("##### ")
-}
+ pub fn read_maker(line: &str) -> &str {
+ let trimmed = line.trim_start();
+ if trimmed.starts_with('#') {
+ if trimmed.starts_with("# ")
+ || trimmed.starts_with("## ")
+ || trimmed.starts_with("### ")
+ || trimmed.starts_with("#### ")
+ || trimmed.starts_with("##### ")
+ {
+ let parts: Vec<&str> = trimmed.splitn(2, ' ').collect();
+ if parts.len() == 2 {
+ return parts[1].trim();
+ }
+ }
+ }
+ ""
+ }
-pub fn helper_read_maker(line: &str) -> &str {
- let trimmed = line.trim_start();
- if trimmed.starts_with('#') {
- if trimmed.starts_with("# ")
- || trimmed.starts_with("## ")
- || trimmed.starts_with("### ")
- || trimmed.starts_with("#### ")
- || trimmed.starts_with("##### ")
- {
+ pub fn is_character(line: &str) -> bool {
+ line.starts_with("######")
+ }
+
+ pub fn read_character(line: &str) -> (&str, bool) {
+ let trimmed = line.trim_start();
+ if trimmed.starts_with("######") {
let parts: Vec<&str> = trimmed.splitn(2, ' ').collect();
if parts.len() == 2 {
- return parts[1].trim();
+ let character = parts[1].trim();
+ if character.starts_with('*') && character.ends_with('*') {
+ let trimmed = character.trim_matches('*');
+ return (trimmed.trim(), true);
+ } else {
+ return (character.trim(), false);
+ }
}
}
+ ("", false)
}
- ""
-}
-pub fn helper_is_character(line: &str) -> bool {
- line.starts_with("######")
-}
-
-pub fn helper_read_character(line: &str) -> (&str, bool) {
- let trimmed = line.trim_start();
- if trimmed.starts_with("######") {
- let parts: Vec<&str> = trimmed.splitn(2, ' ').collect();
- if parts.len() == 2 {
- let character = parts[1].trim();
- if character.starts_with('*') && character.ends_with('*') {
- let trimmed = character.trim_matches('*');
- return (trimmed.trim(), true);
- } else {
- return (character.trim(), false);
- }
+ pub fn get_jump_from_line(line: &str) -> (String, Option<String>) {
+ let pattern = r"\[\]\(#([^)]+)\)$";
+ let re = Regex::new(pattern).unwrap();
+
+ if let Some(caps) = re.captures(line.trim_end()) {
+ let target = caps.get(1).unwrap().as_str();
+ let line_without_jump = line
+ .trim_end()
+ .replace(&format!(" [](#{})", target), "")
+ .to_string();
+ return (
+ line_without_jump.trim_start_matches("- ").to_string(),
+ Some(format!("{}", target)),
+ );
}
+
+ (line.trim_start_matches("- ").to_string(), None)
}
- ("", false)
}
-pub fn helper_get_jump_from_line(line: &str) -> (String, Option<String>) {
- let pattern = r"\[\]\(#([^)]+)\)$";
- let re = Regex::new(pattern).unwrap();
+pub mod markdown_strip_invalid_jump {
+ use crate::error::Exit;
+ use regex::Regex;
- if let Some(caps) = re.captures(line.trim_end()) {
- let target = caps.get(1).unwrap().as_str();
- let line_without_jump = line
- .trim_end()
- .replace(&format!(" [](#{})", target), "")
- .to_string();
- return (
- line_without_jump.trim_start_matches("- ").to_string(),
- Some(format!("{}", target)),
- );
- }
+ /// Strip all jumps that have not appeared
+ pub fn proc(input: String) -> Result<String, Exit> {
+ let lines: Vec<&str> = input.lines().collect();
+ let mut valid_ids = std::collections::HashSet::new();
- (line.trim_start_matches("- ").to_string(), None)
-}
+ for line in &lines {
+ if line.starts_with("@@@@@@@@@@ ") {
+ let id = line.trim_start_matches("@@@@@@@@@@ ").trim();
+ valid_ids.insert(id.to_string());
+ }
+ }
-/// Strip all jumps that have not appeared
-pub fn strip_invalid_jump(input: String) -> Result<String, Exit> {
- let lines: Vec<&str> = input.lines().collect();
- let mut valid_ids = std::collections::HashSet::new();
+ let mut result_lines = Vec::new();
+ let link_re = Regex::new(r"\[#([^)]+)\]").unwrap();
- for line in &lines {
- if line.starts_with("@@@@@@@@@@ ") {
- let id = line.trim_start_matches("@@@@@@@@@@ ").trim();
- valid_ids.insert(id.to_string());
+ for line in lines {
+ let processed_line = link_re.replace_all(line, |caps: &regex::Captures| {
+ let id = &caps[1];
+ if valid_ids.contains(id) {
+ format!("[#{}]", id)
+ } else {
+ "[]".to_string()
+ }
+ });
+ result_lines.push(processed_line.to_string());
}
+
+ Ok(result_lines.join("\n"))
}
+}
+
+pub mod markdown_convert_image {
+ use regex::Regex;
- let mut result_lines = Vec::new();
- let link_re = Regex::new(r"\[#([^)]+)\]").unwrap();
+ use crate::error::Exit;
- for line in lines {
- let processed_line = link_re.replace_all(line, |caps: &regex::Captures| {
- let id = &caps[1];
- if valid_ids.contains(id) {
- format!("[#{}]", id)
+ /// Convert image lines to code lines
+ pub fn proc(input: String) -> Result<String, Exit> {
+ let mut result = String::new();
+ let lines: Vec<&str> = input.lines().collect();
+ let image_re = Regex::new(r"^!\[[^\]]*\]\(([^)]+)\)$").unwrap();
+
+ for line in lines {
+ if let Some(caps) = image_re.captures(line) {
+ let image_path = caps.get(1).unwrap().as_str();
+ result.push_str(&format!("`image \"{}\"`\n", image_path));
} else {
- "[]".to_string()
+ result.push_str(line);
+ result.push('\n');
}
- });
- result_lines.push(processed_line.to_string());
- }
-
- Ok(result_lines.join("\n"))
-}
+ }
-/// Convert image lines to code lines
-pub fn convert_image_to_code(input: String) -> Result<String, Exit> {
- let mut result = String::new();
- let lines: Vec<&str> = input.lines().collect();
- let image_re = Regex::new(r"^!\[[^\]]*\]\(([^)]+)\)$").unwrap();
-
- for line in lines {
- if let Some(caps) = image_re.captures(line) {
- let image_path = caps.get(1).unwrap().as_str();
- result.push_str(&format!("`image \"{}\"`\n", image_path));
- } else {
- result.push_str(line);
- result.push('\n');
+ // Remove trailing newline if present
+ if result.ends_with('\n') {
+ result.pop();
}
- }
- // Remove trailing newline if present
- if result.ends_with('\n') {
- result.pop();
+ Ok(result)
}
-
- Ok(result)
}
-/// Apply code lines to sentences
-pub fn apply_code_lines(input: String) -> Result<String, Exit> {
- let mut out = String::new();
- let lines: Vec<&str> = input.lines().collect();
+pub mod markdown_apply_codes {
+ use crate::error::Exit;
- let mut i = 0;
- while i < lines.len() {
- let line = lines[i];
+ /// Apply code lines to sentences
+ pub fn proc(input: String) -> Result<String, Exit> {
+ let mut out = String::new();
+ let lines: Vec<&str> = input.lines().collect();
- if !line.trim_start().starts_with('`') {
- out.push_str(line);
- out.push('\n');
- i += 1;
- continue;
- }
+ let mut i = 0;
+ while i < lines.len() {
+ let line = lines[i];
- let mut code_buf = String::new();
- while i < lines.len() && {
- let line: &str = lines[i];
- line.trim_start().starts_with('`')
- } {
- code_buf.push_str(lines[i].trim());
- i += 1;
- }
+ if !line.trim_start().starts_with('`') {
+ out.push_str(line);
+ out.push('\n');
+ i += 1;
+ continue;
+ }
- if i >= lines.len()
- || !{
+ let mut code_buf = String::new();
+ while i < lines.len() && {
let line: &str = lines[i];
- line.trim_start().starts_with('[')
+ line.trim_start().starts_with('`')
+ } {
+ code_buf.push_str(lines[i].trim());
+ i += 1;
}
- {
- continue;
- }
-
- if i + 1 < lines.len() && {
- let line: &str = lines[i + 1];
- line.trim_start().starts_with('[')
- } {
- continue;
- }
- let merged = helper_merge_code_into_sentence(&code_buf, lines[i]);
- out.push_str(&merged);
- out.push('\n');
- i += 1;
- }
-
- Ok(out)
-}
+ if i >= lines.len()
+ || !{
+ let line: &str = lines[i];
+ line.trim_start().starts_with('[')
+ }
+ {
+ continue;
+ }
-fn helper_merge_code_into_sentence(code: &str, sentence: &str) -> String {
- if let Some(start) = sentence.find(":[") {
- if let Some(_) = sentence[start + 2..].find(']') {
- let content_start = start + 2;
+ if i + 1 < lines.len() && {
+ let line: &str = lines[i + 1];
+ line.trim_start().starts_with('[')
+ } {
+ continue;
+ }
- let mut result = String::new();
- result.push_str(&sentence[..content_start]);
- result.push_str(code);
- result.push_str(&sentence[content_start..]);
- return result;
+ let merged = merge_code_into_sentence(&code_buf, lines[i]);
+ out.push_str(&merged);
+ out.push('\n');
+ i += 1;
}
+
+ Ok(out)
}
- sentence.to_string()
-}
+ fn merge_code_into_sentence(code: &str, sentence: &str) -> String {
+ if let Some(start) = sentence.find(":[") {
+ if let Some(_) = sentence[start + 2..].find(']') {
+ let content_start = start + 2;
-/// Split sentences into embeddable tokens and perform Unicode encoding
-pub fn split_sentence_and_encode(input: String) -> Result<String, Exit> {
- let mut result = String::new();
- let lines: Vec<&str> = input.lines().collect();
-
- for line in lines {
- if line.starts_with('[') && line.contains("]:[") && line.contains("]->[") {
- if let Some(start) = line.find("]:[") {
- if let Some(end) = line.find("]->[") {
- let content = &line[start + 3..end];
- let processed_content = helper_process_sentence_content(content);
-
- let suffix = &line[end + 1..];
-
- let char_end = start;
- let char_start = 1;
- let character = &line[char_start..char_end];
- let encoded_character = helper_encode_unicode(character);
-
- // Build the new line with encoded character and processed content
- let new_line =
- format!("[{}]:{}{}", encoded_character, processed_content, suffix);
- result.push_str(&format!("{}\n", new_line));
- continue;
- }
+ let mut result = String::new();
+ result.push_str(&sentence[..content_start]);
+ result.push_str(code);
+ result.push_str(&sentence[content_start..]);
+ return result;
}
}
- result.push_str(&format!("{}\n", line));
- }
- if result.ends_with('\n') {
- result.pop();
+ sentence.to_string()
}
-
- Ok(result)
}
-fn helper_process_sentence_content(content: &str) -> String {
- let mut result = String::new();
- let mut chars = content.chars().peekable();
- let mut current_text = String::new();
- let mut in_code = false;
- let mut in_bold = false;
- let mut in_italic = false;
- let mut code_buffer = String::new();
- let mut backticks_count = 0;
-
- while let Some(ch) = chars.next() {
- match ch {
- '`' => {
- backticks_count += 1;
- if backticks_count == 1 {
- // Start of code block
- if !current_text.is_empty() {
- let encoded_text = helper_encode_unicode(&current_text);
- result.push_str(&format!("[text:[{}]]", encoded_text));
- current_text.clear();
+pub mod markdown_split_and_encode {
+ use crate::error::Exit;
+
+ /// Split sentences into embeddable tokens and perform Unicode encoding
+ pub fn proc(input: String) -> Result<String, Exit> {
+ let mut result = String::new();
+ let lines: Vec<&str> = input.lines().collect();
+
+ for line in lines {
+ if line.starts_with('[') && line.contains("]:[") && line.contains("]->[") {
+ if let Some(start) = line.find("]:[") {
+ if let Some(end) = line.find("]->[") {
+ let content = &line[start + 3..end];
+ let processed_content = process_sentence_content(content);
+
+ let suffix = &line[end + 1..];
+
+ let char_end = start;
+ let char_start = 1;
+ let character = &line[char_start..char_end];
+ let encoded_character = encode_unicode(character);
+
+ // Build the new line with encoded character and processed content
+ let new_line =
+ format!("[{}]:{}{}", encoded_character, processed_content, suffix);
+ result.push_str(&format!("{}\n", new_line));
+ continue;
}
- code_buffer.push(ch);
- in_code = true;
- } else if backticks_count == 2 && in_code {
- // End of code block
- code_buffer.push(ch);
- let encoded_code = helper_encode_unicode(&code_buffer);
- result.push_str(&format!("[code:[{}]]", encoded_code));
- code_buffer.clear();
- backticks_count = 0;
- in_code = false;
- } else if backticks_count == 1 && !in_code {
- // Single backtick in text
- current_text.push(ch);
}
}
- '*' => {
- if in_code {
- code_buffer.push(ch);
- continue;
- }
+ result.push_str(&format!("{}\n", line));
+ }
- // Check for bold
- if chars.peek() == Some(&'*') {
- chars.next(); // Consume the second '*'
+ if result.ends_with('\n') {
+ result.pop();
+ }
- if in_bold {
- // End bold
- if !current_text.is_empty() {
- let encoded_text = helper_encode_unicode(&current_text);
- result.push_str(&format!("[bold:[{}]]", encoded_text));
- current_text.clear();
- }
- in_bold = false;
- } else if in_italic {
- if !current_text.is_empty() {
- let encoded_text = helper_encode_unicode(&current_text);
- result.push_str(&format!("[italic:[{}]]", encoded_text));
- current_text.clear();
- }
- in_italic = false;
- // Start bold_italic
- in_bold = true;
- } else {
- // Start bold
+ Ok(result)
+ }
+
+ fn process_sentence_content(content: &str) -> String {
+ let mut result = String::new();
+ let mut chars = content.chars().peekable();
+ let mut current_text = String::new();
+ let mut in_code = false;
+ let mut in_bold = false;
+ let mut in_italic = false;
+ let mut code_buffer = String::new();
+ let mut backticks_count = 0;
+
+ while let Some(ch) = chars.next() {
+ match ch {
+ '`' => {
+ backticks_count += 1;
+ if backticks_count == 1 {
+ // Start of code block
if !current_text.is_empty() {
- let encoded_text = helper_encode_unicode(&current_text);
+ let encoded_text = encode_unicode(&current_text);
result.push_str(&format!("[text:[{}]]", encoded_text));
current_text.clear();
}
- in_bold = true;
+ code_buffer.push(ch);
+ in_code = true;
+ } else if backticks_count == 2 && in_code {
+ // End of code block
+ code_buffer.push(ch);
+ let encoded_code = encode_unicode(&code_buffer);
+ result.push_str(&format!("[code:[{}]]", encoded_code));
+ code_buffer.clear();
+ backticks_count = 0;
+ in_code = false;
+ } else if backticks_count == 1 && !in_code {
+ // Single backtick in text
+ current_text.push(ch);
}
- } else {
- if in_italic {
- // End italic
- if !current_text.is_empty() {
- let encoded_text = helper_encode_unicode(&current_text);
- result.push_str(&format!("[italic:[{}]]", encoded_text));
- current_text.clear();
- }
- in_italic = false;
- } else if in_bold {
- if !current_text.is_empty() {
- let encoded_text = helper_encode_unicode(&current_text);
- result.push_str(&format!("[bold:[{}]]", encoded_text));
- current_text.clear();
+ }
+ '*' => {
+ if in_code {
+ code_buffer.push(ch);
+ continue;
+ }
+
+ // Check for bold
+ if chars.peek() == Some(&'*') {
+ chars.next(); // Consume the second '*'
+
+ if in_bold {
+ // End bold
+ if !current_text.is_empty() {
+ let encoded_text = encode_unicode(&current_text);
+ result.push_str(&format!("[bold:[{}]]", encoded_text));
+ current_text.clear();
+ }
+ in_bold = false;
+ } else if in_italic {
+ if !current_text.is_empty() {
+ let encoded_text = encode_unicode(&current_text);
+ result.push_str(&format!("[italic:[{}]]", encoded_text));
+ current_text.clear();
+ }
+ in_italic = false;
+ // Start bold_italic
+ in_bold = true;
+ } else {
+ // Start bold
+ if !current_text.is_empty() {
+ let encoded_text = encode_unicode(&current_text);
+ result.push_str(&format!("[text:[{}]]", encoded_text));
+ current_text.clear();
+ }
+ in_bold = true;
}
- // Start bold_italic
- in_bold = true;
- in_italic = true;
} else {
- // Start italic
- if !current_text.is_empty() {
- let encoded_text = helper_encode_unicode(&current_text);
- result.push_str(&format!("[text:[{}]]", encoded_text));
- current_text.clear();
+ if in_italic {
+ // End italic
+ if !current_text.is_empty() {
+ let encoded_text = encode_unicode(&current_text);
+ result.push_str(&format!("[italic:[{}]]", encoded_text));
+ current_text.clear();
+ }
+ in_italic = false;
+ } else if in_bold {
+ if !current_text.is_empty() {
+ let encoded_text = encode_unicode(&current_text);
+ result.push_str(&format!("[bold:[{}]]", encoded_text));
+ current_text.clear();
+ }
+ // Start bold_italic
+ in_bold = true;
+ in_italic = true;
+ } else {
+ // Start italic
+ if !current_text.is_empty() {
+ let encoded_text = encode_unicode(&current_text);
+ result.push_str(&format!("[text:[{}]]", encoded_text));
+ current_text.clear();
+ }
+ in_italic = true;
}
- in_italic = true;
}
}
- }
- _ => {
- if in_code {
- code_buffer.push(ch);
- } else {
- current_text.push(ch);
+ _ => {
+ if in_code {
+ code_buffer.push(ch);
+ } else {
+ current_text.push(ch);
+ }
}
}
}
- }
- // Handle any remaining text
- if !code_buffer.is_empty() {
- let encoded_code = helper_encode_unicode(&code_buffer);
- result.push_str(&format!("[code:[{}]]", encoded_code));
- }
+ // Handle any remaining text
+ if !code_buffer.is_empty() {
+ let encoded_code = encode_unicode(&code_buffer);
+ result.push_str(&format!("[code:[{}]]", encoded_code));
+ }
- if !current_text.is_empty() {
- let style = match (in_bold, in_italic) {
- (true, true) => "bold_italic",
- (true, false) => "bold",
- (false, true) => "italic",
- (false, false) => "text",
- };
- let encoded_text = helper_encode_unicode(&current_text);
- result.push_str(&format!("[{}:[{}]]", style, encoded_text));
- }
+ if !current_text.is_empty() {
+ let style = match (in_bold, in_italic) {
+ (true, true) => "bold_italic",
+ (true, false) => "bold",
+ (false, true) => "italic",
+ (false, false) => "text",
+ };
+ let encoded_text = encode_unicode(&current_text);
+ result.push_str(&format!("[{}:[{}]]", style, encoded_text));
+ }
- result
-}
+ result
+ }
-fn helper_encode_unicode(s: &str) -> String {
- let mut result = String::new();
- for ch in s.chars() {
- let code = ch as u32;
- if code <= 0x7F {
- result.push(ch);
- } else {
- result.push_str(&format!("\\u{:X}", code));
+ fn encode_unicode(s: &str) -> String {
+ let mut result = String::new();
+ for ch in s.chars() {
+ let code = ch as u32;
+ if code <= 0x7F {
+ result.push(ch);
+ } else {
+ result.push_str(&format!("\\u{:X}", code));
+ }
}
+ result
}
- result
}
diff --git a/converter/src/syntax_checker.rs b/converter/src/syntax_checker.rs
index 334fa9d..52021c7 100644
--- a/converter/src/syntax_checker.rs
+++ b/converter/src/syntax_checker.rs
@@ -1,3 +1,5 @@
+use regex::Regex;
+
use crate::error::Exit;
pub fn check_markdown_syntax(i: &String) -> Result<(), Exit> {
@@ -199,3 +201,21 @@ pub fn check_markdown_syntax(i: &String) -> Result<(), Exit> {
Ok(())
}
+
+/// Check for duplicate markers
+pub fn check_duplicate_marker(input: &String) -> Result<(), Exit> {
+ let mut seen = std::collections::HashSet::new();
+ let heading_re = Regex::new(r"^(#{1,5})\s+(.+)$").unwrap();
+
+ for line in input.lines() {
+ if let Some(caps) = heading_re.captures(line) {
+ let heading_text = caps[2].trim().to_string();
+ if seen.contains(&heading_text) {
+ return Err(Exit::DuplicateMarker(heading_text));
+ }
+ seen.insert(heading_text);
+ }
+ }
+
+ Ok(())
+}
diff --git a/gen/Cargo.toml b/gen/Cargo.toml
new file mode 100644
index 0000000..52bba1c
--- /dev/null
+++ b/gen/Cargo.toml
@@ -0,0 +1,7 @@
+[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
new file mode 100644
index 0000000..59867f8
--- /dev/null
+++ b/gen/macros/Cargo.toml
@@ -0,0 +1,13 @@
+[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
new file mode 100644
index 0000000..f66b7a9
--- /dev/null
+++ b/gen/macros/src/lib.rs
@@ -0,0 +1,584 @@
+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)?;
+
+ // Check for formatting markers
+ let trimmed_content = decoded_content.trim_matches('`');
+
+ match label {
+ "text" => {
+ // Check text formatting
+ if trimmed_content.starts_with("**") && trimmed_content.ends_with("**") {
+ let inner = &trimmed_content[2..trimmed_content.len() - 2];
+ Ok(TokenData::BoldText(inner.to_string()))
+ } else if trimmed_content.starts_with("*") && trimmed_content.ends_with("*") {
+ let inner = &trimmed_content[1..trimmed_content.len() - 1];
+ Ok(TokenData::ItalicText(inner.to_string()))
+ } else if trimmed_content.starts_with("***") && trimmed_content.ends_with("***") {
+ let inner = &trimmed_content[3..trimmed_content.len() - 3];
+ Ok(TokenData::BoldItalicText(inner.to_string()))
+ } else {
+ Ok(TokenData::Text(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);
+ }
+
+ // 格式: [#step_id]
+ 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
new file mode 100644
index 0000000..833f39c
--- /dev/null
+++ b/gen/src/lib.rs
@@ -0,0 +1 @@
+pub use res_gen_macros::*;
diff --git a/player/Cargo.toml b/player/Cargo.toml
index 192dcfa..69108fd 100644
--- a/player/Cargo.toml
+++ b/player/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "markdialog_player"
-workspaces.version = true
edition = "2024"
+version.workspace = true
[dependencies]
diff --git a/player/src/lib.rs b/player/src/lib.rs
index 8b13789..4723252 100644
--- a/player/src/lib.rs
+++ b/player/src/lib.rs
@@ -1 +1,3 @@
-
+pub fn player_main() {
+ println!("Hello Markdialog Player")
+}
diff --git a/src/lib.rs b/src/lib.rs
index 584b845..6d48a91 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,14 +1,14 @@
// markdialog::player
-// pub mod player {
-// pub use markdialog_player::*;
-// }
+pub mod player {
+ pub use markdialog_player::*;
+}
// markdialog::converter
pub mod converter {
pub use markdialog_converter::*;
}
-// markdialog::res
-pub mod res {
- pub use built_res::*;
+// markdialog::generate
+pub mod generate {
+ pub use res_gen::*;
}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..e6f342c
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,4 @@
+fn main() {
+ // `cargo run` will invoke player
+ markdialog_player::player_main();
+}