diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-05-03 00:57:45 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-05-03 00:57:45 +0800 |
| commit | 8f44a3e779fb8ee66c8f51b49d6c77e5a80e3821 (patch) | |
| tree | 5b281084e889eabc941b8125cc3a66240ab9f287 | |
| parent | 332e52af1883bec57e2a6d1f1291e434f18cd0c9 (diff) | |
Add tool to fix blank lines around code blocks in Markdown
| -rw-r--r-- | dev_tools/src/bin/docs-code-box-fix.rs | 165 | ||||
| -rw-r--r-- | docs/_zh_CN/pages/1-creating-your-first-program.md | 54 | ||||
| -rw-r--r-- | docs/_zh_CN/pages/2-implementing-fallbacks.md | 40 | ||||
| -rw-r--r-- | docs/pages/1-creating-your-first-program.md | 56 | ||||
| -rw-r--r-- | docs/pages/2-implementing-fallbacks.md | 46 |
5 files changed, 263 insertions, 98 deletions
diff --git a/dev_tools/src/bin/docs-code-box-fix.rs b/dev_tools/src/bin/docs-code-box-fix.rs new file mode 100644 index 0000000..0212259 --- /dev/null +++ b/dev_tools/src/bin/docs-code-box-fix.rs @@ -0,0 +1,165 @@ +use std::fs; +use std::path::Path; + +/// Docsify code blocks require that blank lines before and after code blocks are not completely empty, +/// but must contain at least one space, otherwise code block rendering will have issues. +/// +/// This tool scans all `.md` files in the docs directory, +/// and replaces completely empty lines before and after code blocks with blank lines containing a single space. + +const DOCS_DIR: &str = "./docs"; + +fn main() { + println!("Fixing code box empty lines in docs/**/*.md ..."); + let repo_root = find_git_repo().expect("Cannot find git repo root"); + let docs_dir = repo_root.join(DOCS_DIR); + + let mut fixed_count = 0; + let mut file_count = 0; + + collect_md_files(&docs_dir, &mut |path| { + if let Some(name) = path.file_name() { + let name = name.to_string_lossy(); + if name.to_lowercase() == "_sidebar.md" { + return; + } + } + + let content = fs::read_to_string(path).unwrap_or_default(); + if content.is_empty() { + return; + } + + let new_content = fix_code_box_empty_lines(&content); + if new_content != content { + fs::write(path, &new_content).unwrap(); + println!(" Fixed: {}", path.display()); + fixed_count += 1; + } + file_count += 1; + }); + + println!( + "Done. Scanned {} files, fixed {} files.", + file_count, fixed_count + ); +} + +fn fix_code_box_empty_lines(content: &str) -> String { + let mut result = String::new(); + let lines: Vec<&str> = content.lines().collect(); + let len = lines.len(); + + let mut i = 0; + while i < len { + let line = lines[i]; + + // detect beginning of code block: beginning with ``` + if line.trim_start().starts_with("```") { + // record the beginning line of the code block + result.push_str(line); + result.push('\n'); + i += 1; + + // find the end of the code block + let mut found_end = false; + let code_start = i; // record starting position of code content + let mut code_end = len; // index of code block end line + + while i < len { + let cline = lines[i]; + if cline.trim_start().starts_with("```") && cline.trim() != "" { + // this is the closing marker + code_end = i; + found_end = true; + break; + } + i += 1; + } + + // check the blank line before the code block + // if result ends with \n\n, add a space to turn it into \n \n + ensure_space_before_code_block(&mut result); + + // output code content + for j in code_start..code_end { + let code_line = lines[j]; + if code_line.is_empty() { + result.push(' '); + } else { + result.push_str(code_line); + } + result.push('\n'); + } + + if found_end { + result.push_str(lines[code_end]); + result.push('\n'); + i += 1; + + // check the blank line after the code block + // if the next line is blank, change it to one with a space + if i < len && lines[i].trim().is_empty() && lines[i].is_empty() { + // skip the original blank line, write " \n" + result.push(' '); + result.push('\n'); + i += 1; + } + } + } else { + result.push_str(line); + result.push('\n'); + i += 1; + } + } + + // remove trailing newlines + while result.ends_with('\n') { + result.pop(); + } + result.push('\n'); + + result +} + +/// ensure there is a blank line with a space before the code block +fn ensure_space_before_code_block(result: &mut String) { + // if result ends with \n\n, + // turn it into \n \n + let len = result.len(); + if len >= 2 && result[len - 2..] == *"\n\n" { + // insert a space before the last \n + result.insert(len - 1, ' '); + } +} + +/// recursively collect all .md files in the docs directory +fn collect_md_files(dir: &Path, callback: &mut dyn FnMut(&Path)) { + if let Ok(entries) = fs::read_dir(dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + collect_md_files(&path, callback); + } else if path.extension().is_some_and(|ext| ext == "md") { + callback(&path); + } + } + } +} + +fn find_git_repo() -> Option<std::path::PathBuf> { + let mut current_dir = std::env::current_dir().ok()?; + + loop { + let git_dir = current_dir.join(".git"); + if git_dir.exists() && git_dir.is_dir() { + return Some(current_dir); + } + + if !current_dir.pop() { + break; + } + } + + None +} diff --git a/docs/_zh_CN/pages/1-creating-your-first-program.md b/docs/_zh_CN/pages/1-creating-your-first-program.md index 27a7f6d..96932ba 100644 --- a/docs/_zh_CN/pages/1-creating-your-first-program.md +++ b/docs/_zh_CN/pages/1-creating-your-first-program.md @@ -26,11 +26,11 @@ ```toml [dependencies] mingling = "0.1.7" - + # 如果您要尝鲜,可以试试 Github 上托管的版本 mingling = { git = "https://github.com/catilgrass/mingling", branch = "main" } ``` - + > [!NOTE] > > 该版本基于文档编写时的 **Mingling** 版本,您可以前往 [crates.io](https://crates.io/crates/mingling) 查看最新的版本!😄 @@ -48,12 +48,12 @@ fn main() { // 创建 ThisProgram,并执行 ThisProgram::new().exec(); } - + // gen_program! 宏将会收集 *它之前* 的所有组件、类型 // 然后生成程序 `ThisProgram` mingling::macros::gen_program!(); ``` - + > [!TIP] > > `gen_Sprogram!()` 宏展开时,会收集在它之前展开的其他组件、类型的信息,这意味着您需要将 `gen_program!()` 放在整个 crate 中最后被展开的位置 @@ -70,15 +70,15 @@ mingling::macros::gen_program!(); fn main() { // ... } - + // 创建分发器,并将 GreetCommand 绑定在 "greet" 子命令 // 在用户指定该命令时,向调度器发送 GreetEntry dispatcher!("greet", GreetCommand => GreetEntry); - + // ... gen_program!(); ``` - + 不要被突然多出来的一个宏和两个类型所吓到!我来逐一解释这个宏干了什么: ##### 关于 `dispatcher!` 宏 💡 @@ -112,7 +112,7 @@ fn main() { program.exec(); } ``` - + 这样,`ThisProgram` 就认得 `"greet"` 子命令了,但是框架还不知道 `"greet"` 的行为是怎样的。此时我们便需要实现具体的逻辑: @@ -124,24 +124,24 @@ fn main() { ```rust // ... dispatcher!("greet", GreetCommand => GreetEntry); - + // 声明渲染器 `render_greet`,并表示前一个类型是 `GreetEntry` #[renderer] fn render_greet(_prev: GreetEntry) { r_println!("Hello, World!"); } - + // ... gen_program!(); // 渲染器会被注册到程序 ``` - + 对于 `#[renderer]` 属性宏标记的函数,**Mingling** 严格规定只允许使用一种函数签名: ```rust #[renderer] fn renderer_name (_prev: PreviousType) { } ``` - + 宏会读取到第一个参数的类型,并告诉 `gen_program!` 该函数用来渲染该类型。 ##### 关于 `r_println!()` 💡 @@ -158,10 +158,10 @@ fn renderer_name (_prev: PreviousType) { } ```rust dispatcher!("greet", GreetCommand => GreetEntry); - + // 包装中间类型 `ResultGreetSomeone` pack!(ResultGreetSomeone = String); - + #[chain] fn handle_greet_entry(prev: GreetEntry) -> NextProcess { let args = prev.inner; @@ -169,18 +169,18 @@ fn handle_greet_entry(prev: GreetEntry) -> NextProcess { .first() .cloned() .unwrap_or_else(|| "World".to_string()); - + // 包装为中间类型 ResultGreetSomeone::new(name) } - + #[renderer] fn render_greet_someone(prev: ResultGreetSomeone) { // 解引用 prev 拿到原始类型 r_println!("Hello, {}!", *prev); } ``` - + 像 `#[renderer]` 一样,我们创建了一个 `#[chain]`,它处理类型 `GreetEntr`,输出 `ResultGreetSomeone` 这样我们就在原本的 `Dispatcher` 和 `Renderer` 中间插入了一个 `Chain`:它可以将用户输入的参数提取出来(或回退到默认值 "World"),再交由渲染器打印到终端。 @@ -204,7 +204,7 @@ fn render_greet_someone(prev: ResultGreetSomeone) { ```rust pack!(PackedType = RawType); ``` - + 不过请注意:`pack!` 宏不支持带有生命周期的类型包装,因为类型在调度器之间的流转方式永远都是 `move` 而非 `borrow`。 @@ -215,33 +215,33 @@ pack!(PackedType = RawType); ```rust use mingling::macros::{chain, dispatcher, gen_program, pack, r_println, renderer}; - + fn main() { let mut program = ThisProgram::new(); program.with_dispatcher(GreetCommand); program.exec(); } - + dispatcher!("greet", GreetCommand => GreetEntry); - + pack!(ResultGreetSomeone = String); - + #[chain] fn handle_greet_entry(prev: GreetEntry) -> NextProcess { let args = prev.inner; let name = args.first().cloned().unwrap_or_else(|| "World".to_string()); - + ResultGreetSomeone::new(name) } - + #[renderer] fn render_greet_someone(prev: ResultGreetSomeone) { r_println!("Hello, {}!", *prev); } - + gen_program!(); ``` - + 运行结果: ```bash @@ -250,7 +250,7 @@ Hello, World! ~> your-bin greet Alice Hello, Alice! ``` - + <p align="center" style="font-size: 0.85em; color: gray;"> Written by @Weicao-CatilGrass </p> diff --git a/docs/_zh_CN/pages/2-implementing-fallbacks.md b/docs/_zh_CN/pages/2-implementing-fallbacks.md index 071a5d1..5ed7242 100644 --- a/docs/_zh_CN/pages/2-implementing-fallbacks.md +++ b/docs/_zh_CN/pages/2-implementing-fallbacks.md @@ -13,7 +13,7 @@ ~> your-bin hello ~> your-bin hello Alice ``` - + **它没有任何反应!** 👆 让我来解释为什么:**Mingling** 不自作主张,无论发生什么它都不会输出内容到终端(除了 `unwind` 下的 `panic!`) @@ -34,20 +34,20 @@ ```rust // 1. 定义 `greet` 命令 dispatcher!("greet", GreetCommand => GreetEntry); - + fn main() { // ->> 用户输入 "hello Alice" let mut program = ThisProgram::new(); - + // 2. 导入 `greet` 命令 program.with_dispatcher(GreetCommand); - + // 3. 执行程序 program.exec(); } - + // ... - + // 5. 接收 DispatcherNotFound 调度 #[renderer] fn dispatcher_not_found(prev: DispatcherNotFound) { @@ -57,7 +57,7 @@ fn dispatcher_not_found(prev: DispatcherNotFound) { prev.join(" ") ); } - + // 4. 无法匹配到任何名为 `hello` 的分发器 // 将用户参数原样分发到 DispatcherNotFound gen_program!(); @@ -68,11 +68,11 @@ gen_program!(); ```bash ~> omg hello Cannot match any command! Current input: "hello" - + ~> omg hello Alice Cannot match any command! Current input: "hello Alice" ``` - + 现在若用户输入了不匹配的命令,**Mingling** 将会输出对应的内容! ## `RendererNotFound` 类型 @@ -86,30 +86,30 @@ Cannot match any command! Current input: "hello Alice" ```rust dispatcher!("greet", GreetCommand => GreetEntry); - + fn main() { let mut program = ThisProgram::new(); - + program.with_dispatcher(GreetCommand); program.exec(); } - + pack!(ResultGreetSomeone = String); - + #[chain] fn handle_greet_entry(prev: GreetEntry) -> NextProcess { let args = prev.inner; let name = args.first().cloned().unwrap_or_else(|| "World".to_string()); - + ResultGreetSomeone::new(name) } - + // 让我们故意去除 `ResultGreetSomeone` 类型的渲染器实现 // #[renderer] // fn render_greet_someone(prev: ResultGreetSomeone) { // r_println!("Hello, {}!", *prev); // } - + #[renderer] fn renderer_not_found(prev: RendererNotFound) { if *prev == "DispatcherNotFound" { @@ -119,16 +119,16 @@ fn renderer_not_found(prev: RendererNotFound) { // 当未找到渲染器时触发 `panic!` panic!("Renderer \"{}\" not found!", *prev); } - + gen_program!(); - + ``` - + 上述程序的运行效果为: ```bash ~> your-bin greet Alice - + thread 'main' (90772) panicked at src/bin/your-bin.rs:30:5: Renderer "ResultGreetSomeone" not found! note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace diff --git a/docs/pages/1-creating-your-first-program.md b/docs/pages/1-creating-your-first-program.md index f905d8a..75c5081 100644 --- a/docs/pages/1-creating-your-first-program.md +++ b/docs/pages/1-creating-your-first-program.md @@ -26,11 +26,11 @@ ```toml [dependencies] mingling = "0.1.7" - + # If you want the latest, try the version hosted on Github mingling = { git = "https://github.com/catilgrass/mingling", branch = "main" } ``` - + > [!NOTE] > > This version matches the **Mingling** version used when writing this doc. Check [crates.io](https://crates.io/crates/mingling) for the latest release! 😄 @@ -48,12 +48,12 @@ fn main() { // Create ThisProgram and run it ThisProgram::new().exec(); } - + // The gen_program! macro collects *all preceding* components & types // then generates the `ThisProgram` struct mingling::macros::gen_program!(); ``` - + > [!TIP] > > When `gen_program!()` expands, it gathers info from other components & types that were expanded before it. This means you must place `gen_program!()` at the very last expansion point in the crate. @@ -70,15 +70,15 @@ mingling::macros::gen_program!(); fn main() { // ... } - + // Create a dispatcher, binding GreetCommand to the "greet" sub-command // When the user specifies this command, send GreetEntry to the dispatcher dispatcher!("greet", GreetCommand => GreetEntry); - + // ... gen_program!(); ``` - + Don't be scared by the sudden macro and two new types! Let me explain what this macro does: ##### About the `dispatcher!` macro 💡 @@ -106,13 +106,13 @@ gen_program!(); ```rust fn main() { let mut program = ThisProgram::new(); - + // Register the dispatcher program.with_dispatcher(GreetCommand); program.exec(); } ``` - + Now `ThisProgram` recognizes the `"greet"` sub-command, but the framework still doesn't know what `"greet"` should do. That's where we implement the actual logic: @@ -124,24 +124,24 @@ fn main() { ```rust // ... dispatcher!("greet", GreetCommand => GreetEntry); - + // Declare a renderer `render_greet`, specifying the previous type as `GreetEntry` #[renderer] fn render_greet(_prev: GreetEntry) { r_println!("Hello, World!"); } - + // ... gen_program!(); // The renderer will be registered with the program ``` - + For functions marked with `#[renderer]`, **Mingling** strictly enforces only one function signature: ```rust #[renderer] fn renderer_name (_prev: PreviousType) { } ``` - + The macro reads the type of the first param and tells `gen_program!` that this function renders that type. ##### About `r_println!()` 💡 @@ -158,10 +158,10 @@ fn renderer_name (_prev: PreviousType) { } ```rust dispatcher!("greet", GreetCommand => GreetEntry); - + // Wrap the intermediate type `ResultGreetSomeone` pack!(ResultGreetSomeone = String); - + #[chain] fn handle_greet_entry(prev: GreetEntry) -> NextProcess { let args = prev.inner; @@ -169,18 +169,18 @@ fn handle_greet_entry(prev: GreetEntry) -> NextProcess { .first() .cloned() .unwrap_or_else(|| "World".to_string()); - + // Wrap into intermediate type ResultGreetSomeone::new(name) } - + #[renderer] fn render_greet_someone(prev: ResultGreetSomeone) { // Deref prev to get the raw type r_println!("Hello, {}!", *prev); } ``` - + Just like `#[renderer]`, we created a `#[chain]` that processes type `GreetEntry` and outputs `ResultGreetSomeone`. This inserts a `Chain` between the original `Dispatcher` and `Renderer`: it extracts the user's input params (or falls back to "World"), then passes them to the renderer to print to the terminal. @@ -204,7 +204,7 @@ fn render_greet_someone(prev: ResultGreetSomeone) { ```rust pack!(PackedType = RawType); ``` - + Note: `pack!` doesn't support types with lifetimes, because types are always moved (not borrowed) between dispatchers. @@ -215,33 +215,33 @@ pack!(PackedType = RawType); ```rust use mingling::macros::{chain, dispatcher, gen_program, pack, r_println, renderer}; - + fn main() { let mut program = ThisProgram::new(); program.with_dispatcher(GreetCommand); program.exec(); } - + dispatcher!("greet", GreetCommand => GreetEntry); - + pack!(ResultGreetSomeone = String); - + #[chain] fn handle_greet_entry(prev: GreetEntry) -> NextProcess { let args = prev.inner; let name = args.first().cloned().unwrap_or_else(|| "World".to_string()); - + ResultGreetSomeone::new(name) } - + #[renderer] fn render_greet_someone(prev: ResultGreetSomeone) { r_println!("Hello, {}!", *prev); } - + gen_program!(); ``` - + Output: ```bash @@ -250,7 +250,7 @@ Hello, World! ~> your-bin greet Alice Hello, Alice! ``` - + <p align="center" style="font-size: 0.85em; color: gray;"> Written by @Weicao-CatilGrass </p> diff --git a/docs/pages/2-implementing-fallbacks.md b/docs/pages/2-implementing-fallbacks.md index a376580..a820fa1 100644 --- a/docs/pages/2-implementing-fallbacks.md +++ b/docs/pages/2-implementing-fallbacks.md @@ -13,7 +13,7 @@ ~> your-bin hello ~> your-bin hello Alice ``` - + **It does nothing!** 👆 Let me explain why: **Mingling** doesn't presume to act; it will not output anything to the terminal no matter what happens (except for `panic!` under `unwind`) @@ -34,20 +34,20 @@ ```rust // 1. Define the `greet` command dispatcher!("greet", GreetCommand => GreetEntry); - + fn main() { // ->> User enters "hello Alice" let mut program = ThisProgram::new(); - + // 2. Import the `greet` command program.with_dispatcher(GreetCommand); - + // 3. Execute the program program.exec(); } - + // ... - + // 5. Receive the DispatcherNotFound dispatch #[renderer] fn dispatcher_not_found(prev: DispatcherNotFound) { @@ -57,22 +57,22 @@ fn dispatcher_not_found(prev: DispatcherNotFound) { prev.join(" ") ); } - + // 4. Cannot match any dispatcher named `hello` // Forward the user's arguments as-is to DispatcherNotFound gen_program!(); ``` - + The output of the above program is: ```bash ~> omg hello Cannot match any command! Current input: "hello" - + ~> omg hello Alice Cannot match any command! Current input: "hello Alice" ``` - + Now, if the user enters a command that doesn't match, **Mingling** will output the appropriate message! ## The `RendererNotFound` Type @@ -86,54 +86,54 @@ Cannot match any command! Current input: "hello Alice" ```rust dispatcher!("greet", GreetCommand => GreetEntry); - + fn main() { let mut program = ThisProgram::new(); - + program.with_dispatcher(GreetCommand); program.exec(); } - + pack!(ResultGreetSomeone = String); - + #[chain] fn handle_greet_entry(prev: GreetEntry) -> NextProcess { let args = prev.inner; let name = args.first().cloned().unwrap_or_else(|| "World".to_string()); - + ResultGreetSomeone::new(name) } - + // Let's intentionally remove the renderer implementation for `ResultGreetSomeone` // #[renderer] // fn render_greet_someone(prev: ResultGreetSomeone) { // r_println!("Hello, {}!", *prev); // } - + #[renderer] fn renderer_not_found(prev: RendererNotFound) { if *prev == "DispatcherNotFound" { return; // Exclude the "DispatcherNotFound" type } - + // Trigger `panic!` when a renderer is not found panic!("Renderer \"{}\" not found!", *prev); } - + gen_program!(); - + ``` - + The output of the above program is: ```bash ~> your-bin greet Alice - + thread 'main' (90772) panicked at src/bin/your-bin.rs:30:5: Renderer "ResultGreetSomeone" not found! note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace ``` - + <p align="center" style="font-size: 0.85em; color: gray;"> Written by @Weicao-CatilGrass </p> |
