aboutsummaryrefslogtreecommitdiff
path: root/.run/src/bin
diff options
context:
space:
mode:
Diffstat (limited to '.run/src/bin')
-rw-r--r--.run/src/bin/build-all.ps18
-rw-r--r--.run/src/bin/build-all.sh6
-rw-r--r--.run/src/bin/ci.rs225
-rw-r--r--.run/src/bin/clippy-fix.ps18
-rw-r--r--.run/src/bin/clippy-fix.sh6
-rw-r--r--.run/src/bin/clippy.ps18
-rw-r--r--.run/src/bin/clippy.sh6
-rw-r--r--.run/src/bin/doc.ps11
-rw-r--r--.run/src/bin/doc.sh3
-rw-r--r--.run/src/bin/docs-code-box-fix.rs166
-rw-r--r--.run/src/bin/docsify-sidebar-gen.rs262
-rw-r--r--.run/src/bin/http-page-preview.ps13
-rw-r--r--.run/src/bin/http-page-preview.sh2
-rw-r--r--.run/src/bin/install-mling.ps17
-rw-r--r--.run/src/bin/install-mling.sh6
-rw-r--r--.run/src/bin/refresh-docs.rs178
-rw-r--r--.run/src/bin/refresh-feature-mod.rs97
-rw-r--r--.run/src/bin/sync-examples.rs131
-rw-r--r--.run/src/bin/test-all-markdown-code.rs276
-rw-r--r--.run/src/bin/test-all.ps18
-rw-r--r--.run/src/bin/test-all.sh6
-rw-r--r--.run/src/bin/test-examples.rs158
-rw-r--r--.run/src/bin/update-version.rs98
-rw-r--r--.run/src/bin/windows-folder-hide.ps143
24 files changed, 1712 insertions, 0 deletions
diff --git a/.run/src/bin/build-all.ps1 b/.run/src/bin/build-all.ps1
new file mode 100644
index 0000000..4f35ed8
--- /dev/null
+++ b/.run/src/bin/build-all.ps1
@@ -0,0 +1,8 @@
+$starting_dir = Get-Location
+Get-ChildItem -Recurse -Filter "Cargo.toml" | ForEach-Object {
+ $project_dir = $_.DirectoryName
+ Push-Location $project_dir
+ cargo build
+ Pop-Location
+}
+Set-Location $starting_dir
diff --git a/.run/src/bin/build-all.sh b/.run/src/bin/build-all.sh
new file mode 100644
index 0000000..2036b41
--- /dev/null
+++ b/.run/src/bin/build-all.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+find . -name "Cargo.toml" -type f | while read -r cargo_file; do
+ project_dir=$(dirname "$cargo_file")
+ (cd "$project_dir" && cargo build)
+done
diff --git a/.run/src/bin/ci.rs b/.run/src/bin/ci.rs
new file mode 100644
index 0000000..f8aed79
--- /dev/null
+++ b/.run/src/bin/ci.rs
@@ -0,0 +1,225 @@
+use std::io::Write as _;
+use std::process::exit;
+
+use tools::{
+ cargo_tomls, crate_name_from, eprintln_cargo_style, println_cargo_style, run_cmd, run_parallel,
+};
+
+fn get_ignore_dirs() -> Vec<String> {
+ vec![
+ ".temp".to_string(),
+ "mling/res".to_string(),
+ "mling\\res".to_string(),
+ ]
+}
+
+fn print_help() {
+ println!(
+ r"
+Usage: ci [options]
+Options:
+ -h, --help Print this help message
+ -y Auto-confirm temporary commits
+ --dirty Run CI on dirty workspace (skip temp commit & clean check)
+ --refresh-docs Refresh documentation files
+ --test-docs Run documentation tests (build, clippy, test)
+ --test-codes Test examples and documentation code blocks
+
+If no specific options are given, all checks are run.
+ "
+ );
+}
+
+fn main() {
+ #[cfg(windows)]
+ let _ = colored::control::set_virtual_terminal(true);
+ println!("{}", include_str!("../../../docs/res/ci_banner.txt"));
+
+ let args: Vec<String> = std::env::args().collect();
+
+ if args.iter().any(|a| a == "-h" || a == "--help") {
+ print_help();
+ return;
+ }
+
+ let auto_yes = args.iter().any(|a| a == "-y");
+ let dirty = args.iter().any(|a| a == "--dirty");
+
+ let test_docs = args.iter().any(|a| a == "--test-docs");
+ let refresh_docs = args.iter().any(|a| a == "--refresh-docs");
+ let test_codes = args.iter().any(|a| a == "--test-codes");
+ let any_specified = test_docs || refresh_docs || test_codes;
+ let run_all = !any_specified;
+
+ let needs_commit_temp = !dirty && !{ run_cmd!("git diff-index --quiet HEAD --").is_ok() };
+
+ if needs_commit_temp {
+ if auto_yes {
+ run_cmd!("git add .").unwrap();
+ run_cmd!("git commit -m \"[DO NOT PUSH] CI TEMP [DO NOT PUSH]\"").unwrap();
+ } else {
+ print!("Working tree is not clean, temporarily commit? [y/N]:");
+ std::io::stdout().flush().unwrap();
+ let mut input = String::new();
+ std::io::stdin().read_line(&mut input).unwrap();
+ let input = input.trim();
+ if input == "y" || input == "Y" || input == "yes" || input == "Yes" {
+ run_cmd!("git add .").unwrap();
+ run_cmd!("git commit -m \"[DO NOT PUSH] CI TEMP [DO NOT PUSH]\"").unwrap();
+ } else {
+ eprintln_cargo_style!("Aborting.");
+ exit(2)
+ }
+ }
+ }
+
+ if let Err(exit_code) = ci(test_docs, test_codes, run_all) {
+ restore_workspace(needs_commit_temp).unwrap();
+ exit(exit_code)
+ }
+
+ if !dirty {
+ let is_worktree_clean = run_cmd!("git diff-index --quiet HEAD --").is_ok();
+ if !is_worktree_clean {
+ eprintln_cargo_style!("The repository was contaminated during CI, failing!");
+
+ // Print git status
+ println!();
+ let _ = run_cmd!("git status");
+
+ if needs_commit_temp {
+ restore_workspace(true).unwrap();
+ }
+ exit(1)
+ }
+ }
+
+ println_cargo_style!("Done: All check passed!");
+
+ if needs_commit_temp {
+ restore_workspace(true).unwrap();
+ }
+}
+
+fn restore_workspace(undo_commit: bool) -> Result<(), i32> {
+ run_cmd!("git reset --hard --quiet")?;
+ if undo_commit {
+ run_cmd!("git reset --soft HEAD~1 --quiet")?;
+ run_cmd!("git reset --quiet")?;
+ }
+ Ok(())
+}
+
+fn ci(test_docs: bool, test_codes: bool, run_all: bool) -> Result<(), i32> {
+ if run_all || test_codes {
+ println_cargo_style!("Phase: Scan and build all crates");
+ build_all()?;
+
+ println_cargo_style!("Phase: Run clippy for all crates");
+ clippy_all()?;
+
+ println_cargo_style!("Phase: Test all crates");
+ test_all()?;
+ }
+
+ if run_all || test_docs {
+ println_cargo_style!("Phase: Test all examples");
+ test_examples()?;
+
+ println_cargo_style!("Phase: Verify all *.md document code blocks are compilable");
+ test_docs_code_blocks()?;
+
+ println_cargo_style!("Phase: Check all documentation is up to date");
+ docs_refresh()?;
+ }
+
+ run_cmd!("git add --renormalize .")?;
+
+ Ok(())
+}
+
+fn test_examples() -> Result<(), i32> {
+ run_cmd!("cargo run --manifest-path dev_tools/Cargo.toml --color always --bin test-examples")
+}
+
+fn test_docs_code_blocks() -> Result<(), i32> {
+ run_cmd!(
+ "cargo run --manifest-path dev_tools/Cargo.toml --color always --bin test-all-markdown-code"
+ )
+}
+
+fn build_all() -> Result<(), i32> {
+ let ignore_dirs = get_ignore_dirs();
+ let cargo_tomls = cargo_tomls();
+ let mut tasks = Vec::new();
+ for cargo_toml in cargo_tomls {
+ let path = cargo_toml.parent().unwrap_or(std::path::Path::new(""));
+ let path_str = path.to_string_lossy();
+ if ignore_dirs.iter().any(|d| path_str.contains(d.as_str())) {
+ continue;
+ }
+ let label = format!("Build: {}", cargo_toml.to_string_lossy());
+ let crate_name = crate_name_from(&cargo_toml);
+ let cmd = format!(
+ "cargo build --manifest-path {} --color always",
+ cargo_toml.to_string_lossy()
+ );
+ tasks.push((label, crate_name, cmd));
+ }
+ run_parallel("Building", tasks)
+}
+
+fn clippy_all() -> Result<(), i32> {
+ let ignore_dirs = get_ignore_dirs();
+ let cargo_tomls = cargo_tomls();
+ let mut tasks = Vec::new();
+ for cargo_toml in cargo_tomls {
+ let path = cargo_toml.parent().unwrap_or(std::path::Path::new(""));
+ let path_str = path.to_string_lossy();
+ if ignore_dirs.iter().any(|d| path_str.contains(d.as_str())) {
+ continue;
+ }
+ let label = format!("Clippy: {}", cargo_toml.to_string_lossy());
+ let crate_name = crate_name_from(&cargo_toml);
+ let cmd = format!(
+ "cargo clippy --manifest-path {} --color always -- -D warnings",
+ cargo_toml.to_string_lossy()
+ );
+ tasks.push((label, crate_name, cmd));
+ }
+ run_parallel("Clippy", tasks)
+}
+
+fn test_all() -> Result<(), i32> {
+ let ignore_dirs = get_ignore_dirs();
+ let cargo_tomls = cargo_tomls();
+ let mut tasks = Vec::new();
+ for cargo_toml in cargo_tomls {
+ let path = cargo_toml.parent().unwrap_or(std::path::Path::new(""));
+ let path_str = path.to_string_lossy();
+ if ignore_dirs.iter().any(|d| path_str.contains(d.as_str())) {
+ continue;
+ }
+ let label = format!("Testing: {}", cargo_toml.to_string_lossy());
+ let crate_name = crate_name_from(&cargo_toml);
+ let cmd = format!(
+ "cargo test --manifest-path {} --color always",
+ cargo_toml.to_string_lossy()
+ );
+ tasks.push((label, crate_name, cmd));
+ }
+ run_parallel("Testing", tasks)
+}
+
+fn docs_refresh() -> Result<(), i32> {
+ println_cargo_style!("Refresh: document at `./docs/`");
+
+ run_cmd!("cargo run --manifest-path dev_tools/Cargo.toml --bin docs-code-box-fix")?;
+ run_cmd!("cargo run --manifest-path dev_tools/Cargo.toml --bin docsify-sidebar-gen")?;
+ run_cmd!("cargo run --manifest-path dev_tools/Cargo.toml --bin refresh-docs")?;
+ run_cmd!("cargo run --manifest-path dev_tools/Cargo.toml --bin refresh-feature-mod")?;
+ run_cmd!("cargo run --manifest-path dev_tools/Cargo.toml --bin sync-examples")?;
+ run_cmd!("cargo fmt")?;
+
+ Ok(())
+}
diff --git a/.run/src/bin/clippy-fix.ps1 b/.run/src/bin/clippy-fix.ps1
new file mode 100644
index 0000000..1d24f92
--- /dev/null
+++ b/.run/src/bin/clippy-fix.ps1
@@ -0,0 +1,8 @@
+$starting_dir = Get-Location
+Get-ChildItem -Recurse -Filter "Cargo.toml" | ForEach-Object {
+ $project_dir = $_.DirectoryName
+ Push-Location $project_dir
+ cargo clippy --fix --allow-dirty --allow-no-vcs --quiet
+ Pop-Location
+}
+Set-Location $starting_dir
diff --git a/.run/src/bin/clippy-fix.sh b/.run/src/bin/clippy-fix.sh
new file mode 100644
index 0000000..9771ad4
--- /dev/null
+++ b/.run/src/bin/clippy-fix.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+find . -name "Cargo.toml" -type f | while read -r cargo_file; do
+ project_dir=$(dirname "$cargo_file")
+ (cd "$project_dir" && cargo clippy --fix --allow-dirty --allow-no-vcs --quiet)
+done
diff --git a/.run/src/bin/clippy.ps1 b/.run/src/bin/clippy.ps1
new file mode 100644
index 0000000..1858873
--- /dev/null
+++ b/.run/src/bin/clippy.ps1
@@ -0,0 +1,8 @@
+$starting_dir = Get-Location
+Get-ChildItem -Recurse -Filter "Cargo.toml" | ForEach-Object {
+ $project_dir = $_.DirectoryName
+ Push-Location $project_dir
+ cargo clippy --quiet
+ Pop-Location
+}
+Set-Location $starting_dir
diff --git a/.run/src/bin/clippy.sh b/.run/src/bin/clippy.sh
new file mode 100644
index 0000000..b393545
--- /dev/null
+++ b/.run/src/bin/clippy.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+find . -name "Cargo.toml" -type f | while read -r cargo_file; do
+ project_dir=$(dirname "$cargo_file")
+ (cd "$project_dir" && cargo clippy --quiet)
+done
diff --git a/.run/src/bin/doc.ps1 b/.run/src/bin/doc.ps1
new file mode 100644
index 0000000..987f0de
--- /dev/null
+++ b/.run/src/bin/doc.ps1
@@ -0,0 +1 @@
+cargo doc --workspace --no-deps --features builds,structural_renderer,repl,comp,parser,clap,extra_macros --open
diff --git a/.run/src/bin/doc.sh b/.run/src/bin/doc.sh
new file mode 100644
index 0000000..5e8a311
--- /dev/null
+++ b/.run/src/bin/doc.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+cargo doc --workspace --no-deps --features builds,structural_renderer,repl,comp,parser,clap,extra_macros --open
diff --git a/.run/src/bin/docs-code-box-fix.rs b/.run/src/bin/docs-code-box-fix.rs
new file mode 100644
index 0000000..21d2cce
--- /dev/null
+++ b/.run/src/bin/docs-code-box-fix.rs
@@ -0,0 +1,166 @@
+use std::fs;
+use std::path::Path;
+
+use tools::println_cargo_style;
+
+/// 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_cargo_style!("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_cargo_style!("Fixed: {}", path.display());
+ fixed_count += 1;
+ }
+ file_count += 1;
+ });
+
+ println_cargo_style!(
+ "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 code_line in lines.iter().take(code_end).skip(code_start) {
+ 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/.run/src/bin/docsify-sidebar-gen.rs b/.run/src/bin/docsify-sidebar-gen.rs
new file mode 100644
index 0000000..5beda7f
--- /dev/null
+++ b/.run/src/bin/docsify-sidebar-gen.rs
@@ -0,0 +1,262 @@
+use std::collections::BTreeMap;
+use std::fmt::Write;
+use std::path::{Path, PathBuf};
+
+use tools::println_cargo_style;
+
+const SIDEBAR_HEAD: &str = "- [Welcome!](README)\n";
+
+fn main() {
+ println_cargo_style!("Refresh: _sidebar.md");
+ gen_all_sidebars();
+}
+
+/// Find all README.md under docs/, treat each as a site, and generate _sidebar.md for it.
+fn gen_all_sidebars() {
+ let repo_root = find_git_repo().unwrap();
+ let docs_root = repo_root.join("docs");
+
+ let readme_paths = find_all_readmes(&docs_root);
+
+ for readme_path in &readme_paths {
+ let site_root = readme_path.parent().unwrap();
+
+ let content_dir = find_content_dir(site_root);
+
+ if let Some(content_dir) = content_dir {
+ let lines = build_sidebar_content(site_root, &content_dir, SIDEBAR_HEAD);
+
+ let sidebar_path = site_root.join("_sidebar.md");
+ std::fs::write(&sidebar_path, lines).unwrap();
+ println_cargo_style!("Generated: {}", sidebar_path.display());
+ }
+ }
+}
+
+/// Recursively find all README.md files under a directory.
+fn find_all_readmes(dir: &Path) -> Vec<PathBuf> {
+ let mut results = Vec::new();
+ if let Ok(read_dir) = std::fs::read_dir(dir) {
+ let mut entries: Vec<_> = read_dir.flatten().collect();
+ entries.sort_by_key(|e| e.path());
+ for entry in entries {
+ let path = entry.path();
+ if path.is_dir() {
+ results.extend(find_all_readmes(&path));
+ } else if path.file_name().is_some_and(|n| n == "README.md") {
+ results.push(path);
+ }
+ }
+ }
+ results
+}
+
+/// Find the content directory for a site:
+/// 1. Prefer `pages/` if it exists (backward compatible)
+/// 2. Fall back to the first subdirectory that contains .md files
+fn find_content_dir(site_root: &Path) -> Option<PathBuf> {
+ // Try pages/ first
+ let pages_dir = site_root.join("pages");
+ if pages_dir.exists() && pages_dir.is_dir() {
+ return Some(pages_dir);
+ }
+
+ // Fall back to any subdirectory containing .md files
+ if let Ok(read_dir) = std::fs::read_dir(site_root) {
+ let mut entries: Vec<_> = read_dir.flatten().collect();
+ entries.sort_by_key(|e| e.path());
+ for entry in entries {
+ let path = entry.path();
+ if path.is_dir()
+ && has_markdown_files(&path) {
+ return Some(path);
+ }
+ }
+ }
+
+ None
+}
+
+/// Check if a directory (recursively) contains any .md files.
+fn has_markdown_files(dir: &Path) -> bool {
+ if let Ok(read_dir) = std::fs::read_dir(dir) {
+ for entry in read_dir.flatten() {
+ let path = entry.path();
+ if path.is_dir() {
+ if has_markdown_files(&path) {
+ return true;
+ }
+ } else if path.extension().is_some_and(|ext| ext == "md") {
+ return true;
+ }
+ }
+ }
+ false
+}
+
+/// Build sidebar content: scan .md files in `pages_dir` and return a formatted sidebar string
+fn build_sidebar_content(base_dir: &Path, pages_dir: &Path, sidebar_head: &str) -> String {
+ let mut lines = String::from(sidebar_head);
+
+ // Collect and sort entries at root level first
+ let mut root_files: Vec<SidebarEntry> = Vec::new();
+ // Subdirectory name -> its files
+ let mut sub_dirs: BTreeMap<String, Vec<SidebarEntry>> = BTreeMap::new();
+
+ if let Ok(read_dir) = std::fs::read_dir(pages_dir) {
+ for entry in read_dir.flatten() {
+ let path = entry.path();
+ if path.is_dir() {
+ let dir_name = entry.file_name().to_string_lossy().to_string();
+ let entries = collect_markdown_files(&path, base_dir);
+ if !entries.is_empty() {
+ // Check for .name file to override directory display name
+ let display_name = get_directory_display_name(&path, &dir_name);
+ sub_dirs.insert(display_name, entries);
+ }
+ } else if path.extension().is_some_and(|ext| ext == "md") {
+ let title = extract_title(&path);
+ let relative = path
+ .strip_prefix(base_dir)
+ .unwrap()
+ .to_string_lossy()
+ .replace('\\', "/");
+ let link = relative
+ .strip_suffix(".md")
+ .unwrap_or(&relative)
+ .to_string();
+ root_files.push(SidebarEntry { title, link });
+ }
+ }
+ }
+
+ // Sort root files — natural order (1, 2, ..., 10, 11)
+ root_files.sort_by(|a, b| natural_cmp(&a.link, &b.link));
+
+ // Append root-level files
+ for f in &root_files {
+ let _ = writeln!(lines, "* [{}]({})", f.title, f.link);
+ }
+
+ // Append subdirectory groups
+ for (dir_name, entries) in &sub_dirs {
+ let mut sorted_entries = entries.clone();
+ sorted_entries.sort_by(|a, b| natural_cmp(&a.link, &b.link));
+
+ // Directory header with 2-space indent
+ let _ = writeln!(lines, "* {dir_name}");
+ for f in &sorted_entries {
+ let _ = writeln!(lines, " * [{}]({})", f.title, f.link);
+ }
+ }
+
+ lines
+}
+
+#[derive(Clone)]
+struct SidebarEntry {
+ title: String,
+ link: String,
+}
+
+/// Collect all `.md` files directly under `dir`
+fn collect_markdown_files(dir: &Path, base_dir: &Path) -> Vec<SidebarEntry> {
+ let mut entries = Vec::new();
+
+ if let Ok(read_dir) = std::fs::read_dir(dir) {
+ for entry in read_dir.flatten() {
+ let path = entry.path();
+ if path.extension().is_some_and(|ext| ext == "md") {
+ let title = extract_title(&path);
+ let relative = path
+ .strip_prefix(base_dir)
+ .unwrap()
+ .to_string_lossy()
+ .replace('\\', "/");
+ let link = relative
+ .strip_suffix(".md")
+ .unwrap_or(&relative)
+ .to_string();
+ entries.push(SidebarEntry { title, link });
+ }
+ }
+ }
+
+ entries
+}
+
+/// Extract title from the first line `<h1 align="center">TITLE</h1>`.
+/// Fallback to filename stem.
+fn extract_title(path: &Path) -> String {
+ let content = std::fs::read_to_string(path).unwrap_or_default();
+ if let Some(first_line) = content.lines().next() {
+ let trimmed = first_line.trim();
+ // Find `>TITLE<` between `<h1 align="center">` and `</h1>`
+ if let Some(start) = trimmed.find('>') {
+ let after_start = &trimmed[start + 1..];
+ if let Some(end) = after_start.find('<') {
+ return after_start[..end].to_string();
+ }
+ }
+ }
+ // Fallback: use file stem
+ path.file_stem().map_or_else(
+ || "Untitled".to_string(),
+ |s| s.to_string_lossy().to_string(),
+ )
+}
+
+/// Read `.name` file inside a directory to get its display name for the sidebar.
+/// Falls back to the directory name itself if no `.name` file exists.
+fn get_directory_display_name(dir_path: &std::path::Path, fallback: &str) -> String {
+ let name_file = dir_path.join(".name");
+ if name_file.exists() && name_file.is_file() {
+ std::fs::read_to_string(&name_file)
+ .ok()
+ .map(|s| s.trim().to_string())
+ .filter(|s| !s.is_empty())
+ .unwrap_or_else(|| fallback.to_string())
+ } else {
+ fallback.to_string()
+ }
+}
+
+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
+}
+
+/// Natural (numeric-aware) comparison for sidebar links.
+///
+/// Files prefixed with a number (e.g. `1-getting-started`) are sorted by that number;
+/// files without a numeric prefix fall back to lexicographic order (after numbers).
+fn natural_cmp(a: &str, b: &str) -> std::cmp::Ordering {
+ let num_a = extract_leading_number(a);
+ let num_b = extract_leading_number(b);
+ num_a.cmp(&num_b).then_with(|| a.cmp(b))
+}
+
+/// Extract the leading numeric prefix from a sidebar link path.
+///
+/// Looks at the filename stem (after the last `/`) for a number before the first `-`.
+/// Returns `usize::MAX` for entries without a numeric prefix.
+fn extract_leading_number(link: &str) -> usize {
+ if let Some(file_stem) = link.rsplit('/').next()
+ && let Some(num_end) = file_stem.find('-')
+ && let Ok(num) = file_stem[..num_end].parse::<usize>() {
+ return num;
+ }
+ usize::MAX
+}
diff --git a/.run/src/bin/http-page-preview.ps1 b/.run/src/bin/http-page-preview.ps1
new file mode 100644
index 0000000..8cc3579
--- /dev/null
+++ b/.run/src/bin/http-page-preview.ps1
@@ -0,0 +1,3 @@
+$starting_dir = Get-Location
+python -m http.server 3000
+Set-Location $starting_dir
diff --git a/.run/src/bin/http-page-preview.sh b/.run/src/bin/http-page-preview.sh
new file mode 100644
index 0000000..bed4b1c
--- /dev/null
+++ b/.run/src/bin/http-page-preview.sh
@@ -0,0 +1,2 @@
+#!/bin/bash
+python3 -m http.server 3000
diff --git a/.run/src/bin/install-mling.ps1 b/.run/src/bin/install-mling.ps1
new file mode 100644
index 0000000..bebe9ff
--- /dev/null
+++ b/.run/src/bin/install-mling.ps1
@@ -0,0 +1,7 @@
+cargo install --path mling
+
+New-Item -ItemType Directory -Force -Path .temp/comp | Out-Null
+# Copy all files containing _comp from the debug directory
+Get-ChildItem .temp/target/release/*_comp* | ForEach-Object {
+ Copy-Item $_.FullName .temp/comp/
+}
diff --git a/.run/src/bin/install-mling.sh b/.run/src/bin/install-mling.sh
new file mode 100644
index 0000000..5f2ee7a
--- /dev/null
+++ b/.run/src/bin/install-mling.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+cargo install --path mling
+
+mkdir -p .temp/comp
+cp .temp/target/release/*_comp.* .temp/comp/ 2>/dev/null || echo "No matching files found"
diff --git a/.run/src/bin/refresh-docs.rs b/.run/src/bin/refresh-docs.rs
new file mode 100644
index 0000000..82ef906
--- /dev/null
+++ b/.run/src/bin/refresh-docs.rs
@@ -0,0 +1,178 @@
+use std::path::Path;
+
+use just_fmt::snake_case;
+use just_template::{Template, tmpl};
+use tools::println_cargo_style;
+
+const EXAMPLE_ROOT: &str = "./examples/";
+const OUTPUT_PATH: &str = "./mingling/src/example_docs.rs";
+
+const TEMPLATE_CONTENT: &str = include_str!("../../../mingling/src/example_docs.rs.tmpl");
+
+fn main() {
+ gen_example_doc_module();
+}
+
+fn gen_example_doc_module() {
+ let mut template = Template::from(TEMPLATE_CONTENT);
+ let repo_root = find_git_repo().unwrap();
+ let example_root = repo_root.join(EXAMPLE_ROOT);
+ let mut examples = Vec::new();
+ if let Ok(entries) = std::fs::read_dir(&example_root) {
+ for entry in entries.flatten() {
+ if let Ok(file_type) = entry.file_type()
+ && file_type.is_dir()
+ {
+ let example_name = entry.file_name().to_string_lossy().to_string();
+ // Ignore directories that don't start with "example-"
+ if !example_name.starts_with("example-") {
+ continue;
+ }
+ let example_content = ExampleContent::read(&example_name);
+ examples.push(example_content);
+ }
+ }
+ }
+
+ examples.sort();
+
+ for example in examples {
+ tmpl!(template += {
+ examples {
+ (
+ example_header = example.header,
+ example_import = example.cargo_toml,
+ example_code = example.code,
+ example_name = snake_case!(&example.name)
+ )
+ }
+ });
+ println_cargo_style!("Refresh: {}", example.name);
+ }
+
+ let template_str = template.to_string();
+ let template_str = template_str
+ .lines()
+ .map(str::trim_end)
+ .collect::<Vec<_>>()
+ .join("\n")
+ + "\n";
+ std::fs::write(repo_root.join(OUTPUT_PATH), template_str).unwrap();
+}
+
+struct ExampleContent {
+ name: String,
+ header: String,
+ code: String,
+ cargo_toml: String,
+}
+
+impl PartialOrd for ExampleContent {
+ fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
+impl Ord for ExampleContent {
+ fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+ self.name.cmp(&other.name)
+ }
+}
+
+impl PartialEq for ExampleContent {
+ fn eq(&self, other: &Self) -> bool {
+ self.name == other.name
+ }
+}
+
+impl Eq for ExampleContent {}
+
+impl ExampleContent {
+ pub fn read(name: &str) -> Self {
+ let repo = find_git_repo().unwrap();
+ let cargo_toml = Self::read_cargo_toml(&repo, name);
+ let (header, code) = Self::read_header_and_code(&repo, name);
+
+ let cargo_toml = cargo_toml
+ .lines()
+ .map(|line| format!("/// {line}"))
+ .collect::<Vec<_>>()
+ .join("\n");
+
+ let header = header
+ .lines()
+ .map(|line| format!("/// {line}"))
+ .collect::<Vec<_>>()
+ .join("\n");
+
+ let code = code
+ .lines()
+ .map(|line| format!("/// {line}"))
+ .collect::<Vec<_>>()
+ .join("\n");
+
+ ExampleContent {
+ name: name.to_string(),
+ header,
+ code,
+ cargo_toml,
+ }
+ }
+
+ fn read_header_and_code(repo: &Path, name: &str) -> (String, String) {
+ let file_path = repo
+ .join(EXAMPLE_ROOT)
+ .join(name)
+ .join("src")
+ .join("main.rs");
+ let content = std::fs::read_to_string(&file_path).unwrap_or_default();
+ let mut lines = content.lines();
+ let mut header = String::new();
+ let mut code = String::new();
+
+ // Collect header lines (starting with //!)
+ for line in lines.by_ref() {
+ if line.trim_start().starts_with("//!") {
+ let trimmed = line.trim_start_matches("//!");
+ header.push_str(trimmed);
+ header.push('\n');
+ } else {
+ // First non-header line found, start collecting code
+ code.push_str(line);
+ code.push('\n');
+ break;
+ }
+ }
+
+ // Collect remaining code lines
+ for line in lines {
+ code.push_str(line);
+ code.push('\n');
+ }
+
+ (header.trim().to_string(), code.trim().to_string())
+ }
+
+ fn read_cargo_toml(repo: &Path, name: &str) -> String {
+ let file_path = repo.join(EXAMPLE_ROOT).join(name).join("Cargo.toml");
+
+ std::fs::read_to_string(&file_path).unwrap_or_default()
+ }
+}
+
+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/.run/src/bin/refresh-feature-mod.rs b/.run/src/bin/refresh-feature-mod.rs
new file mode 100644
index 0000000..2255dbc
--- /dev/null
+++ b/.run/src/bin/refresh-feature-mod.rs
@@ -0,0 +1,97 @@
+use std::collections::BTreeSet;
+use std::path::Path;
+
+use just_fmt::snake_case;
+use just_template::{tmpl, Template};
+use tools::println_cargo_style;
+
+const CARGO_TOML_PATH: &str = "./mingling/Cargo.toml";
+const OUTPUT_PATH: &str = "./mingling/src/features.rs";
+
+const TEMPLATE_CONTENT: &str = include_str!("../../../mingling/src/features.rs.tmpl");
+
+fn main() {
+ gen_feature_module();
+}
+
+fn gen_feature_module() {
+ let repo_root = find_git_repo().unwrap();
+
+ let cargo_toml_path = repo_root.join(CARGO_TOML_PATH);
+ let output_path = repo_root.join(OUTPUT_PATH);
+
+ let features = parse_features(&cargo_toml_path);
+
+ let mut template = Template::from(TEMPLATE_CONTENT);
+
+ for feat_name in &features {
+ let feat_const_name = snake_case!(feat_name).to_uppercase();
+
+ tmpl!(template += {
+ features {
+ (
+ feat_name = feat_name,
+ feat_const_name = feat_const_name
+ )
+ }
+ });
+ println_cargo_style!("Refresh: feature `{}`", feat_name);
+ }
+
+ let template_str = template.to_string();
+ let template_str = template_str
+ .lines()
+ .map(str::trim_end)
+ .collect::<Vec<_>>()
+ .join("\n")
+ + "\n";
+ std::fs::write(&output_path, template_str).unwrap();
+
+ println_cargo_style!("Written: features module to {}", OUTPUT_PATH);
+}
+
+/// Parse all feature names from the `[features]` section of a Cargo.toml.
+fn parse_features(cargo_toml_path: &Path) -> Vec<String> {
+ let content = std::fs::read_to_string(cargo_toml_path)
+ .unwrap_or_else(|e| panic!("Failed to read {}: {}", cargo_toml_path.display(), e));
+
+ let cargo_toml: toml::Value = content
+ .parse()
+ .unwrap_or_else(|e| panic!("Failed to parse {}: {}", cargo_toml_path.display(), e));
+
+ let features_table = cargo_toml
+ .get("features")
+ .and_then(|v| v.as_table())
+ .unwrap_or_else(|| {
+ panic!(
+ "No [features] section found in {}",
+ cargo_toml_path.display()
+ )
+ });
+
+ let mut feature_names: BTreeSet<String> = BTreeSet::new();
+ for key in features_table.keys() {
+ feature_names.insert(key.clone());
+ }
+
+ let mut result: Vec<String> = feature_names.into_iter().collect();
+ result.sort();
+ result
+}
+
+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/.run/src/bin/sync-examples.rs b/.run/src/bin/sync-examples.rs
new file mode 100644
index 0000000..0923b33
--- /dev/null
+++ b/.run/src/bin/sync-examples.rs
@@ -0,0 +1,131 @@
+use std::fs;
+use std::path::Path;
+
+use serde::{Deserialize, Serialize};
+use tools::println_cargo_style;
+
+#[derive(Serialize)]
+struct ExampleMeta {
+ id: String,
+ name: String,
+ icon: String,
+ category: String,
+ desc: String,
+ tags: Vec<String>,
+ files: Vec<String>,
+}
+
+#[derive(Deserialize)]
+struct PageToml {
+ example: PageTomlExample,
+}
+
+#[derive(Deserialize)]
+struct PageTomlExample {
+ id: String,
+ #[serde(default)]
+ name: String,
+ #[serde(default = "default_icon")]
+ icon: String,
+ #[serde(default)]
+ category: String,
+ #[serde(default)]
+ desc: String,
+ #[serde(default)]
+ tags: Vec<String>,
+ #[serde(default = "default_files")]
+ files: Vec<String>,
+}
+
+fn default_icon() -> String {
+ "📦".to_string()
+}
+
+fn default_files() -> Vec<String> {
+ vec!["Cargo.toml".to_string(), "src/main.rs".to_string()]
+}
+
+fn main() {
+ #[cfg(windows)]
+ let _ = colored::control::set_virtual_terminal(true);
+
+ let examples_dir = Path::new("examples");
+ let output_dir = Path::new("docs/example-pages");
+ fs::create_dir_all(output_dir).expect("failed to create docs/example-pages");
+
+ let mut examples: Vec<ExampleMeta> = Vec::new();
+
+ let entries = fs::read_dir(examples_dir).expect("failed to read examples/");
+ for entry in entries.flatten() {
+ let path = entry.path();
+ if !path.is_dir() {
+ continue;
+ }
+
+ let dir_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
+
+ let id = dir_name.to_string();
+ let page_toml_path = path.join("page.toml");
+
+ let meta = if page_toml_path.exists() {
+ match fs::read_to_string(&page_toml_path)
+ .map_err(|e| e.to_string())
+ .and_then(|content| toml::from_str::<PageToml>(&content).map_err(|e| e.to_string()))
+ {
+ Ok(page) => {
+ let ex = page.example;
+ ExampleMeta {
+ id: if ex.id.is_empty() { id.clone() } else { ex.id },
+ name: if ex.name.is_empty() {
+ id.clone()
+ } else {
+ ex.name
+ },
+ icon: ex.icon,
+ category: ex.category,
+ desc: ex.desc,
+ tags: ex.tags,
+ files: if ex.files.is_empty() {
+ default_files()
+ } else {
+ ex.files
+ },
+ }
+ }
+ Err(e) => {
+ eprintln!(
+ "Warning: failed to parse {}: {}",
+ page_toml_path.display(),
+ e
+ );
+ continue;
+ }
+ }
+ } else {
+ continue;
+ };
+
+ examples.push(meta);
+ }
+
+ // Sort: basic first, then alphabetical
+ examples.sort_by(|a, b| {
+ if a.id == "example-basic" {
+ return std::cmp::Ordering::Less;
+ }
+ if b.id == "example-basic" {
+ return std::cmp::Ordering::Greater;
+ }
+ a.id.cmp(&b.id)
+ });
+
+ let json = serde_json::to_string_pretty(&examples).expect("failed to serialize");
+ let output_path = output_dir.join("examples.json");
+ fs::write(&output_path, &json).expect("failed to write examples.json");
+
+ println_cargo_style!(
+ "Sync: {} examples -> {}",
+ examples.len(),
+ output_path.display()
+ );
+}
diff --git a/.run/src/bin/test-all-markdown-code.rs b/.run/src/bin/test-all-markdown-code.rs
new file mode 100644
index 0000000..280fca7
--- /dev/null
+++ b/.run/src/bin/test-all-markdown-code.rs
@@ -0,0 +1,276 @@
+use std::collections::HashMap;
+use std::env;
+use std::path::{Path, PathBuf};
+
+use colored::Colorize;
+use indicatif::ProgressBar;
+use tools::verify::{
+ build_block, compute_block_hash, generate_cargo_toml, generate_main_rs, generate_build_rs,
+ is_block_testable, parse_code_blocks, write_summary_report,
+};
+use tools::{eprintln_cargo_style, println_cargo_style};
+
+/// Config from verified-docs.toml
+#[derive(serde::Deserialize)]
+struct Config {
+ verified: VerifiedPaths,
+}
+
+#[derive(serde::Deserialize)]
+struct VerifiedPaths {
+ readme: String,
+ #[serde(default)]
+ documents_en_us: Option<String>,
+ #[serde(default)]
+ documents_zh_cn: Option<String>,
+}
+
+#[tokio::main]
+async fn main() {
+ #[cfg(windows)]
+ let _ = colored::control::set_virtual_terminal(true);
+
+ let config_path = PathBuf::from("verified-docs.toml");
+ if !config_path.exists() {
+ eprintln_cargo_style!("verified-docs.toml not found in current directory");
+ std::process::exit(1);
+ }
+
+ let config: Config = {
+ let content = std::fs::read_to_string(&config_path).unwrap_or_else(|_e| {
+ eprintln_cargo_style!("Failed to read verified-docs.toml");
+ std::process::exit(1);
+ });
+ toml::from_str(&content).unwrap_or_else(|_e| {
+ eprintln_cargo_style!("Failed to parse verified-docs.toml");
+ std::process::exit(1);
+ })
+ };
+
+ // Parse optional path argument from env args
+ let single_file: Option<PathBuf> = {
+ let args: Vec<String> = env::args().collect();
+ if args.len() > 1 {
+ let p = PathBuf::from(&args[1]);
+ if p.exists() {
+ Some(p)
+ } else {
+ eprintln_cargo_style!("error: specified file '{}' does not exist", args[1]);
+ std::process::exit(1);
+ }
+ } else {
+ None
+ }
+ };
+
+ // Collect all markdown files from config
+ let mut files: Vec<(String, PathBuf)> = Vec::new();
+
+ // README
+ let readme_path = PathBuf::from(&config.verified.readme);
+ if readme_path.exists() {
+ files.push(("README".to_string(), readme_path));
+ }
+
+ // English docs
+ if let Some(pattern) = &config.verified.documents_en_us {
+ let base = pattern.trim_end_matches("/**").trim_end_matches('*');
+ let dir = PathBuf::from(base);
+ if dir.exists() && dir.is_dir() {
+ collect_md_files(&dir, &mut files, "en");
+ }
+ }
+
+ // Chinese docs
+ if let Some(pattern) = &config.verified.documents_zh_cn {
+ let base = pattern.trim_end_matches("/**").trim_end_matches('*');
+ let dir = PathBuf::from(base);
+ if dir.exists() && dir.is_dir() {
+ collect_md_files(&dir, &mut files, "zh_CN");
+ }
+ }
+
+ // If a single file was specified, filter the list to only that file
+ if let Some(ref target) = single_file {
+ let target_canon = std::fs::canonicalize(target).unwrap_or_else(|_| target.clone());
+ files.retain(|(_, path)| {
+ std::fs::canonicalize(path)
+ .map(|p| p == target_canon)
+ .unwrap_or(false)
+ });
+ if files.is_empty() {
+ eprintln_cargo_style!(
+ "error: specified file '{}' is not among the configured documentation files",
+ target.display()
+ );
+ std::process::exit(1);
+ }
+ }
+
+ if files.is_empty() {
+ eprintln_cargo_style!("No markdown files found to verify");
+ std::process::exit(1);
+ }
+
+ // Parse all code blocks into a flat list with global indices
+ let mut flat_blocks: Vec<(usize, tools::verify::CodeBlock)> = Vec::new();
+
+ for (label, path) in &files {
+ let content = std::fs::read_to_string(path).unwrap_or_else(|e| {
+ eprintln_cargo_style!("Failed to read {}: {}", path.display(), e);
+ String::new()
+ });
+ let source_file = format!("{label}/{}", path.file_name().unwrap().to_string_lossy());
+ let blocks = parse_code_blocks(&content, &source_file);
+ let testable: Vec<_> = blocks.into_iter().filter(is_block_testable).collect();
+ for block in testable {
+ let idx = flat_blocks.len() + 1; // 1-based global index
+ flat_blocks.push((idx, block));
+ }
+ }
+
+ let total_testable = flat_blocks.len();
+
+ if total_testable == 0 {
+ println_cargo_style!("No testable code blocks found");
+ return;
+ }
+
+ // Create a shared progress bar
+ let bar = ProgressBar::new(total_testable as u64);
+ bar.set_style(
+ indicatif::ProgressStyle::default_bar()
+ .template(&format!(
+ "{} [{{bar:28}}] {{pos}}/{{len}}: {{msg}}",
+ " Testing".bold().bright_cyan()
+ ))
+ .unwrap()
+ .progress_chars("=> "),
+ );
+ bar.set_message("blocks");
+
+ // Group blocks by dependency hash
+ let mut groups: HashMap<String, Vec<(usize, tools::verify::CodeBlock)>> = HashMap::new();
+ for (idx, block) in flat_blocks {
+ let hash = compute_block_hash(&block);
+ groups.entry(hash).or_default().push((idx, block));
+ }
+
+ let temp_base = PathBuf::from(".temp/doc-test");
+
+ // Sort groups by hash for deterministic output order
+ let mut group_vec: Vec<(String, Vec<(usize, tools::verify::CodeBlock)>)> =
+ groups.into_iter().collect();
+ group_vec.sort_by(|a, b| a.0.cmp(&b.0));
+
+ // Spawn a blocking task per group — groups run in parallel, blocks within a group are serial
+ let mut handles = Vec::new();
+ for (hash, blocks) in group_vec {
+ let temp_base = temp_base.clone();
+ let bar = bar.clone(); // clone shares the same underlying progress
+ let handle = tokio::task::spawn_blocking(move || {
+ let crate_dir = temp_base.join(&hash);
+ let src_dir = crate_dir.join("src");
+ let manifest_path = crate_dir.join("Cargo.toml");
+
+ // Generate a single Cargo.toml for the whole group (all blocks share same deps)
+ let first_block = &blocks[0].1;
+ let cargo_toml = generate_cargo_toml(first_block, "test-doc", &manifest_path);
+
+ let mut group_results: Vec<(String, usize, bool, String)> = Vec::new();
+ for (block_idx, block) in &blocks {
+ let block_label =
+ format!("Block {block_idx} ({}:{})", block.source_file, block.line);
+
+ bar.set_message(block_label.clone());
+
+ let main_rs = if block.is_build_time {
+ // For build-time blocks, write a stub main.rs and generate build.rs
+ generate_build_rs(block)
+ } else {
+ generate_main_rs(block)
+ };
+ let (ok, err) = build_block(
+ &src_dir,
+ &manifest_path,
+ &cargo_toml,
+ &main_rs,
+ block.is_build_time,
+ );
+ if ok {
+ bar.inc(1);
+ } else {
+ bar.inc(1);
+ bar.println(format!(
+ " {} {block_label}",
+ "failed".bold().bright_red()
+ ));
+ bar.println(format!(" {block_label} FAILED:\n{err}"));
+ }
+ group_results.push((block.source_file.clone(), block.line, ok, err));
+ }
+ group_results
+ });
+ handles.push(handle);
+ }
+
+ // Collect results from all groups
+ let mut results: Vec<(String, usize, bool, String)> = Vec::new();
+ let mut passed = 0usize;
+ let mut failed = 0usize;
+
+ for handle in handles {
+ match handle.await {
+ Ok(group_results) => {
+ for (file, line, ok, err) in group_results {
+ if ok {
+ passed += 1;
+ } else {
+ failed += 1;
+ }
+ results.push((file, line, ok, err));
+ }
+ }
+ Err(e) => {
+ eprintln_cargo_style!("Task panicked: {}", e);
+ std::process::exit(1);
+ }
+ }
+ }
+
+ bar.finish_and_clear();
+
+ let result_msg = format!("Result: {passed}/{total_testable} blocks passed");
+ println_cargo_style!(result_msg);
+
+ write_summary_report(
+ Path::new(".temp/DOCS-TEST-RESULT.md"),
+ "Documentation Code Block Test Report",
+ &results,
+ total_testable,
+ passed,
+ failed,
+ );
+
+ if failed > 0 {
+ let fail_msg = format!("{failed} block(s) failed to build");
+ eprintln_cargo_style!(fail_msg);
+ std::process::exit(1);
+ }
+
+ println_cargo_style!("Done: All verified code blocks build successfully!");
+}
+
+/// Recursively collect all `.md` files under a directory
+fn collect_md_files(dir: &Path, files: &mut Vec<(String, PathBuf)>, lang: &str) {
+ if let Ok(entries) = std::fs::read_dir(dir) {
+ for entry in entries.flatten() {
+ let path = entry.path();
+ if path.is_dir() {
+ collect_md_files(&path, files, lang);
+ } else if path.extension().is_some_and(|ext| ext == "md") {
+ files.push((lang.to_string(), path));
+ }
+ }
+ }
+}
diff --git a/.run/src/bin/test-all.ps1 b/.run/src/bin/test-all.ps1
new file mode 100644
index 0000000..231698a
--- /dev/null
+++ b/.run/src/bin/test-all.ps1
@@ -0,0 +1,8 @@
+$starting_dir = Get-Location
+Get-ChildItem -Recurse -Filter "Cargo.toml" | ForEach-Object {
+ $project_dir = $_.DirectoryName
+ Push-Location $project_dir
+ cargo test
+ Pop-Location
+}
+Set-Location $starting_dir
diff --git a/.run/src/bin/test-all.sh b/.run/src/bin/test-all.sh
new file mode 100644
index 0000000..b387463
--- /dev/null
+++ b/.run/src/bin/test-all.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+find . -name "Cargo.toml" -type f | while read -r cargo_file; do
+ project_dir=$(dirname "$cargo_file")
+ (cd "$project_dir" && cargo test)
+done
diff --git a/.run/src/bin/test-examples.rs b/.run/src/bin/test-examples.rs
new file mode 100644
index 0000000..539459e
--- /dev/null
+++ b/.run/src/bin/test-examples.rs
@@ -0,0 +1,158 @@
+use std::collections::HashMap;
+
+use colored::Colorize;
+use indicatif::ProgressBar;
+use serde::Deserialize;
+use tools::{eprintln_cargo_style, println_cargo_style};
+
+#[derive(Deserialize)]
+struct TestConfig {
+ test: HashMap<String, Vec<TestCase>>,
+}
+
+#[derive(Deserialize)]
+struct TestCase {
+ command: String,
+ expect: Expect,
+}
+
+#[derive(Deserialize)]
+struct Expect {
+ #[serde(rename = "exit-code")]
+ exit_code: i32,
+ result: String,
+}
+
+fn main() {
+ #[cfg(windows)]
+ let _ = colored::control::set_virtual_terminal(true);
+
+ let config = load_config();
+
+ // Count total test cases upfront
+ let total: usize = config.test.values().map(|cases| cases.len()).sum();
+ let bar = ProgressBar::new(total as u64);
+ bar.set_style(
+ indicatif::ProgressStyle::default_bar()
+ .template(&format!(
+ "{} [{{bar:28}}] {{pos}}/{{len}}: {{msg}}",
+ " Testing".bold().bright_cyan()
+ ))
+ .unwrap()
+ .progress_chars("=> "),
+ );
+ bar.set_message("examples");
+
+ let passed = run_all_tests(&config, &bar);
+
+ bar.finish_and_clear();
+
+ println_cargo_style!("Result: {}/{} tests passed", passed, total);
+
+ if passed != total {
+ eprintln_cargo_style!("{} test(s) failed", total - passed);
+ std::process::exit(1);
+ }
+}
+
+/// Parse test config from TOML file
+fn load_config() -> TestConfig {
+ let content = std::fs::read_to_string("examples/test-examples.toml").unwrap_or_else(|e| {
+ eprintln_cargo_style!("Failed to read TOML config file: {}", e);
+ std::process::exit(1);
+ });
+
+ toml::from_str(&content).unwrap_or_else(|e| {
+ eprintln_cargo_style!("Failed to parse TOML config: {}", e);
+ std::process::exit(1);
+ })
+}
+
+/// Run all example test groups, return number passed
+fn run_all_tests(config: &TestConfig, bar: &ProgressBar) -> usize {
+ let mut passed = 0;
+
+ for (example_name, test_cases) in &config.test {
+ bar.set_message(example_name.clone());
+
+ if !build_example(example_name) {
+ bar.inc(test_cases.len() as u64);
+ continue;
+ }
+
+ for test_case in test_cases {
+ if run_single_test(example_name, test_case, bar) {
+ passed += 1;
+ }
+ bar.inc(1);
+ }
+ }
+
+ passed
+}
+
+/// Build the example binary, return true on success
+fn build_example(example_name: &str) -> bool {
+ let manifest = format!("examples/{example_name}/Cargo.toml");
+ tools::run_cmd_capture(format!(
+ "cargo build --manifest-path {manifest} --color always",
+ ))
+ .is_ok()
+}
+
+/// Run a single test case, return true on pass
+fn run_single_test(example_name: &str, test_case: &TestCase, bar: &ProgressBar) -> bool {
+ let binary_path = format!(".temp/target/debug/{}", get_binary_name(example_name));
+ let args: Vec<&str> = test_case.command.split_whitespace().collect();
+
+ let output = match std::process::Command::new(&binary_path)
+ .args(&args)
+ .output()
+ {
+ Ok(o) => o,
+ Err(e) => {
+ bar.println(format!("'{}' - failed to run: {}", test_case.command, e));
+ return false;
+ }
+ };
+
+ let actual_exit_code = output.status.code().unwrap_or(-1);
+ let actual_stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
+ let actual_stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
+
+ let exit_ok = actual_exit_code == test_case.expect.exit_code;
+ let result_ok = actual_stdout == test_case.expect.result
+ || actual_stdout.contains(&test_case.expect.result);
+
+ if exit_ok && result_ok {
+ true
+ } else {
+ bar.println(format!("failed: '{}'", test_case.command));
+ if !exit_ok {
+ bar.println(format!(
+ " Expected exit code: {}, actual: {}",
+ test_case.expect.exit_code, actual_exit_code
+ ));
+ }
+ if !result_ok {
+ bar.println(format!(" Expected output: {:?}", test_case.expect.result));
+ bar.println(format!(" Actual stdout: {:?}", actual_stdout));
+ if !actual_stderr.is_empty() {
+ bar.println(format!(" Actual stderr: {:?}", actual_stderr));
+ }
+ }
+ false
+ }
+}
+
+/// Resolve binary filename for the given example
+///
+/// The binary name matches the package name. On Windows, the `.exe` suffix is required.
+fn get_binary_name(example_name: &str) -> String {
+ let base = example_name;
+ if cfg!(target_os = "windows") {
+ format!("{base}.exe")
+ } else {
+ base.to_string()
+ }
+}
diff --git a/.run/src/bin/update-version.rs b/.run/src/bin/update-version.rs
new file mode 100644
index 0000000..475e54b
--- /dev/null
+++ b/.run/src/bin/update-version.rs
@@ -0,0 +1,98 @@
+use std::io::Write as _;
+use std::path::Path;
+
+use serde::Deserialize;
+use tools::println_cargo_style;
+
+#[derive(Deserialize)]
+struct VersionFile {
+ file: String,
+ pattern: String,
+}
+
+#[derive(Deserialize)]
+struct Config {
+ #[serde(rename = "file")]
+ files: Vec<VersionFile>,
+}
+
+fn main() {
+ let args: Vec<String> = std::env::args().collect();
+
+ // Get new version
+ let new_ver = if args.len() > 1 {
+ args[1].clone()
+ } else {
+ print!("Update version to: ");
+ std::io::stdout().flush().unwrap();
+ let mut input = String::new();
+ std::io::stdin().read_line(&mut input).unwrap();
+ input.trim().to_string()
+ };
+
+ if new_ver.is_empty() {
+ eprintln!("Error: Version cannot be empty.");
+ std::process::exit(1);
+ }
+
+ // Read current version from root Cargo.toml's workspace.package.version
+ let root_cargo_path = "Cargo.toml";
+ let root_cargo_content =
+ std::fs::read_to_string(root_cargo_path).expect("Failed to read Cargo.toml");
+ let cargo_value: toml::Value = root_cargo_content.parse().expect("Failed to parse Cargo.toml");
+
+ let current_ver = cargo_value["workspace"]["package"]["version"]
+ .as_str()
+ .expect("workspace.package.version not found in Cargo.toml")
+ .to_string();
+
+ if new_ver == current_ver {
+ println!("Version is already {}. Nothing to do.", current_ver);
+ return;
+ }
+
+ println_cargo_style!("Version: {} -> {}", current_ver, new_ver);
+
+ // Read version-files.toml
+ let config_path = Path::new("dev_tools").join("version-files.toml");
+ let config_str =
+ std::fs::read_to_string(&config_path).expect("Failed to read dev_tools/version-files.toml");
+ let config: Config = toml::from_str(&config_str)
+ .expect("Failed to parse dev_tools/version-files.toml");
+
+ let mut updated_count = 0;
+ let mut skipped_count = 0;
+
+ for vf in &config.files {
+ let file_path = &vf.file;
+ let old_pattern = vf.pattern.replace("{VER}", &current_ver);
+ let new_pattern = vf.pattern.replace("{VER}", &new_ver);
+
+ let content = match std::fs::read_to_string(file_path) {
+ Ok(c) => c,
+ Err(e) => {
+ eprintln!("Warning: Could not read {}: {}", file_path, e);
+ skipped_count += 1;
+ continue;
+ }
+ };
+
+ let new_content = content.replace(&old_pattern, &new_pattern);
+
+ if new_content == content {
+ eprintln!(
+ "Warning: Pattern '{}' not found in {}",
+ old_pattern, file_path
+ );
+ skipped_count += 1;
+ continue;
+ }
+
+ std::fs::write(file_path, &new_content)
+ .unwrap_or_else(|e| panic!("Failed to write {}: {}", file_path, e));
+ println_cargo_style!("Updated: {}", file_path);
+ updated_count += 1;
+ }
+
+ println_cargo_style!("Done: {} file(s) updated, {} file(s) skipped", updated_count, skipped_count);
+}
diff --git a/.run/src/bin/windows-folder-hide.ps1 b/.run/src/bin/windows-folder-hide.ps1
new file mode 100644
index 0000000..0ab2632
--- /dev/null
+++ b/.run/src/bin/windows-folder-hide.ps1
@@ -0,0 +1,43 @@
+# Check `last_check`
+
+$lastCheckFile = Join-Path $PSScriptRoot "last_check"
+$currentTime = Get-Date
+$timeThreshold = 10
+
+if (Test-Path $lastCheckFile) {
+ $lastCheckTime = Get-Content $lastCheckFile | Get-Date
+ $timeDiff = ($currentTime - $lastCheckTime).TotalMinutes
+
+ if ($timeDiff -lt $timeThreshold) {
+ exit
+ }
+}
+
+$currentTime.ToString() | Out-File -FilePath $lastCheckFile -Force
+
+# Hide Files
+
+Set-Location -Path (Join-Path $PSScriptRoot "..\..")
+
+Get-ChildItem -Path . -Force -Recurse -ErrorAction SilentlyContinue | Where-Object {
+ $_.FullName -notmatch '\\.temp\\' -and $_.FullName -notmatch '\\.git\\'
+} | ForEach-Object {
+ attrib -h $_.FullName 2>&1 | Out-Null
+}
+
+Get-ChildItem -Path . -Force -Recurse -ErrorAction SilentlyContinue | Where-Object {
+ $_.Name -match '^\..*' -and $_.FullName -notmatch '\\\.\.$' -and $_.FullName -notmatch '\\\.$'
+} | ForEach-Object {
+ attrib +h $_.FullName 2>&1 | Out-Null
+}
+
+if (Get-Command git -ErrorAction SilentlyContinue) {
+ git status --ignored --short | ForEach-Object {
+ if ($_ -match '^!!\s+(.+)$') {
+ $ignoredPath = $matches[1]
+ if ($ignoredPath -notmatch '\.lnk$' -and (Test-Path $ignoredPath)) {
+ attrib +h $ignoredPath 2>&1 | Out-Null
+ }
+ }
+ }
+}