diff --git a/.gitignore b/.gitignore index eb5a316..9087ecb 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ target +.aider* diff --git a/Cargo.lock b/Cargo.lock index 50f4d18..fd806ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,20 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "const-random", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -66,6 +80,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + [[package]] name = "base64" version = "0.22.1" @@ -207,6 +227,26 @@ dependencies = [ "xdg", ] +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom", + "once_cell", + "tiny-keccak", +] + [[package]] name = "crc32fast" version = "1.4.2" @@ -216,6 +256,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "darling" version = "0.20.10" @@ -302,6 +348,8 @@ name = "draftsmith_render" version = "0.1.0" dependencies = [ "comrak", + "regex", + "rhai", ] [[package]] @@ -352,6 +400,17 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "hashbrown" version = "0.15.0" @@ -380,6 +439,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -437,6 +505,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.20.2" @@ -546,6 +623,34 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rhai" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61797318be89b1a268a018a92a7657096d83f3ecb31418b9e9c16dcbb043b702" +dependencies = [ + "ahash", + "bitflags 2.6.0", + "instant", + "num-traits", + "once_cell", + "rhai_codegen", + "smallvec", + "smartstring", + "thin-vec", +] + +[[package]] +name = "rhai_codegen" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5a11a05ee1ce44058fa3d5961d05194fdbe3ad6b40f904af764d81b86450e6b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "rustix" version = "0.38.38" @@ -628,6 +733,29 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "smartstring" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" +dependencies = [ + "autocfg", + "static_assertions", + "version_check", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.1" @@ -678,6 +806,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "thin-vec" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a38c90d48152c236a3ab59271da4f4ae63d678c5d7ad6b7714d7cb9760be5e4b" + [[package]] name = "thiserror" version = "1.0.65" @@ -729,6 +863,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinyvec" version = "1.8.0" @@ -777,6 +920,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "walkdir" version = "2.5.0" @@ -787,6 +936,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "wasm-bindgen" version = "0.2.95" @@ -947,3 +1102,23 @@ checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" dependencies = [ "linked-hash-map", ] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index ea8bb0a..c466efc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,3 +5,5 @@ edition = "2021" [dependencies] comrak = "0.29.0" +regex = "1.11.1" +rhai = "1.19.0" diff --git a/src/bin/md_example.rs b/src/bin/md_example.rs index 85a2520..2048828 100644 --- a/src/bin/md_example.rs +++ b/src/bin/md_example.rs @@ -1,15 +1,54 @@ use draftsmith_render::replace_text; const DOC: &str = r#" +
+this is inline html +
+ +:::foo + + +this is an admonition block + + +::: + + + # Heading # Admonitions -
-

Also maps closer to tailwind css (easier for me)

-
+:::tip +This works + + :::foo + Also maps closer to tailwind css (easier for me) + ::: + +```rust +fn main() { + println!("Hello, world!"); +} +``` + +::: + +This is some code: + +```markdown +:::tip +This works + +::: +Also maps closer to tailwind css (easier for me) +::: + +::: + +``` # Basic Text @@ -74,14 +113,9 @@ fn main() { let doc = DOC; let orig = "my"; let repl = "your"; + let test_string = std::fs::read_to_string("tests/fixtures/input_divs_code_and_inline_code.md").unwrap(); + let expected = std::fs::read_to_string("tests/fixtures/expected_output_divs_code_and_inline_code.md").unwrap().trim_end_matches('\n').to_string(); let html = replace_text(&doc, &orig, &repl); - println!("{}", html); - // Output: - // - //

This is your input.

- //
    - //
  1. Also your input.
  2. - //
  3. Certainly your input.
  4. - //
+ // println!("{}", html); } diff --git a/src/lib.rs b/src/lib.rs index 649a9bf..80cf65c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,9 @@ +mod processor; + use comrak::nodes::NodeValue; use comrak::{format_html, parse_document, Arena, Options}; use comrak::{ComrakOptions, ExtensionOptions, ParseOptions, RenderOptions}; +use processor::Processor; pub fn add(left: u64, right: u64) -> u64 { left + right @@ -32,6 +35,11 @@ pub fn replace_text(document: &str, orig_string: &str, replacement: &str) -> Str let mut options = Options::default(); config_opts(&mut options); + // Preprocess the document + let mut processor = Processor::default(); + let document = processor.process(document).as_str(); + + // get the AST let root = parse_document(&arena, document, &options); diff --git a/src/processor.rs b/src/processor.rs new file mode 100644 index 0000000..de523fb --- /dev/null +++ b/src/processor.rs @@ -0,0 +1,245 @@ +//! This module provides functionality for processing markdown-like text +//! with custom admonitions and code blocks. + +use regex::Regex; +use rhai::{Engine, Scope}; + +const ADMONITION_START_PATTERN: &str = r"^\s*:::([\w!\{\}-]+)$"; +const ADMONITION_END_PATTERN: &str = r"^\s*(:::)$"; +const CODE_START_PATTERN: &str = r"^\s*```\{rhai\}$"; +const RHAI_DISPLAY_START_PATTERN: &str = r"^\s*```\{rhai-display\}$"; +const CODE_END_PATTERN: &str = r"^\s*```$"; +const LAMBDA_PATTERN: &str = r"λ#\(((?s).*?)\)#"; + +/// A processor for handling custom markdown-like syntax. +pub struct Processor<'a> { + admonition_start_regex: Regex, + admonition_end_regex: Regex, + code_start_regex: Regex, + rhai_display_start_regex: Regex, + code_end_regex: Regex, + lambda_regex: Regex, + div_stack: Vec, + eval_stack: bool, + is_rhai_display: bool, + contents: Vec, + rhai_engine: Engine, + rhai_scope: Scope<'a>, +} + +impl<'a> Default for Processor<'a> { + fn default() -> Self { + Self { + admonition_start_regex: Regex::new(ADMONITION_START_PATTERN) + .expect("Failed to compile regex"), + admonition_end_regex: Regex::new(ADMONITION_END_PATTERN) + .expect("Failed to compile regex"), + code_start_regex: Regex::new(CODE_START_PATTERN).expect("Failed to compile regex"), + rhai_display_start_regex: Regex::new(RHAI_DISPLAY_START_PATTERN) + .expect("Failed to compile regex"), + code_end_regex: Regex::new(CODE_END_PATTERN).expect("Failed to compile regex"), + lambda_regex: Regex::new(LAMBDA_PATTERN).expect("Failed to compile regex"), + div_stack: Vec::new(), + eval_stack: false, + is_rhai_display: false, + contents: Vec::new(), + rhai_engine: Engine::new(), + rhai_scope: Scope::new(), + } + } +} + +impl<'a> Processor<'a> { + /// Processes the input string and returns the transformed output. + /// + /// # Arguments + /// + /// * `input` - A string slice that holds the text to be processed. + /// + /// # Returns + /// + /// A `String` containing the processed text with custom syntax transformed. + pub fn process(&mut self, input: &str) -> String { + input + .lines() + .map(|line| self.process_line(line)) + .collect::>() + .join("") + .trim_end_matches('\n') + .to_string() + } + + /// Evaluates Rhai code and returns a formatted string of the results. + /// + /// # Arguments + /// + /// * `engine` - A reference to the Rhai Engine. + /// * `scope` - A mutable reference to the Rhai Scope. + /// * `captured` - A string slice containing the Rhai code to evaluate. + /// + /// # Returns + /// + /// A `String` containing the formatted evaluation results. + fn process_lambda(engine: &Engine, scope: &mut Scope, captured: &str) -> String { + match engine.eval_with_scope::(scope, captured) { + Ok(result) => format!("{}", result), + Err(err) => format!("Error: {}", err), + } + } + + /// Processes a single line of text. + /// + /// # Arguments + /// + /// * `line` - A string slice that holds the line to be processed. + /// + /// # Returns + /// + /// A `String` containing the processed line. + fn process_line(&mut self, line: &str) -> String { + if self.code_start_regex.is_match(line) { + self.handle_code_start(false) + } else if self.rhai_display_start_regex.is_match(line) { + self.handle_code_start(true) + } else if self.code_end_regex.is_match(line) { + self.handle_code_end() + } else if self.admonition_start_regex.is_match(line) { + if let Some(caps) = self.admonition_start_regex.captures(line) { + self.handle_admonition_start(&caps[1]) + } else { + String::new() + } + } else if self.admonition_end_regex.is_match(line) { + self.handle_admonition_end() + } else { + self.handle_regular_line(line) + } + } + + /// Handles the start of an admonition block. + /// + /// # Arguments + /// + /// * `class` - The class of the admonition. + /// + /// # Returns + /// + /// A `String` containing the opening HTML div tag for the admonition. + fn handle_admonition_start(&mut self, class: &str) -> String { + if class.is_empty() { + return String::new(); // Return an empty string for empty admonitions + } + let html = match class { + "alert" => format!("
"), + "info" => format!("
"), + "success" => format!("
"), + "warning" => format!("
"), + "error" => format!("
"), + "tip" => format!("
"), + "fold" => format!("
"), + "summary" => format!(""), + "col" => format!("
"), + "card" => format!("
"), + _ => format!("
", class), + }; + self.div_stack.push(class.to_string()); + format!("{}\n", html) + } + + /// Handles the end of an admonition block. + /// + /// # Returns + /// + /// A `String` containing the closing HTML div tag for the admonition. + fn handle_admonition_end(&mut self) -> String { + if self.div_stack.pop().is_some() { + "
\n".to_string() + } else { + String::from(":::\n") + } + } + + /// Handles the start of a code block. + /// + /// # Returns + /// + /// An empty `String` as the code block start is not directly output. + fn handle_code_start(&mut self, is_display: bool) -> String { + self.eval_stack = true; + self.is_rhai_display = is_display; + self.contents.clear(); // Clear any previous contents + String::new() + } + + /// Handles the end of a code block. + /// + /// # Returns + /// + /// A `String` containing the evaluated code output wrapped in HTML. + fn handle_code_end(&mut self) -> String { + if self.eval_stack { + self.eval_stack = false; + if !self.contents.is_empty() { + let code = self.contents.join("\n"); + let results = Self::process_lambda(&self.rhai_engine, &mut self.rhai_scope, &code); + self.contents.clear(); + + if self.is_rhai_display { + if results.trim().is_empty() { + return String::new(); + } else { + format!( + "
\n```rust\n{}\n```\n
\n```\n{}\n```\n
\n
\n", + code, results + ) + } + } else { + return String::new(); + } + } else { + String::new() + } + } else { + String::new() + } + } + + /// Handles a regular line of text. + /// + /// # Arguments + /// + /// * `line` - A string slice that holds the line to be processed. + /// + /// # Returns + /// + /// A `String` containing the processed line. + fn handle_regular_line(&mut self, line: &str) -> String { + if self.eval_stack { + self.contents.push(line.to_string()); + String::new() // Return an empty string when in eval_stack mode + } else { + let mut result = String::new(); + let mut last_end = 0; + let mut scope = self.rhai_scope.clone(); + let engine = &self.rhai_engine; + + for cap in self.lambda_regex.captures_iter(line) { + let whole_match = cap.get(0).unwrap(); + let captured = &cap[1]; + result.push_str(&line[last_end..whole_match.start()]); + result.push_str(&Self::process_lambda(engine, &mut scope, captured)); + last_end = whole_match.end(); + } + result.push_str(&line[last_end..]); + + // Update the main scope with any changes from the cloned scope + self.rhai_scope = scope; + + if result.trim().is_empty() { + "\n".to_string() // Always return a newline for empty lines + } else { + format!("{}\n", result) // Add a newline after each non-empty line + } + } + } +}