aboutsummaryrefslogtreecommitdiff
path: root/mingling_ci/src/tools
diff options
context:
space:
mode:
Diffstat (limited to 'mingling_ci/src/tools')
-rw-r--r--mingling_ci/src/tools/docsify_refresh.rs373
-rw-r--r--mingling_ci/src/tools/example_refresh.rs279
-rw-r--r--mingling_ci/src/tools/features_refresh.rs96
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
-}