summaryrefslogtreecommitdiff
path: root/converter/src
diff options
context:
space:
mode:
Diffstat (limited to 'converter/src')
-rw-r--r--converter/src/bin/mdialogc.rs95
-rw-r--r--converter/src/error.rs121
-rw-r--r--converter/src/lib.rs5
-rw-r--r--converter/src/macros.rs33
-rw-r--r--converter/src/parse.rs1303
-rw-r--r--converter/src/syntax_checker.rs221
-rw-r--r--converter/src/utils.rs1
-rw-r--r--converter/src/utils/path_fmt.rs123
8 files changed, 0 insertions, 1902 deletions
diff --git a/converter/src/bin/mdialogc.rs b/converter/src/bin/mdialogc.rs
deleted file mode 100644
index ebe7804..0000000
--- a/converter/src/bin/mdialogc.rs
+++ /dev/null
@@ -1,95 +0,0 @@
-use markdialog_converter::{
- error::{Exit, handle_exit},
- parse::parse,
- special_argument, special_flag,
-};
-use std::{path::PathBuf, str::FromStr};
-
-fn process() -> Result<(), Exit> {
- let mut args: Vec<String> = std::env::args().skip(1).collect();
-
- let help = special_flag!(args, "--help") || special_flag!(args, "-h");
- let version = special_flag!(args, "--version") || special_flag!(args, "-v");
-
- if version {
- let version = include_str!("../../version.txt");
- println!("{}", version.trim());
- return Err(Exit::Code(0));
- }
-
- if help || args.len() < 1 {
- let usage = include_str!("../../usage.txt");
- println!("{}", usage.trim());
- return Err(Exit::Code(0));
- }
-
- let input_file = get_input_file(&mut args)?;
- let output_ir_file = get_output_ir_file(&mut args)?.unwrap_or_else(|| {
- let mut path = input_file.clone();
- if let Some(file_name) = path.file_name() {
- let mut new_name = std::ffi::OsString::new();
- new_name.push(file_name);
- // Change extension to .dialog
- path.set_extension("dialog");
- } else {
- path.set_file_name("ir.dialog");
- }
- path
- });
-
- parse(input_file, output_ir_file)?;
-
- Ok(())
-}
-
-fn get_input_file(args: &mut Vec<String>) -> Result<PathBuf, Exit> {
- let input = match special_argument!(args, "--input") {
- Some(i) => i,
- None => match special_argument!(args, "-i") {
- Some(i) => i,
- None => {
- eprintln!("Missing required input argument. Use --input or -i.");
- std::process::exit(2);
- }
- },
- };
-
- let input_file = PathBuf::from_str(&input).map_err(|_| {
- eprintln!("Invalid file path `{}`!", input);
- Exit::Code(2)
- })?;
-
- Ok(input_file)
-}
-
-fn get_output_ir_file(args: &mut Vec<String>) -> Result<Option<PathBuf>, Exit> {
- let input = match special_argument!(args, "--output") {
- Some(i) => Some(i),
- None => match special_argument!(args, "-o") {
- Some(i) => Some(i),
- None => None,
- },
- };
-
- match input {
- Some(i) => {
- let input_file = PathBuf::from_str(&i).map_err(|_| {
- eprintln!("Invalid file path `{}`!", i);
- return Exit::Code(2);
- })?;
- Ok(Some(input_file))
- }
- None => Ok(None),
- }
-}
-
-fn main() {
- // Init colored
- #[cfg(windows)]
- colored::control::set_virtual_terminal(true).unwrap();
-
- match process() {
- Ok(_) => {}
- Err(e) => handle_exit(e),
- }
-}
diff --git a/converter/src/error.rs b/converter/src/error.rs
deleted file mode 100644
index b594165..0000000
--- a/converter/src/error.rs
+++ /dev/null
@@ -1,121 +0,0 @@
-use std::{i64, path::PathBuf, process::exit};
-
-use colored::Colorize;
-use unicode_width::UnicodeWidthStr;
-
-#[derive(Debug)]
-pub enum Exit {
- Code(i32),
- IoError(std::io::Error),
- FileNotFound(PathBuf),
- SyntaxError {
- content: String,
- reason: String,
- line: i64,
- begin: i64,
- end: i64,
- },
- DuplicateMarker(String),
- CycleDependency(PathBuf),
-}
-
-impl From<std::io::Error> for Exit {
- fn from(error: std::io::Error) -> Self {
- Exit::IoError(error)
- }
-}
-
-pub fn handle_exit(e: Exit) {
- match e {
- Exit::Code(code) => exit(code),
- Exit::IoError(error) => print_parse_error(error.to_string()),
- Exit::FileNotFound(path_buf) => {
- eprintln!("File `{}` not found!", path_buf.display());
- exit(1)
- }
- Exit::SyntaxError {
- content,
- reason,
- line,
- begin,
- end,
- } => {
- print_syntax_error(content, reason, line, begin, end);
- }
- Exit::DuplicateMarker(marker) => {
- eprintln!("Duplicate marker `{}` found!", marker);
- exit(1)
- }
- Exit::CycleDependency(dialog) => {
- eprintln!("Dialog `{}` depends on itself!", dialog.display());
- exit(1)
- }
- }
-}
-
-fn print_parse_error(content: impl AsRef<str>) {
- eprintln!("Parse Error !");
- eprintln!("{}", content.as_ref().trim());
- exit(1);
-}
-
-macro_rules! line {
- ($line:expr, $N:expr) => {
- if $line + $N <= 0 {
- " ".to_string()
- } else {
- ($line + $N).to_string()
- }
- };
-}
-
-pub fn print_syntax_error(content: String, reason: String, line: i64, begin: i64, end: i64) {
- let content_len = content.width() as i64;
- let end = end.clamp(begin, content_len);
-
- eprintln!("{}", "Parse Failed: Syntax Error".bright_yellow());
- eprintln!("{}{}", line!(line, -1), "|");
-
- let before: String = content.chars().take(begin.max(0) as usize).collect();
- let highlight_len = (end - begin).max(1) as usize;
- let highlight: String = content
- .chars()
- .skip(begin.max(0) as usize)
- .take(highlight_len)
- .collect();
- let after: String = content
- .chars()
- .skip((begin.max(0) + highlight_len as i64) as usize)
- .collect();
-
- eprintln!(
- "{}{} {}{}{}",
- line.to_string().cyan(),
- "|".cyan(),
- before.cyan(),
- highlight.bright_cyan(),
- after.cyan()
- );
-
- let prefix_chars: String = content.chars().take(begin.max(0) as usize).collect();
- let prefix_width = prefix_chars.width() as usize;
-
- eprintln!(
- "{}{} {}",
- line!(line, 1),
- "|",
- format!(
- "{}{}____ {}",
- " ".repeat(prefix_width),
- "^".repeat(((end - begin).max(1)) as usize),
- reason
- )
- .bright_cyan()
- );
- eprintln!("{}{}", line!(line, 2), "|");
- eprintln!(
- "{}",
- "Please fix the issue and run the program again".bright_yellow()
- );
- exit(1);
-}
diff --git a/converter/src/lib.rs b/converter/src/lib.rs
deleted file mode 100644
index d7caac3..0000000
--- a/converter/src/lib.rs
+++ /dev/null
@@ -1,5 +0,0 @@
-pub mod error;
-pub mod macros;
-pub mod parse;
-pub mod syntax_checker;
-pub mod utils;
diff --git a/converter/src/macros.rs b/converter/src/macros.rs
deleted file mode 100644
index 894b3f4..0000000
--- a/converter/src/macros.rs
+++ /dev/null
@@ -1,33 +0,0 @@
-#[macro_export]
-macro_rules! special_flag {
- ($args:expr, $flag:expr) => {{
- let flag = $flag;
- let found = $args.iter().any(|arg| arg == flag);
- $args.retain(|arg| arg != flag);
- found
- }};
-}
-
-#[macro_export]
-macro_rules! special_argument {
- ($args:expr, $flag:expr) => {{
- let flag = $flag;
- let mut value: Option<String> = None;
- let mut i = 0;
- while i < $args.len() {
- if $args[i] == flag {
- if i + 1 < $args.len() {
- value = Some($args[i + 1].clone());
- $args.remove(i + 1);
- $args.remove(i);
- } else {
- value = None;
- $args.remove(i);
- }
- break;
- }
- i += 1;
- }
- value
- }};
-}
diff --git a/converter/src/parse.rs b/converter/src/parse.rs
deleted file mode 100644
index 292a761..0000000
--- a/converter/src/parse.rs
+++ /dev/null
@@ -1,1303 +0,0 @@
-use std::path::PathBuf;
-
-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::proc(result, input)?;
-
- check_duplicate_marker(&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(())
-}
-
-pub mod unwrap_includes {
- use crate::{error::Exit, utils::path_fmt::format_path};
- use regex::Regex;
- use std::collections::HashMap;
- use std::path::{Path, PathBuf};
-
- /// Expand text includes of [[markdown]] (searches for markdown.md) and image paths
- pub fn proc(input: String, self_path: PathBuf) -> Result<String, Exit> {
- let mut stack = Vec::<PathBuf>::new();
- let mut cache = HashMap::<String, PathBuf>::new();
- let mut img_cache = HashMap::<String, PathBuf>::new();
- let root_path = self_path.clone();
- expand_recursive(
- input,
- &self_path,
- &root_path,
- &mut stack,
- &mut cache,
- &mut img_cache,
- )
- }
-
- fn expand_recursive(
- content: String,
- current_path: &Path,
- root_path: &Path,
- stack: &mut Vec<PathBuf>,
- cache: &mut HashMap<String, PathBuf>,
- img_cache: &mut HashMap<String, PathBuf>,
- ) -> Result<String, Exit> {
- let mut output = String::new();
- let mut in_code_block = false;
- let obsidian_image_re = Regex::new(r"!\[\[([^\]]+)\]\]").unwrap();
-
- let current_norm = format_path(current_path)?;
-
- if stack.contains(&current_norm) {
- return Err(Exit::CycleDependency(current_norm));
- }
-
- stack.push(current_norm.clone());
-
- for line in content.lines() {
- if line.trim().starts_with("```") {
- in_code_block = !in_code_block;
- output.push_str(line);
- output.push('\n');
- continue;
- }
-
- if in_code_block {
- output.push_str(line);
- output.push('\n');
- continue;
- }
-
- if let Some(include_name) = extract_include(line) {
- let include_abs = if let Some(cached_path) = cache.get(include_name) {
- cached_path.clone()
- } else {
- let base_dir = current_path.parent().unwrap();
- let mut queue = vec![base_dir.to_path_buf()];
- let mut visited = std::collections::HashSet::new();
- let mut found_path = None;
-
- while let Some(dir) = queue.pop() {
- if visited.contains(&dir) {
- continue;
- }
- visited.insert(dir.clone());
-
- let candidate1 = dir.join(include_name);
- if candidate1.exists()
- && candidate1.extension().map_or(false, |ext| ext == "md")
- {
- found_path = Some(format_path(&candidate1)?);
- break;
- }
-
- // Try add .md extension
- let candidate2 = dir.join(format!("{}.md", include_name));
- if candidate2.exists() {
- found_path = Some(format_path(&candidate2)?);
- break;
- }
-
- if let Some(parent) = dir.parent() {
- queue.push(parent.to_path_buf());
- }
-
- if let Ok(entries) = std::fs::read_dir(&dir) {
- for entry in entries.flatten() {
- if let Ok(file_type) = entry.file_type() {
- if file_type.is_dir() {
- queue.push(entry.path());
- }
- }
- }
- }
- }
-
- match found_path {
- Some(path) => {
- cache.insert(include_name.to_string(), path.clone());
- path
- }
- None => {
- return Err(Exit::FileNotFound(
- format!(
- "{} or {}.md (searched from {:?})",
- include_name, include_name, base_dir
- )
- .into(),
- ));
- }
- }
- };
-
- 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,
- root_path,
- stack,
- cache,
- img_cache,
- )?;
- output.push_str(&expanded);
- } else {
- // Process image links
- let mut processed_line = line.to_string();
-
- // First process Obsidian image syntax ![[...]]
- let mut obsidian_matches: Vec<(usize, usize, String)> = Vec::new();
-
- for caps in obsidian_image_re.captures_iter(line) {
- let full_match = caps.get(0).unwrap();
- let image_ref = caps.get(1).unwrap().as_str();
-
- // Try to get from img_cache first
- let image_abs = if let Some(cached_path) = img_cache.get(image_ref) {
- cached_path.clone()
- } else {
- // Start search from root directory for Obsidian syntax
- let root_dir = root_path.parent().unwrap();
- let mut queue = vec![root_dir.to_path_buf()];
- let mut visited = std::collections::HashSet::new();
- let mut found_image_path = None;
-
- while let Some(dir) = queue.pop() {
- if visited.contains(&dir) {
- continue;
- }
- visited.insert(dir.clone());
-
- // Check if file exists in this directory
- let candidate = dir.join(image_ref);
- if candidate.exists() {
- found_image_path = Some(format_path(&candidate)?);
- break;
- }
-
- // Add parent directory to queue (breadth-first upward)
- if let Some(parent) = dir.parent() {
- queue.push(parent.to_path_buf());
- }
-
- // Add subdirectories to queue (breadth-first downward)
- if let Ok(entries) = std::fs::read_dir(&dir) {
- for entry in entries.flatten() {
- if let Ok(file_type) = entry.file_type() {
- if file_type.is_dir() {
- queue.push(entry.path());
- }
- }
- }
- }
- }
-
- match found_image_path {
- Some(path) => {
- img_cache.insert(image_ref.to_string(), path.clone());
- path
- }
- None => {
- // If image not found, keep the original syntax
- continue;
- }
- }
- };
-
- // Store the replacement information
- let start = full_match.start();
- let end = full_match.end();
- let new_image_markdown = format!("![]({})", image_abs.display());
- obsidian_matches.push((start, end, new_image_markdown));
- }
-
- // Apply Obsidian replacements in reverse order to maintain correct indices
- for (start, end, replacement) in obsidian_matches.into_iter().rev() {
- processed_line.replace_range(start..end, &replacement);
- }
-
- output.push_str(&processed_line);
- output.push('\n');
- }
- }
-
- stack.pop();
-
- Ok(output)
- }
-
- fn extract_include(line: &str) -> Option<&str> {
- line.trim()
- .strip_prefix("[[")
- .and_then(|s| s.strip_suffix("]]"))
- }
-}
-
-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());
- }
-
- Ok(cleaned.join("\n"))
- }
-
- #[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) = 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) = 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) = 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) = 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) = 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) = 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) = proc(input) else {
- panic!("Parse error!");
- };
- assert_eq!(result, expected);
- }
- }
-}
-
-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');
- }
-
- if result.ends_with('\n') {
- result.pop();
- }
-
- Ok(result)
- }
-
- 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);
- }
- } else {
- processed.push(ch);
- }
- }
-
- (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();
-
- while let Some(&ch) = chars.peek() {
- chars.next();
- if ch == ']' {
- break;
- }
- link_text.push(ch);
- }
-
- if chars.next() != Some('(') || chars.next() != Some('#') {
- return None;
- }
-
- let mut link_dest = String::new();
- while let Some(ch) = chars.next() {
- if ch == ')' {
- break;
- }
- link_dest.push(ch);
- }
-
- let cleaned_dest = link_dest.trim().replace(' ', "").replace('#', "");
-
- Some((link_text, cleaned_dest, chars.clone()))
- }
-
- 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,
- }
- }
-
- 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);
- }
- }
- }
-
- line
- }
-
- #[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) = 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) = 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) = 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) = 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) = 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) = 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) = 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) = 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) = 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) = 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) = 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) = 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) = 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) = 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) = 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) = proc(input) else {
- panic!("Parse error!");
- };
- assert_eq!(result, expected);
- }
- }
-}
-
-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();
-
- 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());
- }
- }
- 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)
- }
-}
-
-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;
- }
-
- // 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
- }
- }
-
- // 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;
- }
-
- // Option recording
- 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');
-
- // Start option recording mode
- if !option_record_mode {
- option_record_mode = true;
- }
- continue;
- }
-
- // 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 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)
- }
-
- 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_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 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 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 {
- 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 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)
- }
-}
-
-pub mod markdown_strip_invalid_jump {
- use crate::error::Exit;
- use regex::Regex;
-
- /// 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();
-
- for line in &lines {
- if line.starts_with("@@@@@@@@@@ ") {
- let id = line.trim_start_matches("@@@@@@@@@@ ").trim();
- valid_ids.insert(id.to_string());
- }
- }
-
- let mut result_lines = Vec::new();
- let link_re = Regex::new(r"\[#([^)]+)\]").unwrap();
-
- 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;
-
- use crate::error::Exit;
-
- /// 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 {
- result.push_str(line);
- result.push('\n');
- }
- }
-
- // Remove trailing newline if present
- if result.ends_with('\n') {
- result.pop();
- }
-
- Ok(result)
- }
-}
-
-pub mod markdown_apply_codes {
- use crate::error::Exit;
-
- /// 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();
-
- let mut i = 0;
- while i < lines.len() {
- let line = lines[i];
-
- if !line.trim_start().starts_with('`') {
- out.push_str(line);
- out.push('\n');
- i += 1;
- continue;
- }
-
- 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 i >= lines.len()
- || !{
- let line: &str = lines[i];
- line.trim_start().starts_with('[')
- }
- {
- continue;
- }
-
- if i + 1 < lines.len() && {
- let line: &str = lines[i + 1];
- line.trim_start().starts_with('[')
- } {
- continue;
- }
-
- let merged = merge_code_into_sentence(&code_buf, lines[i]);
- out.push_str(&merged);
- out.push('\n');
- i += 1;
- }
-
- Ok(out)
- }
-
- 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;
-
- let mut result = String::new();
- result.push_str(&sentence[..content_start]);
- result.push_str(code);
- result.push_str(&sentence[content_start..]);
- return result;
- }
- }
-
- sentence.to_string()
- }
-}
-
-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;
- }
- }
- }
- result.push_str(&format!("{}\n", line));
- }
-
- if result.ends_with('\n') {
- result.pop();
- }
-
- 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 = encode_unicode(&current_text);
- result.push_str(&format!("[text:[{}]]", encoded_text));
- current_text.clear();
- }
- 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);
- }
- }
- '*' => {
- if in_code {
- code_buffer.push(ch);
- continue;
- }
-
- // Check for bold
- if chars.peek() == Some(&'*') {
- chars.next(); // Consume the second '*'
-
- // Check for bold_italic (***)
- if chars.peek() == Some(&'*') {
- chars.next(); // Consume the third '*'
-
- if in_bold && in_italic {
- // End bold_italic
- if !current_text.is_empty() {
- let encoded_text = encode_unicode(&current_text);
- result.push_str(&format!("[bold_italic:[{}]]", encoded_text));
- current_text.clear();
- }
- in_bold = false;
- in_italic = false;
- } else {
- // Start bold_italic
- 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;
- in_italic = true;
- }
- } else {
- // Handle ** (bold)
- if in_bold && !in_italic {
- // 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 {
- // Currently in italic, encountering ** means we need to end italic and start bold_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;
- 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;
- }
- }
- } else {
- // Single * (italic)
- if in_italic && !in_bold {
- // 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 {
- // Currently in bold, encountering * means we need to start bold_italic
- if !current_text.is_empty() {
- let encoded_text = encode_unicode(&current_text);
- result.push_str(&format!("[bold:[{}]]", encoded_text));
- current_text.clear();
- }
- 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;
- }
- }
- }
- _ => {
- if in_code {
- code_buffer.push(ch);
- } else {
- current_text.push(ch);
- }
- }
- }
- }
-
- // 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 = encode_unicode(&current_text);
- result.push_str(&format!("[{}:[{}]]", style, encoded_text));
- }
-
- result
- }
-
- 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
- }
-}
diff --git a/converter/src/syntax_checker.rs b/converter/src/syntax_checker.rs
deleted file mode 100644
index 52021c7..0000000
--- a/converter/src/syntax_checker.rs
+++ /dev/null
@@ -1,221 +0,0 @@
-use regex::Regex;
-
-use crate::error::Exit;
-
-pub fn check_markdown_syntax(i: &String) -> Result<(), Exit> {
- let mut stack = Vec::new();
- let lines: Vec<&str> = i.lines().collect();
- let mut anchors = Vec::new();
- let mut heading_ids = Vec::new();
-
- for (line_num, line) in lines.iter().enumerate() {
- let line_num = line_num as i64 + 1;
-
- // Check for headings to collect anchor IDs
- if line.starts_with('#') {
- let heading_text = line.trim_start_matches('#').trim();
- let id = heading_text
- .to_lowercase()
- .chars()
- .filter(|c| c.is_alphanumeric() || *c == '-' || *c == '_')
- .collect::<String>();
- if !id.is_empty() {
- heading_ids.push(id);
- }
- }
-
- let mut chars = line.chars().enumerate().peekable();
- while let Some((pos, ch)) = chars.next() {
- let pos = pos as i64 + 1;
-
- match ch {
- '[' => {
- // Check if it's a link or image
- let is_image = chars.peek().map(|&(_, c)| c) == Some('!');
- if is_image {
- chars.next(); // Skip '!'
- }
- stack.push(('['.to_string(), line_num, pos, is_image));
- }
- ']' => {
- if let Some((last, _l, b, is_image)) = stack.pop() {
- if last != "[" {
- return Err(Exit::SyntaxError {
- content: line.to_string(),
- reason: format!(
- "Mismatched bracket: expected '[' but found '{}'",
- last
- ),
- line: line_num,
- begin: b,
- end: pos,
- });
- }
- // Check if it's followed by '(' for a link
- if chars.peek().map(|&(_, c)| c) == Some('(') {
- chars.next(); // Skip '('
- // Look for closing ')'
- let mut found = false;
- let mut anchor_started = false;
- let mut anchor = String::new();
- while let Some((_, c)) = chars.next() {
- if c == ')' {
- found = true;
- break;
- }
- if c == '#' && !anchor_started {
- anchor_started = true;
- continue;
- }
- if anchor_started {
- anchor.push(c);
- }
- }
- if !found {
- return Err(Exit::SyntaxError {
- content: line.to_string(),
- reason: "Link parentheses not closed".to_string(),
- line: line_num,
- begin: pos,
- end: pos,
- });
- }
- if !anchor.is_empty() {
- // Remove whitespace from anchor
- let anchor = anchor.replace(|c: char| c.is_whitespace(), "");
- anchors.push((anchor, line_num, pos));
- }
- } else if !is_image {
- // It's a reference link, collect the anchor
- // Check for anchor like [](#anchor)
- if chars.peek().map(|&(_, c)| c) == Some('(') {
- chars.next(); // Skip '('
- if chars.peek().map(|&(_, c)| c) == Some('#') {
- chars.next(); // Skip '#'
- let mut anchor = String::new();
- while let Some(&(_, c)) = chars.peek() {
- if c == ')' {
- break;
- }
- anchor.push(c);
- chars.next();
- }
- if !anchor.is_empty() {
- // Remove whitespace from anchor
- let anchor =
- anchor.replace(|c: char| c.is_whitespace(), "");
- anchors.push((anchor, line_num, pos));
- }
- }
- }
- }
- } else {
- return Err(Exit::SyntaxError {
- content: line.to_string(),
- reason: "Unmatched ']'".to_string(),
- line: line_num,
- begin: pos,
- end: pos,
- });
- }
- }
- '(' => {
- // Check for standalone anchor like (#anchor)
- if chars.peek().map(|&(_, c)| c) == Some('#') {
- chars.next(); // Skip '#'
- let mut anchor = String::new();
- while let Some(&(_, c)) = chars.peek() {
- if c == ')' {
- break;
- }
- anchor.push(c);
- chars.next();
- }
- if !anchor.is_empty() {
- // Remove whitespace from anchor
- let anchor = anchor.replace(|c: char| c.is_whitespace(), "");
- anchors.push((anchor, line_num, pos));
- }
- } else {
- stack.push(('('.to_string(), line_num, pos, false));
- }
- }
- ')' => {
- if let Some((last, _l, b, _)) = stack.pop() {
- if last != "(" {
- return Err(Exit::SyntaxError {
- content: line.to_string(),
- reason: format!(
- "Mismatched parenthesis: expected '(' but found '{}'",
- last
- ),
- line: line_num,
- begin: b,
- end: pos,
- });
- }
- } else {
- return Err(Exit::SyntaxError {
- content: line.to_string(),
- reason: "Unmatched ')'".to_string(),
- line: line_num,
- begin: pos,
- end: pos,
- });
- }
- }
- '`' => {
- // Check for backticks
- let mut count = 1;
- while chars.peek().map(|&(_, c)| c) == Some('`') {
- count += 1;
- chars.next();
- }
- let marker = "`".repeat(count);
-
- if let Some((last, _, _, _)) = stack.last() {
- if last == &marker {
- stack.pop();
- } else {
- stack.push((marker.clone(), line_num, pos, false));
- }
- } else {
- stack.push((marker, line_num, pos, false));
- }
- }
- _ => {}
- }
- }
- }
-
- // Check for unclosed brackets/parentheses
- if let Some((last, line, begin, _)) = stack.pop() {
- return Err(Exit::SyntaxError {
- content: lines[(line - 1) as usize].to_string(),
- reason: format!("Unclosed '{}'", last),
- line,
- begin,
- end: begin,
- });
- }
-
- 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/converter/src/utils.rs b/converter/src/utils.rs
deleted file mode 100644
index 0fbb516..0000000
--- a/converter/src/utils.rs
+++ /dev/null
@@ -1 +0,0 @@
-pub mod path_fmt;
diff --git a/converter/src/utils/path_fmt.rs b/converter/src/utils/path_fmt.rs
deleted file mode 100644
index 8750db6..0000000
--- a/converter/src/utils/path_fmt.rs
+++ /dev/null
@@ -1,123 +0,0 @@
-use std::path::{Path, PathBuf};
-
-/// Normalize an input path string into a canonical, platform‑agnostic form.
-///
-/// This function removes ANSI escape sequences, unifies separators to `/`,
-/// collapses duplicate slashes, strips unfriendly characters (`*`, `?`, `"`, `<`, `>`, `|`),
-/// resolves simple `..` components, and preserves a trailing slash when present.
-///
-/// See examples below for the exact normalization behavior.
-///
-/// # Examples
-///
-/// ```
-/// # use string_proc::format_path::format_path_str;
-/// use std::io::Error;
-///
-/// # fn main() -> Result<(), Error> {
-/// assert_eq!(format_path_str("C:\\Users\\\\test")?, "C:/Users/test");
-/// assert_eq!(
-/// format_path_str("/path/with/*unfriendly?chars")?,
-/// "/path/with/unfriendlychars"
-/// );
-/// assert_eq!(format_path_str("\x1b[31m/path\x1b[0m")?, "/path");
-/// assert_eq!(format_path_str("/home/user/dir/")?, "/home/user/dir/");
-/// assert_eq!(
-/// format_path_str("/home/user/file.txt")?,
-/// "/home/user/file.txt"
-/// );
-/// assert_eq!(
-/// format_path_str("/home/my_user/DOCS/JVCS_TEST/Workspace/../Vault/")?,
-/// "/home/my_user/DOCS/JVCS_TEST/Vault/"
-/// );
-/// assert_eq!(format_path_str("./home/file.txt")?, "home/file.txt");
-/// assert_eq!(format_path_str("./home/path/")?, "home/path/");
-/// assert_eq!(format_path_str("./")?, "");
-/// # Ok(())
-/// # }
-/// ```
-pub fn format_path_str(path: impl Into<String>) -> Result<String, std::io::Error> {
- let path_str = path.into();
- let ends_with_slash = path_str.ends_with('/');
-
- // ANSI Strip
- let cleaned = strip_ansi_escapes::strip(&path_str);
- let path_without_ansi = String::from_utf8(cleaned)
- .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
-
- let path_with_forward_slash = path_without_ansi.replace('\\', "/");
- let mut result = String::new();
- let mut prev_char = '\0';
-
- for c in path_with_forward_slash.chars() {
- if c == '/' && prev_char == '/' {
- continue;
- }
- result.push(c);
- prev_char = c;
- }
-
- let unfriendly_chars = ['*', '?', '"', '<', '>', '|'];
- result = result
- .chars()
- .filter(|c| !unfriendly_chars.contains(c))
- .collect();
-
- // Handle ".." path components
- let path_buf = PathBuf::from(&result);
- let normalized_path = normalize_path(&path_buf);
- result = normalized_path.to_string_lossy().replace('\\', "/");
-
- // Restore trailing slash if original path had one
- if ends_with_slash && !result.ends_with('/') {
- result.push('/');
- }
-
- // Special case: when result is only "./", return ""
- if result == "./" {
- return Ok(String::new());
- }
-
- Ok(result)
-}
-
-/// Normalize path by resolving ".." components without requiring file system access
-fn normalize_path(path: &Path) -> PathBuf {
- let mut components = Vec::new();
-
- for component in path.components() {
- match component {
- std::path::Component::ParentDir => {
- if !components.is_empty() {
- components.pop();
- }
- }
- std::path::Component::CurDir => {
- // Skip current directory components
- }
- _ => {
- components.push(component);
- }
- }
- }
-
- if components.is_empty() {
- PathBuf::from(".")
- } else {
- components.iter().collect()
- }
-}
-
-/// Format a [`PathBuf`] into its canonical string form and convert it back.
-///
-/// This is a convenience wrapper around [`format_path_str`], preserving
-/// the semantics of [`PathBuf`] while applying the same normalization rules:
-/// - normalize separators to `/`
-/// - remove duplicated separators
-/// - strip ANSI escape sequences
-/// - remove unfriendly characters (`*`, `?`, etc.)
-/// - resolve simple `..` segments
-pub fn format_path(path: impl Into<PathBuf>) -> Result<PathBuf, std::io::Error> {
- let path_str = format_path_str(path.into().display().to_string())?;
- Ok(PathBuf::from(path_str))
-}