diff options
Diffstat (limited to 'mingling_ci/src/tools')
| -rw-r--r-- | mingling_ci/src/tools/docsify_refresh.rs | 373 | ||||
| -rw-r--r-- | mingling_ci/src/tools/example_refresh.rs | 279 | ||||
| -rw-r--r-- | mingling_ci/src/tools/features_refresh.rs | 96 |
3 files changed, 0 insertions, 748 deletions
diff --git a/mingling_ci/src/tools/docsify_refresh.rs b/mingling_ci/src/tools/docsify_refresh.rs deleted file mode 100644 index dfb9b11..0000000 --- a/mingling_ci/src/tools/docsify_refresh.rs +++ /dev/null @@ -1,373 +0,0 @@ -//! Docsify maintenance: fix code-box blank lines and regenerate `_sidebar.md` -//! files under `docs/`. - -use std::collections::BTreeMap; -use std::fmt::Write as _; -use std::fs; -use std::path::{Path, PathBuf}; - -use mingling::{ - Grouped, RenderResult, Routable, - macros::{buffer, command, r_println, renderer}, -}; - -use crate::Next; -use crate::res::{CargoError, MessagePrinter}; - -const DOCS_DIR: &str = "./docs"; -const SIDEBAR_HEAD: &str = "- [Welcome!](README)\n"; - -#[command(node = "docsify-refresh")] -pub fn docsify_refresh() -> Next { - match refresh_all() { - Ok(written) => ResultDocsifyRefresh { written }.to_chain(), - Err(e) => ErrorDocsifyRefresh(e).to_chain(), - } -} - -fn refresh_all() -> Result<Vec<String>, String> { - let mut written = Vec::new(); - written.extend(fix_code_boxes()); - written.extend(gen_sidebars()?); - Ok(written) -} - -/// Part 1: docsify renders code blocks poorly when the blank lines around -/// them are completely empty — replace them with a single space. -fn fix_code_boxes() -> Vec<String> { - let mut file_count = 0; - let mut fixed_count = 0; - let mut written = Vec::new(); - - collect_md_files(Path::new(DOCS_DIR), &mut |path| { - if path - .file_name() - .is_some_and(|n| n.to_string_lossy().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(); - written.push(format!("fixed: {}", path.display())); - fixed_count += 1; - } - file_count += 1; - }); - - written.push(format!("scanned {file_count} files, fixed {fixed_count}")); - written -} - -/// Replaces completely empty lines adjacent to fenced code blocks with lines -/// containing a single space. -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]; - result.push_str(line); - result.push('\n'); - i += 1; - - if !line.trim_start().starts_with("```") { - continue; - } - - // In a code block: find the closing fence. - let code_start = i; - let mut code_end = len; - let mut found_end = false; - while i < len { - let cline = lines[i]; - if cline.trim_start().starts_with("```") && !cline.trim().is_empty() { - code_end = i; - found_end = true; - break; - } - i += 1; - } - - ensure_space_before_code_block(&mut result); - - 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; - - if i < len && lines[i].trim().is_empty() && lines[i].is_empty() { - result.push(' '); - result.push('\n'); - i += 1; - } - } - } - - while result.ends_with('\n') { - result.pop(); - } - result.push('\n'); - result -} - -/// Turns a trailing `\n\n` before a code block into `\n \n`. -fn ensure_space_before_code_block(result: &mut String) { - let len = result.len(); - if len >= 2 && &result[len - 2..] == "\n\n" { - result.insert(len - 1, ' '); - } -} - -/// Part 2: find every README.md under `docs/` (each is a site root) and -/// regenerate its `_sidebar.md`. -fn gen_sidebars() -> Result<Vec<String>, String> { - let mut written = Vec::new(); - for readme_path in find_all_readmes(Path::new(DOCS_DIR)) { - let site_root = readme_path - .parent() - .ok_or_else(|| format!("{} has no parent", readme_path.display()))?; - if let Some(content_dir) = find_content_dir(site_root) { - let lines = build_sidebar_content(site_root, &content_dir, SIDEBAR_HEAD); - let sidebar_path = site_root.join("_sidebar.md"); - fs::write(&sidebar_path, lines) - .map_err(|e| format!("failed to write {}: {e}", sidebar_path.display()))?; - written.push(format!("generated: {}", sidebar_path.display())); - } - } - Ok(written) -} - -/// Recursively finds 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) = fs::read_dir(dir) { - let mut entries: Vec<_> = read_dir.flatten().collect(); - entries.sort_by_key(std::fs::DirEntry::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 -} - -/// The content directory of a site: `pages/` if present, else the first -/// subdirectory containing markdown files. -fn find_content_dir(site_root: &Path) -> Option<PathBuf> { - let pages_dir = site_root.join("pages"); - if pages_dir.is_dir() { - return Some(pages_dir); - } - if let Ok(read_dir) = fs::read_dir(site_root) { - let mut entries: Vec<_> = read_dir.flatten().collect(); - entries.sort_by_key(std::fs::DirEntry::path); - for entry in entries { - let path = entry.path(); - if path.is_dir() && has_markdown_files(&path) { - return Some(path); - } - } - } - None -} - -fn has_markdown_files(dir: &Path) -> bool { - if let Ok(read_dir) = 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 -} - -#[derive(Clone)] -struct SidebarEntry { - title: String, - link: String, -} - -/// Builds the sidebar content from the markdown files under `pages_dir`. -fn build_sidebar_content(base_dir: &Path, pages_dir: &Path, sidebar_head: &str) -> String { - let mut lines = String::from(sidebar_head); - - let mut root_files: Vec<SidebarEntry> = Vec::new(); - let mut sub_dirs: BTreeMap<String, Vec<SidebarEntry>> = BTreeMap::new(); - - if let Ok(read_dir) = 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().into_owned(); - let entries = collect_markdown_files(&path, base_dir); - if !entries.is_empty() { - 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") { - root_files.push(SidebarEntry { - title: extract_title(&path), - link: relative_link(&path, base_dir), - }); - } - } - } - - root_files.sort_by(|a, b| natural_cmp(&a.link, &b.link)); - for f in &root_files { - let _ = writeln!(lines, "* [{}]({})", f.title, f.link); - } - - 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)); - let _ = writeln!(lines, "* {dir_name}"); - for f in &sorted_entries { - let _ = writeln!(lines, " * [{}]({})", f.title, f.link); - } - } - - lines -} - -/// All `.md` files directly under `dir`, as sidebar entries. -fn collect_markdown_files(dir: &Path, base_dir: &Path) -> Vec<SidebarEntry> { - let mut entries = Vec::new(); - if let Ok(read_dir) = fs::read_dir(dir) { - for entry in read_dir.flatten() { - let path = entry.path(); - if path.extension().is_some_and(|ext| ext == "md") { - entries.push(SidebarEntry { - title: extract_title(&path), - link: relative_link(&path, base_dir), - }); - } - } - } - entries -} - -/// The link of a file relative to `base_dir`, without the `.md` suffix. -fn relative_link(path: &Path, base_dir: &Path) -> String { - path.strip_prefix(base_dir) - .unwrap_or(path) - .to_string_lossy() - .replace('\\', "/") - .strip_suffix(".md") - .unwrap_or_default() - .to_string() -} - -/// Extracts the title from the first line `<h1 align="center">TITLE</h1>`, -/// falling back to the file stem. -fn extract_title(path: &Path) -> String { - let content = fs::read_to_string(path).unwrap_or_default(); - if let Some(first_line) = content.lines().next() { - let trimmed = first_line.trim(); - 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(); - } - } - } - path.file_stem().map_or_else( - || "Untitled".to_string(), - |s| s.to_string_lossy().into_owned(), - ) -} - -/// Reads a directory's `.name` file to override its sidebar display name. -fn get_directory_display_name(dir_path: &Path, fallback: &str) -> String { - let name_file = dir_path.join(".name"); - if name_file.is_file() { - 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() - } -} - -/// Numeric-aware comparison: `1-x` sorts before `10-x`, unnumbered last. -fn natural_cmp(a: &str, b: &str) -> std::cmp::Ordering { - extract_leading_number(a) - .cmp(&extract_leading_number(b)) - .then_with(|| a.cmp(b)) -} - -/// The leading numeric prefix of a link's file stem, `usize::MAX` if absent. -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 -} - -/// Recursively collects all `.md` files under a 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); - } - } - } -} - -/// Files written by `docsify-refresh`. -#[derive(Grouped)] -pub struct ResultDocsifyRefresh { - pub written: Vec<String>, -} - -#[derive(Grouped, Default)] -pub struct ErrorDocsifyRefresh(pub String); - -#[renderer(buffer)] -pub fn render_docsify_refresh(r: ResultDocsifyRefresh) { - for item in r.written { - r_println!("{item}"); - } -} - -#[renderer] -pub fn render_error_docsify_refresh(e: ErrorDocsifyRefresh, error: &CargoError) -> RenderResult { - let render_result = RenderResult::new(); - error.println(vec![e.0]); - render_result -} diff --git a/mingling_ci/src/tools/example_refresh.rs b/mingling_ci/src/tools/example_refresh.rs deleted file mode 100644 index ca8443c..0000000 --- a/mingling_ci/src/tools/example_refresh.rs +++ /dev/null @@ -1,279 +0,0 @@ -//! Regenerates the example documentation module and the examples index. - -use std::collections::HashMap; -use std::fs; -use std::path::Path; - -use just_fmt::snake_case; -use just_template::Template; -use mingling::{ - Grouped, RenderResult, Routable, - macros::{buffer, command, r_println, renderer}, -}; -use serde::Serialize; - -use crate::Next; -use crate::res::{CargoError, MessagePrinter}; - -const EXAMPLE_ROOT: &str = "./examples"; -const EXAMPLE_DOCS_OUTPUT: &str = "./mingling/src/example_docs.rs"; -const EXAMPLE_DOCS_TEMPLATE: &str = include_str!("../../../mingling/src/example_docs.rs.tmpl"); -const EXAMPLES_JSON_OUTPUT: &str = "./docs/example-pages/examples.json"; - -#[command(node = "example-refresh")] -pub fn example_refresh() -> Next { - match refresh_all() { - Ok(written) => ResultExampleRefresh { written }.to_chain(), - Err(e) => ErrorExampleRefresh(e).to_chain(), - } -} - -fn refresh_all() -> Result<Vec<String>, String> { - let mut written = Vec::new(); - written.extend(refresh_example_docs()?); - written.extend(sync_examples()?); - Ok(written) -} - -/// Part 1: regenerate `mingling/src/example_docs.rs` from the examples' -/// `src/main.rs` (header `//!` + code) and `Cargo.toml`. -fn refresh_example_docs() -> Result<Vec<String>, String> { - let mut template = Template::from(EXAMPLE_DOCS_TEMPLATE); - - let mut examples = Vec::new(); - let entries = - fs::read_dir(EXAMPLE_ROOT).map_err(|e| format!("failed to read {EXAMPLE_ROOT}: {e}"))?; - for entry in entries.flatten() { - let path = entry.path(); - if !path.is_dir() { - continue; - } - let name = entry.file_name().to_string_lossy().into_owned(); - if !name.starts_with("example-") { - continue; - } - examples.push(ExampleContent::read(&name)); - } - examples.sort_by(|a, b| a.name.cmp(&b.name)); - - let mut written = Vec::new(); - for example in examples { - template - .add_impl("examples".to_string()) - .push(HashMap::from([ - ("example_header".to_string(), example.header), - ("example_import".to_string(), example.cargo_toml), - ("example_code".to_string(), example.code), - ("example_name".to_string(), snake_case!(&example.name)), - ])); - written.push(format!("example_docs: {}", example.name)); - } - - let template_str = template.to_string(); - let template_str = template_str - .lines() - .map(str::trim_end) - .collect::<Vec<_>>() - .join("\n") - + "\n"; - fs::write(EXAMPLE_DOCS_OUTPUT, template_str) - .map_err(|e| format!("failed to write {EXAMPLE_DOCS_OUTPUT}: {e}"))?; - written.push(format!("written: {EXAMPLE_DOCS_OUTPUT}")); - Ok(written) -} - -struct ExampleContent { - name: String, - header: String, - code: String, - cargo_toml: String, -} - -impl ExampleContent { - fn read(name: &str) -> Self { - let prefix = |s: &str| { - s.lines() - .map(|line| format!("/// {line}")) - .collect::<Vec<_>>() - .join("\n") - }; - - let (header, code) = read_header_and_code(name); - Self { - name: name.to_string(), - header: prefix(&header), - code: prefix(&code), - cargo_toml: prefix(&read_cargo_toml(name)), - } - } -} - -/// Reads an example's `src/main.rs`, splitting `//!` doc header from code. -fn read_header_and_code(name: &str) -> (String, String) { - let content = fs::read_to_string(Path::new(EXAMPLE_ROOT).join(name).join("src/main.rs")) - .unwrap_or_default(); - let mut lines = content.lines(); - let mut header = String::new(); - let mut code = String::new(); - - for line in lines.by_ref() { - if line.trim_start().starts_with("//!") { - header.push_str(line.trim_start_matches("//!")); - header.push('\n'); - } else { - code.push_str(line); - code.push('\n'); - break; - } - } - for line in lines { - code.push_str(line); - code.push('\n'); - } - - (header.trim().to_string(), code.trim().to_string()) -} - -fn read_cargo_toml(name: &str) -> String { - fs::read_to_string(Path::new(EXAMPLE_ROOT).join(name).join("Cargo.toml")).unwrap_or_default() -} - -/// Part 2: regenerate `docs/example-pages/examples.json` from each example's -/// `page.toml`. -fn sync_examples() -> Result<Vec<String>, String> { - fs::create_dir_all("docs/example-pages") - .map_err(|e| format!("failed to create docs/example-pages: {e}"))?; - - let mut examples = Vec::new(); - let entries = - fs::read_dir(EXAMPLE_ROOT).map_err(|e| format!("failed to read {EXAMPLE_ROOT}: {e}"))?; - for entry in entries.flatten() { - let path = entry.path(); - if !path.is_dir() { - continue; - } - let dir_name = entry.file_name().to_string_lossy().into_owned(); - let page_toml = path.join("page.toml"); - if !page_toml.is_file() { - continue; - } - let Ok(content) = fs::read_to_string(&page_toml) else { - continue; - }; - let Ok(table) = content.parse::<toml::Value>() else { - eprintln!("Warning: failed to parse {}", page_toml.display()); - continue; - }; - let Some(example) = table.get("example") else { - continue; - }; - - let get = |key: &str| { - example - .get(key) - .and_then(|v| v.as_str()) - .unwrap_or_default() - }; - let str_vec = |key: &str| { - example - .get(key) - .and_then(|v| v.as_array()) - .map(|a| { - a.iter() - .filter_map(|v| v.as_str().map(str::to_string)) - .collect() - }) - .unwrap_or_default() - }; - - let id = get("id"); - examples.push(ExampleMeta { - id: if id.is_empty() { - dir_name.clone() - } else { - id.to_string() - }, - name: { - let name = get("name"); - if name.is_empty() { - dir_name.clone() - } else { - name.to_string() - } - }, - icon: { - let icon = get("icon"); - if icon.is_empty() { - "📦".to_string() - } else { - icon.to_string() - } - }, - category: get("category").to_string(), - desc: get("desc").to_string(), - tags: str_vec("tags"), - files: { - let files = str_vec("files"); - if files.is_empty() { - vec!["Cargo.toml".to_string(), "src/main.rs".to_string()] - } else { - files - } - }, - }); - } - - // Basic first, then alphabetical. - examples.sort_by( - |a, b| match (a.id == "example-basic", b.id == "example-basic") { - (true, false) => std::cmp::Ordering::Less, - (false, true) => std::cmp::Ordering::Greater, - _ => a.id.cmp(&b.id), - }, - ); - - let json = serde_json::to_string_pretty(&examples) - .map_err(|e| format!("failed to serialize examples: {e}"))?; - fs::write(EXAMPLES_JSON_OUTPUT, json) - .map_err(|e| format!("failed to write {EXAMPLES_JSON_OUTPUT}: {e}"))?; - - Ok(vec![format!( - "synced: {} examples -> {EXAMPLES_JSON_OUTPUT}", - examples.len() - )]) -} - -/// One entry of `docs/example-pages/examples.json`. -#[derive(Serialize)] -struct ExampleMeta { - id: String, - name: String, - icon: String, - category: String, - desc: String, - tags: Vec<String>, - files: Vec<String>, -} - -/// Files written by `example-refresh`. -#[derive(Grouped)] -pub struct ResultExampleRefresh { - pub written: Vec<String>, -} - -#[derive(Grouped, Default)] -pub struct ErrorExampleRefresh(pub String); - -#[renderer(buffer)] -pub fn render_example_refresh(r: ResultExampleRefresh) { - for item in r.written { - r_println!("{item}"); - } -} - -#[renderer] -pub fn render_error_example_refresh(e: ErrorExampleRefresh, error: &CargoError) -> RenderResult { - let render_result = RenderResult::new(); - error.println(vec![e.0]); - render_result -} diff --git a/mingling_ci/src/tools/features_refresh.rs b/mingling_ci/src/tools/features_refresh.rs deleted file mode 100644 index 87aeead..0000000 --- a/mingling_ci/src/tools/features_refresh.rs +++ /dev/null @@ -1,96 +0,0 @@ -//! Regenerates `mingling/src/features.rs` from the `[features]` section of -//! `mingling/Cargo.toml`. - -use std::collections::HashMap; -use std::fs; - -use just_fmt::snake_case; -use just_template::Template; -use mingling::{ - Grouped, RenderResult, Routable, - macros::{buffer, command, r_println, renderer}, -}; - -use crate::Next; -use crate::res::{CargoError, MessagePrinter}; - -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"); - -#[command(node = "features-refresh")] -pub fn features_refresh() -> Next { - match gen_feature_module() { - Ok(written) => ResultFeaturesRefresh { written }.to_chain(), - Err(e) => ErrorFeaturesRefresh(e).to_chain(), - } -} - -fn gen_feature_module() -> Result<Vec<String>, String> { - let features = parse_features()?; - - let mut template = Template::from(TEMPLATE_CONTENT); - let mut written = Vec::new(); - for feat_name in &features { - let feat_const_name = snake_case!(feat_name).to_uppercase(); - template - .add_impl("features".to_string()) - .push(HashMap::from([ - ("feat_name".to_string(), feat_name.clone()), - ("feat_const_name".to_string(), feat_const_name), - ])); - written.push(format!("feature: {feat_name}")); - } - - let template_str = template.to_string(); - let template_str = template_str - .lines() - .map(str::trim_end) - .collect::<Vec<_>>() - .join("\n") - + "\n"; - fs::write(OUTPUT_PATH, template_str) - .map_err(|e| format!("failed to write {OUTPUT_PATH}: {e}"))?; - written.push(format!("written: {OUTPUT_PATH}")); - Ok(written) -} - -/// All feature names from the `[features]` section, sorted. -fn parse_features() -> Result<Vec<String>, String> { - let content = fs::read_to_string(CARGO_TOML_PATH) - .map_err(|e| format!("failed to read {CARGO_TOML_PATH}: {e}"))?; - let table: toml::Value = content - .parse() - .map_err(|e| format!("failed to parse {CARGO_TOML_PATH}: {e}"))?; - let features = table - .get("features") - .and_then(|v| v.as_table()) - .ok_or_else(|| format!("no [features] section in {CARGO_TOML_PATH}"))?; - - let mut names: Vec<String> = features.keys().cloned().collect(); - names.sort(); - Ok(names) -} - -/// Feature names written by `features-refresh`. -#[derive(Grouped)] -pub struct ResultFeaturesRefresh { - pub written: Vec<String>, -} - -#[derive(Grouped, Default)] -pub struct ErrorFeaturesRefresh(pub String); - -#[renderer(buffer)] -pub fn render_features_refresh(r: ResultFeaturesRefresh) { - for item in r.written { - r_println!("{item}"); - } -} - -#[renderer] -pub fn render_error_features_refresh(e: ErrorFeaturesRefresh, error: &CargoError) -> RenderResult { - let render_result = RenderResult::new(); - error.println(vec![e.0]); - render_result -} |
