aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.run/src/bin/check-docs-structure.rs352
-rw-r--r--.run/src/bin/ci.rs464
-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/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.rs261
-rw-r--r--.run/src/bin/test-examples.rs197
9 files changed, 0 insertions, 2108 deletions
diff --git a/.run/src/bin/check-docs-structure.rs b/.run/src/bin/check-docs-structure.rs
deleted file mode 100644
index ac13da2..0000000
--- a/.run/src/bin/check-docs-structure.rs
+++ /dev/null
@@ -1,352 +0,0 @@
-//! Checks that every translated docs directory mirrors the structure of the
-//! reference (English) docs directory exactly.
-//!
-//! The language directories are declared in `.config/docs-lang.txt`, one path
-//! per line (relative to `./docs/`). The first line is the reference
-//! directory; every other line is a translation that must match it.
-//!
-//! For each file pair the tool compares a *structural signature*: one token per
-//! line, classifying headings (both Markdown `#` and HTML `<hN>`), fenced code
-//! blocks (including their language tag), `@@@` hidden-compilation lines, blank
-//! lines, blockquotes, lists and plain text. Translated text is allowed to
-//! differ; the structure is not.
-
-use std::collections::BTreeSet;
-use std::fs;
-use std::path::{Path, PathBuf};
-
-use colored::Colorize;
-use tools::println_cargo_style;
-
-const DOCS_DIR: &str = "./docs";
-const LANG_CONFIG: &str = ".config/docs-lang.txt";
-
-fn main() {
- println_cargo_style!("Checking: docs structure consistency across languages ...");
-
- let repo_root = find_git_repo().expect("Cannot find git repo root");
- let docs_dir = repo_root.join(DOCS_DIR);
-
- let lang_lines = read_lang_config(&repo_root);
- if lang_lines.is_empty() {
- println!("No language directories declared in {LANG_CONFIG}, nothing to check.");
- return;
- }
-
- let reference = docs_dir.join(&lang_lines[0]);
- if !reference.is_dir() {
- eprintln!(
- "Reference docs directory `{}` does not exist.",
- reference.display()
- );
- std::process::exit(1);
- }
-
- let mut failed = false;
-
- for lang in &lang_lines[1..] {
- let lang_dir = docs_dir.join(lang);
- println!("\nChecking `{lang}` against `{}` ...", lang_lines[0]);
- if !lang_dir.is_dir() {
- eprintln!(" ERROR: `{}` does not exist.", lang_dir.display());
- failed = true;
- continue;
- }
- if check_lang_dir(&reference, &lang_dir).is_err() {
- failed = true;
- }
- }
-
- if failed {
- println!();
- eprintln!(
- "{} Fix the differences above.",
- "Docs structure check FAILED.".red().bold()
- );
- std::process::exit(1);
- }
-
- println_cargo_style!("Done: docs structure is consistent across all languages!");
-}
-
-fn read_lang_config(repo_root: &Path) -> Vec<String> {
- let path = repo_root.join(LANG_CONFIG);
- let Ok(content) = fs::read_to_string(&path) else {
- return Vec::new();
- };
- content
- .lines()
- .map(str::trim)
- .filter(|l| !l.is_empty() && !l.starts_with('#'))
- .map(|l| l.trim_start_matches("./").to_string())
- .collect()
-}
-
-/// Returns `Err(())` when the translated directory does not mirror the reference.
-fn check_lang_dir(reference: &Path, lang: &Path) -> Result<(), ()> {
- let mut failed = false;
-
- let ref_files = collect_md_files(reference);
- let lang_files = collect_md_files(lang);
-
- let ref_set: BTreeSet<PathBuf> = ref_files.clone().into_iter().collect();
- let lang_set: BTreeSet<PathBuf> = lang_files.clone().into_iter().collect();
-
- let missing: Vec<PathBuf> = ref_set.difference(&lang_set).cloned().collect();
- let extra: Vec<PathBuf> = lang_set.difference(&ref_set).cloned().collect();
-
- if !missing.is_empty() {
- failed = true;
- println!(" ERROR: files missing in translation:");
- for f in &missing {
- println!(" - {}", f.display());
- }
- }
- if !extra.is_empty() {
- failed = true;
- println!(" ERROR: extra files in translation:");
- for f in &extra {
- println!(" - {}", f.display());
- }
- }
-
- // Compare the structural signature of every file present in both sides.
- for file in &ref_files {
- if !lang_set.contains(file) {
- continue;
- }
- let ref_path = reference.join(file);
- let lang_path = lang.join(file);
- match compare_signature(&ref_path, &lang_path) {
- Ok(()) => {}
- Err(diff) => {
- failed = true;
- eprintln!(
- " {}: structure mismatch in `{}`",
- "ERROR".red().bold(),
- file.display().to_string().cyan()
- );
- for line in diff {
- println!(" {line}");
- }
- }
- }
- }
-
- if failed { Err(()) } else { Ok(()) }
-}
-
-fn collect_md_files(dir: &Path) -> Vec<PathBuf> {
- let mut out = Vec::new();
- let mut stack = vec![dir.to_path_buf()];
- while let Some(current) = stack.pop() {
- let Ok(entries) = fs::read_dir(&current) else {
- continue;
- };
- for entry in entries.flatten() {
- let path = entry.path();
- if path.is_dir() {
- stack.push(path);
- } else if path.extension().is_some_and(|e| e == "md") {
- out.push(path.strip_prefix(dir).unwrap_or(&path).to_path_buf());
- }
- }
- }
- out.sort();
- out
-}
-
-/// Compare the structural signatures of two markdown files.
-///
-/// Returns a list of human-readable diff lines on the first structural
-/// difference found (all differences up to a small window are reported).
-fn compare_signature(ref_path: &Path, lang_path: &Path) -> Result<(), Vec<String>> {
- let ref_content = fs::read_to_string(ref_path).unwrap_or_default();
- let lang_content = fs::read_to_string(lang_path).unwrap_or_default();
-
- let ref_sig = signature_of(&ref_content);
- let lang_sig = signature_of(&lang_content);
-
- if ref_sig == lang_sig {
- return Ok(());
- }
-
- let ref_lines: Vec<&str> = ref_content.lines().collect();
- let lang_lines: Vec<&str> = lang_content.lines().collect();
-
- let mut diffs = Vec::new();
- let mut window = 0;
- let max = ref_sig.len().max(lang_sig.len());
- for i in 0..max {
- let ref_tok = ref_sig.get(i);
- let lang_tok = lang_sig.get(i);
- if ref_tok == lang_tok {
- continue;
- }
- if window >= 5 {
- diffs.push(format!("... ({}-line window truncated)", max - i));
- break;
- }
- window += 1;
- let ref_line = ref_lines.get(i).copied().unwrap_or("<missing>");
- let lang_line = lang_lines.get(i).copied().unwrap_or("<missing>");
- diffs.push(format!(
- " {}: {}",
- "line".yellow().bold(),
- (i + 1).to_string().yellow()
- ));
- diffs.push(format!(
- " {} : {} {}",
- "expect".green().bold(),
- format!("`{}`", token_label(ref_tok.map_or("<eof>", String::as_str))).green(),
- display_line(ref_line).cyan()
- ));
- diffs.push(format!(
- " {} : {} {}",
- "found".red().bold(),
- format!(
- "`{}`",
- token_label(lang_tok.map_or("<eof>", String::as_str))
- )
- .red(),
- display_line(lang_line).cyan()
- ));
- if ref_sig.len() != lang_sig.len() && window >= 5 {
- diffs.push(format!(
- " note: reference has {} lines, translation has {} lines",
- ref_sig.len(),
- lang_sig.len()
- ));
- break;
- }
- }
- if diffs.is_empty() {
- diffs.push("signatures differ in length (see line count note)".to_string());
- }
- Err(diffs)
-}
-
-/// Human-readable label for a structural token.
-fn token_label(token: &str) -> String {
- match token {
- "B" => "blank".to_string(),
- "A" => "@@@".to_string(),
- "Q" => "quote".to_string(),
- "L" => "list".to_string(),
- "P" => "text".to_string(),
- t if t.starts_with("H") => format!("heading-{}", &t[1..]),
- t if t.starts_with("F:") => {
- let lang = &t[2..];
- if lang.is_empty() {
- "fence".to_string()
- } else {
- format!("fence:{lang}")
- }
- }
- _ => token.to_string(),
- }
-}
-
-/// Render a source line for display: blank lines become `<blank>`.
-fn display_line(line: &str) -> String {
- if line.trim().is_empty() {
- "<blank>".to_string()
- } else {
- truncate(line)
- }
-}
-
-/// Build the structural signature of a markdown file.
-fn signature_of(content: &str) -> Vec<String> {
- let mut sig = Vec::new();
- let mut in_fence = false;
- let mut fence_lang = String::new();
-
- for raw_line in content.lines() {
- let line = raw_line.trim();
-
- if in_fence {
- if line.starts_with("```") {
- in_fence = false;
- sig.push(format!("F:{}", fence_lang));
- } else if line.starts_with("@@@") {
- sig.push("A".to_string());
- } else if line.is_empty() {
- sig.push("B".to_string());
- } else {
- sig.push("P".to_string());
- }
- continue;
- }
-
- if line.starts_with("```") {
- in_fence = true;
- fence_lang = line.trim_start_matches("```").trim().to_string();
- sig.push(format!("F:{fence_lang}"));
- } else if line.starts_with('#') {
- let level = line.chars().take_while(|c| *c == '#').count();
- sig.push(format!("H{level}"));
- } else if line.starts_with("<h") || line.starts_with("</h") {
- // HTML headings (e.g. `<h1 align="center">` / `</h1>`)
- let level = line
- .trim_start_matches(['<', '/'])
- .chars()
- .next()
- .and_then(|c| c.to_digit(10))
- .unwrap_or(1);
- sig.push(format!("H{level}"));
- } else if line.starts_with("@@@") {
- sig.push("A".to_string());
- } else if line.is_empty() {
- sig.push("B".to_string());
- } else if line.starts_with('>') {
- sig.push("Q".to_string());
- } else if is_list_line(line) {
- sig.push("L".to_string());
- } else {
- sig.push("P".to_string());
- }
- }
-
- // An unclosed fence is still a fence line; the signature already recorded it.
- sig
-}
-
-fn is_list_line(line: &str) -> bool {
- let trimmed = line.trim_start();
- trimmed.starts_with("- ")
- || trimmed.starts_with("* ")
- || trimmed.starts_with("+ ")
- || is_numbered_list(trimmed)
-}
-
-/// A numbered list item: `1. text`, `1) text`, `10. text`, ...
-fn is_numbered_list(line: &str) -> bool {
- let digit_count = line.chars().take_while(|c| c.is_ascii_digit()).count();
- if digit_count == 0 {
- return false;
- }
- let rest = &line[digit_count..];
- (rest.starts_with(". ") || rest.starts_with(") "))
- && rest.chars().nth(1).is_some_and(|c| c == ' ' || c == '\t')
-}
-
-fn truncate(line: &str) -> String {
- const MAX: usize = 60;
- if line.chars().count() <= MAX {
- line.to_string()
- } else {
- let cut: String = line.chars().take(MAX).collect();
- format!("{cut}...")
- }
-}
-
-fn find_git_repo() -> Option<PathBuf> {
- let mut current = std::env::current_dir().ok()?;
- loop {
- if current.join(".git").is_dir() {
- return Some(current);
- }
- current = current.parent()?.to_path_buf();
- }
-}
diff --git a/.run/src/bin/ci.rs b/.run/src/bin/ci.rs
deleted file mode 100644
index b6d92b8..0000000
--- a/.run/src/bin/ci.rs
+++ /dev/null
@@ -1,464 +0,0 @@
-use std::io::Write as _;
-use std::path::{Path, PathBuf};
-use std::process::exit;
-
-use arg_picker::{Picker, macros::arg};
-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()]
-}
-
-/// A single CI step, each individually toggleable via `--check-*`.
-struct Checks {
- build: bool,
- clippy: bool,
- test: bool,
- arg_picker: bool,
- markdown_code: bool,
- examples: bool,
- docs_refresh: bool,
- docs_structure: bool,
- api_docs: bool,
-}
-
-impl Checks {
- fn any(&self) -> bool {
- self.build
- || self.clippy
- || self.test
- || self.arg_picker
- || self.markdown_code
- || self.examples
- || self.docs_refresh
- || self.docs_structure
- || self.api_docs
- }
-}
-
-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)
- --check-build Build all crates
- --check-clippy Run clippy on all crates (-D warnings)
- --check-test Run unit tests for all crates
- --check-arg-picker Test the arg-picker crate
- --check-markdown-code Verify all *.md code blocks compile
- --check-examples Test all examples
- --check-docs-refresh Refresh docs and fail if the tree is contaminated
- --check-docs-structure Verify translated docs mirror the English structure
- --check-api-docs Build API docs with docs.rs features
-
-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 (
- auto_yes,
- dirty,
- check_build,
- check_clippy,
- check_test,
- check_arg_picker,
- check_markdown_code,
- check_examples,
- check_docs_refresh,
- check_docs_structure,
- check_api_docs,
- help,
- ) = Picker::from_args()
- .pick_or_default(&arg![yes: bool, 'y'])
- .pick_or_default(&arg![dirty: bool])
- .pick_or_default(&arg![check_build: bool])
- .pick_or_default(&arg![check_clippy: bool])
- .pick_or_default(&arg![check_test: bool])
- .pick_or_default(&arg![check_arg_picker: bool])
- .pick_or_default(&arg![check_markdown_code: bool])
- .pick_or_default(&arg![check_examples: bool])
- .pick_or_default(&arg![check_docs_refresh: bool])
- .pick_or_default(&arg![check_docs_structure: bool])
- .pick_or_default(&arg![check_api_docs: bool])
- .pick_or_default(&arg![help: bool, 'h'])
- .unwrap();
-
- if help {
- print_help();
- return;
- }
-
- let checks = Checks {
- build: check_build,
- clippy: check_clippy,
- test: check_test,
- arg_picker: check_arg_picker,
- markdown_code: check_markdown_code,
- examples: check_examples,
- docs_refresh: check_docs_refresh,
- docs_structure: check_docs_structure,
- api_docs: check_api_docs,
- };
- let run_all = !checks.any();
-
- 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(&checks, 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(())
-}
-
-/// Run one CI step.
-///
-/// When `continue_on_error` is set (used for the documentation steps in
-/// "run all" mode), a failing step is recorded and the remaining steps still
-/// execute, so every problem is reported in a single run.
-fn run_step(
- exit_code: &mut i32,
- phase: &str,
- step: fn() -> Result<(), i32>,
- continue_on_error: bool,
-) -> Result<(), i32> {
- println_cargo_style!(phase);
- match step() {
- Ok(()) => Ok(()),
- Err(code) if continue_on_error => {
- *exit_code = (*exit_code).max(code);
- Ok(())
- }
- Err(code) => Err(code),
- }
-}
-
-fn ci(checks: &Checks, run_all: bool) -> Result<(), i32> {
- let mut exit_code = 0;
-
- if run_all || checks.build {
- run_step(
- &mut exit_code,
- "Phase: Scan and build all crates",
- build_all,
- false,
- )?;
- }
- if run_all || checks.clippy {
- run_step(
- &mut exit_code,
- "Phase: Run clippy for all crates",
- clippy_all,
- false,
- )?;
- }
- if run_all || checks.test {
- run_step(&mut exit_code, "Phase: Test all crates", test_all, false)?;
- }
- if run_all || checks.arg_picker {
- run_step(
- &mut exit_code,
- "Phase: Test arg picker",
- test_arg_picker,
- false,
- )?;
- }
-
- if run_all || checks.markdown_code {
- run_step(
- &mut exit_code,
- "Phase: Verify all *.md document code blocks are compilable",
- test_docs_code_blocks,
- run_all,
- )?;
- }
- if run_all || checks.examples {
- run_step(
- &mut exit_code,
- "Phase: Test all examples",
- test_examples,
- run_all,
- )?;
- }
- if run_all || checks.docs_refresh {
- run_step(
- &mut exit_code,
- "Phase: Check all documentation is up to date",
- docs_refresh,
- run_all,
- )?;
- }
- if run_all || checks.docs_structure {
- run_step(
- &mut exit_code,
- "Phase: Check translated docs structure consistency",
- docs_structure,
- run_all,
- )?;
- }
- if run_all || checks.api_docs {
- run_step(
- &mut exit_code,
- "Phase: Try Build API docs",
- deploy_api_docs,
- run_all,
- )?;
- }
-
- if exit_code != 0 {
- return Err(exit_code);
- }
-
- run_cmd!("git add --renormalize .")?;
-
- Ok(())
-}
-
-fn test_examples() -> Result<(), i32> {
- run_cmd!("cargo run --manifest-path .run/Cargo.toml --color always --bin test-examples")
-}
-
-fn test_docs_code_blocks() -> Result<(), i32> {
- run_cmd!(
- "cargo run --manifest-path .run/Cargo.toml --color always --bin test-all-markdown-code"
- )
-}
-
-/// Returns the manifest paths of all workspace members (via `cargo metadata --no-deps`).
-///
-/// These crates are tested/built/clipped together with `--workspace` so that
-/// feature-gated code is covered, instead of relying on each crate's default features.
-fn workspace_manifests() -> Vec<PathBuf> {
- let Ok(output) = tools::run_cmd_capture("cargo metadata --no-deps --format-version 1") else {
- return Vec::new();
- };
- let Ok(json) = serde_json::from_str::<serde_json::Value>(&output) else {
- return Vec::new();
- };
- json["packages"]
- .as_array()
- .into_iter()
- .flatten()
- .filter_map(|p| p["manifest_path"].as_str().map(PathBuf::from))
- .collect()
-}
-
-fn same_path(a: &Path, b: &Path) -> bool {
- let norm = |p: &Path| std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
- norm(a) == norm(b)
-}
-
-fn build_all() -> Result<(), i32> {
- let ignore_dirs = get_ignore_dirs();
- let cargo_tomls = cargo_tomls();
- let workspace_manifests = workspace_manifests();
- let mut tasks = Vec::new();
-
- // Workspace members: build with all documented features (same set used by cov-test)
- let features_arg = doc_features_arg();
- tasks.push((
- "Build: workspace".to_string(),
- "workspace".to_string(),
- format!("cargo build --workspace{features_arg} --color always"),
- ));
-
- for cargo_toml in cargo_tomls {
- let path = cargo_toml.parent().unwrap_or(Path::new(""));
- let path_str = path.to_string_lossy();
- if ignore_dirs.iter().any(|d| path_str.contains(d.as_str())) {
- continue;
- }
- if workspace_manifests
- .iter()
- .any(|m| same_path(m, &cargo_toml))
- {
- 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 workspace_manifests = workspace_manifests();
- let mut tasks = Vec::new();
-
- // Workspace members: clippy with all documented features
- let features_arg = doc_features_arg();
- tasks.push((
- "Clippy: workspace".to_string(),
- "workspace".to_string(),
- format!("cargo clippy --workspace{features_arg} --color always -- -D warnings"),
- ));
-
- for cargo_toml in cargo_tomls {
- let path = cargo_toml.parent().unwrap_or(Path::new(""));
- let path_str = path.to_string_lossy();
- if ignore_dirs.iter().any(|d| path_str.contains(d.as_str())) {
- continue;
- }
- if workspace_manifests
- .iter()
- .any(|m| same_path(m, &cargo_toml))
- {
- 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)
-}
-
-/// ` --features "<docs.rs features>"` (empty string when unavailable)
-fn doc_features_arg() -> String {
- match tools::read_features() {
- Ok(features) if !features.is_empty() => format!(" --features \"{}\"", features.join(",")),
- _ => String::new(),
- }
-}
-
-fn test_all() -> Result<(), i32> {
- let ignore_dirs = get_ignore_dirs();
- let cargo_tomls = cargo_tomls();
- let workspace_manifests = workspace_manifests();
- let mut tasks = Vec::new();
-
- // Workspace members: test with all documented features so that feature-gated
- // tests (comp/repl/picker/structural_renderer/...) are actually executed.
- // `arg-picker` is excluded here and tested separately via [`test_arg_picker`].
- let features_arg = doc_features_arg();
- tasks.push((
- "Test: workspace".to_string(),
- "workspace".to_string(),
- format!("cargo test --workspace{features_arg} --exclude arg-picker --color always"),
- ));
-
- for cargo_toml in cargo_tomls {
- let path = cargo_toml.parent().unwrap_or(Path::new(""));
- let path_str = path.to_string_lossy();
- if ignore_dirs.iter().any(|d| path_str.contains(d.as_str())) {
- continue;
- }
- if workspace_manifests
- .iter()
- .any(|m| same_path(m, &cargo_toml))
- {
- continue;
- }
- let label = format!("Test: {}", 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)
-}
-
-/// `arg-picker` is excluded from the workspace test command: when built with
-/// `mingling_support` (enabled via `mingling/picker`), its README doctests
-/// expand `arg!` to `::mingling::picker::PickerArg`, which is not available
-/// inside the arg-picker crate itself. Test it separately with its default
-/// features instead.
-fn test_arg_picker() -> Result<(), i32> {
- run_cmd!("cargo test -p arg-picker --color always")
-}
-
-fn deploy_api_docs() -> Result<(), i32> {
- run_cmd!(
- "cargo run --manifest-path .run/Cargo.toml --color always --bin deploy-api-docs -- --docsrs"
- )
-}
-
-fn docs_refresh() -> Result<(), i32> {
- println_cargo_style!("Refresh: document at `./docs/`");
-
- run_cmd!("cargo run --manifest-path .run/Cargo.toml --bin docs-code-box-fix")?;
- run_cmd!("cargo run --manifest-path .run/Cargo.toml --bin docsify-sidebar-gen")?;
- run_cmd!("cargo run --manifest-path .run/Cargo.toml --bin refresh-docs")?;
- run_cmd!("cargo run --manifest-path .run/Cargo.toml --bin refresh-feature-mod")?;
- run_cmd!("cargo run --manifest-path .run/Cargo.toml --bin sync-examples")?;
- run_cmd!("cargo fmt")?;
-
- Ok(())
-}
-
-fn docs_structure() -> Result<(), i32> {
- println_cargo_style!("Check: docs structure consistency across languages");
-
- run_cmd!("cargo run --manifest-path .run/Cargo.toml --bin check-docs-structure")
-}
diff --git a/.run/src/bin/docs-code-box-fix.rs b/.run/src/bin/docs-code-box-fix.rs
deleted file mode 100644
index 21d2cce..0000000
--- a/.run/src/bin/docs-code-box-fix.rs
+++ /dev/null
@@ -1,166 +0,0 @@
-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
deleted file mode 100644
index 15ae184..0000000
--- a/.run/src/bin/docsify-sidebar-gen.rs
+++ /dev/null
@@ -1,262 +0,0 @@
-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/refresh-docs.rs b/.run/src/bin/refresh-docs.rs
deleted file mode 100644
index 82ef906..0000000
--- a/.run/src/bin/refresh-docs.rs
+++ /dev/null
@@ -1,178 +0,0 @@
-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
deleted file mode 100644
index 4cd6532..0000000
--- a/.run/src/bin/refresh-feature-mod.rs
+++ /dev/null
@@ -1,97 +0,0 @@
-use std::collections::BTreeSet;
-use std::path::Path;
-
-use just_fmt::snake_case;
-use just_template::{Template, tmpl};
-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
deleted file mode 100644
index 0923b33..0000000
--- a/.run/src/bin/sync-examples.rs
+++ /dev/null
@@ -1,131 +0,0 @@
-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
deleted file mode 100644
index 35c8bbe..0000000
--- a/.run/src/bin/test-all-markdown-code.rs
+++ /dev/null
@@ -1,261 +0,0 @@
-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_build_rs, generate_cargo_toml, generate_main_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: HashMap<String, String>,
-}
-
-#[tokio::main]
-async fn main() {
- #[cfg(windows)]
- let _ = colored::control::set_virtual_terminal(true);
-
- let config_path = PathBuf::from(".config/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
- // Keys are used as labels; values are either single file paths or directory globs
- let mut files: Vec<(String, PathBuf)> = Vec::new();
-
- for (key, value) in &config.verified {
- let candidate = PathBuf::from(value);
- if candidate.is_dir() {
- // Directory — walk it for all .md files, using the key as label
- collect_md_files(&candidate, &mut files, key);
- } else if candidate.exists() && candidate.is_file() {
- // Single file
- files.push((key.to_string(), candidate));
- } else if candidate.extension().is_none() {
- // No extension — treat as a glob like "docs/pages/**", walk the base dir instead
- let base = PathBuf::from(value.trim_end_matches("/**").trim_end_matches('*'));
- if base.is_dir() {
- collect_md_files(&base, &mut files, key);
- }
- }
- }
-
- // Sort for deterministic ordering
- files.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
-
- // 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-examples.rs b/.run/src/bin/test-examples.rs
deleted file mode 100644
index 617a745..0000000
--- a/.run/src/bin/test-examples.rs
+++ /dev/null
@@ -1,197 +0,0 @@
-use std::path::Path;
-
-use colored::Colorize;
-use indicatif::ProgressBar;
-use serde::Deserialize;
-use tools::{eprintln_cargo_style, println_cargo_style, run_parallel};
-
-/// An example's `test.toml` (`[[runs]]` entries).
-#[derive(Deserialize)]
-struct TestConfig {
- runs: Vec<TestCase>,
-}
-
-/// A single `[[runs]]` entry of an example's `test.toml`.
-#[derive(Deserialize)]
-struct TestCase {
- input: Vec<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 configs = load_all_test_configs();
-
- // Phase 1: build all examples in parallel.
- if let Err(code) = build_all_examples(&configs) {
- // `run_parallel` already printed every failed build above.
- std::process::exit(code);
- }
-
- // Phase 2: run the tests serially against the pre-built binaries.
- let total: usize = configs.iter().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(&configs, &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);
- }
-}
-
-/// Load `examples/<name>/test.toml` for every example that has one, in
-/// alphabetical order of the example directory name.
-fn load_all_test_configs() -> Vec<(String, Vec<TestCase>)> {
- let examples_dir = Path::new("examples");
- let mut configs = Vec::new();
-
- let entries = std::fs::read_dir(examples_dir).unwrap_or_else(|e| {
- eprintln_cargo_style!("Failed to read examples dir: {}", e);
- std::process::exit(1);
- });
-
- for entry in entries.flatten() {
- let path = entry.path();
- if !path.is_dir() {
- continue;
- }
- let test_toml = path.join("test.toml");
- if !test_toml.is_file() {
- continue;
- }
- let name = path
- .file_name()
- .and_then(|n| n.to_str())
- .unwrap_or_default()
- .to_string();
- let content = std::fs::read_to_string(&test_toml).unwrap_or_else(|e| {
- eprintln_cargo_style!("Failed to read {}: {}", test_toml.display(), e);
- std::process::exit(1);
- });
- let config: TestConfig = toml::from_str(&content).unwrap_or_else(|e| {
- eprintln_cargo_style!("Failed to parse {}: {}", test_toml.display(), e);
- std::process::exit(1);
- });
- configs.push((name, config.runs));
- }
-
- configs.sort_by(|a, b| a.0.cmp(&b.0));
- configs
-}
-
-/// Phase 1: build every example that has a `test.toml` in parallel.
-///
-/// Build tasks are spawned in parallel (like `ci.rs`'s `build_all`); on any
-/// build failure the whole run aborts with the first failure's exit code.
-fn build_all_examples(configs: &[(String, Vec<TestCase>)]) -> Result<(), i32> {
- let tasks: Vec<(String, String, String)> = configs
- .iter()
- .map(|(name, _)| {
- (
- format!("Build: {name}"),
- name.clone(),
- format!("cargo build --manifest-path examples/{name}/Cargo.toml --color always"),
- )
- })
- .collect();
- run_parallel("Building", tasks)
-}
-
-/// Phase 2: run all example test groups serially, return number passed
-fn run_all_tests(configs: &[(String, Vec<TestCase>)], bar: &ProgressBar) -> usize {
- let mut passed = 0;
-
- for (example_name, test_cases) in configs {
- bar.set_message(example_name.clone());
-
- for test_case in test_cases {
- if run_single_test(example_name, test_case, bar) {
- passed += 1;
- }
- bar.inc(1);
- }
- }
-
- passed
-}
-
-/// 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 command = test_case.input.join(" ");
-
- let output = match std::process::Command::new(&binary_path)
- .args(&test_case.input)
- .output()
- {
- Ok(o) => o,
- Err(e) => {
- bar.println(format!("'{command}' - failed to run: {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: '{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()
- }
-}