├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── bake.sh ├── examples ├── README.md └── snippets.rs └── src └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "md-bakery" 3 | version = "1.2.0" 4 | authors = ["Patryk 'PsichiX' Budzynski "] 5 | edition = "2018" 6 | description = "Markdown Bakery CLI app" 7 | license = "MIT" 8 | homepage = "https://github.com/PsichiX/md-bakery" 9 | repository = "https://github.com/PsichiX/md-bakery" 10 | documentation = "https://docs.rs/md-bakery" 11 | readme = "README.md" 12 | 13 | [[bin]] 14 | name = "mdbakery" 15 | path = "./src/main.rs" 16 | 17 | [dependencies] 18 | regex = "1.4" 19 | clap = "2.33" 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Patryk Budzyński 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Markdown Bakery 2 | ### CLI tool to bake your fresh and hot MD files 3 | 4 | ## Install 5 | 6 | You can install from crates.io using `cargo`: 7 | 8 | ```bash 9 | cargo install md-bakery 10 | ``` 11 | 12 | > **Note:** The command used to run the program after installing is `mdbakery` **not** `md-bakery`. 13 | 14 | ## About 15 | At least once in your Rust dev lifetime you wanted to make sure all code examples 16 | in your markdown files are up-to-date, correct and code is formated, but you 17 | couldn't make that done with already existing tools - fear not! 18 | 19 | Markdown Bakery allows you to create markdown template files with rust code 20 | injected from source files using special code block: 21 | 22 | # Content of `./src/main.rs`: 23 | 24 | ```rust: source 25 | ./src/main.rs 26 | ``` 27 | 28 | After language part you tell MD Bakery this code part has to inject source code from path specified inside the code block. 29 | 30 | If you want to escape MD Bakery code blocks you can put `!` in front of `source` (sadly we can't show it here since that would print double `!` in the readme file you read right now, more about why later). 31 | 32 | If you prefer to inject different parts of the code file instead of keeping each code snippet in separate files, all you have to do is to inject named source: 33 | 34 | # Snippet A: 35 | 36 | ```rust: source @ snippet-a 37 | ./src/main.rs 38 | ``` 39 | 40 | # Snippet B: 41 | 42 | ```rust: source @ snippet-b 43 | ./src/main.rs 44 | ``` 45 | 46 | And then in your source file you specify named blocks of the code where snippets are located, using comments like this: 47 | 48 | ```rust 49 | use std::collections::HashMap; 50 | 51 | // [md-bakery: begin @ snippet-a] 52 | #[derive(Debug)] 53 | struct Foo { 54 | bar: HashMap, 55 | } 56 | // [md-bakery: end] 57 | 58 | fn main() { 59 | // [md-bakery: begin @ snippet-b] 60 | let foo = Foo { 61 | bar: { 62 | let mut result = HashMap::new(); 63 | result.insert("answer".to_owned(), 42); 64 | result 65 | }, 66 | }; 67 | 68 | println!("{:?}", foo); 69 | // [md-bakery: end] 70 | } 71 | ``` 72 | 73 | And then all of that renders into: 74 | 75 | # Snippet A: 76 | 77 | ```rust 78 | #[derive(Debug)] 79 | struct Foo { 80 | bar: HashMap, 81 | } 82 | ``` 83 | 84 | # Snippet B: 85 | 86 | ```rust 87 | let foo = Foo { 88 | bar: { 89 | let mut result = HashMap::new(); 90 | result.insert("answer".to_owned(), 42); 91 | result 92 | }, 93 | }; 94 | 95 | println!("{:?}", foo); 96 | ``` 97 | 98 | **Of course this readme file was baked too, you can see template sources in `/examples` folder!** 99 | So to see how to escape MD Bakery code blocks go there and look it up yourself :D 100 | 101 | ## Usage: 102 | ```bash 103 | md-bakery 1.0.0 104 | Patryk 'PsichiX' Budzynski 105 | Markdown Bakery CLI app 106 | 107 | USAGE: 108 | mdbakery.exe [OPTIONS] --input --output 109 | 110 | FLAGS: 111 | -h, --help Prints help information 112 | -V, --version Prints version information 113 | 114 | OPTIONS: 115 | -i, --input Markdown template file name 116 | -o, --output Markdown generated file name 117 | -r, --root Source files root path 118 | ``` 119 | 120 | ## TODO: 121 | - [ ] Add `exec` code block variant that runs executable with parameters specified in block content, then put its stdout: 122 | 123 | ```bash: exec 124 | cargo run -- --help 125 | ``` 126 | -------------------------------------------------------------------------------- /bake.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cargo fmt --all 4 | cargo build --all 5 | cargo build --examples 6 | cargo test --all 7 | cargo run -- -i ./examples/README.md -o ./README.md -r ./examples 8 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Markdown Bakery 2 | ### CLI tool to bake your fresh and hot MD files 3 | 4 | ## Install 5 | 6 | You can install from crates.io using `cargo`: 7 | 8 | ```bash 9 | cargo install md-bakery 10 | ``` 11 | 12 | > **Note:** The command used to run the program after installing is `mdbakery` **not** `md-bakery`. 13 | 14 | ## About 15 | At least once in your Rust dev lifetime you wanted to make sure all code examples 16 | in your markdown files are up-to-date, correct and code is formated, but you 17 | couldn't make that done with already existing tools - fear not! 18 | 19 | Markdown Bakery allows you to create markdown template files with rust code 20 | injected from source files using special code block: 21 | 22 | # Content of `./src/main.rs`: 23 | 24 | ```rust: !source 25 | ./src/main.rs 26 | ``` 27 | 28 | After language part you tell MD Bakery this code part has to inject source code from path specified inside the code block. 29 | 30 | If you want to escape MD Bakery code blocks you can put `!` in front of `source` (sadly we can't show it here since that would print double `!` in the readme file you read right now, more about why later). 31 | 32 | If you prefer to inject different parts of the code file instead of keeping each code snippet in separate files, all you have to do is to inject named source: 33 | 34 | # Snippet A: 35 | 36 | ```rust: !source @ snippet-a 37 | ./src/main.rs 38 | ``` 39 | 40 | # Snippet B: 41 | 42 | ```rust: !source @ snippet-b 43 | ./src/main.rs 44 | ``` 45 | 46 | And then in your source file you specify named blocks of the code where snippets are located, using comments like this: 47 | 48 | ```rust: source 49 | ./snippets.rs 50 | ``` 51 | 52 | And then all of that renders into: 53 | 54 | # Snippet A: 55 | 56 | ```rust: source @ snippet-a 57 | ./snippets.rs 58 | ``` 59 | 60 | # Snippet B: 61 | 62 | ```rust: source @ snippet-b 63 | ./snippets.rs 64 | ``` 65 | 66 | **Of course this readme file was baked too, you can see template sources in `/examples` folder!** 67 | So to see how to escape MD Bakery code blocks go there and look it up yourself :D 68 | 69 | ## Usage: 70 | ```bash 71 | md-bakery 1.0.0 72 | Patryk 'PsichiX' Budzynski 73 | Markdown Bakery CLI app 74 | 75 | USAGE: 76 | mdbakery.exe [OPTIONS] --input --output 77 | 78 | FLAGS: 79 | -h, --help Prints help information 80 | -V, --version Prints version information 81 | 82 | OPTIONS: 83 | -i, --input Markdown template file name 84 | -o, --output Markdown generated file name 85 | -r, --root Source files root path 86 | ``` 87 | 88 | ## TODO: 89 | - [ ] Add `exec` code block variant that runs executable with parameters specified in block content, then put its stdout: 90 | 91 | ```bash: exec 92 | cargo run -- --help 93 | ``` 94 | -------------------------------------------------------------------------------- /examples/snippets.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | // [md-bakery: begin @ snippet-a] 4 | #[derive(Debug)] 5 | struct Foo { 6 | bar: HashMap, 7 | } 8 | // [md-bakery: end] 9 | 10 | fn main() { 11 | // [md-bakery: begin @ snippet-b] 12 | let foo = Foo { 13 | bar: { 14 | let mut result = HashMap::new(); 15 | result.insert("answer".to_owned(), 42); 16 | result 17 | }, 18 | }; 19 | 20 | println!("{:?}", foo); 21 | // [md-bakery: end] 22 | } 23 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::{App, Arg}; 2 | use regex::{Captures, Regex}; 3 | use std::{ 4 | fs::{read_to_string, write}, 5 | path::PathBuf, 6 | }; 7 | 8 | fn main() { 9 | let matches = App::new(env!("CARGO_PKG_NAME")) 10 | .version(env!("CARGO_PKG_VERSION")) 11 | .author(env!("CARGO_PKG_AUTHORS")) 12 | .about(env!("CARGO_PKG_DESCRIPTION")) 13 | .arg( 14 | Arg::with_name("input") 15 | .short("i") 16 | .long("input") 17 | .value_name("FILE") 18 | .help("Markdown template file name") 19 | .takes_value(true) 20 | .required(true), 21 | ) 22 | .arg( 23 | Arg::with_name("output") 24 | .short("o") 25 | .long("output") 26 | .value_name("FILE") 27 | .help("Markdown generated file name") 28 | .takes_value(true) 29 | .required(true), 30 | ) 31 | .arg( 32 | Arg::with_name("root") 33 | .short("r") 34 | .long("root") 35 | .value_name("FILE") 36 | .help("Source files root path") 37 | .takes_value(true) 38 | .required(false), 39 | ) 40 | .get_matches(); 41 | let input = matches.value_of("input").unwrap(); 42 | let output = matches.value_of("output").unwrap(); 43 | let root = PathBuf::from(matches.value_of("root").unwrap_or_default()); 44 | let pattern = r"([\t ]*)```\s*(\w+)\s*:\s*source(\s*@\s*(\S+))?\s+(\S+)\s+```"; 45 | let pattern = Regex::new(pattern) 46 | .unwrap_or_else(|error| panic!("Could not build pattern: {} | {:?}", pattern, error)); 47 | let pattern_escape = r"(```\s*\w+\s*:\s*)!(\s*source(\s*@\s*\S+)?[\t ]*)"; 48 | let pattern_escape = Regex::new(pattern_escape).unwrap_or_else(|error| { 49 | panic!( 50 | "Could not build escape-pattern: {} | {:?}", 51 | pattern_escape, error 52 | ) 53 | }); 54 | let pattern_begin = r"//\s*\[\s*md-bakery\s*:\s*begin(\s*@\s*(\S+))?\s*\]"; 55 | let pattern_begin = Regex::new(pattern_begin).unwrap_or_else(|error| { 56 | panic!( 57 | "Could not build begin-pattern: {} | {:?}", 58 | pattern_begin, error 59 | ) 60 | }); 61 | let pattern_end = r"//\s*\[\s*md-bakery\s*:\s*end\s*\]"; 62 | let pattern_end = Regex::new(pattern_end).unwrap_or_else(|error| { 63 | panic!("Could not build end-pattern: {} | {:?}", pattern_end, error) 64 | }); 65 | let content = read_to_string(input) 66 | .unwrap_or_else(|error| panic!("Could not load input file: {} | {:?}", input, error)); 67 | let content = pattern.replace_all(&content, |captures: &Captures| { 68 | let indent = captures.get(1).unwrap().as_str(); 69 | let lang = captures.get(2).unwrap().as_str(); 70 | let name = match captures.get(4) { 71 | Some(name) => name.as_str().to_owned(), 72 | None => String::new(), 73 | }; 74 | let path = root.join(captures.get(5).unwrap().as_str().trim()); 75 | let content = read_to_string(&path) 76 | .unwrap_or_else(|error| panic!("Could not load source file: {:?} | {:?}", path, error)); 77 | let lines = content 78 | .lines() 79 | .map(|line| line.to_owned()) 80 | // .map(|line| format!("{}{}", indent, line)) 81 | .collect::>(); 82 | let found = lines.iter().any(|line| { 83 | if let Some(captures) = pattern_begin.captures(line) { 84 | if let Some(n) = captures.get(2) { 85 | if n.as_str() == name { 86 | return true; 87 | } 88 | } 89 | } 90 | false 91 | }); 92 | let lines = if found { 93 | let mut record = false; 94 | lines 95 | .into_iter() 96 | .filter(|line| { 97 | if record { 98 | if pattern_end.is_match(line) { 99 | record = false; 100 | } 101 | record 102 | } else { 103 | if let Some(captures) = pattern_begin.captures(line) { 104 | let n = match captures.get(2) { 105 | Some(n) => n.as_str().to_owned(), 106 | None => String::new(), 107 | }; 108 | if n == name { 109 | record = true; 110 | } 111 | } 112 | false 113 | } 114 | }) 115 | .collect::>() 116 | } else { 117 | lines 118 | .into_iter() 119 | .map(|line| format!("{}{}", indent, line)) 120 | .collect::>() 121 | }; 122 | let common_prefix = common_whitespace_prefix(&lines); 123 | let content = lines 124 | .into_iter() 125 | .map(|line| { 126 | format!( 127 | "{}{}", 128 | indent, 129 | line.strip_prefix(&common_prefix).unwrap_or_default() 130 | ) 131 | }) 132 | .collect::>() 133 | .join("\n"); 134 | format!("{}```{}\n{}\n{}```", indent, lang, content, indent) 135 | }); 136 | let content = pattern_escape.replace_all(&content, |captures: &Captures| { 137 | let first = captures.get(1).unwrap().as_str(); 138 | let second = captures.get(2).unwrap().as_str(); 139 | format!("{}{}", first, second) 140 | }); 141 | write(output, &*content) 142 | .unwrap_or_else(|error| panic!("Could not write output file: {} | {:?}", output, error)); 143 | } 144 | 145 | fn common_whitespace_prefix(lines: &[String]) -> String { 146 | if lines.is_empty() { 147 | return String::new(); 148 | } 149 | let mut result: Option = None; 150 | for line in lines { 151 | if !line.is_empty() { 152 | let prefix = take_whitespaces_prefix(&line); 153 | if let Some(result) = result.as_mut() { 154 | *result = result 155 | .chars() 156 | .zip(prefix.chars()) 157 | .take_while(|(a, b)| a == b) 158 | .map(|(a, _)| a) 159 | .collect::(); 160 | } else { 161 | result = Some(prefix); 162 | } 163 | } 164 | } 165 | result.unwrap_or_default() 166 | } 167 | 168 | fn take_whitespaces_prefix(value: &str) -> String { 169 | value 170 | .chars() 171 | .take_while(|c| c.is_whitespace()) 172 | .collect::() 173 | } 174 | --------------------------------------------------------------------------------