├── .serena ├── .gitignore └── project.yml ├── .gitignore ├── src ├── output │ ├── markdown │ │ ├── mod.rs │ │ └── tokenizer.rs │ ├── mod.rs │ ├── manager.rs │ ├── format │ │ ├── structured.rs │ │ ├── csv.rs │ │ ├── mod.rs │ │ └── markdown.rs │ └── dependency.rs ├── main.rs ├── lib.rs ├── app │ ├── logging.rs │ ├── mod.rs │ ├── commands │ │ └── mod.rs │ └── pipeline.rs ├── errors.rs ├── sources.rs ├── travert.rs ├── config.rs └── cli.rs ├── tests ├── cli_tests.rs ├── config_tests.rs ├── converter_tests.rs ├── travert_tests.rs ├── dependency_tests.rs ├── source_tests.rs ├── markdown_tokenizer_tests.rs ├── markdown_format_tests.rs └── output_format_tests.rs ├── .vscode ├── settings.json ├── i18n-ally-custom-framework.yml └── launch.json ├── LICENSE.md ├── Cargo.toml ├── justfile ├── assets └── output │ ├── THANKU_en.csv │ ├── converted │ ├── THANKU_en.csv │ ├── THANKU_en_ml.md │ ├── THANKU_en_mt.md │ ├── THANKU_en.toml │ ├── THANKU_en.yaml │ └── THANKU_en.json │ ├── THANKU_list_en.md │ ├── THANKU_table_origin.md │ ├── THANKU_table_en.md │ ├── THANKU_list_origin_zh.md │ ├── THANKU_yaml_en.yaml │ └── THANKU_json_en.json ├── THANKU.md ├── .github └── workflows │ └── ci.yml ├── README_CN.md └── README.md /.serena/.gitignore: -------------------------------------------------------------------------------- 1 | /cache 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/[Cc]onverted/[Cc]onverted -------------------------------------------------------------------------------- /src/output/markdown/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod tokenizer; 2 | -------------------------------------------------------------------------------- /tests/cli_tests.rs: -------------------------------------------------------------------------------- 1 | use cargo_thanku::cli::build_cli; 2 | 3 | #[test] 4 | fn verify_cli() { 5 | build_cli().debug_assert(); 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "i18n-ally.localesPaths": [ 3 | "locales" 4 | ], 5 | "i18n-ally.sourceLanguage": "app" 6 | } -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use cargo_thanku::app; 3 | 4 | #[tokio::main] 5 | async fn main() -> Result<()> { 6 | app::run().await 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/i18n-ally-custom-framework.yml: -------------------------------------------------------------------------------- 1 | languageIds: 2 | - rust 3 | 4 | usageMatchRegex: 5 | - "[^\\w\\d]t!\\([\\s\\n\\r]*['\"]({key})['\"]" 6 | 7 | monopoly: true -------------------------------------------------------------------------------- /src/output/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod dependency; 2 | pub mod format; 3 | mod manager; 4 | pub mod markdown; 5 | 6 | pub use dependency::{DependencyInfo, DependencyKind, DependencyStats}; 7 | pub use format::{Formatter, OutputFormat}; 8 | pub use manager::OutputManager; 9 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate rust_i18n; 3 | 4 | rust_i18n::i18n!( 5 | "locales", 6 | fallback = ["zh", "en", "ja", "ko", "es", "fr", "de", "it"] 7 | ); 8 | 9 | pub mod app; 10 | pub mod cli; 11 | pub mod config; 12 | pub mod errors; 13 | pub mod output; 14 | pub mod sources; 15 | pub mod travert; 16 | -------------------------------------------------------------------------------- /src/app/logging.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use tracing::{Level, instrument}; 3 | 4 | #[instrument] 5 | pub fn init(log_level: Level) -> Result<()> { 6 | let mut log_fmt = tracing_subscriber::fmt() 7 | .with_env_filter( 8 | tracing_subscriber::EnvFilter::builder() 9 | .with_default_directive(log_level.into()) 10 | .from_env_lossy(), 11 | ) 12 | .with_level(true); 13 | 14 | #[cfg(debug_assertions)] 15 | { 16 | log_fmt = log_fmt 17 | .with_target(true) 18 | .with_thread_ids(true) 19 | .with_line_number(true) 20 | .with_file(true); 21 | } 22 | 23 | log_fmt.init(); 24 | Ok(()) 25 | } 26 | -------------------------------------------------------------------------------- /src/output/manager.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | 3 | use anyhow::Result; 4 | 5 | use super::{ 6 | dependency::DependencyInfo, 7 | format::{Formatter, OutputFormat}, 8 | }; 9 | 10 | pub struct OutputManager { 11 | formatter: Box, 12 | writer: W, 13 | } 14 | 15 | impl OutputManager { 16 | pub fn new(format: OutputFormat, writer: W) -> Self { 17 | let formatter = ::new(format).expect("invalid formatter"); 18 | Self { formatter, writer } 19 | } 20 | 21 | pub fn write(&mut self, deps: &[DependencyInfo]) -> Result<()> { 22 | let content = self.formatter.format(deps)?; 23 | self.writer.write_all(content.as_bytes())?; 24 | self.writer.flush()?; 25 | Ok(()) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/config_tests.rs: -------------------------------------------------------------------------------- 1 | use cargo_thanku::config::{Config, OutputWriter}; 2 | 3 | #[test] 4 | fn output_writer_stdout_dash() { 5 | let mut config = Config::default(); 6 | config.output = Some("-".into()); 7 | match config.get_output_writer().expect("stdout writer") { 8 | OutputWriter::Stdout(_) => {} 9 | _ => panic!("expected stdout writer"), 10 | } 11 | } 12 | 13 | #[test] 14 | fn output_writer_file() { 15 | let temp = assert_fs::NamedTempFile::new("test-output.md").expect("temp file"); 16 | let mut config = Config::default(); 17 | config.output = Some(temp.path().to_path_buf()); 18 | match config.get_output_writer().expect("file writer") { 19 | OutputWriter::File(_) => {} 20 | _ => panic!("expected file writer"), 21 | } 22 | } 23 | 24 | #[test] 25 | fn output_writer_default_stdout() { 26 | let config = Config::default(); 27 | match config.get_output_writer().expect("stdout") { 28 | OutputWriter::Stdout(_) => {} 29 | _ => panic!("expected stdout writer"), 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 unic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /tests/converter_tests.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, path::PathBuf}; 2 | 3 | use assert_fs::prelude::*; 4 | use cargo_thanku::travert::Converter; 5 | 6 | fn sample_markdown() -> &'static str { 7 | "| 名称 | 描述 | 链接 | 来源 | 统计 | 状态 |\n|---|---|---|---|---|---|\n| 🔍 | Normal | | | | |\n| anyhow | desc | [anyhow](https://crates.io/crates/anyhow) | [GitHub](https://github.com/dtolnay/anyhow) | 🌟 1 | ✅ |" 8 | } 9 | 10 | #[test] 11 | fn converter_writes_targets() { 12 | let temp = assert_fs::TempDir::new().expect("temp dir"); 13 | let input = temp.child("input.md"); 14 | input.write_str(sample_markdown()).expect("write md"); 15 | 16 | let converted_dir = temp.child("converted"); 17 | converted_dir.create_dir_all().expect("converted dir"); 18 | let output = converted_dir.child("thanks.json"); 19 | let target_path: PathBuf = output.path().to_path_buf(); 20 | 21 | let converter = Converter::new(input.path(), [target_path.as_path()]).expect("converter"); 22 | converter.convert().expect("convert"); 23 | 24 | assert!(output.path().is_file()); 25 | let content = fs::read_to_string(output.path()).expect("read output"); 26 | assert!(content.contains("anyhow")); 27 | } 28 | -------------------------------------------------------------------------------- /tests/travert_tests.rs: -------------------------------------------------------------------------------- 1 | use assert_fs::prelude::*; 2 | use cargo_thanku::{output::OutputFormat, travert::Travert}; 3 | 4 | fn write_temp(content: &str, name: &str) -> assert_fs::NamedTempFile { 5 | let file = assert_fs::NamedTempFile::new(name).expect("temp file"); 6 | file.write_str(content).expect("write content"); 7 | file 8 | } 9 | 10 | #[test] 11 | fn detect_markdown_table_format() { 12 | let file = write_temp("| Name | Desc |\n|---|---|\n| a | b |", "table.md"); 13 | let travert = Travert::new(file.path()).expect("travert"); 14 | assert_eq!(travert.format, OutputFormat::MarkdownTable); 15 | } 16 | 17 | #[test] 18 | fn detect_markdown_list_format() { 19 | let file = write_temp("- item\n- item2", "list.md"); 20 | let travert = Travert::new(file.path()).expect("travert"); 21 | assert_eq!(travert.format, OutputFormat::MarkdownList); 22 | } 23 | 24 | #[test] 25 | fn detect_complex_table_format() { 26 | let content = "| Left | Right |\n|:---|---:|\n| data | data |"; 27 | let file = write_temp(content, "complex.md"); 28 | let travert = Travert::new(file.path()).expect("travert"); 29 | assert_eq!(travert.format, OutputFormat::MarkdownTable); 30 | } 31 | -------------------------------------------------------------------------------- /src/app/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use tracing::Level; 3 | 4 | use crate::{cli::build_cli, config::Config}; 5 | 6 | pub mod commands; 7 | mod logging; 8 | pub mod pipeline; 9 | 10 | pub async fn run() -> Result<()> { 11 | let cli = build_cli(); 12 | let matches = cli.get_matches(); 13 | 14 | let language = matches 15 | .get_one::("language") 16 | .cloned() 17 | .unwrap_or_else(|| "zh".to_string()); 18 | let verbose = matches.get_flag("verbose"); 19 | 20 | rust_i18n::set_locale(&language); 21 | logging::init(if verbose { Level::DEBUG } else { Level::INFO })?; 22 | 23 | let config = Config::from_matches(&matches)?; 24 | Config::init(config)?; 25 | 26 | if let Some(matches) = matches.subcommand_matches("completions") { 27 | return commands::completions(matches); 28 | } 29 | 30 | #[cfg(debug_assertions)] 31 | { 32 | if let Some(matches) = matches.subcommand_matches("test") { 33 | return commands::test(matches); 34 | } 35 | } 36 | 37 | if let Some(matches) = matches.subcommand_matches("convert") { 38 | return commands::convert(matches); 39 | } 40 | 41 | pipeline::process_dependencies().await 42 | } 43 | -------------------------------------------------------------------------------- /tests/dependency_tests.rs: -------------------------------------------------------------------------------- 1 | use cargo_thanku::output::dependency::DependencyInfo; 2 | 3 | #[test] 4 | fn parse_status_variants() { 5 | let (failed, error) = DependencyInfo::parse_status("✅").unwrap(); 6 | assert!(!failed); 7 | assert!(error.is_none()); 8 | 9 | let (failed, error) = 10 | DependencyInfo::parse_status("❌ Unknown error: failed to fetch repository info").unwrap(); 11 | assert!(failed); 12 | assert_eq!( 13 | error, 14 | Some("Unknown error: failed to fetch repository info".to_string()) 15 | ); 16 | } 17 | 18 | #[test] 19 | fn parse_stats_variants() { 20 | let (stars, downloads) = DependencyInfo::parse_stats("🌟 1000 📦 100").unwrap(); 21 | assert_eq!(stars, Some(1000)); 22 | assert_eq!(downloads, Some(100)); 23 | 24 | let (stars, downloads) = DependencyInfo::parse_stats("🌟 1000").unwrap(); 25 | assert_eq!(stars, Some(1000)); 26 | assert!(downloads.is_none()); 27 | } 28 | 29 | #[test] 30 | fn parse_md_link_cases() { 31 | let (label, url) = 32 | DependencyInfo::parse_md_link("[GitHub](https://github.com/serde-rs/serde)").unwrap(); 33 | assert_eq!(label, "GitHub"); 34 | assert_eq!(url, Some("https://github.com/serde-rs/serde".to_string())); 35 | } 36 | -------------------------------------------------------------------------------- /tests/source_tests.rs: -------------------------------------------------------------------------------- 1 | use cargo_thanku::sources::{CratesioClient, Source}; 2 | use url::Url; 3 | 4 | #[test] 5 | fn source_from_github_url() { 6 | let url = Url::parse("https://github.com/owner/repo").unwrap(); 7 | match Source::from_url(&Some(url)) { 8 | Some(Source::GitHub { owner, repo, .. }) => { 9 | assert_eq!(owner, "owner"); 10 | assert_eq!(repo, "repo"); 11 | } 12 | _ => panic!("expected GitHub source"), 13 | } 14 | } 15 | 16 | #[test] 17 | fn source_from_cratesio_url() { 18 | let url = Url::parse("https://crates.io/crates/serde").unwrap(); 19 | match Source::from_url(&Some(url)) { 20 | Some(Source::CratesIo { name, .. }) => assert_eq!(name, "crates/serde"), 21 | _ => panic!("expected crates source"), 22 | } 23 | } 24 | 25 | #[test] 26 | fn source_from_link_url() { 27 | let url = Url::parse("https://example.com/path/to/resource").unwrap(); 28 | match Source::from_url(&Some(url)) { 29 | Some(Source::Link { url }) => assert_eq!(url, "https://example.com/path/to/resource"), 30 | _ => panic!("expected generic link"), 31 | } 32 | } 33 | 34 | #[tokio::test] 35 | #[ignore = "requires crates.io network access"] 36 | async fn cratesio_client_fetches_crate() { 37 | let client = CratesioClient::new(); 38 | let info = client.get_crate_info("serde").await.unwrap(); 39 | assert_eq!(info.name, "serde"); 40 | assert!(info.downloads > 0); 41 | } 42 | -------------------------------------------------------------------------------- /tests/markdown_tokenizer_tests.rs: -------------------------------------------------------------------------------- 1 | use cargo_thanku::output::dependency::DependencyKind; 2 | use cargo_thanku::output::markdown::tokenizer::{ 3 | ListEntry, MarkdownListTokenizer, MarkdownSection, section_kind_from_header, 4 | }; 5 | 6 | #[test] 7 | fn parse_list_entry_segments() { 8 | let line = "- serde : desc - [serde](https://crates.io/crates/serde) [GitHub](https://github.com/serde-rs/serde) (🌟 42 📦 10) ✅"; 9 | let entry = ListEntry::from_line(line).unwrap(); 10 | assert_eq!(entry.name, "serde"); 11 | assert_eq!(entry.description, Some("desc")); 12 | assert!(entry.crate_segment.starts_with("[serde]")); 13 | assert!(entry.status_segment.contains("✅")); 14 | } 15 | 16 | #[test] 17 | fn tokenizer_detects_headers_and_items() { 18 | let content = "# Dependencies\n## Normal\n- serde : desc - [serde](https://crates.io/crates/serde) [GitHub](https://github.com/serde-rs/serde) (🌟 42) ✅"; 19 | let mut saw_header = false; 20 | let mut saw_item = false; 21 | 22 | for token in MarkdownListTokenizer::new(content) { 23 | match token.unwrap() { 24 | MarkdownSection::Header(line) => { 25 | if section_kind_from_header(line) == Some(DependencyKind::Normal) { 26 | saw_header = true; 27 | } 28 | } 29 | MarkdownSection::Item(entry) => { 30 | saw_item = true; 31 | assert_eq!(entry.name, "serde"); 32 | } 33 | } 34 | } 35 | 36 | assert!(saw_header && saw_item); 37 | } 38 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-thanku" 3 | version = "0.5.1" 4 | edition = "2024" 5 | authors = ["unic "] 6 | description = "Generate acknowledgments for your Rust project dependencies" 7 | categories = [ 8 | "development-tools", 9 | "command-line-utilities", 10 | "development-tools::cargo-plugins", 11 | ] 12 | keywords = ["cargo", "cli", "thanks", "acknowledgements", "dependencies"] 13 | license = "MIT" 14 | repository = "https://github.com/yuniqueunic/cargo-thanku" 15 | readme = "README.md" 16 | 17 | [[bin]] 18 | name = "cargo-thanku" 19 | path = "src/main.rs" 20 | 21 | [dependencies] 22 | anyhow = "1.0" 23 | thiserror = "2.0" 24 | clap = { version = "4.5", features = ["cargo", "env"] } 25 | clap_complete = "4.5" 26 | tokio = { version = "1.36", features = ["full"] } 27 | futures = "0.3" 28 | reqwest = { version = "0.12", features = [ 29 | "json", 30 | "rustls-tls", 31 | ], default-features = false } 32 | serde = { version = "1.0", features = ["derive"] } 33 | serde_json = "1.0" 34 | toml = "0.9" 35 | url = { version = "2.5", features = ["serde"] } 36 | cargo_metadata = "0.23" 37 | tracing = "0.1" 38 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 39 | rust-i18n = "3.1.3" 40 | strsim = "0.11.1" 41 | serde_yaml = "0.9.34" 42 | regex = "1.11.1" 43 | 44 | [dev-dependencies] 45 | tokio-test = "0.4" 46 | pretty_assertions = "1.4" 47 | assert_fs = "1.1.2" 48 | 49 | 50 | [profile.release] 51 | opt-level = "z" 52 | lto = true 53 | codegen-units = 1 54 | panic = "abort" 55 | strip = "debuginfo" 56 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[allow(clippy::enum_variant_names, dead_code)] 4 | #[derive(Error, Debug)] 5 | pub enum AppError { 6 | #[error("HTTP client error: {0}")] 7 | HttpError(#[from] reqwest::Error), 8 | 9 | #[error("Cargo metadata error: {0}")] 10 | MetadataError(#[from] cargo_metadata::Error), 11 | 12 | #[error("I/O error: {0}")] 13 | IoError(#[from] std::io::Error), 14 | 15 | #[error("JSON error: {0}")] 16 | JsonError(#[from] serde_json::Error), 17 | 18 | #[error("Invalid URL: {0}")] 19 | UrlError(#[from] url::ParseError), 20 | 21 | #[error("Invalid output format: {0}")] 22 | InvalidOutputFormat(String), 23 | 24 | #[error("Invalid link source: {0}")] 25 | InvalidLinkSource(String), 26 | 27 | #[error("Unknown error: {0}")] 28 | Unknown(String), 29 | 30 | #[error("Invalid CSV content: {0}")] 31 | InvalidCsvContent(String), 32 | 33 | #[error("Invalid dependency kind: {0}")] 34 | InvalidDependencyKind(String), 35 | 36 | #[error("Invalid source link: {0}")] 37 | InvalidSourceLink(String), 38 | 39 | #[error("Invalid status: {0}")] 40 | InvalidStatus(String), 41 | 42 | #[error("Invalid stats: {0}")] 43 | InvalidStats(String), 44 | 45 | #[error("Invalid table header: {0}")] 46 | InvalidTableHeader(String), 47 | 48 | #[error("Invalid list line: {0}")] 49 | InvalidListLine(String), 50 | 51 | #[error("Invalid table line: {0}")] 52 | InvalidTableLine(String), 53 | } 54 | 55 | impl From for AppError { 56 | fn from(error: String) -> Self { 57 | Self::Unknown(error) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Debug executable 'cargo-thanku'", 11 | "cargo": { 12 | "args": [ 13 | "build", 14 | "--bin=cargo-thanku", 15 | "--package=cargo-thanku" 16 | ], 17 | "filter": { 18 | "name": "cargo-thanku", 19 | "kind": "bin" 20 | } 21 | }, 22 | "args": [ 23 | "convert", 24 | "--input=./assets/output/THANKU_en.csv", 25 | "--outputs=mt", 26 | "-v" 27 | ], 28 | "cwd": "${workspaceFolder}" 29 | }, 30 | { 31 | "type": "lldb", 32 | "request": "launch", 33 | "name": "Debug unit tests in executable 'cargo-thanku'", 34 | "cargo": { 35 | "args": [ 36 | "test", 37 | "--no-run", 38 | "--bin=cargo-thanku", 39 | "--package=cargo-thanku" 40 | ], 41 | "filter": { 42 | "name": "cargo-thanku", 43 | "kind": "bin" 44 | } 45 | }, 46 | "args": [], 47 | "cwd": "${workspaceFolder}" 48 | } 49 | ] 50 | } -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | # 根据操作系统自动设置 shell 2 | set windows-shell := ["pwsh", "-c"] 3 | set shell := ["bash", "-c"] 4 | 5 | # 默认显示帮助信息 6 | default: 7 | @just --list 8 | 9 | # 安装依赖工具 10 | setup: 11 | cargo install cargo-edit cargo-watch cargo-release 12 | # Already installed that those commands can be run directly 13 | # so, no need to install just 14 | # cargo install just 15 | 16 | # ai 生成 git commit message 并且 commit. 需要先登录 github 17 | git-commit: 18 | git add . && git commit -m "$(git status | aichat -r git)" 19 | 20 | # ai 生成 git commit message 并且 push. 需要先登录 github 21 | git-push: 22 | git add . && git commit -m "$(git status | aichat -r git)" && git push 23 | 24 | # 检查代码格式 25 | fmt: 26 | cargo fmt --all -- --check 27 | 28 | # 运行 clippy 检查 29 | lint: 30 | cargo clippy --all-targets --all-features -- -D warnings 31 | 32 | # 运行测试 33 | test: 34 | cargo test --all-features 35 | 36 | # 构建发布版本 37 | build: 38 | cargo build --release 39 | 40 | # 运行所有检查(格式、lint、测试) 41 | check: fmt lint test 42 | 43 | # 清理构建产物 44 | clean: 45 | cargo clean 46 | 47 | # 启动开发模式(代码变更自动重新编译) 48 | dev: 49 | cargo watch -x run 50 | 51 | # 创建新的发布版本 52 | # 用法: just release [major|minor|patch] [--execute/留空也是--dry-run] 53 | release level x="": git-commit 54 | cargo release {{level}} --no-publish --no-verify {{x}} -v 55 | 56 | release-only level x="": 57 | cargo release {{level}} --no-publish --no-verify {{x}} -v 58 | 59 | 60 | # 发布到 crates.io 61 | publish: 62 | cargo publish 63 | 64 | # 生成 CHANGELOG 65 | changelog: 66 | git cliff -o CHANGELOG.md 67 | 68 | # 安装到本地系统 69 | install: 70 | cargo install --path . 71 | 72 | # 运行带有调试日志的程序 73 | run-debug *args: 74 | RUST_LOG=debug cargo run -- {{args}} 75 | 76 | # 运行带有跟踪日志的程序 77 | run-trace *args: 78 | RUST_LOG=trace cargo run -- {{args}} -------------------------------------------------------------------------------- /src/output/format/structured.rs: -------------------------------------------------------------------------------- 1 | use std::io::BufRead; 2 | 3 | use anyhow::Result; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::output::dependency::DependencyInfo; 7 | 8 | use super::Formatter; 9 | 10 | #[derive(Debug, Serialize, Deserialize)] 11 | struct DependencyList { 12 | dependencies: Vec, 13 | } 14 | 15 | pub struct JsonFormatter; 16 | 17 | impl Formatter for JsonFormatter { 18 | fn format(&self, deps: &[DependencyInfo]) -> Result { 19 | Ok(serde_json::to_string_pretty(deps)?) 20 | } 21 | 22 | fn parse(&self, content: &str) -> Result> { 23 | Ok(serde_json::from_str(content)?) 24 | } 25 | 26 | fn parse_reader(&self, reader: &mut dyn BufRead) -> Result> { 27 | Ok(serde_json::from_reader(reader)?) 28 | } 29 | } 30 | 31 | pub struct TomlFormatter; 32 | 33 | impl Formatter for TomlFormatter { 34 | fn format(&self, deps: &[DependencyInfo]) -> Result { 35 | let deps_list = DependencyList { 36 | dependencies: deps.to_vec(), 37 | }; 38 | Ok(toml::to_string_pretty(&deps_list)?) 39 | } 40 | 41 | fn parse(&self, content: &str) -> Result> { 42 | let deps_list: DependencyList = toml::from_str(content)?; 43 | Ok(deps_list.dependencies) 44 | } 45 | } 46 | 47 | pub struct YamlFormatter; 48 | 49 | impl Formatter for YamlFormatter { 50 | fn format(&self, deps: &[DependencyInfo]) -> Result { 51 | Ok(serde_yaml::to_string(deps)?) 52 | } 53 | 54 | fn parse(&self, content: &str) -> Result> { 55 | Ok(serde_yaml::from_str(content)?) 56 | } 57 | 58 | fn parse_reader(&self, reader: &mut dyn BufRead) -> Result> { 59 | Ok(serde_yaml::from_reader(reader)?) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/markdown_format_tests.rs: -------------------------------------------------------------------------------- 1 | use cargo_thanku::output::{ 2 | Formatter, 3 | dependency::{DependencyInfo, DependencyKind, DependencyStats}, 4 | format::markdown::{MarkdownListFormatter, MarkdownTableFormatter}, 5 | }; 6 | 7 | fn sample_dep(kind: DependencyKind) -> DependencyInfo { 8 | DependencyInfo { 9 | name: format!("dep-{kind:?}"), 10 | description: Some("desc".into()), 11 | dependency_kind: kind, 12 | crate_url: Some("https://crates.io/crates/sample".into()), 13 | source_type: "GitHub".into(), 14 | source_url: Some("https://github.com/example/repo".into()), 15 | stats: DependencyStats { 16 | stars: Some(42), 17 | downloads: Some(100), 18 | }, 19 | failed: false, 20 | error_message: None, 21 | } 22 | } 23 | 24 | #[test] 25 | fn markdown_table_roundtrip() { 26 | let deps = vec![sample_dep(DependencyKind::Normal)]; 27 | let formatter = MarkdownTableFormatter; 28 | let content = formatter.format(&deps).unwrap(); 29 | let parsed = formatter.parse(&content).unwrap(); 30 | assert_eq!(parsed[0].name, deps[0].name); 31 | } 32 | 33 | #[test] 34 | fn markdown_list_roundtrip() { 35 | let deps = vec![sample_dep(DependencyKind::Development)]; 36 | let formatter = MarkdownListFormatter; 37 | let content = formatter.format(&deps).unwrap(); 38 | let parsed = formatter.parse(&content).unwrap(); 39 | assert_eq!(parsed[0].dependency_kind, DependencyKind::Development); 40 | } 41 | 42 | #[test] 43 | fn markdown_table_skips_noise() { 44 | let deps = vec![sample_dep(DependencyKind::Normal)]; 45 | let formatter = MarkdownTableFormatter; 46 | let mut content = String::from("Not a table\n"); 47 | content.push_str(&formatter.format(&deps).unwrap()); 48 | content.push_str("\nRandom footer"); 49 | let parsed = formatter.parse(&content).unwrap(); 50 | assert_eq!(parsed.len(), 1); 51 | } 52 | 53 | #[test] 54 | fn markdown_list_skips_noise() { 55 | let deps = vec![sample_dep(DependencyKind::Build)]; 56 | let formatter = MarkdownListFormatter; 57 | let mut content = formatter.format(&deps).unwrap(); 58 | content.push_str("\n> appendix note"); 59 | let parsed = formatter.parse(&content).unwrap(); 60 | assert_eq!(parsed.len(), 1); 61 | } 62 | -------------------------------------------------------------------------------- /tests/output_format_tests.rs: -------------------------------------------------------------------------------- 1 | use cargo_thanku::output::{ 2 | Formatter, OutputFormat, OutputManager, 3 | dependency::{DependencyInfo, DependencyKind, DependencyStats}, 4 | format::{CsvFormatter, JsonFormatter, TomlFormatter, YamlFormatter}, 5 | }; 6 | 7 | fn sample_dep() -> DependencyInfo { 8 | DependencyInfo { 9 | name: "serde".into(), 10 | description: Some("Serialization".into()), 11 | dependency_kind: DependencyKind::Normal, 12 | crate_url: Some("https://crates.io/crates/serde".into()), 13 | source_type: "GitHub".into(), 14 | source_url: Some("https://github.com/serde-rs/serde".into()), 15 | stats: DependencyStats { 16 | stars: Some(1000), 17 | downloads: Some(10_000), 18 | }, 19 | failed: false, 20 | error_message: None, 21 | } 22 | } 23 | 24 | #[test] 25 | fn csv_roundtrip() { 26 | let deps = vec![sample_dep()]; 27 | let formatter = CsvFormatter; 28 | let content = formatter.format(&deps).unwrap(); 29 | let parsed = formatter.parse(&content).unwrap(); 30 | assert_eq!(parsed[0].name, "serde"); 31 | } 32 | 33 | #[test] 34 | fn toml_roundtrip() { 35 | let deps = vec![sample_dep()]; 36 | let formatter = TomlFormatter; 37 | let content = formatter.format(&deps).unwrap(); 38 | let parsed = formatter.parse(&content).unwrap(); 39 | assert_eq!(parsed.len(), 1); 40 | } 41 | 42 | #[test] 43 | fn json_roundtrip() { 44 | let deps = vec![sample_dep()]; 45 | let formatter = JsonFormatter; 46 | let content = formatter.format(&deps).unwrap(); 47 | let parsed = formatter.parse(&content).unwrap(); 48 | assert_eq!(parsed[0].source_type, "GitHub"); 49 | } 50 | 51 | #[test] 52 | fn yaml_roundtrip() { 53 | let deps = vec![sample_dep()]; 54 | let formatter = YamlFormatter; 55 | let content = formatter.format(&deps).unwrap(); 56 | let parsed = formatter.parse(&content).unwrap(); 57 | assert_eq!( 58 | parsed[0].crate_url.as_deref(), 59 | Some("https://crates.io/crates/serde") 60 | ); 61 | } 62 | 63 | #[test] 64 | fn output_manager_writes_to_buffer() { 65 | let deps = vec![sample_dep()]; 66 | let mut buffer = Vec::new(); 67 | let mut manager = OutputManager::new(OutputFormat::MarkdownTable, &mut buffer); 68 | manager.write(&deps).unwrap(); 69 | let output = String::from_utf8(buffer).unwrap(); 70 | assert!(output.contains("serde")); 71 | } 72 | -------------------------------------------------------------------------------- /src/output/format/csv.rs: -------------------------------------------------------------------------------- 1 | use std::io::BufRead; 2 | 3 | use anyhow::Result; 4 | use rust_i18n::t; 5 | 6 | use crate::{errors::AppError, output::dependency::DependencyInfo}; 7 | 8 | use super::Formatter; 9 | 10 | pub struct CsvFormatter; 11 | 12 | impl CsvFormatter { 13 | fn header() -> impl AsRef { 14 | t!("output.csv_header").replace(',', ",") 15 | } 16 | 17 | fn column_num() -> usize { 18 | CsvFormatter::header().as_ref().split(',').count() 19 | } 20 | } 21 | 22 | impl Formatter for CsvFormatter { 23 | fn format(&self, deps: &[DependencyInfo]) -> Result { 24 | let header = CsvFormatter::header(); 25 | let mut output = String::new(); 26 | output.push_str(&format!("\n{}\n", header.as_ref())); 27 | 28 | for dep in deps { 29 | let (name, description, crates_link, source_link, stats, status) = dep.to_strings(); 30 | let dependency_kind = dep.dependency_kind.to_string(); 31 | let description = description.replace(',', ";"); 32 | 33 | output.push_str(&format!( 34 | "{},{},{},{},{},{},{}\n", 35 | name, description, dependency_kind, crates_link, source_link, stats, status, 36 | )); 37 | } 38 | 39 | Ok(output) 40 | } 41 | 42 | fn parse(&self, content: &str) -> Result> { 43 | let mut cursor = std::io::Cursor::new(content.as_bytes()); 44 | self.parse_reader(&mut cursor) 45 | } 46 | 47 | fn parse_reader(&self, reader: &mut dyn BufRead) -> Result> { 48 | let mut header = String::new(); 49 | loop { 50 | header.clear(); 51 | if reader.read_line(&mut header)? == 0 { 52 | return Ok(vec![]); 53 | } 54 | if !header.trim().is_empty() { 55 | break; 56 | } 57 | } 58 | let header = header.trim(); 59 | let columns = header.split(',').collect::>(); 60 | if columns.len() != CsvFormatter::column_num() { 61 | return Err(AppError::InvalidCsvContent(header.to_string()).into()); 62 | } 63 | 64 | let mut deps = Vec::new(); 65 | let mut line = String::new(); 66 | loop { 67 | line.clear(); 68 | let bytes = reader.read_line(&mut line)?; 69 | if bytes == 0 { 70 | break; 71 | } 72 | if line.trim().is_empty() { 73 | continue; 74 | } 75 | let trimmed = line.trim_end_matches(&['\r', '\n'][..]); 76 | deps.push(DependencyInfo::try_from_csv_line(trimmed, columns.len())?); 77 | } 78 | 79 | Ok(deps) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/output/format/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{io::BufRead, str::FromStr}; 2 | 3 | use anyhow::Result; 4 | 5 | use crate::{errors::AppError, output::dependency::DependencyInfo}; 6 | 7 | pub(crate) mod csv; 8 | pub mod markdown; 9 | pub(crate) mod structured; 10 | 11 | pub use csv::CsvFormatter; 12 | pub use markdown::{MarkdownListFormatter, MarkdownTableFormatter}; 13 | pub use structured::{JsonFormatter, TomlFormatter, YamlFormatter}; 14 | 15 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 16 | pub enum OutputFormat { 17 | MarkdownTable, 18 | MarkdownList, 19 | Csv, 20 | Json, 21 | Yaml, 22 | Toml, 23 | } 24 | 25 | impl OutputFormat { 26 | pub fn to_identifier(&self) -> &str { 27 | match self { 28 | Self::MarkdownTable => "mt", 29 | Self::MarkdownList => "ml", 30 | _ => "", 31 | } 32 | } 33 | 34 | pub fn to_extension(&self) -> &str { 35 | match self { 36 | Self::MarkdownTable | Self::MarkdownList => "md", 37 | Self::Csv => "csv", 38 | Self::Json => "json", 39 | Self::Yaml => "yaml", 40 | Self::Toml => "toml", 41 | } 42 | } 43 | } 44 | 45 | impl Default for OutputFormat { 46 | fn default() -> Self { 47 | Self::MarkdownTable 48 | } 49 | } 50 | 51 | impl FromStr for OutputFormat { 52 | type Err = AppError; 53 | 54 | fn from_str(s: &str) -> Result { 55 | Ok(match s.to_lowercase().as_str() { 56 | "mt" | "markdown-table" => Self::MarkdownTable, 57 | "ml" | "markdown-list" => Self::MarkdownList, 58 | "csv" => Self::Csv, 59 | "json" => Self::Json, 60 | "toml" => Self::Toml, 61 | "yml" | "yaml" => Self::Yaml, 62 | _ => return Err(AppError::InvalidOutputFormat(s.to_string())), 63 | }) 64 | } 65 | } 66 | 67 | pub trait Formatter { 68 | fn format(&self, deps: &[DependencyInfo]) -> Result; 69 | fn parse(&self, content: &str) -> Result>; 70 | 71 | fn parse_reader(&self, reader: &mut dyn BufRead) -> Result> { 72 | let mut buffer = String::new(); 73 | reader.read_to_string(&mut buffer)?; 74 | self.parse(&buffer) 75 | } 76 | } 77 | 78 | impl dyn Formatter { 79 | pub fn new(format: OutputFormat) -> Result> { 80 | Ok(match format { 81 | OutputFormat::MarkdownTable => Box::new(MarkdownTableFormatter), 82 | OutputFormat::MarkdownList => Box::new(MarkdownListFormatter), 83 | OutputFormat::Csv => Box::new(CsvFormatter), 84 | OutputFormat::Json => Box::new(JsonFormatter), 85 | OutputFormat::Toml => Box::new(TomlFormatter), 86 | OutputFormat::Yaml => Box::new(YamlFormatter), 87 | }) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/app/commands/mod.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use anyhow::Result; 4 | use clap::ArgMatches; 5 | use tracing::{info, instrument}; 6 | 7 | use crate::{cli::generate_completions, output::OutputFormat, travert::Converter}; 8 | 9 | #[instrument(skip(matches))] 10 | pub fn completions(matches: &ArgMatches) -> Result<()> { 11 | if let Some(shell) = matches.get_one::("shell") { 12 | generate_completions(shell).map_err(|e| { 13 | anyhow::anyhow!(t!( 14 | "main.failed_generate_completions", 15 | error = e.to_string() 16 | )) 17 | })?; 18 | } 19 | Ok(()) 20 | } 21 | 22 | #[cfg(debug_assertions)] 23 | #[instrument(skip(matches))] 24 | pub fn test(matches: &ArgMatches) -> Result<()> { 25 | info!(?matches, "test subcommand invoked"); 26 | println!("{}", t!("app.description")); 27 | println!("test: {:?}", matches); 28 | Ok(()) 29 | } 30 | 31 | #[instrument(skip(matches))] 32 | pub fn convert(matches: &ArgMatches) -> Result<()> { 33 | let input = matches 34 | .get_one::("input") 35 | .ok_or_else(|| anyhow::anyhow!(t!("main.convert_input_required")))?; 36 | 37 | if !input.is_file() { 38 | return Err(anyhow::anyhow!(t!("main.convert_input_not_file"))); 39 | } 40 | 41 | let (name, _ext) = input 42 | .file_name() 43 | .and_then(|n| n.to_str()) 44 | .map(|_| { 45 | let stem = input 46 | .file_stem() 47 | .and_then(|s| s.to_str()) 48 | .unwrap_or_default(); 49 | 50 | let ext = input 51 | .extension() 52 | .and_then(|e| e.to_str()) 53 | .unwrap_or_default(); 54 | 55 | (stem, ext) 56 | }) 57 | .ok_or_else(|| anyhow::anyhow!(t!("main.convert_invalid_filename")))?; 58 | 59 | let input_dir = input 60 | .parent() 61 | .ok_or_else(|| anyhow::anyhow!(t!("main.convert_invalid_filename")))?; 62 | 63 | let output_dir = input_dir.join("converted"); 64 | std::fs::create_dir_all(&output_dir)?; 65 | 66 | let outputs = matches 67 | .get_many::("outputs") 68 | .unwrap_or_default() 69 | .map(|format| format.parse::().unwrap_or_default()) 70 | .map(|format| { 71 | let file_name = format!( 72 | "{}_{}", 73 | name.trim_end_matches(['_']), 74 | format.to_identifier() 75 | ); 76 | output_dir.join(format!( 77 | "{}.{}", 78 | file_name.trim_end_matches(['_']), 79 | format.to_extension() 80 | )) 81 | }) 82 | .collect::>(); 83 | 84 | let converter = Converter::new(input, &outputs)?; 85 | converter.convert()?; 86 | 87 | Ok(()) 88 | } 89 | -------------------------------------------------------------------------------- /assets/output/THANKU_en.csv: -------------------------------------------------------------------------------- 1 | 2 | name,description,dependency_kind,crates_link,source_link,stats,status 3 | anyhow,Flexible concrete Error type built on std::error::Error,Normal,[anyhow](https://crates.io/crates/anyhow),[GitHub](https://github.com/dtolnay/anyhow),❓,✅ 4 | cargo_metadata,structured access to the output of `cargo metadata`,Normal,[cargo_metadata](https://crates.io/crates/cargo_metadata),[GitHub](https://github.com/oli-obk/cargo_metadata),❓,✅ 5 | clap,A simple to use; efficient; and full-featured Command Line Argument Parser,Normal,[clap](https://crates.io/crates/clap),[GitHub](https://github.com/clap-rs/clap),❓,✅ 6 | clap_complete,Generate shell completion scripts for your clap::Command,Normal,[clap_complete](https://crates.io/crates/clap_complete),[GitHub](https://github.com/clap-rs/clap),❓,✅ 7 | futures,An implementation of futures and streams featuring zero allocations; composability; and iterator-like interfaces.,Normal,[futures](https://crates.io/crates/futures),[GitHub](https://github.com/rust-lang/futures-rs),❓,✅ 8 | reqwest,higher level HTTP client library,Normal,[reqwest](https://crates.io/crates/reqwest),[GitHub](https://github.com/seanmonstar/reqwest),❓,✅ 9 | rust-i18n,Rust I18n is use Rust codegen for load YAML file storage translations on compile time; and give you a t! macro for simply get translation texts.,Normal,[rust-i18n](https://crates.io/crates/rust-i18n),[GitHub](https://github.com/longbridge/rust-i18n),❓,✅ 10 | serde,A generic serialization/deserialization framework,Normal,[serde](https://crates.io/crates/serde),[GitHub](https://github.com/serde-rs/serde),❓,✅ 11 | serde_json,A JSON serialization file format,Normal,[serde_json](https://crates.io/crates/serde_json),[GitHub](https://github.com/serde-rs/json),❓,✅ 12 | serde_yaml,YAML data format for Serde,Normal,[serde_yaml](https://crates.io/crates/serde_yaml),[GitHub](https://github.com/dtolnay/serde-yaml),❓,✅ 13 | strsim,Implementations of string similarity metrics. Includes Hamming; Levenshtein; OSA; Damerau-Levenshtein; Jaro; Jaro-Winkler; and Sørensen-Dice.,Normal,[strsim](https://crates.io/crates/strsim),[GitHub](https://github.com/rapidfuzz/strsim-rs),❓,✅ 14 | thiserror,derive(Error),Normal,[thiserror](https://crates.io/crates/thiserror),[GitHub](https://github.com/dtolnay/thiserror),❓,✅ 15 | tokio,An event-driven; non-blocking I/O platform for writing asynchronous I/O backed applications.,Normal,[tokio](https://crates.io/crates/tokio),[GitHub](https://github.com/tokio-rs/tokio),❓,✅ 16 | toml,A native Rust encoder and decoder of TOML-formatted files and streams. Provides implementations of the standard Serialize/Deserialize traits for TOML data to facilitate deserializing and serializing Rust structures.,Normal,[toml](https://crates.io/crates/toml),[GitHub](https://github.com/toml-rs/toml),❓,✅ 17 | tracing,Application-level tracing for Rust.,Normal,[tracing](https://crates.io/crates/tracing),[GitHub](https://github.com/tokio-rs/tracing),❓,✅ 18 | tracing-subscriber,Utilities for implementing and composing `tracing` subscribers.,Normal,[tracing-subscriber](https://crates.io/crates/tracing-subscriber),[GitHub](https://github.com/tokio-rs/tokio),❓,✅ 19 | url,URL library for Rust; based on the WHATWG URL Standard,Normal,[url](https://crates.io/crates/url),[GitHub](https://github.com/servo/rust-url),❓,✅ 20 | assert_fs,Filesystem fixtures and assertions for testing.,Development,[assert_fs](https://crates.io/crates/assert_fs),[GitHub](https://github.com/assert-rs/assert_fs.git),❓,✅ 21 | pretty_assertions,Overwrite `assert_eq!` and `assert_ne!` with drop-in replacements; adding colorful diffs.,Development,[pretty_assertions](https://crates.io/crates/pretty_assertions),[GitHub](https://github.com/rust-pretty-assertions/rust-pretty-assertions),❓,✅ 22 | tokio-test,Testing utilities for Tokio- and futures-based code,Development,[tokio-test](https://crates.io/crates/tokio-test),[GitHub](https://github.com/tokio-rs/tokio),❓,✅ 23 | -------------------------------------------------------------------------------- /assets/output/converted/THANKU_en.csv: -------------------------------------------------------------------------------- 1 | 2 | name,description,dependency_kind,crates_link,source_link,stats,status 3 | anyhow,Flexible concrete Error type built on std::error::Error,Normal,[anyhow](https://crates.io/crates/anyhow),[GitHub](https://github.com/dtolnay/anyhow),❓,✅ 4 | cargo_metadata,structured access to the output of `cargo metadata`,Normal,[cargo_metadata](https://crates.io/crates/cargo_metadata),[GitHub](https://github.com/oli-obk/cargo_metadata),❓,✅ 5 | clap,A simple to use; efficient; and full-featured Command Line Argument Parser,Normal,[clap](https://crates.io/crates/clap),[GitHub](https://github.com/clap-rs/clap),❓,✅ 6 | clap_complete,Generate shell completion scripts for your clap::Command,Normal,[clap_complete](https://crates.io/crates/clap_complete),[GitHub](https://github.com/clap-rs/clap),❓,✅ 7 | futures,An implementation of futures and streams featuring zero allocations; composability; and iterator-like interfaces.,Normal,[futures](https://crates.io/crates/futures),[GitHub](https://github.com/rust-lang/futures-rs),❓,✅ 8 | reqwest,higher level HTTP client library,Normal,[reqwest](https://crates.io/crates/reqwest),[GitHub](https://github.com/seanmonstar/reqwest),❓,✅ 9 | rust-i18n,Rust I18n is use Rust codegen for load YAML file storage translations on compile time; and give you a t! macro for simply get translation texts.,Normal,[rust-i18n](https://crates.io/crates/rust-i18n),[GitHub](https://github.com/longbridge/rust-i18n),❓,✅ 10 | serde,A generic serialization/deserialization framework,Normal,[serde](https://crates.io/crates/serde),[GitHub](https://github.com/serde-rs/serde),❓,✅ 11 | serde_json,A JSON serialization file format,Normal,[serde_json](https://crates.io/crates/serde_json),[GitHub](https://github.com/serde-rs/json),❓,✅ 12 | serde_yaml,YAML data format for Serde,Normal,[serde_yaml](https://crates.io/crates/serde_yaml),[GitHub](https://github.com/dtolnay/serde-yaml),❓,✅ 13 | strsim,Implementations of string similarity metrics. Includes Hamming; Levenshtein; OSA; Damerau-Levenshtein; Jaro; Jaro-Winkler; and Sørensen-Dice.,Normal,[strsim](https://crates.io/crates/strsim),[GitHub](https://github.com/rapidfuzz/strsim-rs),❓,✅ 14 | thiserror,derive(Error),Normal,[thiserror](https://crates.io/crates/thiserror),[GitHub](https://github.com/dtolnay/thiserror),❓,✅ 15 | tokio,An event-driven; non-blocking I/O platform for writing asynchronous I/O backed applications.,Normal,[tokio](https://crates.io/crates/tokio),[GitHub](https://github.com/tokio-rs/tokio),❓,✅ 16 | toml,A native Rust encoder and decoder of TOML-formatted files and streams. Provides implementations of the standard Serialize/Deserialize traits for TOML data to facilitate deserializing and serializing Rust structures.,Normal,[toml](https://crates.io/crates/toml),[GitHub](https://github.com/toml-rs/toml),❓,✅ 17 | tracing,Application-level tracing for Rust.,Normal,[tracing](https://crates.io/crates/tracing),[GitHub](https://github.com/tokio-rs/tracing),❓,✅ 18 | tracing-subscriber,Utilities for implementing and composing `tracing` subscribers.,Normal,[tracing-subscriber](https://crates.io/crates/tracing-subscriber),[GitHub](https://github.com/tokio-rs/tokio),❓,✅ 19 | url,URL library for Rust; based on the WHATWG URL Standard,Normal,[url](https://crates.io/crates/url),[GitHub](https://github.com/servo/rust-url),❓,✅ 20 | assert_fs,Filesystem fixtures and assertions for testing.,Development,[assert_fs](https://crates.io/crates/assert_fs),[GitHub](https://github.com/assert-rs/assert_fs.git),❓,✅ 21 | pretty_assertions,Overwrite `assert_eq!` and `assert_ne!` with drop-in replacements; adding colorful diffs.,Development,[pretty_assertions](https://crates.io/crates/pretty_assertions),[GitHub](https://github.com/rust-pretty-assertions/rust-pretty-assertions),❓,✅ 22 | tokio-test,Testing utilities for Tokio- and futures-based code,Development,[tokio-test](https://crates.io/crates/tokio-test),[GitHub](https://github.com/tokio-rs/tokio),❓,✅ 23 | -------------------------------------------------------------------------------- /assets/output/converted/THANKU_en_ml.md: -------------------------------------------------------------------------------- 1 | 2 | # Dependencies 3 | 4 | ## Normal 5 | - anyhow : Flexible concrete Error type built on std::error::Error - [anyhow](https://crates.io/crates/anyhow) [GitHub](https://github.com/dtolnay/anyhow) (❓) ✅ 6 | - cargo_metadata : structured access to the output of `cargo metadata` - [cargo_metadata](https://crates.io/crates/cargo_metadata) [GitHub](https://github.com/oli-obk/cargo_metadata) (❓) ✅ 7 | - clap : A simple to use, efficient, and full-featured Command Line Argument Parser - [clap](https://crates.io/crates/clap) [GitHub](https://github.com/clap-rs/clap) (❓) ✅ 8 | - clap_complete : Generate shell completion scripts for your clap::Command - [clap_complete](https://crates.io/crates/clap_complete) [GitHub](https://github.com/clap-rs/clap) (❓) ✅ 9 | - futures : An implementation of futures and streams featuring zero allocations, composability, and iterator-like interfaces. - [futures](https://crates.io/crates/futures) [GitHub](https://github.com/rust-lang/futures-rs) (❓) ✅ 10 | - reqwest : higher level HTTP client library - [reqwest](https://crates.io/crates/reqwest) [GitHub](https://github.com/seanmonstar/reqwest) (❓) ✅ 11 | - rust-i18n : Rust I18n is use Rust codegen for load YAML file storage translations on compile time, and give you a t! macro for simply get translation texts. - [rust-i18n](https://crates.io/crates/rust-i18n) [GitHub](https://github.com/longbridge/rust-i18n) (❓) ✅ 12 | - serde : A generic serialization/deserialization framework - [serde](https://crates.io/crates/serde) [GitHub](https://github.com/serde-rs/serde) (❓) ✅ 13 | - serde_json : A JSON serialization file format - [serde_json](https://crates.io/crates/serde_json) [GitHub](https://github.com/serde-rs/json) (❓) ✅ 14 | - serde_yaml : YAML data format for Serde - [serde_yaml](https://crates.io/crates/serde_yaml) [GitHub](https://github.com/dtolnay/serde-yaml) (❓) ✅ 15 | - strsim : Implementations of string similarity metrics. Includes Hamming, Levenshtein, OSA, Damerau-Levenshtein, Jaro, Jaro-Winkler, and Sørensen-Dice. - [strsim](https://crates.io/crates/strsim) [GitHub](https://github.com/rapidfuzz/strsim-rs) (❓) ✅ 16 | - thiserror : derive(Error) - [thiserror](https://crates.io/crates/thiserror) [GitHub](https://github.com/dtolnay/thiserror) (❓) ✅ 17 | - tokio : An event-driven, non-blocking I/O platform for writing asynchronous I/O backed applications. - [tokio](https://crates.io/crates/tokio) [GitHub](https://github.com/tokio-rs/tokio) (❓) ✅ 18 | - toml : A native Rust encoder and decoder of TOML-formatted files and streams. Provides implementations of the standard Serialize/Deserialize traits for TOML data to facilitate deserializing and serializing Rust structures. - [toml](https://crates.io/crates/toml) [GitHub](https://github.com/toml-rs/toml) (❓) ✅ 19 | - tracing : Application-level tracing for Rust. - [tracing](https://crates.io/crates/tracing) [GitHub](https://github.com/tokio-rs/tracing) (❓) ✅ 20 | - tracing-subscriber : Utilities for implementing and composing `tracing` subscribers. - [tracing-subscriber](https://crates.io/crates/tracing-subscriber) [GitHub](https://github.com/tokio-rs/tokio) (❓) ✅ 21 | - url : URL library for Rust, based on the WHATWG URL Standard - [url](https://crates.io/crates/url) [GitHub](https://github.com/servo/rust-url) (❓) ✅ 22 | 23 | ## Development 24 | - assert_fs : Filesystem fixtures and assertions for testing. - [assert_fs](https://crates.io/crates/assert_fs) [GitHub](https://github.com/assert-rs/assert_fs.git) (❓) ✅ 25 | - pretty_assertions : Overwrite `assert_eq!` and `assert_ne!` with drop-in replacements, adding colorful diffs. - [pretty_assertions](https://crates.io/crates/pretty_assertions) [GitHub](https://github.com/rust-pretty-assertions/rust-pretty-assertions) (❓) ✅ 26 | - tokio-test : Testing utilities for Tokio- and futures-based code - [tokio-test](https://crates.io/crates/tokio-test) [GitHub](https://github.com/tokio-rs/tokio) (❓) ✅ 27 | -------------------------------------------------------------------------------- /assets/output/converted/THANKU_en_mt.md: -------------------------------------------------------------------------------- 1 | 2 | # Dependencies 3 | 4 | ## Normal 5 | - anyhow : Flexible concrete Error type built on std::error::Error - [anyhow](https://crates.io/crates/anyhow) [GitHub](https://github.com/dtolnay/anyhow) (❓) ✅ 6 | - cargo_metadata : structured access to the output of `cargo metadata` - [cargo_metadata](https://crates.io/crates/cargo_metadata) [GitHub](https://github.com/oli-obk/cargo_metadata) (❓) ✅ 7 | - clap : A simple to use, efficient, and full-featured Command Line Argument Parser - [clap](https://crates.io/crates/clap) [GitHub](https://github.com/clap-rs/clap) (❓) ✅ 8 | - clap_complete : Generate shell completion scripts for your clap::Command - [clap_complete](https://crates.io/crates/clap_complete) [GitHub](https://github.com/clap-rs/clap) (❓) ✅ 9 | - futures : An implementation of futures and streams featuring zero allocations, composability, and iterator-like interfaces. - [futures](https://crates.io/crates/futures) [GitHub](https://github.com/rust-lang/futures-rs) (❓) ✅ 10 | - reqwest : higher level HTTP client library - [reqwest](https://crates.io/crates/reqwest) [GitHub](https://github.com/seanmonstar/reqwest) (❓) ✅ 11 | - rust-i18n : Rust I18n is use Rust codegen for load YAML file storage translations on compile time, and give you a t! macro for simply get translation texts. - [rust-i18n](https://crates.io/crates/rust-i18n) [GitHub](https://github.com/longbridge/rust-i18n) (❓) ✅ 12 | - serde : A generic serialization/deserialization framework - [serde](https://crates.io/crates/serde) [GitHub](https://github.com/serde-rs/serde) (❓) ✅ 13 | - serde_json : A JSON serialization file format - [serde_json](https://crates.io/crates/serde_json) [GitHub](https://github.com/serde-rs/json) (❓) ✅ 14 | - serde_yaml : YAML data format for Serde - [serde_yaml](https://crates.io/crates/serde_yaml) [GitHub](https://github.com/dtolnay/serde-yaml) (❓) ✅ 15 | - strsim : Implementations of string similarity metrics. Includes Hamming, Levenshtein, OSA, Damerau-Levenshtein, Jaro, Jaro-Winkler, and Sørensen-Dice. - [strsim](https://crates.io/crates/strsim) [GitHub](https://github.com/rapidfuzz/strsim-rs) (❓) ✅ 16 | - thiserror : derive(Error) - [thiserror](https://crates.io/crates/thiserror) [GitHub](https://github.com/dtolnay/thiserror) (❓) ✅ 17 | - tokio : An event-driven, non-blocking I/O platform for writing asynchronous I/O backed applications. - [tokio](https://crates.io/crates/tokio) [GitHub](https://github.com/tokio-rs/tokio) (❓) ✅ 18 | - toml : A native Rust encoder and decoder of TOML-formatted files and streams. Provides implementations of the standard Serialize/Deserialize traits for TOML data to facilitate deserializing and serializing Rust structures. - [toml](https://crates.io/crates/toml) [GitHub](https://github.com/toml-rs/toml) (❓) ✅ 19 | - tracing : Application-level tracing for Rust. - [tracing](https://crates.io/crates/tracing) [GitHub](https://github.com/tokio-rs/tracing) (❓) ✅ 20 | - tracing-subscriber : Utilities for implementing and composing `tracing` subscribers. - [tracing-subscriber](https://crates.io/crates/tracing-subscriber) [GitHub](https://github.com/tokio-rs/tokio) (❓) ✅ 21 | - url : URL library for Rust, based on the WHATWG URL Standard - [url](https://crates.io/crates/url) [GitHub](https://github.com/servo/rust-url) (❓) ✅ 22 | 23 | ## Development 24 | - assert_fs : Filesystem fixtures and assertions for testing. - [assert_fs](https://crates.io/crates/assert_fs) [GitHub](https://github.com/assert-rs/assert_fs.git) (❓) ✅ 25 | - pretty_assertions : Overwrite `assert_eq!` and `assert_ne!` with drop-in replacements, adding colorful diffs. - [pretty_assertions](https://crates.io/crates/pretty_assertions) [GitHub](https://github.com/rust-pretty-assertions/rust-pretty-assertions) (❓) ✅ 26 | - tokio-test : Testing utilities for Tokio- and futures-based code - [tokio-test](https://crates.io/crates/tokio-test) [GitHub](https://github.com/tokio-rs/tokio) (❓) ✅ 27 | -------------------------------------------------------------------------------- /assets/output/THANKU_list_en.md: -------------------------------------------------------------------------------- 1 | 2 | # Dependencies 3 | 4 | ## Normal 5 | - anyhow : Flexible concrete Error type built on std::error::Error - [anyhow](https://crates.io/crates/anyhow) [GitHub](https://github.com/dtolnay/anyhow) (❓) ✅ 6 | - cargo_metadata : structured access to the output of `cargo metadata` - [cargo_metadata](https://crates.io/crates/cargo_metadata) [GitHub](https://github.com/oli-obk/cargo_metadata) (❓) ✅ 7 | - clap : A simple to use, efficient, and full-featured Command Line Argument Parser - [clap](https://crates.io/crates/clap) [GitHub](https://github.com/clap-rs/clap) (❓) ✅ 8 | - clap_complete : Generate shell completion scripts for your clap::Command - [clap_complete](https://crates.io/crates/clap_complete) [GitHub](https://github.com/clap-rs/clap) (❓) ✅ 9 | - futures : An implementation of futures and streams featuring zero allocations, composability, and iterator-like interfaces. - [futures](https://crates.io/crates/futures) [GitHub](https://github.com/rust-lang/futures-rs) (❓) ✅ 10 | - reqwest : higher level HTTP client library - [reqwest](https://crates.io/crates/reqwest) [GitHub](https://github.com/seanmonstar/reqwest) (❓) ✅ 11 | - rust-i18n : Rust I18n is use Rust codegen for load YAML file storage translations on compile time, and give you a t! macro for simply get translation texts. - [rust-i18n](https://crates.io/crates/rust-i18n) [GitHub](https://github.com/longbridge/rust-i18n) (❓) ✅ 12 | - serde : A generic serialization/deserialization framework - [serde](https://crates.io/crates/serde) [GitHub](https://github.com/serde-rs/serde) (❓) ✅ 13 | - serde_json : A JSON serialization file format - [serde_json](https://crates.io/crates/serde_json) [GitHub](https://github.com/serde-rs/json) (❓) ✅ 14 | - serde_yaml : YAML data format for Serde - [serde_yaml](https://crates.io/crates/serde_yaml) [GitHub](https://github.com/dtolnay/serde-yaml) (❓) ✅ 15 | - strsim : Implementations of string similarity metrics. Includes Hamming, Levenshtein, OSA, Damerau-Levenshtein, Jaro, Jaro-Winkler, and Sørensen-Dice. - [strsim](https://crates.io/crates/strsim) [GitHub](https://github.com/rapidfuzz/strsim-rs) (❓) ✅ 16 | - thiserror : derive(Error) - [thiserror](https://crates.io/crates/thiserror) [GitHub](https://github.com/dtolnay/thiserror) (❓) ✅ 17 | - tokio : An event-driven, non-blocking I/O platform for writing asynchronous I/O backed applications. - [tokio](https://crates.io/crates/tokio) [GitHub](https://github.com/tokio-rs/tokio) (❓) ✅ 18 | - toml : A native Rust encoder and decoder of TOML-formatted files and streams. Provides implementations of the standard Serialize/Deserialize traits for TOML data to facilitate deserializing and serializing Rust structures. - [toml](https://crates.io/crates/toml) [GitHub](https://github.com/toml-rs/toml) (❓) ✅ 19 | - tracing : Application-level tracing for Rust. - [tracing](https://crates.io/crates/tracing) [GitHub](https://github.com/tokio-rs/tracing) (❓) ✅ 20 | - tracing-subscriber : Utilities for implementing and composing `tracing` subscribers. - [tracing-subscriber](https://crates.io/crates/tracing-subscriber) [GitHub](https://github.com/tokio-rs/tokio) (❓) ✅ 21 | - url : URL library for Rust, based on the WHATWG URL Standard - [url](https://crates.io/crates/url) [GitHub](https://github.com/servo/rust-url) (❓) ✅ 22 | 23 | ## Development 24 | - assert_fs : Filesystem fixtures and assertions for testing. - [assert_fs](https://crates.io/crates/assert_fs) [GitHub](https://github.com/assert-rs/assert_fs.git) (❓) ✅ 25 | - pretty_assertions : Overwrite `assert_eq!` and `assert_ne!` with drop-in replacements, adding colorful diffs. - [pretty_assertions](https://crates.io/crates/pretty_assertions) [GitHub](https://github.com/rust-pretty-assertions/rust-pretty-assertions) (❓) ✅ 26 | - tokio-test : Testing utilities for Tokio- and futures-based code - [tokio-test](https://crates.io/crates/tokio-test) [GitHub](https://github.com/tokio-rs/tokio) (❓) ✅ 27 | 28 | ## Unknown 29 | 30 | ### Failed Test -------------------------------------------------------------------------------- /assets/output/THANKU_table_origin.md: -------------------------------------------------------------------------------- 1 | | Name | Description | Crates.io | Source | Stats | Status | 2 | |---|---|---|---|---|---| 3 | | 🔍 | Normal | | | | | 4 | | anyhow | Flexible concrete Error type built on std::error::Error | [anyhow](https://crates.io/crates/anyhow) | [GitHub](https://github.com/dtolnay/anyhow) | ❓ | ✅ | 5 | | cargo_metadata | structured access to the output of `cargo metadata` | [cargo_metadata](https://crates.io/crates/cargo_metadata) | [GitHub](https://github.com/oli-obk/cargo_metadata) | ❓ | ✅ | 6 | | clap | A simple to use, efficient, and full-featured Command Line Argument Parser | [clap](https://crates.io/crates/clap) | [GitHub](https://github.com/clap-rs/clap) | ❓ | ✅ | 7 | | clap_complete | Generate shell completion scripts for your clap::Command | [clap_complete](https://crates.io/crates/clap_complete) | [GitHub](https://github.com/clap-rs/clap) | ❓ | ✅ | 8 | | futures | An implementation of futures and streams featuring zero allocations, composability, and iterator-like interfaces. | [futures](https://crates.io/crates/futures) | [GitHub](https://github.com/rust-lang/futures-rs) | ❓ | ✅ | 9 | | reqwest | higher level HTTP client library | [reqwest](https://crates.io/crates/reqwest) | [GitHub](https://github.com/seanmonstar/reqwest) | ❓ | ✅ | 10 | | rust-i18n | Rust I18n is use Rust codegen for load YAML file storage translations on compile time, and give you a t! macro for simply get translation texts. | [rust-i18n](https://crates.io/crates/rust-i18n) | [GitHub](https://github.com/longbridge/rust-i18n) | ❓ | ✅ | 11 | | serde | A generic serialization/deserialization framework | [serde](https://crates.io/crates/serde) | [GitHub](https://github.com/serde-rs/serde) | ❓ | ✅ | 12 | | serde_json | A JSON serialization file format | [serde_json](https://crates.io/crates/serde_json) | [GitHub](https://github.com/serde-rs/json) | ❓ | ✅ | 13 | | serde_yaml | YAML data format for Serde | [serde_yaml](https://crates.io/crates/serde_yaml) | [GitHub](https://github.com/dtolnay/serde-yaml) | ❓ | ✅ | 14 | | strsim | Implementations of string similarity metrics. Includes Hamming, Levenshtein, OSA, Damerau-Levenshtein, Jaro, Jaro-Winkler, and Sørensen-Dice. | [strsim](https://crates.io/crates/strsim) | [GitHub](https://github.com/rapidfuzz/strsim-rs) | ❓ | ✅ | 15 | | thiserror | derive(Error) | [thiserror](https://crates.io/crates/thiserror) | [GitHub](https://github.com/dtolnay/thiserror) | ❓ | ✅ | 16 | | tokio | An event-driven, non-blocking I/O platform for writing asynchronous I/O backed applications. | [tokio](https://crates.io/crates/tokio) | [GitHub](https://github.com/tokio-rs/tokio) | ❓ | ✅ | 17 | | toml | A native Rust encoder and decoder of TOML-formatted files and streams. Provides implementations of the standard Serialize/Deserialize traits for TOML data to facilitate deserializing and serializing Rust structures. | [toml](https://crates.io/crates/toml) | [GitHub](https://github.com/toml-rs/toml) | ❓ | ✅ | 18 | | tracing | Application-level tracing for Rust. | [tracing](https://crates.io/crates/tracing) | [GitHub](https://github.com/tokio-rs/tracing) | ❓ | ✅ | 19 | | tracing-subscriber | Utilities for implementing and composing `tracing` subscribers. | [tracing-subscriber](https://crates.io/crates/tracing-subscriber) | [GitHub](https://github.com/tokio-rs/tokio) | ❓ | ✅ | 20 | | url | URL library for Rust, based on the WHATWG URL Standard | [url](https://crates.io/crates/url) | [GitHub](https://github.com/servo/rust-url) | ❓ | ✅ | 21 | | 🔧 | Development | | | | | 22 | | assert_fs | Filesystem fixtures and assertions for testing. | [assert_fs](https://crates.io/crates/assert_fs) | [GitHub](https://github.com/assert-rs/assert_fs.git) | ❓ | ✅ | 23 | | pretty_assertions | Overwrite `assert_eq!` and `assert_ne!` with drop-in replacements, adding colorful diffs. | [pretty_assertions](https://crates.io/crates/pretty_assertions) | [GitHub](https://github.com/rust-pretty-assertions/rust-pretty-assertions) | ❓ | ✅ | 24 | | tokio-test | Testing utilities for Tokio- and futures-based code | [tokio-test](https://crates.io/crates/tokio-test) | [GitHub](https://github.com/tokio-rs/tokio) | ❓ | ✅ | -------------------------------------------------------------------------------- /assets/output/THANKU_table_en.md: -------------------------------------------------------------------------------- 1 | 2 | | Name | Description | Crates.io | Source | Stats | Status | 3 | |---|---|---|---|---|---| 4 | | 🔍 | Normal | | | | | 5 | | anyhow | Flexible concrete Error type built on std::error::Error | [anyhow](https://crates.io/crates/anyhow) | [GitHub](https://github.com/dtolnay/anyhow) | ❓ | ✅ | 6 | | cargo_metadata | structured access to the output of `cargo metadata` | [cargo_metadata](https://crates.io/crates/cargo_metadata) | [GitHub](https://github.com/oli-obk/cargo_metadata) | ❓ | ✅ | 7 | | clap | A simple to use, efficient, and full-featured Command Line Argument Parser | [clap](https://crates.io/crates/clap) | [GitHub](https://github.com/clap-rs/clap) | ❓ | ✅ | 8 | | clap_complete | Generate shell completion scripts for your clap::Command | [clap_complete](https://crates.io/crates/clap_complete) | [GitHub](https://github.com/clap-rs/clap) | ❓ | ✅ | 9 | | futures | An implementation of futures and streams featuring zero allocations, composability, and iterator-like interfaces. | [futures](https://crates.io/crates/futures) | [GitHub](https://github.com/rust-lang/futures-rs) | ❓ | ✅ | 10 | | reqwest | higher level HTTP client library | [reqwest](https://crates.io/crates/reqwest) | [GitHub](https://github.com/seanmonstar/reqwest) | ❓ | ✅ | 11 | | rust-i18n | Rust I18n is use Rust codegen for load YAML file storage translations on compile time, and give you a t! macro for simply get translation texts. | [rust-i18n](https://crates.io/crates/rust-i18n) | [GitHub](https://github.com/longbridge/rust-i18n) | ❓ | ✅ | 12 | | serde | A generic serialization/deserialization framework | [serde](https://crates.io/crates/serde) | [GitHub](https://github.com/serde-rs/serde) | ❓ | ✅ | 13 | | serde_json | A JSON serialization file format | [serde_json](https://crates.io/crates/serde_json) | [GitHub](https://github.com/serde-rs/json) | ❓ | ✅ | 14 | | serde_yaml | YAML data format for Serde | [serde_yaml](https://crates.io/crates/serde_yaml) | [GitHub](https://github.com/dtolnay/serde-yaml) | ❓ | ✅ | 15 | | strsim | Implementations of string similarity metrics. Includes Hamming, Levenshtein, OSA, Damerau-Levenshtein, Jaro, Jaro-Winkler, and Sørensen-Dice. | [strsim](https://crates.io/crates/strsim) | [GitHub](https://github.com/rapidfuzz/strsim-rs) | ❓ | ✅ | 16 | | thiserror | derive(Error) | [thiserror](https://crates.io/crates/thiserror) | [GitHub](https://github.com/dtolnay/thiserror) | ❓ | ✅ | 17 | | tokio | An event-driven, non-blocking I/O platform for writing asynchronous I/O backed applications. | [tokio](https://crates.io/crates/tokio) | [GitHub](https://github.com/tokio-rs/tokio) | ❓ | ✅ | 18 | | toml | A native Rust encoder and decoder of TOML-formatted files and streams. Provides implementations of the standard Serialize/Deserialize traits for TOML data to facilitate deserializing and serializing Rust structures. | [toml](https://crates.io/crates/toml) | [GitHub](https://github.com/toml-rs/toml) | ❓ | ✅ | 19 | | tracing | Application-level tracing for Rust. | [tracing](https://crates.io/crates/tracing) | [GitHub](https://github.com/tokio-rs/tracing) | ❓ | ✅ | 20 | | tracing-subscriber | Utilities for implementing and composing `tracing` subscribers. | [tracing-subscriber](https://crates.io/crates/tracing-subscriber) | [GitHub](https://github.com/tokio-rs/tokio) | ❓ | ✅ | 21 | | url | URL library for Rust, based on the WHATWG URL Standard | [url](https://crates.io/crates/url) | [GitHub](https://github.com/servo/rust-url) | ❓ | ✅ | 22 | | 🔧 | Development | | | | | 23 | | assert_fs | Filesystem fixtures and assertions for testing. | [assert_fs](https://crates.io/crates/assert_fs) | [GitHub](https://github.com/assert-rs/assert_fs.git) | ❓ | ✅ | 24 | | pretty_assertions | Overwrite `assert_eq!` and `assert_ne!` with drop-in replacements, adding colorful diffs. | [pretty_assertions](https://crates.io/crates/pretty_assertions) | [GitHub](https://github.com/rust-pretty-assertions/rust-pretty-assertions) | ❓ | ✅ | 25 | | tokio-test | Testing utilities for Tokio- and futures-based code | [tokio-test](https://crates.io/crates/tokio-test) | [GitHub](https://github.com/tokio-rs/tokio) | ❓ | ✅ | 26 | -------------------------------------------------------------------------------- /THANKU.md: -------------------------------------------------------------------------------- 1 | 2 | | Name | Description | Crates.io | Source | Stats | Status | 3 | |------|--------|--------|-------|-------|--------| 4 | |🔍|Normal| | | | | 5 | | anyhow | Flexible concrete Error type built on std::error::Error | [anyhow](https://crates.io/crates/anyhow) | [GitHub](https://github.com/dtolnay/anyhow) | ❓ | ✅ | 6 | | cargo_metadata | structured access to the output of `cargo metadata` | [cargo_metadata](https://crates.io/crates/cargo_metadata) | [GitHub](https://github.com/oli-obk/cargo_metadata) | ❓ | ✅ | 7 | | clap | A simple to use, efficient, and full-featured Command Line Argument Parser | [clap](https://crates.io/crates/clap) | [GitHub](https://github.com/clap-rs/clap) | ❓ | ✅ | 8 | | clap_complete | Generate shell completion scripts for your clap::Command | [clap_complete](https://crates.io/crates/clap_complete) | [GitHub](https://github.com/clap-rs/clap) | ❓ | ✅ | 9 | | futures | An implementation of futures and streams featuring zero allocations, composability, and iterator-like interfaces. | [futures](https://crates.io/crates/futures) | [GitHub](https://github.com/rust-lang/futures-rs) | ❓ | ✅ | 10 | | reqwest | higher level HTTP client library | [reqwest](https://crates.io/crates/reqwest) | [GitHub](https://github.com/seanmonstar/reqwest) | ❓ | ✅ | 11 | | rust-i18n | Rust I18n is use Rust codegen for load YAML file storage translations on compile time, and give you a t! macro for simply get translation texts. | [rust-i18n](https://crates.io/crates/rust-i18n) | [GitHub](https://github.com/longbridge/rust-i18n) | ❓ | ✅ | 12 | | serde | A generic serialization/deserialization framework | [serde](https://crates.io/crates/serde) | [GitHub](https://github.com/serde-rs/serde) | ❓ | ✅ | 13 | | serde_json | A JSON serialization file format | [serde_json](https://crates.io/crates/serde_json) | [GitHub](https://github.com/serde-rs/json) | ❓ | ✅ | 14 | | serde_yaml | YAML data format for Serde | [serde_yaml](https://crates.io/crates/serde_yaml) | [GitHub](https://github.com/dtolnay/serde-yaml) | ❓ | ✅ | 15 | | strsim | Implementations of string similarity metrics. Includes Hamming, Levenshtein, OSA, Damerau-Levenshtein, Jaro, Jaro-Winkler, and Sørensen-Dice. | [strsim](https://crates.io/crates/strsim) | [GitHub](https://github.com/rapidfuzz/strsim-rs) | ❓ | ✅ | 16 | | thiserror | derive(Error) | [thiserror](https://crates.io/crates/thiserror) | [GitHub](https://github.com/dtolnay/thiserror) | ❓ | ✅ | 17 | | tokio | An event-driven, non-blocking I/O platform for writing asynchronous I/O backed applications. | [tokio](https://crates.io/crates/tokio) | [GitHub](https://github.com/tokio-rs/tokio) | ❓ | ✅ | 18 | | toml | A native Rust encoder and decoder of TOML-formatted files and streams. Provides implementations of the standard Serialize/Deserialize traits for TOML data to facilitate deserializing and serializing Rust structures. | [toml](https://crates.io/crates/toml) | [GitHub](https://github.com/toml-rs/toml) | ❓ | ✅ | 19 | | tracing | Application-level tracing for Rust. | [tracing](https://crates.io/crates/tracing) | [GitHub](https://github.com/tokio-rs/tracing) | ❓ | ✅ | 20 | | tracing-subscriber | Utilities for implementing and composing `tracing` subscribers. | [tracing-subscriber](https://crates.io/crates/tracing-subscriber) | [GitHub](https://github.com/tokio-rs/tracing) | ❓ | ✅ | 21 | | url | URL library for Rust, based on the WHATWG URL Standard | [url](https://crates.io/crates/url) | [GitHub](https://github.com/servo/rust-url) | ❓ | ✅ | 22 | |🔧|Development| | | | | 23 | | assert_fs | Filesystem fixtures and assertions for testing. | [assert_fs](https://crates.io/crates/assert_fs) | [GitHub](https://github.com/assert-rs/assert_fs.git) | ❓ | ✅ | 24 | | pretty_assertions | Overwrite `assert_eq!` and `assert_ne!` with drop-in replacements, adding colorful diffs. | [pretty_assertions](https://crates.io/crates/pretty_assertions) | [GitHub](https://github.com/rust-pretty-assertions/rust-pretty-assertions) | ❓ | ✅ | 25 | | tokio-test | Testing utilities for Tokio- and futures-based code | [tokio-test](https://crates.io/crates/tokio-test) | [GitHub](https://github.com/tokio-rs/tokio) | ❓ | ✅ | 26 | -------------------------------------------------------------------------------- /assets/output/THANKU_list_origin_zh.md: -------------------------------------------------------------------------------- 1 | # 依赖 2 | 3 | ## 普通 4 | - anyhow : Flexible concrete Error type built on std::error::Error - [anyhow](https://crates.io/crates/anyhow) [GitHub](https://github.com/dtolnay/anyhow) (❓) ✅ 5 | - cargo_metadata : structured access to the output of `cargo metadata` - [cargo_metadata](https://crates.io/crates/cargo_metadata) [GitHub](https://github.com/oli-obk/cargo_metadata) (❓) ✅ 6 | - clap : A simple to use, efficient, and full-featured Command Line Argument Parser - [clap](https://crates.io/crates/clap) [GitHub](https://github.com/clap-rs/clap) (❓) ✅ 7 | - clap_complete : Generate shell completion scripts for your clap::Command - [clap_complete](https://crates.io/crates/clap_complete) [GitHub](https://github.com/clap-rs/clap) (❓) ✅ 8 | - futures : An implementation of futures and streams featuring zero allocations, composability, and iterator-like interfaces. - [futures](https://crates.io/crates/futures) [GitHub](https://github.com/rust-lang/futures-rs) (❓) ✅ 9 | - regex : An implementation of regular expressions for Rust. This implementation uses finite automata and guarantees linear time matching on all inputs. - [regex](https://crates.io/crates/regex) [GitHub](https://github.com/rust-lang/regex) (❓) ✅ 10 | - reqwest : higher level HTTP client library - [reqwest](https://crates.io/crates/reqwest) [GitHub](https://github.com/seanmonstar/reqwest) (❓) ✅ 11 | - rust-i18n : Rust I18n is use Rust codegen for load YAML file storage translations on compile time, and give you a t! macro for simply get translation texts. - [rust-i18n](https://crates.io/crates/rust-i18n) [GitHub](https://github.com/longbridge/rust-i18n) (❓) ✅ 12 | - serde : A generic serialization/deserialization framework - [serde](https://crates.io/crates/serde) [GitHub](https://github.com/serde-rs/serde) (❓) ✅ 13 | - serde_json : A JSON serialization file format - [serde_json](https://crates.io/crates/serde_json) [GitHub](https://github.com/serde-rs/json) (❓) ✅ 14 | - serde_yaml : YAML data format for Serde - [serde_yaml](https://crates.io/crates/serde_yaml) [GitHub](https://github.com/dtolnay/serde-yaml) (❓) ✅ 15 | - strsim : Implementations of string similarity metrics. Includes Hamming, Levenshtein, OSA, Damerau-Levenshtein, Jaro, Jaro-Winkler, and Sørensen-Dice. - [strsim](https://crates.io/crates/strsim) [GitHub](https://github.com/rapidfuzz/strsim-rs) (❓) ✅ 16 | - thiserror : derive(Error) - [thiserror](https://crates.io/crates/thiserror) [GitHub](https://github.com/dtolnay/thiserror) (❓) ✅ 17 | - tokio : An event-driven, non-blocking I/O platform for writing asynchronous I/O backed applications. - [tokio](https://crates.io/crates/tokio) [GitHub](https://github.com/tokio-rs/tokio) (❓) ✅ 18 | - toml : A native Rust encoder and decoder of TOML-formatted files and streams. Provides implementations of the standard Serialize/Deserialize traits for TOML data to facilitate deserializing and serializing Rust structures. - [toml](https://crates.io/crates/toml) [GitHub](https://github.com/toml-rs/toml) (❓) ✅ 19 | - tracing : Application-level tracing for Rust. - [tracing](https://crates.io/crates/tracing) [GitHub](https://github.com/tokio-rs/tracing) (❓) ✅ 20 | - tracing-subscriber : Utilities for implementing and composing `tracing` subscribers. - [tracing-subscriber](https://crates.io/crates/tracing-subscriber) [GitHub](https://github.com/tokio-rs/tracing) (❓) ✅ 21 | - url : URL library for Rust, based on the WHATWG URL Standard - [url](https://crates.io/crates/url) [GitHub](https://github.com/servo/rust-url) (❓) ✅ 22 | 23 | ## 开发 24 | - assert_fs : Filesystem fixtures and assertions for testing. - [assert_fs](https://crates.io/crates/assert_fs) [GitHub](https://github.com/assert-rs/assert_fs.git) (❓) ✅ 25 | - pretty_assertions : Overwrite `assert_eq!` and `assert_ne!` with drop-in replacements, adding colorful diffs. - [pretty_assertions](https://crates.io/crates/pretty_assertions) [GitHub](https://github.com/rust-pretty-assertions/rust-pretty-assertions) (❓) ✅ 26 | - tokio-test : Testing utilities for Tokio- and futures-based code - [tokio-test](https://crates.io/crates/tokio-test) [GitHub](https://github.com/tokio-rs/tokio) (❓) ✅ 27 | -------------------------------------------------------------------------------- /src/sources.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use reqwest::{ 3 | Client, 4 | header::{self, HeaderValue}, 5 | }; 6 | use serde::Deserialize; 7 | use std::time::Duration; 8 | use tracing::instrument; 9 | use url::Url; 10 | 11 | #[allow(unused)] 12 | #[derive(Debug, Clone)] 13 | pub enum Source { 14 | GitHub { 15 | owner: String, 16 | repo: String, 17 | stars: Option, 18 | }, 19 | CratesIo { 20 | name: String, 21 | downloads: Option, 22 | }, 23 | Link { 24 | url: String, 25 | }, 26 | Other { 27 | description: String, 28 | }, 29 | } 30 | 31 | #[allow(unused)] 32 | impl Source { 33 | pub fn from_url(url: &Option) -> Option { 34 | url.as_ref().and_then(|u| match u.host_str()? { 35 | "github.com" => { 36 | let path = u.path().trim_matches('/'); 37 | let mut parts = path.splitn(2, '/'); 38 | Some(Self::GitHub { 39 | owner: parts.next()?.to_string(), 40 | repo: parts.next()?.trim_end_matches(".git").to_string(), 41 | stars: None, 42 | }) 43 | } 44 | "crates.io" => { 45 | let name = u.path().trim_matches('/').to_string(); 46 | Some(Self::CratesIo { 47 | name, 48 | downloads: None, 49 | }) 50 | } 51 | _ => Some(Self::Link { url: u.to_string() }), 52 | }) 53 | } 54 | } 55 | 56 | #[allow(unused)] 57 | #[derive(Debug, Deserialize)] 58 | pub struct CrateInfo { 59 | pub name: String, 60 | pub description: Option, 61 | // pub repository: Option, 62 | pub repository: Option, 63 | pub downloads: u32, 64 | } 65 | 66 | pub struct CratesioClient { 67 | client: Client, 68 | } 69 | 70 | impl Default for CratesioClient { 71 | fn default() -> Self { 72 | Self::new() 73 | } 74 | } 75 | 76 | impl CratesioClient { 77 | pub fn new() -> Self { 78 | Self { 79 | client: Client::builder() 80 | .timeout(Duration::from_secs(10)) 81 | .user_agent(concat!( 82 | env!("CARGO_PKG_NAME"), 83 | "/", 84 | env!("CARGO_PKG_VERSION") 85 | )) 86 | .build() 87 | .unwrap_or_else(|_| panic!("{}", t!("sources.failed_to_create_http_client"))), 88 | } 89 | } 90 | 91 | pub fn get_crate_url(name: &str) -> String { 92 | format!("https://crates.io/crates/{}", name) 93 | } 94 | 95 | #[instrument(skip(self))] 96 | pub async fn get_crate_info(&self, name: &str) -> Result { 97 | let url = format!("https://crates.io/api/v1/crates/{}", name); 98 | let response = self.client.get(&url).send().await?; 99 | let data = response.json::().await?; 100 | let crate_info = data["crate"].clone(); 101 | 102 | Ok(serde_json::from_value(crate_info)?) 103 | } 104 | } 105 | 106 | pub struct GitHubClient { 107 | client: Client, 108 | } 109 | 110 | impl GitHubClient { 111 | pub fn new(token: &str) -> Result { 112 | const ACCEPT: &str = "application/vnd.github+json"; 113 | const API_VERSION: &str = "2022-11-28"; 114 | 115 | let client = Client::builder() 116 | .timeout(Duration::from_secs(10)) 117 | .user_agent(concat!( 118 | env!("CARGO_PKG_NAME"), 119 | "/", 120 | env!("CARGO_PKG_VERSION") 121 | )) 122 | .default_headers({ 123 | let mut headers = header::HeaderMap::new(); 124 | headers.insert( 125 | header::AUTHORIZATION, 126 | HeaderValue::from_str(&format!("Bearer {}", token))?, 127 | ); 128 | headers.insert(header::ACCEPT, HeaderValue::from_static(ACCEPT)); 129 | headers.insert( 130 | header::HeaderName::from_static("x-github-api-version"), 131 | HeaderValue::from_static(API_VERSION), 132 | ); 133 | headers 134 | }) 135 | .build()?; 136 | 137 | Ok(Self { client }) 138 | } 139 | 140 | #[instrument(skip(self))] 141 | pub async fn star_repository(&self, owner: &str, repo: &str) -> Result<()> { 142 | let url = format!("https://api.github.com/user/starred/{}/{}", owner, repo); 143 | self.client 144 | .put(&url) 145 | .header(header::CONTENT_LENGTH, 0) 146 | .send() 147 | .await? 148 | .error_for_status()?; 149 | Ok(()) 150 | } 151 | 152 | #[instrument(skip(self))] 153 | pub async fn get_repository_info(&self, owner: &str, repo: &str) -> Result { 154 | let url = format!("https://api.github.com/repos/{}/{}", owner, repo); 155 | let response = self.client.get(&url).send().await?.error_for_status()?; 156 | Ok(response.json().await?) 157 | } 158 | } 159 | 160 | #[allow(unused)] 161 | #[derive(Debug, Deserialize)] 162 | pub struct RepositoryInfo { 163 | pub full_name: String, 164 | pub description: Option, 165 | pub stargazers_count: u32, 166 | pub html_url: String, 167 | } 168 | -------------------------------------------------------------------------------- /src/travert.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use std::{ 3 | fs::File, 4 | io::{BufReader, BufWriter, Write}, 5 | path::{Path, PathBuf}, 6 | }; 7 | 8 | use crate::output::{self, OutputFormat}; 9 | 10 | #[derive(Debug, Clone)] 11 | pub struct Travert { 12 | pub path: PathBuf, 13 | pub format: OutputFormat, 14 | } 15 | 16 | #[allow(dead_code)] 17 | impl Travert { 18 | pub fn new>(path: P) -> Result { 19 | Ok(Self { 20 | path: path.as_ref().to_path_buf(), 21 | format: Self::judge_format(path)?, 22 | }) 23 | } 24 | 25 | pub fn new_with_format>(path: P, format: OutputFormat) -> Self { 26 | Self { 27 | path: path.as_ref().to_path_buf(), 28 | format, 29 | } 30 | } 31 | 32 | fn detect_markdown_format_by_name(path: &Path) -> Result { 33 | let file_name = path.file_stem(); 34 | 35 | if file_name.is_none() { 36 | return Ok(OutputFormat::MarkdownList); 37 | } 38 | 39 | let file_name = file_name.unwrap().to_string_lossy().to_string(); 40 | let format_identifier = file_name.split(['_']).next_back(); 41 | 42 | match format_identifier { 43 | Some("md") => Ok(OutputFormat::MarkdownTable), 44 | Some("mt") => Ok(OutputFormat::MarkdownList), 45 | _ => Ok(OutputFormat::MarkdownList), 46 | } 47 | } 48 | 49 | /// 判断是 markdown 表格还是 markdown 列表 50 | fn detect_markdown_content_format(content: &str) -> Result { 51 | let table_re = regex::Regex::new( 52 | r"(?x)(?m) 53 | ^\s*\| 54 | ([^|\n]+(?:\s*\|\s*[^|\n]+)*) 55 | \|\s*$\n 56 | ^\s*\| 57 | ([-:]+(?:\s*\|\s*[-:]+)*) 58 | \|\s*$", 59 | )?; 60 | 61 | // find the first match 62 | // then split it into groups 63 | if let Some(captures) = table_re.captures(content) 64 | && captures.len() >= 3 65 | && let (Some(header_match), Some(separator_match)) = (captures.get(1), captures.get(2)) 66 | { 67 | let header_line = header_match.as_str().trim(); 68 | let separator_line = separator_match.as_str().trim(); 69 | 70 | let header_parts: Vec<&str> = header_line.split('|').collect(); 71 | 72 | let separator_parts: Vec<&str> = separator_line.split('|').collect(); 73 | 74 | if !header_parts.is_empty() && header_parts.len() == separator_parts.len() { 75 | return Ok(OutputFormat::MarkdownTable); 76 | } 77 | } 78 | 79 | Ok(OutputFormat::MarkdownList) 80 | } 81 | 82 | fn judge_format>(path: P) -> Result { 83 | let path = path.as_ref(); 84 | 85 | let file_exists = path.exists(); 86 | let extension = path.extension().unwrap_or_default().to_ascii_lowercase(); 87 | // println!("extension: {}", &extension.clone().into_string().unwrap()); 88 | 89 | match extension.to_str() { 90 | Some("md") => { 91 | if file_exists { 92 | let content = std::fs::read_to_string(path)?; 93 | Self::detect_markdown_content_format(&content) 94 | } else { 95 | Self::detect_markdown_format_by_name(path) 96 | } 97 | } 98 | Some("csv") => Ok(OutputFormat::Csv), 99 | Some("toml") => Ok(OutputFormat::Toml), 100 | Some("yml") => Ok(OutputFormat::Yaml), 101 | Some("yaml") => Ok(OutputFormat::Yaml), 102 | Some("json") => Ok(OutputFormat::Json), 103 | _ => anyhow::bail!(t!("travert.failed_to_judge_format", path = path.display())), 104 | } 105 | 106 | // anyhow::bail!(t!("travert.failed_to_judge_format", path = path.display())) 107 | } 108 | } 109 | 110 | #[derive(Debug, Clone)] 111 | pub struct Converter { 112 | pub source: Travert, 113 | pub targets: Vec, 114 | } 115 | 116 | impl Converter { 117 | pub fn new>(source: P, targets: impl IntoIterator) -> Result { 118 | Ok(Self { 119 | source: Travert::new(source)?, 120 | targets: targets 121 | .into_iter() 122 | .map(|p| Travert::new(p)) 123 | .collect::>>()?, 124 | }) 125 | } 126 | 127 | #[allow(dead_code)] 128 | pub fn new_with_format(source: Travert, target: Vec) -> Result { 129 | Ok(Self { 130 | source, 131 | targets: target, 132 | }) 133 | } 134 | 135 | pub fn convert(&self) -> Result<()> { 136 | let mut reader = BufReader::new(File::open(&self.source.path)?); 137 | let formatter = ::new(self.source.format)?; 138 | let dependencies_info = formatter.parse_reader(&mut reader)?; 139 | 140 | for target in &self.targets { 141 | let file = std::fs::File::create(&target.path)?; 142 | let mut writer = BufWriter::new(file); 143 | let mut manager = output::OutputManager::new(target.format, &mut writer); 144 | manager.write(&dependencies_info)?; 145 | writer.flush()?; 146 | println!( 147 | "{}", 148 | t!("travert.write_success", path = target.path.display()) 149 | ); 150 | } 151 | 152 | Ok(()) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | tags: ["v*"] 7 | pull_request: 8 | branches: [main] 9 | 10 | # Add top-level permissions configuration 11 | permissions: 12 | contents: write 13 | packages: write 14 | 15 | env: 16 | CARGO_TERM_COLOR: always 17 | RUST_LOG: info 18 | BINARY_NAME: cargo-thanku 19 | 20 | jobs: 21 | test: 22 | name: Test 23 | runs-on: ${{ matrix.os }} 24 | strategy: 25 | matrix: 26 | os: [ubuntu-latest, windows-latest, macos-latest] 27 | rust: [stable] 28 | 29 | steps: 30 | - uses: actions/checkout@v4 31 | 32 | - name: Install Rust 33 | uses: dtolnay/rust-toolchain@stable 34 | with: 35 | toolchain: ${{ matrix.rust }} 36 | components: clippy, rustfmt 37 | 38 | - name: Cache dependencies 39 | uses: Swatinem/rust-cache@v2 40 | 41 | - name: Check formatting 42 | run: cargo fmt -- --check 43 | 44 | # - name: Clippy 45 | # run: cargo clippy -- -D warnings 46 | 47 | - name: Run tests filter zh 48 | run: cargo test --verbose -- --skip _zh --include-ignored 49 | 50 | - name: Run tests filter en 51 | run: cargo test --verbose -- --skip _en --include-ignored 52 | 53 | - name: Build 54 | run: cargo build --verbose 55 | 56 | build-release: 57 | name: Build Release 58 | needs: [test] 59 | if: startsWith(github.ref, 'refs/tags/v') 60 | strategy: 61 | matrix: 62 | include: 63 | - os: ubuntu-latest 64 | target: x86_64-unknown-linux-gnu 65 | suffix: linux-x86_64 66 | use_cross: true 67 | - os: ubuntu-latest 68 | target: aarch64-unknown-linux-gnu 69 | suffix: linux-aarch64 70 | use_cross: true 71 | - os: windows-latest 72 | target: x86_64-pc-windows-msvc 73 | suffix: windows-x86_64.exe 74 | use_cross: false 75 | - os: macos-latest 76 | target: x86_64-apple-darwin 77 | suffix: darwin-x86_64 78 | use_cross: false 79 | - os: macos-latest 80 | target: aarch64-apple-darwin 81 | suffix: darwin-aarch64 82 | use_cross: false 83 | runs-on: ${{ matrix.os }} 84 | 85 | steps: 86 | - uses: actions/checkout@v4 87 | 88 | - name: Install Rust 89 | uses: dtolnay/rust-toolchain@stable 90 | with: 91 | targets: ${{ matrix.target }} 92 | 93 | - name: Install cross 94 | if: matrix.use_cross 95 | run: cargo install cross 96 | 97 | - name: Build binary 98 | run: | 99 | if [ "${{ matrix.use_cross }}" = "true" ]; then 100 | cross build --release --target ${{ matrix.target }} 101 | else 102 | cargo build --release --target ${{ matrix.target }} 103 | fi 104 | shell: bash 105 | 106 | - name: Prepare binary 107 | shell: bash 108 | run: | 109 | if [ "${{ matrix.os }}" = "windows-latest" ]; then 110 | cp target/${{ matrix.target }}/release/${{ env.BINARY_NAME }}.exe ${{ env.BINARY_NAME }}-${{ matrix.suffix }} 111 | else 112 | cp target/${{ matrix.target }}/release/${{ env.BINARY_NAME }} ${{ env.BINARY_NAME }}-${{ matrix.suffix }} 113 | fi 114 | 115 | - name: Upload artifact 116 | uses: actions/upload-artifact@v4 117 | with: 118 | name: ${{ env.BINARY_NAME }}-${{ matrix.suffix }} 119 | path: ${{ env.BINARY_NAME }}-${{ matrix.suffix }} 120 | 121 | create-release: 122 | name: Create Release 123 | needs: [build-release] 124 | runs-on: ubuntu-latest 125 | if: startsWith(github.ref, 'refs/tags/v') 126 | # Set permissions for this job specifically 127 | permissions: 128 | contents: write 129 | packages: write 130 | 131 | steps: 132 | - uses: actions/checkout@v4 133 | with: 134 | fetch-depth: 0 135 | 136 | - name: Generate changelog file 137 | run: | 138 | # Get the previous tag or use the initial commit if no tag exists 139 | PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || git rev-list --max-parents=0 HEAD) 140 | 141 | # Create a changelog file 142 | echo "# Changes in this release" > changelog.md 143 | echo "" >> changelog.md 144 | 145 | # Add commit messages to the changelog file 146 | git log ${PREV_TAG}..HEAD --pretty=format:"* %s" | sed 's/^/- /' >> changelog.md 147 | 148 | - name: Download artifacts 149 | uses: actions/download-artifact@v4 150 | with: 151 | path: artifacts 152 | 153 | - name: Create release 154 | uses: softprops/action-gh-release@v2 155 | env: 156 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 157 | with: 158 | files: artifacts/**/* 159 | body_path: changelog.md 160 | draft: false 161 | prerelease: false 162 | 163 | publish-crate: 164 | name: Publish to crates.io 165 | needs: [create-release] 166 | runs-on: ubuntu-latest 167 | if: startsWith(github.ref, 'refs/tags/v') 168 | 169 | steps: 170 | - uses: actions/checkout@v4 171 | 172 | - name: Install Rust 173 | uses: dtolnay/rust-toolchain@stable 174 | 175 | - name: Publish to crates.io 176 | run: cargo publish --token ${CRATES_TOKEN} 177 | env: 178 | CRATES_TOKEN: ${{ secrets.CRATES_TOKEN }} -------------------------------------------------------------------------------- /.serena/project.yml: -------------------------------------------------------------------------------- 1 | # list of languages for which language servers are started; choose from: 2 | # al bash clojure cpp csharp csharp_omnisharp 3 | # dart elixir elm erlang fortran go 4 | # haskell java julia kotlin lua markdown 5 | # nix perl php python python_jedi r 6 | # rego ruby ruby_solargraph rust scala swift 7 | # terraform typescript typescript_vts yaml zig 8 | # Note: 9 | # - For C, use cpp 10 | # - For JavaScript, use typescript 11 | # Special requirements: 12 | # - csharp: Requires the presence of a .sln file in the project folder. 13 | # When using multiple languages, the first language server that supports a given file will be used for that file. 14 | # The first language is the default language and the respective language server will be used as a fallback. 15 | # Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored. 16 | languages: 17 | - rust 18 | 19 | # the encoding used by text files in the project 20 | # For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings 21 | encoding: "utf-8" 22 | 23 | # whether to use the project's gitignore file to ignore files 24 | # Added on 2025-04-07 25 | ignore_all_files_in_gitignore: true 26 | 27 | # list of additional paths to ignore 28 | # same syntax as gitignore, so you can use * and ** 29 | # Was previously called `ignored_dirs`, please update your config if you are using that. 30 | # Added (renamed) on 2025-04-07 31 | ignored_paths: [] 32 | 33 | # whether the project is in read-only mode 34 | # If set to true, all editing tools will be disabled and attempts to use them will result in an error 35 | # Added on 2025-04-18 36 | read_only: false 37 | 38 | # list of tool names to exclude. We recommend not excluding any tools, see the readme for more details. 39 | # Below is the complete list of tools for convenience. 40 | # To make sure you have the latest list of tools, and to view their descriptions, 41 | # execute `uv run scripts/print_tool_overview.py`. 42 | # 43 | # * `activate_project`: Activates a project by name. 44 | # * `check_onboarding_performed`: Checks whether project onboarding was already performed. 45 | # * `create_text_file`: Creates/overwrites a file in the project directory. 46 | # * `delete_lines`: Deletes a range of lines within a file. 47 | # * `delete_memory`: Deletes a memory from Serena's project-specific memory store. 48 | # * `execute_shell_command`: Executes a shell command. 49 | # * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced. 50 | # * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type). 51 | # * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). 52 | # * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes. 53 | # * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file. 54 | # * `initial_instructions`: Gets the initial instructions for the current project. 55 | # Should only be used in settings where the system prompt cannot be set, 56 | # e.g. in clients you have no control over, like Claude Desktop. 57 | # * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. 58 | # * `insert_at_line`: Inserts content at a given line in a file. 59 | # * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol. 60 | # * `list_dir`: Lists files and directories in the given directory (optionally with recursion). 61 | # * `list_memories`: Lists memories in Serena's project-specific memory store. 62 | # * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). 63 | # * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context). 64 | # * `read_file`: Reads a file within the project directory. 65 | # * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store. 66 | # * `remove_project`: Removes a project from the Serena configuration. 67 | # * `replace_lines`: Replaces a range of lines within a file with new content. 68 | # * `replace_symbol_body`: Replaces the full definition of a symbol. 69 | # * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen. 70 | # * `search_for_pattern`: Performs a search for a pattern in the project. 71 | # * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase. 72 | # * `switch_modes`: Activates modes by providing a list of their names 73 | # * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information. 74 | # * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task. 75 | # * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed. 76 | # * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store. 77 | excluded_tools: [] 78 | 79 | # initial prompt for the project. It will always be given to the LLM upon activating the project 80 | # (contrary to the memories, which are loaded on demand). 81 | initial_prompt: "" 82 | 83 | project_name: "cargo-thanku" 84 | included_optional_tools: [] 85 | -------------------------------------------------------------------------------- /src/output/markdown/tokenizer.rs: -------------------------------------------------------------------------------- 1 | use crate::{errors::AppError, output::dependency::DependencyKind}; 2 | 3 | #[derive(Debug, Clone, Copy)] 4 | pub enum MarkdownSection<'a> { 5 | Header(&'a str), 6 | Item(ListEntry<'a>), 7 | } 8 | 9 | pub struct MarkdownListTokenizer<'a> { 10 | lines: std::str::Lines<'a>, 11 | } 12 | 13 | impl<'a> MarkdownListTokenizer<'a> { 14 | pub fn new(content: &'a str) -> Self { 15 | Self { 16 | lines: content.lines(), 17 | } 18 | } 19 | } 20 | 21 | impl<'a> Iterator for MarkdownListTokenizer<'a> { 22 | type Item = Result, AppError>; 23 | 24 | fn next(&mut self) -> Option { 25 | for line in self.lines.by_ref() { 26 | let trimmed = line.trim(); 27 | if trimmed.is_empty() { 28 | continue; 29 | } 30 | 31 | if trimmed.starts_with("## ") { 32 | return Some(Ok(MarkdownSection::Header(trimmed))); 33 | } 34 | 35 | if trimmed.starts_with('-') { 36 | return Some(ListEntry::from_line(trimmed).map(MarkdownSection::Item)); 37 | } 38 | } 39 | 40 | None 41 | } 42 | } 43 | 44 | #[derive(Debug, Clone, Copy)] 45 | pub struct ListEntry<'a> { 46 | pub raw_line: &'a str, 47 | pub name: &'a str, 48 | pub description: Option<&'a str>, 49 | pub crate_segment: &'a str, 50 | pub source_segment: &'a str, 51 | pub stats_segment: &'a str, 52 | pub status_segment: &'a str, 53 | } 54 | 55 | impl<'a> ListEntry<'a> { 56 | pub fn from_line(line: &'a str) -> Result { 57 | let content = line.trim_start_matches('-').trim(); 58 | let (name_part, remainder) = content 59 | .split_once(" : ") 60 | .ok_or_else(|| AppError::InvalidListLine(line.to_string()))?; 61 | let name = name_part.trim(); 62 | let (description_part, tail) = remainder 63 | .split_once(" - ") 64 | .ok_or_else(|| AppError::InvalidListLine(line.to_string()))?; 65 | let description = match description_part.trim() { 66 | "" => None, 67 | text => Some(text), 68 | }; 69 | 70 | let mut rest = tail.trim(); 71 | let (crate_segment, leftover) = take_markdown_link(rest, line)?; 72 | rest = leftover; 73 | let (source_segment, leftover) = take_markdown_link(rest, line)?; 74 | rest = leftover; 75 | let (stats_segment, leftover) = take_parenthesized_segment(rest, line)?; 76 | rest = leftover.trim(); 77 | if rest.is_empty() { 78 | return Err(AppError::InvalidListLine(line.to_string())); 79 | } 80 | 81 | Ok(Self { 82 | raw_line: line, 83 | name, 84 | description, 85 | crate_segment, 86 | source_segment, 87 | stats_segment, 88 | status_segment: rest, 89 | }) 90 | } 91 | } 92 | 93 | fn take_markdown_link<'a>(input: &'a str, line: &str) -> Result<(&'a str, &'a str), AppError> { 94 | let trimmed = input.trim_start(); 95 | if !trimmed.starts_with('[') { 96 | return Err(AppError::InvalidListLine(line.to_string())); 97 | } 98 | 99 | let mut closing_bracket = None; 100 | for (idx, ch) in trimmed.char_indices() { 101 | if ch == ']' { 102 | closing_bracket = Some(idx); 103 | break; 104 | } 105 | } 106 | let closing_bracket = 107 | closing_bracket.ok_or_else(|| AppError::InvalidListLine(line.to_string()))?; 108 | let after_bracket = &trimmed[closing_bracket + 1..]; 109 | if !after_bracket.starts_with('(') { 110 | return Err(AppError::InvalidListLine(line.to_string())); 111 | } 112 | 113 | let mut depth = 0i32; 114 | let mut closing_paren = None; 115 | for (offset, ch) in after_bracket.char_indices() { 116 | match ch { 117 | '(' => depth += 1, 118 | ')' => { 119 | depth -= 1; 120 | if depth == 0 { 121 | closing_paren = Some(offset); 122 | break; 123 | } 124 | } 125 | _ => {} 126 | } 127 | } 128 | 129 | let closing_paren = closing_paren.ok_or_else(|| AppError::InvalidListLine(line.to_string()))?; 130 | let segment_len = closing_bracket + 1 + closing_paren + 1; 131 | let segment = &trimmed[..segment_len + 1]; 132 | let remainder = after_bracket[closing_paren + 1..].trim_start(); 133 | Ok((segment, remainder)) 134 | } 135 | 136 | fn take_parenthesized_segment<'a>( 137 | input: &'a str, 138 | line: &str, 139 | ) -> Result<(&'a str, &'a str), AppError> { 140 | let trimmed = input.trim_start(); 141 | if !trimmed.starts_with('(') { 142 | return Err(AppError::InvalidListLine(line.to_string())); 143 | } 144 | 145 | let mut depth = 0i32; 146 | let mut closing_paren = None; 147 | for (idx, ch) in trimmed.char_indices() { 148 | match ch { 149 | '(' => depth += 1, 150 | ')' => { 151 | depth -= 1; 152 | if depth == 0 { 153 | closing_paren = Some(idx); 154 | break; 155 | } 156 | } 157 | _ => {} 158 | } 159 | } 160 | 161 | let closing_paren = closing_paren.ok_or_else(|| AppError::InvalidListLine(line.to_string()))?; 162 | let segment = &trimmed[..=closing_paren]; 163 | let remainder = trimmed[closing_paren + 1..].trim_start(); 164 | Ok((segment, remainder)) 165 | } 166 | 167 | pub fn section_kind_from_header(header: &str) -> Option { 168 | DependencyKind::try_from_list_header_line(header).ok() 169 | } 170 | -------------------------------------------------------------------------------- /assets/output/converted/THANKU_en.toml: -------------------------------------------------------------------------------- 1 | [[dependencies]] 2 | name = "anyhow" 3 | description = "Flexible concrete Error type built on std::error::Error" 4 | dependency_kind = "Normal" 5 | crate_url = "https://crates.io/crates/anyhow" 6 | source_type = "GitHub" 7 | source_url = "https://github.com/dtolnay/anyhow" 8 | failed = false 9 | 10 | [dependencies.stats] 11 | 12 | [[dependencies]] 13 | name = "cargo_metadata" 14 | description = "structured access to the output of `cargo metadata`" 15 | dependency_kind = "Normal" 16 | crate_url = "https://crates.io/crates/cargo_metadata" 17 | source_type = "GitHub" 18 | source_url = "https://github.com/oli-obk/cargo_metadata" 19 | failed = false 20 | 21 | [dependencies.stats] 22 | 23 | [[dependencies]] 24 | name = "clap" 25 | description = "A simple to use, efficient, and full-featured Command Line Argument Parser" 26 | dependency_kind = "Normal" 27 | crate_url = "https://crates.io/crates/clap" 28 | source_type = "GitHub" 29 | source_url = "https://github.com/clap-rs/clap" 30 | failed = false 31 | 32 | [dependencies.stats] 33 | 34 | [[dependencies]] 35 | name = "clap_complete" 36 | description = "Generate shell completion scripts for your clap::Command" 37 | dependency_kind = "Normal" 38 | crate_url = "https://crates.io/crates/clap_complete" 39 | source_type = "GitHub" 40 | source_url = "https://github.com/clap-rs/clap" 41 | failed = false 42 | 43 | [dependencies.stats] 44 | 45 | [[dependencies]] 46 | name = "futures" 47 | description = "An implementation of futures and streams featuring zero allocations, composability, and iterator-like interfaces." 48 | dependency_kind = "Normal" 49 | crate_url = "https://crates.io/crates/futures" 50 | source_type = "GitHub" 51 | source_url = "https://github.com/rust-lang/futures-rs" 52 | failed = false 53 | 54 | [dependencies.stats] 55 | 56 | [[dependencies]] 57 | name = "reqwest" 58 | description = "higher level HTTP client library" 59 | dependency_kind = "Normal" 60 | crate_url = "https://crates.io/crates/reqwest" 61 | source_type = "GitHub" 62 | source_url = "https://github.com/seanmonstar/reqwest" 63 | failed = false 64 | 65 | [dependencies.stats] 66 | 67 | [[dependencies]] 68 | name = "rust-i18n" 69 | description = "Rust I18n is use Rust codegen for load YAML file storage translations on compile time, and give you a t! macro for simply get translation texts." 70 | dependency_kind = "Normal" 71 | crate_url = "https://crates.io/crates/rust-i18n" 72 | source_type = "GitHub" 73 | source_url = "https://github.com/longbridge/rust-i18n" 74 | failed = false 75 | 76 | [dependencies.stats] 77 | 78 | [[dependencies]] 79 | name = "serde" 80 | description = "A generic serialization/deserialization framework" 81 | dependency_kind = "Normal" 82 | crate_url = "https://crates.io/crates/serde" 83 | source_type = "GitHub" 84 | source_url = "https://github.com/serde-rs/serde" 85 | failed = false 86 | 87 | [dependencies.stats] 88 | 89 | [[dependencies]] 90 | name = "serde_json" 91 | description = "A JSON serialization file format" 92 | dependency_kind = "Normal" 93 | crate_url = "https://crates.io/crates/serde_json" 94 | source_type = "GitHub" 95 | source_url = "https://github.com/serde-rs/json" 96 | failed = false 97 | 98 | [dependencies.stats] 99 | 100 | [[dependencies]] 101 | name = "serde_yaml" 102 | description = "YAML data format for Serde" 103 | dependency_kind = "Normal" 104 | crate_url = "https://crates.io/crates/serde_yaml" 105 | source_type = "GitHub" 106 | source_url = "https://github.com/dtolnay/serde-yaml" 107 | failed = false 108 | 109 | [dependencies.stats] 110 | 111 | [[dependencies]] 112 | name = "strsim" 113 | description = "Implementations of string similarity metrics. Includes Hamming, Levenshtein, OSA, Damerau-Levenshtein, Jaro, Jaro-Winkler, and Sørensen-Dice." 114 | dependency_kind = "Normal" 115 | crate_url = "https://crates.io/crates/strsim" 116 | source_type = "GitHub" 117 | source_url = "https://github.com/rapidfuzz/strsim-rs" 118 | failed = false 119 | 120 | [dependencies.stats] 121 | 122 | [[dependencies]] 123 | name = "thiserror" 124 | description = "derive(Error)" 125 | dependency_kind = "Normal" 126 | crate_url = "https://crates.io/crates/thiserror" 127 | source_type = "GitHub" 128 | source_url = "https://github.com/dtolnay/thiserror" 129 | failed = false 130 | 131 | [dependencies.stats] 132 | 133 | [[dependencies]] 134 | name = "tokio" 135 | description = "An event-driven, non-blocking I/O platform for writing asynchronous I/O backed applications." 136 | dependency_kind = "Normal" 137 | crate_url = "https://crates.io/crates/tokio" 138 | source_type = "GitHub" 139 | source_url = "https://github.com/tokio-rs/tokio" 140 | failed = false 141 | 142 | [dependencies.stats] 143 | 144 | [[dependencies]] 145 | name = "toml" 146 | description = "A native Rust encoder and decoder of TOML-formatted files and streams. Provides implementations of the standard Serialize/Deserialize traits for TOML data to facilitate deserializing and serializing Rust structures." 147 | dependency_kind = "Normal" 148 | crate_url = "https://crates.io/crates/toml" 149 | source_type = "GitHub" 150 | source_url = "https://github.com/toml-rs/toml" 151 | failed = false 152 | 153 | [dependencies.stats] 154 | 155 | [[dependencies]] 156 | name = "tracing" 157 | description = "Application-level tracing for Rust." 158 | dependency_kind = "Normal" 159 | crate_url = "https://crates.io/crates/tracing" 160 | source_type = "GitHub" 161 | source_url = "https://github.com/tokio-rs/tracing" 162 | failed = false 163 | 164 | [dependencies.stats] 165 | 166 | [[dependencies]] 167 | name = "tracing-subscriber" 168 | description = "Utilities for implementing and composing `tracing` subscribers." 169 | dependency_kind = "Normal" 170 | crate_url = "https://crates.io/crates/tracing-subscriber" 171 | source_type = "GitHub" 172 | source_url = "https://github.com/tokio-rs/tokio" 173 | failed = false 174 | 175 | [dependencies.stats] 176 | 177 | [[dependencies]] 178 | name = "url" 179 | description = "URL library for Rust, based on the WHATWG URL Standard" 180 | dependency_kind = "Normal" 181 | crate_url = "https://crates.io/crates/url" 182 | source_type = "GitHub" 183 | source_url = "https://github.com/servo/rust-url" 184 | failed = false 185 | 186 | [dependencies.stats] 187 | 188 | [[dependencies]] 189 | name = "assert_fs" 190 | description = "Filesystem fixtures and assertions for testing." 191 | dependency_kind = "Development" 192 | crate_url = "https://crates.io/crates/assert_fs" 193 | source_type = "GitHub" 194 | source_url = "https://github.com/assert-rs/assert_fs.git" 195 | failed = false 196 | 197 | [dependencies.stats] 198 | 199 | [[dependencies]] 200 | name = "pretty_assertions" 201 | description = "Overwrite `assert_eq!` and `assert_ne!` with drop-in replacements, adding colorful diffs." 202 | dependency_kind = "Development" 203 | crate_url = "https://crates.io/crates/pretty_assertions" 204 | source_type = "GitHub" 205 | source_url = "https://github.com/rust-pretty-assertions/rust-pretty-assertions" 206 | failed = false 207 | 208 | [dependencies.stats] 209 | 210 | [[dependencies]] 211 | name = "tokio-test" 212 | description = "Testing utilities for Tokio- and futures-based code" 213 | dependency_kind = "Development" 214 | crate_url = "https://crates.io/crates/tokio-test" 215 | source_type = "GitHub" 216 | source_url = "https://github.com/tokio-rs/tokio" 217 | failed = false 218 | 219 | [dependencies.stats] 220 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use std::path::PathBuf; 3 | use std::sync::OnceLock; 4 | use tracing::instrument; 5 | 6 | use crate::errors::AppError; 7 | use crate::output::OutputFormat; 8 | 9 | #[derive(Debug, Clone)] 10 | pub enum LinkSource { 11 | GitHub, 12 | CratesIo, 13 | LinkEmpty, 14 | Other, 15 | } 16 | 17 | impl Default for LinkSource { 18 | fn default() -> Self { 19 | Self::GitHub 20 | } 21 | } 22 | 23 | impl std::str::FromStr for LinkSource { 24 | type Err = AppError; 25 | 26 | fn from_str(s: &str) -> Result { 27 | Ok(match s { 28 | "github" => Self::GitHub, 29 | "crates-io" => Self::CratesIo, 30 | "link-empty" => Self::LinkEmpty, 31 | "other" => Self::Other, 32 | _ => return Err(AppError::InvalidLinkSource(s.to_string())), 33 | }) 34 | } 35 | } 36 | 37 | /// 输出目标枚举 38 | pub enum OutputWriter { 39 | Stdout(std::io::Stdout), 40 | File(std::fs::File), 41 | } 42 | 43 | impl std::io::Write for OutputWriter { 44 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 45 | match self { 46 | Self::Stdout(stdout) => stdout.write(buf), 47 | Self::File(file) => file.write(buf), 48 | } 49 | } 50 | 51 | fn flush(&mut self) -> std::io::Result<()> { 52 | match self { 53 | Self::Stdout(stdout) => stdout.flush(), 54 | Self::File(file) => file.flush(), 55 | } 56 | } 57 | } 58 | 59 | #[allow(unused)] 60 | #[derive(Debug, Clone)] 61 | pub struct Config { 62 | pub input: PathBuf, 63 | pub output: Option, 64 | pub format: OutputFormat, 65 | pub link_source: LinkSource, 66 | pub github_token: Option, 67 | // pub crates_token: Option, 68 | pub no_relative_libs: bool, 69 | pub language: String, 70 | pub verbose: bool, 71 | pub max_concurrent_requests: usize, 72 | pub max_retries: u32, 73 | } 74 | 75 | impl Default for Config { 76 | fn default() -> Self { 77 | Self { 78 | input: PathBuf::from("Cargo.toml"), 79 | output: None, 80 | format: OutputFormat::default(), 81 | link_source: LinkSource::default(), 82 | github_token: None, 83 | // crates_token: None, 84 | no_relative_libs: false, 85 | language: String::from("zh"), 86 | verbose: false, 87 | max_concurrent_requests: 5, 88 | max_retries: 3, 89 | } 90 | } 91 | } 92 | 93 | static GLOBAL_CONFIG: OnceLock = OnceLock::new(); 94 | 95 | impl Config { 96 | pub fn global() -> Result<&'static Config> { 97 | GLOBAL_CONFIG 98 | .get() 99 | .ok_or_else(|| anyhow::anyhow!(t!("config.failed_to_initialize_global_config"))) 100 | } 101 | 102 | #[instrument] 103 | pub fn init(config: Config) -> Result<()> { 104 | GLOBAL_CONFIG 105 | .set(config) 106 | .map_err(|_| anyhow::anyhow!(t!("config.global_config_already_initialized"))) 107 | } 108 | 109 | #[instrument(skip_all)] 110 | pub fn from_matches(matches: &clap::ArgMatches) -> Result { 111 | let input = matches 112 | .get_one::("input") 113 | .cloned() 114 | .unwrap_or_else(|| PathBuf::from("Cargo.toml")); 115 | 116 | let output = matches.get_one::("output").cloned(); 117 | 118 | let format = matches 119 | .get_one::("format") 120 | .map(|f| f.parse::().unwrap_or_default()) 121 | .unwrap_or_default(); 122 | 123 | let link_source = matches 124 | .get_one::("source") 125 | .map(|l| l.parse::().unwrap_or_default()) 126 | .unwrap_or_default(); 127 | 128 | let github_token = matches.get_one::("token").cloned(); 129 | // let crates_token = matches.get_one::("crates-token").cloned(); 130 | let no_relative_libs = matches.get_flag("no-relative-libs"); 131 | 132 | let language = matches 133 | .get_one::("language") 134 | .cloned() 135 | .unwrap_or_default(); 136 | 137 | let verbose = matches.get_flag("verbose"); 138 | 139 | let max_concurrent_requests = matches.get_one::("concurrent").copied().unwrap_or(5); 140 | 141 | let max_retries = matches.get_one::("retries").copied().unwrap_or(3); 142 | 143 | Ok(Self { 144 | input, 145 | output, 146 | format, 147 | link_source, 148 | github_token, 149 | // crates_token, 150 | no_relative_libs, 151 | language, 152 | verbose, 153 | max_concurrent_requests, 154 | max_retries, 155 | }) 156 | } 157 | 158 | pub fn get_cargo_toml_path(&self) -> Result { 159 | if self.input.is_dir() { 160 | let path = self.input.join("Cargo.toml"); 161 | if path.exists() { 162 | return Ok(path); 163 | } 164 | } 165 | 166 | if self.input.is_file() && self.input.extension().unwrap_or_default() == "toml" { 167 | return Ok(self.input.clone()); 168 | } 169 | 170 | anyhow::bail!(t!( 171 | "config.cargo_toml_not_found", 172 | path = self.input.display() 173 | )); 174 | } 175 | 176 | /// 获取输出位置 (buffer) 177 | /// 178 | /// - 如果输出位置是文件,则返回文件内容进行追加写入 179 | /// - 如果文件不存在,则创建,然后返回文件内容进行写入 180 | /// - 如果输出位置是标准输出,则返回标准输出,进行写入 181 | pub fn get_output_writer(&self) -> Result { 182 | match &self.output { 183 | Some(path) if path.as_os_str() == "-" => Ok(OutputWriter::Stdout(std::io::stdout())), 184 | Some(path) => { 185 | if path.exists() { 186 | // 文件存在,则打开文件进行追加写入 187 | let file = std::fs::OpenOptions::new() 188 | .append(true) 189 | .open(path) 190 | .map_err(|e| { 191 | anyhow::anyhow!(t!( 192 | "config.failed_to_open_output_file", 193 | path = path.display(), 194 | error = e.to_string() 195 | )) 196 | })?; 197 | Ok(OutputWriter::File(file)) 198 | } else { 199 | // 文件不存在,则创建文件并返回文件内容进行写入 200 | let file = std::fs::OpenOptions::new() 201 | .write(true) 202 | .create(true) 203 | .truncate(true) 204 | .open(path) 205 | .map_err(|e| { 206 | anyhow::anyhow!(t!( 207 | "config.failed_to_open_output_file", 208 | path = path.display(), 209 | error = e.to_string() 210 | )) 211 | })?; 212 | Ok(OutputWriter::File(file)) 213 | } 214 | } 215 | None => Ok(OutputWriter::Stdout(std::io::stdout())), 216 | } 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /assets/output/THANKU_yaml_en.yaml: -------------------------------------------------------------------------------- 1 | - name: anyhow 2 | description: Flexible concrete Error type built on std::error::Error 3 | dependency_kind: Normal 4 | crate_url: https://crates.io/crates/anyhow 5 | source_type: GitHub 6 | source_url: https://github.com/dtolnay/anyhow 7 | stats: 8 | stars: null 9 | downloads: null 10 | failed: false 11 | error_message: null 12 | - name: cargo_metadata 13 | description: structured access to the output of `cargo metadata` 14 | dependency_kind: Normal 15 | crate_url: https://crates.io/crates/cargo_metadata 16 | source_type: GitHub 17 | source_url: https://github.com/oli-obk/cargo_metadata 18 | stats: 19 | stars: null 20 | downloads: null 21 | failed: false 22 | error_message: null 23 | - name: clap 24 | description: A simple to use, efficient, and full-featured Command Line Argument Parser 25 | dependency_kind: Normal 26 | crate_url: https://crates.io/crates/clap 27 | source_type: GitHub 28 | source_url: https://github.com/clap-rs/clap 29 | stats: 30 | stars: null 31 | downloads: null 32 | failed: false 33 | error_message: null 34 | - name: clap_complete 35 | description: Generate shell completion scripts for your clap::Command 36 | dependency_kind: Normal 37 | crate_url: https://crates.io/crates/clap_complete 38 | source_type: GitHub 39 | source_url: https://github.com/clap-rs/clap 40 | stats: 41 | stars: null 42 | downloads: null 43 | failed: false 44 | error_message: null 45 | - name: futures 46 | description: An implementation of futures and streams featuring zero allocations, composability, and iterator-like interfaces. 47 | dependency_kind: Normal 48 | crate_url: https://crates.io/crates/futures 49 | source_type: GitHub 50 | source_url: https://github.com/rust-lang/futures-rs 51 | stats: 52 | stars: null 53 | downloads: null 54 | failed: false 55 | error_message: null 56 | - name: reqwest 57 | description: higher level HTTP client library 58 | dependency_kind: Normal 59 | crate_url: https://crates.io/crates/reqwest 60 | source_type: GitHub 61 | source_url: https://github.com/seanmonstar/reqwest 62 | stats: 63 | stars: null 64 | downloads: null 65 | failed: false 66 | error_message: null 67 | - name: rust-i18n 68 | description: Rust I18n is use Rust codegen for load YAML file storage translations on compile time, and give you a t! macro for simply get translation texts. 69 | dependency_kind: Normal 70 | crate_url: https://crates.io/crates/rust-i18n 71 | source_type: GitHub 72 | source_url: https://github.com/longbridge/rust-i18n 73 | stats: 74 | stars: null 75 | downloads: null 76 | failed: false 77 | error_message: null 78 | - name: serde 79 | description: A generic serialization/deserialization framework 80 | dependency_kind: Normal 81 | crate_url: https://crates.io/crates/serde 82 | source_type: GitHub 83 | source_url: https://github.com/serde-rs/serde 84 | stats: 85 | stars: null 86 | downloads: null 87 | failed: false 88 | error_message: null 89 | - name: serde_json 90 | description: A JSON serialization file format 91 | dependency_kind: Normal 92 | crate_url: https://crates.io/crates/serde_json 93 | source_type: GitHub 94 | source_url: https://github.com/serde-rs/json 95 | stats: 96 | stars: null 97 | downloads: null 98 | failed: false 99 | error_message: null 100 | - name: serde_yaml 101 | description: YAML data format for Serde 102 | dependency_kind: Normal 103 | crate_url: https://crates.io/crates/serde_yaml 104 | source_type: GitHub 105 | source_url: https://github.com/dtolnay/serde-yaml 106 | stats: 107 | stars: null 108 | downloads: null 109 | failed: false 110 | error_message: null 111 | - name: strsim 112 | description: Implementations of string similarity metrics. Includes Hamming, Levenshtein, OSA, Damerau-Levenshtein, Jaro, Jaro-Winkler, and Sørensen-Dice. 113 | dependency_kind: Normal 114 | crate_url: https://crates.io/crates/strsim 115 | source_type: GitHub 116 | source_url: https://github.com/rapidfuzz/strsim-rs 117 | stats: 118 | stars: null 119 | downloads: null 120 | failed: false 121 | error_message: null 122 | - name: thiserror 123 | description: derive(Error) 124 | dependency_kind: Normal 125 | crate_url: https://crates.io/crates/thiserror 126 | source_type: GitHub 127 | source_url: https://github.com/dtolnay/thiserror 128 | stats: 129 | stars: null 130 | downloads: null 131 | failed: false 132 | error_message: null 133 | - name: tokio 134 | description: An event-driven, non-blocking I/O platform for writing asynchronous I/O backed applications. 135 | dependency_kind: Normal 136 | crate_url: https://crates.io/crates/tokio 137 | source_type: GitHub 138 | source_url: https://github.com/tokio-rs/tokio 139 | stats: 140 | stars: null 141 | downloads: null 142 | failed: false 143 | error_message: null 144 | - name: toml 145 | description: A native Rust encoder and decoder of TOML-formatted files and streams. Provides implementations of the standard Serialize/Deserialize traits for TOML data to facilitate deserializing and serializing Rust structures. 146 | dependency_kind: Normal 147 | crate_url: https://crates.io/crates/toml 148 | source_type: GitHub 149 | source_url: https://github.com/toml-rs/toml 150 | stats: 151 | stars: null 152 | downloads: null 153 | failed: false 154 | error_message: null 155 | - name: tracing 156 | description: Application-level tracing for Rust. 157 | dependency_kind: Normal 158 | crate_url: https://crates.io/crates/tracing 159 | source_type: GitHub 160 | source_url: https://github.com/tokio-rs/tracing 161 | stats: 162 | stars: null 163 | downloads: null 164 | failed: false 165 | error_message: null 166 | - name: tracing-subscriber 167 | description: Utilities for implementing and composing `tracing` subscribers. 168 | dependency_kind: Normal 169 | crate_url: https://crates.io/crates/tracing-subscriber 170 | source_type: GitHub 171 | source_url: https://github.com/tokio-rs/tokio 172 | stats: 173 | stars: null 174 | downloads: null 175 | failed: false 176 | error_message: null 177 | - name: url 178 | description: URL library for Rust, based on the WHATWG URL Standard 179 | dependency_kind: Normal 180 | crate_url: https://crates.io/crates/url 181 | source_type: GitHub 182 | source_url: https://github.com/servo/rust-url 183 | stats: 184 | stars: null 185 | downloads: null 186 | failed: false 187 | error_message: null 188 | - name: assert_fs 189 | description: Filesystem fixtures and assertions for testing. 190 | dependency_kind: Development 191 | crate_url: https://crates.io/crates/assert_fs 192 | source_type: GitHub 193 | source_url: https://github.com/assert-rs/assert_fs.git 194 | stats: 195 | stars: null 196 | downloads: null 197 | failed: false 198 | error_message: null 199 | - name: pretty_assertions 200 | description: Overwrite `assert_eq!` and `assert_ne!` with drop-in replacements, adding colorful diffs. 201 | dependency_kind: Development 202 | crate_url: https://crates.io/crates/pretty_assertions 203 | source_type: GitHub 204 | source_url: https://github.com/rust-pretty-assertions/rust-pretty-assertions 205 | stats: 206 | stars: null 207 | downloads: null 208 | failed: false 209 | error_message: null 210 | - name: tokio-test 211 | description: Testing utilities for Tokio- and futures-based code 212 | dependency_kind: Development 213 | crate_url: https://crates.io/crates/tokio-test 214 | source_type: GitHub 215 | source_url: https://github.com/tokio-rs/tokio 216 | stats: 217 | stars: null 218 | downloads: null 219 | failed: false 220 | error_message: null 221 | -------------------------------------------------------------------------------- /assets/output/converted/THANKU_en.yaml: -------------------------------------------------------------------------------- 1 | - name: anyhow 2 | description: Flexible concrete Error type built on std::error::Error 3 | dependency_kind: Normal 4 | crate_url: https://crates.io/crates/anyhow 5 | source_type: GitHub 6 | source_url: https://github.com/dtolnay/anyhow 7 | stats: 8 | stars: null 9 | downloads: null 10 | failed: false 11 | error_message: null 12 | - name: cargo_metadata 13 | description: structured access to the output of `cargo metadata` 14 | dependency_kind: Normal 15 | crate_url: https://crates.io/crates/cargo_metadata 16 | source_type: GitHub 17 | source_url: https://github.com/oli-obk/cargo_metadata 18 | stats: 19 | stars: null 20 | downloads: null 21 | failed: false 22 | error_message: null 23 | - name: clap 24 | description: A simple to use, efficient, and full-featured Command Line Argument Parser 25 | dependency_kind: Normal 26 | crate_url: https://crates.io/crates/clap 27 | source_type: GitHub 28 | source_url: https://github.com/clap-rs/clap 29 | stats: 30 | stars: null 31 | downloads: null 32 | failed: false 33 | error_message: null 34 | - name: clap_complete 35 | description: Generate shell completion scripts for your clap::Command 36 | dependency_kind: Normal 37 | crate_url: https://crates.io/crates/clap_complete 38 | source_type: GitHub 39 | source_url: https://github.com/clap-rs/clap 40 | stats: 41 | stars: null 42 | downloads: null 43 | failed: false 44 | error_message: null 45 | - name: futures 46 | description: An implementation of futures and streams featuring zero allocations, composability, and iterator-like interfaces. 47 | dependency_kind: Normal 48 | crate_url: https://crates.io/crates/futures 49 | source_type: GitHub 50 | source_url: https://github.com/rust-lang/futures-rs 51 | stats: 52 | stars: null 53 | downloads: null 54 | failed: false 55 | error_message: null 56 | - name: reqwest 57 | description: higher level HTTP client library 58 | dependency_kind: Normal 59 | crate_url: https://crates.io/crates/reqwest 60 | source_type: GitHub 61 | source_url: https://github.com/seanmonstar/reqwest 62 | stats: 63 | stars: null 64 | downloads: null 65 | failed: false 66 | error_message: null 67 | - name: rust-i18n 68 | description: Rust I18n is use Rust codegen for load YAML file storage translations on compile time, and give you a t! macro for simply get translation texts. 69 | dependency_kind: Normal 70 | crate_url: https://crates.io/crates/rust-i18n 71 | source_type: GitHub 72 | source_url: https://github.com/longbridge/rust-i18n 73 | stats: 74 | stars: null 75 | downloads: null 76 | failed: false 77 | error_message: null 78 | - name: serde 79 | description: A generic serialization/deserialization framework 80 | dependency_kind: Normal 81 | crate_url: https://crates.io/crates/serde 82 | source_type: GitHub 83 | source_url: https://github.com/serde-rs/serde 84 | stats: 85 | stars: null 86 | downloads: null 87 | failed: false 88 | error_message: null 89 | - name: serde_json 90 | description: A JSON serialization file format 91 | dependency_kind: Normal 92 | crate_url: https://crates.io/crates/serde_json 93 | source_type: GitHub 94 | source_url: https://github.com/serde-rs/json 95 | stats: 96 | stars: null 97 | downloads: null 98 | failed: false 99 | error_message: null 100 | - name: serde_yaml 101 | description: YAML data format for Serde 102 | dependency_kind: Normal 103 | crate_url: https://crates.io/crates/serde_yaml 104 | source_type: GitHub 105 | source_url: https://github.com/dtolnay/serde-yaml 106 | stats: 107 | stars: null 108 | downloads: null 109 | failed: false 110 | error_message: null 111 | - name: strsim 112 | description: Implementations of string similarity metrics. Includes Hamming, Levenshtein, OSA, Damerau-Levenshtein, Jaro, Jaro-Winkler, and Sørensen-Dice. 113 | dependency_kind: Normal 114 | crate_url: https://crates.io/crates/strsim 115 | source_type: GitHub 116 | source_url: https://github.com/rapidfuzz/strsim-rs 117 | stats: 118 | stars: null 119 | downloads: null 120 | failed: false 121 | error_message: null 122 | - name: thiserror 123 | description: derive(Error) 124 | dependency_kind: Normal 125 | crate_url: https://crates.io/crates/thiserror 126 | source_type: GitHub 127 | source_url: https://github.com/dtolnay/thiserror 128 | stats: 129 | stars: null 130 | downloads: null 131 | failed: false 132 | error_message: null 133 | - name: tokio 134 | description: An event-driven, non-blocking I/O platform for writing asynchronous I/O backed applications. 135 | dependency_kind: Normal 136 | crate_url: https://crates.io/crates/tokio 137 | source_type: GitHub 138 | source_url: https://github.com/tokio-rs/tokio 139 | stats: 140 | stars: null 141 | downloads: null 142 | failed: false 143 | error_message: null 144 | - name: toml 145 | description: A native Rust encoder and decoder of TOML-formatted files and streams. Provides implementations of the standard Serialize/Deserialize traits for TOML data to facilitate deserializing and serializing Rust structures. 146 | dependency_kind: Normal 147 | crate_url: https://crates.io/crates/toml 148 | source_type: GitHub 149 | source_url: https://github.com/toml-rs/toml 150 | stats: 151 | stars: null 152 | downloads: null 153 | failed: false 154 | error_message: null 155 | - name: tracing 156 | description: Application-level tracing for Rust. 157 | dependency_kind: Normal 158 | crate_url: https://crates.io/crates/tracing 159 | source_type: GitHub 160 | source_url: https://github.com/tokio-rs/tracing 161 | stats: 162 | stars: null 163 | downloads: null 164 | failed: false 165 | error_message: null 166 | - name: tracing-subscriber 167 | description: Utilities for implementing and composing `tracing` subscribers. 168 | dependency_kind: Normal 169 | crate_url: https://crates.io/crates/tracing-subscriber 170 | source_type: GitHub 171 | source_url: https://github.com/tokio-rs/tokio 172 | stats: 173 | stars: null 174 | downloads: null 175 | failed: false 176 | error_message: null 177 | - name: url 178 | description: URL library for Rust, based on the WHATWG URL Standard 179 | dependency_kind: Normal 180 | crate_url: https://crates.io/crates/url 181 | source_type: GitHub 182 | source_url: https://github.com/servo/rust-url 183 | stats: 184 | stars: null 185 | downloads: null 186 | failed: false 187 | error_message: null 188 | - name: assert_fs 189 | description: Filesystem fixtures and assertions for testing. 190 | dependency_kind: Development 191 | crate_url: https://crates.io/crates/assert_fs 192 | source_type: GitHub 193 | source_url: https://github.com/assert-rs/assert_fs.git 194 | stats: 195 | stars: null 196 | downloads: null 197 | failed: false 198 | error_message: null 199 | - name: pretty_assertions 200 | description: Overwrite `assert_eq!` and `assert_ne!` with drop-in replacements, adding colorful diffs. 201 | dependency_kind: Development 202 | crate_url: https://crates.io/crates/pretty_assertions 203 | source_type: GitHub 204 | source_url: https://github.com/rust-pretty-assertions/rust-pretty-assertions 205 | stats: 206 | stars: null 207 | downloads: null 208 | failed: false 209 | error_message: null 210 | - name: tokio-test 211 | description: Testing utilities for Tokio- and futures-based code 212 | dependency_kind: Development 213 | crate_url: https://crates.io/crates/tokio-test 214 | source_type: GitHub 215 | source_url: https://github.com/tokio-rs/tokio 216 | stats: 217 | stars: null 218 | downloads: null 219 | failed: false 220 | error_message: null 221 | -------------------------------------------------------------------------------- /assets/output/THANKU_json_en.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "anyhow", 4 | "description": "Flexible concrete Error type built on std::error::Error", 5 | "dependency_kind": "Normal", 6 | "crate_url": "https://crates.io/crates/anyhow", 7 | "source_type": "GitHub", 8 | "source_url": "https://github.com/dtolnay/anyhow", 9 | "stats": { 10 | "stars": null, 11 | "downloads": null 12 | }, 13 | "failed": false, 14 | "error_message": null 15 | }, 16 | { 17 | "name": "cargo_metadata", 18 | "description": "structured access to the output of `cargo metadata`", 19 | "dependency_kind": "Normal", 20 | "crate_url": "https://crates.io/crates/cargo_metadata", 21 | "source_type": "GitHub", 22 | "source_url": "https://github.com/oli-obk/cargo_metadata", 23 | "stats": { 24 | "stars": null, 25 | "downloads": null 26 | }, 27 | "failed": false, 28 | "error_message": null 29 | }, 30 | { 31 | "name": "clap", 32 | "description": "A simple to use, efficient, and full-featured Command Line Argument Parser", 33 | "dependency_kind": "Normal", 34 | "crate_url": "https://crates.io/crates/clap", 35 | "source_type": "GitHub", 36 | "source_url": "https://github.com/clap-rs/clap", 37 | "stats": { 38 | "stars": null, 39 | "downloads": null 40 | }, 41 | "failed": false, 42 | "error_message": null 43 | }, 44 | { 45 | "name": "clap_complete", 46 | "description": "Generate shell completion scripts for your clap::Command", 47 | "dependency_kind": "Normal", 48 | "crate_url": "https://crates.io/crates/clap_complete", 49 | "source_type": "GitHub", 50 | "source_url": "https://github.com/clap-rs/clap", 51 | "stats": { 52 | "stars": null, 53 | "downloads": null 54 | }, 55 | "failed": false, 56 | "error_message": null 57 | }, 58 | { 59 | "name": "futures", 60 | "description": "An implementation of futures and streams featuring zero allocations, composability, and iterator-like interfaces.", 61 | "dependency_kind": "Normal", 62 | "crate_url": "https://crates.io/crates/futures", 63 | "source_type": "GitHub", 64 | "source_url": "https://github.com/rust-lang/futures-rs", 65 | "stats": { 66 | "stars": null, 67 | "downloads": null 68 | }, 69 | "failed": false, 70 | "error_message": null 71 | }, 72 | { 73 | "name": "reqwest", 74 | "description": "higher level HTTP client library", 75 | "dependency_kind": "Normal", 76 | "crate_url": "https://crates.io/crates/reqwest", 77 | "source_type": "GitHub", 78 | "source_url": "https://github.com/seanmonstar/reqwest", 79 | "stats": { 80 | "stars": null, 81 | "downloads": null 82 | }, 83 | "failed": false, 84 | "error_message": null 85 | }, 86 | { 87 | "name": "rust-i18n", 88 | "description": "Rust I18n is use Rust codegen for load YAML file storage translations on compile time, and give you a t! macro for simply get translation texts.", 89 | "dependency_kind": "Normal", 90 | "crate_url": "https://crates.io/crates/rust-i18n", 91 | "source_type": "GitHub", 92 | "source_url": "https://github.com/longbridge/rust-i18n", 93 | "stats": { 94 | "stars": null, 95 | "downloads": null 96 | }, 97 | "failed": false, 98 | "error_message": null 99 | }, 100 | { 101 | "name": "serde", 102 | "description": "A generic serialization/deserialization framework", 103 | "dependency_kind": "Normal", 104 | "crate_url": "https://crates.io/crates/serde", 105 | "source_type": "GitHub", 106 | "source_url": "https://github.com/serde-rs/serde", 107 | "stats": { 108 | "stars": null, 109 | "downloads": null 110 | }, 111 | "failed": false, 112 | "error_message": null 113 | }, 114 | { 115 | "name": "serde_json", 116 | "description": "A JSON serialization file format", 117 | "dependency_kind": "Normal", 118 | "crate_url": "https://crates.io/crates/serde_json", 119 | "source_type": "GitHub", 120 | "source_url": "https://github.com/serde-rs/json", 121 | "stats": { 122 | "stars": null, 123 | "downloads": null 124 | }, 125 | "failed": false, 126 | "error_message": null 127 | }, 128 | { 129 | "name": "serde_yaml", 130 | "description": "YAML data format for Serde", 131 | "dependency_kind": "Normal", 132 | "crate_url": "https://crates.io/crates/serde_yaml", 133 | "source_type": "GitHub", 134 | "source_url": "https://github.com/dtolnay/serde-yaml", 135 | "stats": { 136 | "stars": null, 137 | "downloads": null 138 | }, 139 | "failed": false, 140 | "error_message": null 141 | }, 142 | { 143 | "name": "strsim", 144 | "description": "Implementations of string similarity metrics. Includes Hamming, Levenshtein, OSA, Damerau-Levenshtein, Jaro, Jaro-Winkler, and Sørensen-Dice.", 145 | "dependency_kind": "Normal", 146 | "crate_url": "https://crates.io/crates/strsim", 147 | "source_type": "GitHub", 148 | "source_url": "https://github.com/rapidfuzz/strsim-rs", 149 | "stats": { 150 | "stars": null, 151 | "downloads": null 152 | }, 153 | "failed": false, 154 | "error_message": null 155 | }, 156 | { 157 | "name": "thiserror", 158 | "description": "derive(Error)", 159 | "dependency_kind": "Normal", 160 | "crate_url": "https://crates.io/crates/thiserror", 161 | "source_type": "GitHub", 162 | "source_url": "https://github.com/dtolnay/thiserror", 163 | "stats": { 164 | "stars": null, 165 | "downloads": null 166 | }, 167 | "failed": false, 168 | "error_message": null 169 | }, 170 | { 171 | "name": "tokio", 172 | "description": "An event-driven, non-blocking I/O platform for writing asynchronous I/O backed applications.", 173 | "dependency_kind": "Normal", 174 | "crate_url": "https://crates.io/crates/tokio", 175 | "source_type": "GitHub", 176 | "source_url": "https://github.com/tokio-rs/tokio", 177 | "stats": { 178 | "stars": null, 179 | "downloads": null 180 | }, 181 | "failed": false, 182 | "error_message": null 183 | }, 184 | { 185 | "name": "toml", 186 | "description": "A native Rust encoder and decoder of TOML-formatted files and streams. Provides implementations of the standard Serialize/Deserialize traits for TOML data to facilitate deserializing and serializing Rust structures.", 187 | "dependency_kind": "Normal", 188 | "crate_url": "https://crates.io/crates/toml", 189 | "source_type": "GitHub", 190 | "source_url": "https://github.com/toml-rs/toml", 191 | "stats": { 192 | "stars": null, 193 | "downloads": null 194 | }, 195 | "failed": false, 196 | "error_message": null 197 | }, 198 | { 199 | "name": "tracing", 200 | "description": "Application-level tracing for Rust.", 201 | "dependency_kind": "Normal", 202 | "crate_url": "https://crates.io/crates/tracing", 203 | "source_type": "GitHub", 204 | "source_url": "https://github.com/tokio-rs/tracing", 205 | "stats": { 206 | "stars": null, 207 | "downloads": null 208 | }, 209 | "failed": false, 210 | "error_message": null 211 | }, 212 | { 213 | "name": "tracing-subscriber", 214 | "description": "Utilities for implementing and composing `tracing` subscribers.", 215 | "dependency_kind": "Normal", 216 | "crate_url": "https://crates.io/crates/tracing-subscriber", 217 | "source_type": "GitHub", 218 | "source_url": "https://github.com/tokio-rs/tokio", 219 | "stats": { 220 | "stars": null, 221 | "downloads": null 222 | }, 223 | "failed": false, 224 | "error_message": null 225 | }, 226 | { 227 | "name": "url", 228 | "description": "URL library for Rust, based on the WHATWG URL Standard", 229 | "dependency_kind": "Normal", 230 | "crate_url": "https://crates.io/crates/url", 231 | "source_type": "GitHub", 232 | "source_url": "https://github.com/servo/rust-url", 233 | "stats": { 234 | "stars": null, 235 | "downloads": null 236 | }, 237 | "failed": false, 238 | "error_message": null 239 | }, 240 | { 241 | "name": "assert_fs", 242 | "description": "Filesystem fixtures and assertions for testing.", 243 | "dependency_kind": "Development", 244 | "crate_url": "https://crates.io/crates/assert_fs", 245 | "source_type": "GitHub", 246 | "source_url": "https://github.com/assert-rs/assert_fs.git", 247 | "stats": { 248 | "stars": null, 249 | "downloads": null 250 | }, 251 | "failed": false, 252 | "error_message": null 253 | }, 254 | { 255 | "name": "pretty_assertions", 256 | "description": "Overwrite `assert_eq!` and `assert_ne!` with drop-in replacements, adding colorful diffs.", 257 | "dependency_kind": "Development", 258 | "crate_url": "https://crates.io/crates/pretty_assertions", 259 | "source_type": "GitHub", 260 | "source_url": "https://github.com/rust-pretty-assertions/rust-pretty-assertions", 261 | "stats": { 262 | "stars": null, 263 | "downloads": null 264 | }, 265 | "failed": false, 266 | "error_message": null 267 | }, 268 | { 269 | "name": "tokio-test", 270 | "description": "Testing utilities for Tokio- and futures-based code", 271 | "dependency_kind": "Development", 272 | "crate_url": "https://crates.io/crates/tokio-test", 273 | "source_type": "GitHub", 274 | "source_url": "https://github.com/tokio-rs/tokio", 275 | "stats": { 276 | "stars": null, 277 | "downloads": null 278 | }, 279 | "failed": false, 280 | "error_message": null 281 | } 282 | ] -------------------------------------------------------------------------------- /assets/output/converted/THANKU_en.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "anyhow", 4 | "description": "Flexible concrete Error type built on std::error::Error", 5 | "dependency_kind": "Normal", 6 | "crate_url": "https://crates.io/crates/anyhow", 7 | "source_type": "GitHub", 8 | "source_url": "https://github.com/dtolnay/anyhow", 9 | "stats": { 10 | "stars": null, 11 | "downloads": null 12 | }, 13 | "failed": false, 14 | "error_message": null 15 | }, 16 | { 17 | "name": "cargo_metadata", 18 | "description": "structured access to the output of `cargo metadata`", 19 | "dependency_kind": "Normal", 20 | "crate_url": "https://crates.io/crates/cargo_metadata", 21 | "source_type": "GitHub", 22 | "source_url": "https://github.com/oli-obk/cargo_metadata", 23 | "stats": { 24 | "stars": null, 25 | "downloads": null 26 | }, 27 | "failed": false, 28 | "error_message": null 29 | }, 30 | { 31 | "name": "clap", 32 | "description": "A simple to use, efficient, and full-featured Command Line Argument Parser", 33 | "dependency_kind": "Normal", 34 | "crate_url": "https://crates.io/crates/clap", 35 | "source_type": "GitHub", 36 | "source_url": "https://github.com/clap-rs/clap", 37 | "stats": { 38 | "stars": null, 39 | "downloads": null 40 | }, 41 | "failed": false, 42 | "error_message": null 43 | }, 44 | { 45 | "name": "clap_complete", 46 | "description": "Generate shell completion scripts for your clap::Command", 47 | "dependency_kind": "Normal", 48 | "crate_url": "https://crates.io/crates/clap_complete", 49 | "source_type": "GitHub", 50 | "source_url": "https://github.com/clap-rs/clap", 51 | "stats": { 52 | "stars": null, 53 | "downloads": null 54 | }, 55 | "failed": false, 56 | "error_message": null 57 | }, 58 | { 59 | "name": "futures", 60 | "description": "An implementation of futures and streams featuring zero allocations, composability, and iterator-like interfaces.", 61 | "dependency_kind": "Normal", 62 | "crate_url": "https://crates.io/crates/futures", 63 | "source_type": "GitHub", 64 | "source_url": "https://github.com/rust-lang/futures-rs", 65 | "stats": { 66 | "stars": null, 67 | "downloads": null 68 | }, 69 | "failed": false, 70 | "error_message": null 71 | }, 72 | { 73 | "name": "reqwest", 74 | "description": "higher level HTTP client library", 75 | "dependency_kind": "Normal", 76 | "crate_url": "https://crates.io/crates/reqwest", 77 | "source_type": "GitHub", 78 | "source_url": "https://github.com/seanmonstar/reqwest", 79 | "stats": { 80 | "stars": null, 81 | "downloads": null 82 | }, 83 | "failed": false, 84 | "error_message": null 85 | }, 86 | { 87 | "name": "rust-i18n", 88 | "description": "Rust I18n is use Rust codegen for load YAML file storage translations on compile time, and give you a t! macro for simply get translation texts.", 89 | "dependency_kind": "Normal", 90 | "crate_url": "https://crates.io/crates/rust-i18n", 91 | "source_type": "GitHub", 92 | "source_url": "https://github.com/longbridge/rust-i18n", 93 | "stats": { 94 | "stars": null, 95 | "downloads": null 96 | }, 97 | "failed": false, 98 | "error_message": null 99 | }, 100 | { 101 | "name": "serde", 102 | "description": "A generic serialization/deserialization framework", 103 | "dependency_kind": "Normal", 104 | "crate_url": "https://crates.io/crates/serde", 105 | "source_type": "GitHub", 106 | "source_url": "https://github.com/serde-rs/serde", 107 | "stats": { 108 | "stars": null, 109 | "downloads": null 110 | }, 111 | "failed": false, 112 | "error_message": null 113 | }, 114 | { 115 | "name": "serde_json", 116 | "description": "A JSON serialization file format", 117 | "dependency_kind": "Normal", 118 | "crate_url": "https://crates.io/crates/serde_json", 119 | "source_type": "GitHub", 120 | "source_url": "https://github.com/serde-rs/json", 121 | "stats": { 122 | "stars": null, 123 | "downloads": null 124 | }, 125 | "failed": false, 126 | "error_message": null 127 | }, 128 | { 129 | "name": "serde_yaml", 130 | "description": "YAML data format for Serde", 131 | "dependency_kind": "Normal", 132 | "crate_url": "https://crates.io/crates/serde_yaml", 133 | "source_type": "GitHub", 134 | "source_url": "https://github.com/dtolnay/serde-yaml", 135 | "stats": { 136 | "stars": null, 137 | "downloads": null 138 | }, 139 | "failed": false, 140 | "error_message": null 141 | }, 142 | { 143 | "name": "strsim", 144 | "description": "Implementations of string similarity metrics. Includes Hamming, Levenshtein, OSA, Damerau-Levenshtein, Jaro, Jaro-Winkler, and Sørensen-Dice.", 145 | "dependency_kind": "Normal", 146 | "crate_url": "https://crates.io/crates/strsim", 147 | "source_type": "GitHub", 148 | "source_url": "https://github.com/rapidfuzz/strsim-rs", 149 | "stats": { 150 | "stars": null, 151 | "downloads": null 152 | }, 153 | "failed": false, 154 | "error_message": null 155 | }, 156 | { 157 | "name": "thiserror", 158 | "description": "derive(Error)", 159 | "dependency_kind": "Normal", 160 | "crate_url": "https://crates.io/crates/thiserror", 161 | "source_type": "GitHub", 162 | "source_url": "https://github.com/dtolnay/thiserror", 163 | "stats": { 164 | "stars": null, 165 | "downloads": null 166 | }, 167 | "failed": false, 168 | "error_message": null 169 | }, 170 | { 171 | "name": "tokio", 172 | "description": "An event-driven, non-blocking I/O platform for writing asynchronous I/O backed applications.", 173 | "dependency_kind": "Normal", 174 | "crate_url": "https://crates.io/crates/tokio", 175 | "source_type": "GitHub", 176 | "source_url": "https://github.com/tokio-rs/tokio", 177 | "stats": { 178 | "stars": null, 179 | "downloads": null 180 | }, 181 | "failed": false, 182 | "error_message": null 183 | }, 184 | { 185 | "name": "toml", 186 | "description": "A native Rust encoder and decoder of TOML-formatted files and streams. Provides implementations of the standard Serialize/Deserialize traits for TOML data to facilitate deserializing and serializing Rust structures.", 187 | "dependency_kind": "Normal", 188 | "crate_url": "https://crates.io/crates/toml", 189 | "source_type": "GitHub", 190 | "source_url": "https://github.com/toml-rs/toml", 191 | "stats": { 192 | "stars": null, 193 | "downloads": null 194 | }, 195 | "failed": false, 196 | "error_message": null 197 | }, 198 | { 199 | "name": "tracing", 200 | "description": "Application-level tracing for Rust.", 201 | "dependency_kind": "Normal", 202 | "crate_url": "https://crates.io/crates/tracing", 203 | "source_type": "GitHub", 204 | "source_url": "https://github.com/tokio-rs/tracing", 205 | "stats": { 206 | "stars": null, 207 | "downloads": null 208 | }, 209 | "failed": false, 210 | "error_message": null 211 | }, 212 | { 213 | "name": "tracing-subscriber", 214 | "description": "Utilities for implementing and composing `tracing` subscribers.", 215 | "dependency_kind": "Normal", 216 | "crate_url": "https://crates.io/crates/tracing-subscriber", 217 | "source_type": "GitHub", 218 | "source_url": "https://github.com/tokio-rs/tokio", 219 | "stats": { 220 | "stars": null, 221 | "downloads": null 222 | }, 223 | "failed": false, 224 | "error_message": null 225 | }, 226 | { 227 | "name": "url", 228 | "description": "URL library for Rust, based on the WHATWG URL Standard", 229 | "dependency_kind": "Normal", 230 | "crate_url": "https://crates.io/crates/url", 231 | "source_type": "GitHub", 232 | "source_url": "https://github.com/servo/rust-url", 233 | "stats": { 234 | "stars": null, 235 | "downloads": null 236 | }, 237 | "failed": false, 238 | "error_message": null 239 | }, 240 | { 241 | "name": "assert_fs", 242 | "description": "Filesystem fixtures and assertions for testing.", 243 | "dependency_kind": "Development", 244 | "crate_url": "https://crates.io/crates/assert_fs", 245 | "source_type": "GitHub", 246 | "source_url": "https://github.com/assert-rs/assert_fs.git", 247 | "stats": { 248 | "stars": null, 249 | "downloads": null 250 | }, 251 | "failed": false, 252 | "error_message": null 253 | }, 254 | { 255 | "name": "pretty_assertions", 256 | "description": "Overwrite `assert_eq!` and `assert_ne!` with drop-in replacements, adding colorful diffs.", 257 | "dependency_kind": "Development", 258 | "crate_url": "https://crates.io/crates/pretty_assertions", 259 | "source_type": "GitHub", 260 | "source_url": "https://github.com/rust-pretty-assertions/rust-pretty-assertions", 261 | "stats": { 262 | "stars": null, 263 | "downloads": null 264 | }, 265 | "failed": false, 266 | "error_message": null 267 | }, 268 | { 269 | "name": "tokio-test", 270 | "description": "Testing utilities for Tokio- and futures-based code", 271 | "dependency_kind": "Development", 272 | "crate_url": "https://crates.io/crates/tokio-test", 273 | "source_type": "GitHub", 274 | "source_url": "https://github.com/tokio-rs/tokio", 275 | "stats": { 276 | "stars": null, 277 | "downloads": null 278 | }, 279 | "failed": false, 280 | "error_message": null 281 | } 282 | ] -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # Cargo Thanku 2 | 3 | [English README.md](./README.md) 4 | 5 | 一个用于生成 Rust 项目依赖致谢的命令行工具。 6 | 7 | ## 主要特性 8 | 9 | - 支持多种输出格式(Markdown 表格/列表、JSON、TOML、YAML、CSV) 10 | - 自动从 crates.io 和 GitHub 获取依赖信息 11 | - 支持可配置的并发处理 12 | - 实现请求失败重试机制 13 | - 提供命令行自动补全(支持 Bash、Zsh、Fish、PowerShell 和 Elvish) 14 | - 支持多语言(中文/英文/日文/韩文/西班牙文/法文/德文/意大利文) 15 | 16 | ## 架构速览 17 | 18 | - `src/app` 负责 CLI 启动、子命令(`convert`、`completions`、`test`)路由以及基于 `FuturesUnordered + Semaphore` 的依赖抓取流水线,可在保持并发的同时限制外部请求。 19 | - `src/output` 拆分为多个子模块:`dependency.rs` 提供领域模型与通用解析;`output/markdown/tokenizer.rs` 为 Markdown 列表提供分词器,`output/format/*` 承载各格式 formatter,`manager.rs` 统一写出逻辑。 20 | - `src/travert.rs` 的 converter 使用 `BufReader` 流式解析,再配合 `BufWriter` 按目标逐个写出文件,避免占用多倍内存,对大文件更友好。 21 | - `tests/` 目录按照模块拆分(如 `config_tests.rs`、`markdown_format_tests.rs`),所有测试都通过公开 API 覆盖核心路径,涉及外网的测试默认 `#[ignore]`,可在需要时单独运行。 22 | 23 | 了解 `app/pipeline.rs`(并发抓取)和 `output/`(格式化/转换)即可快速扩展新的数据源或输出格式。 24 | 25 | ## 安装 26 | 27 | 确保系统已安装 Rust 工具链,然后执行: 28 | 29 | ```bash 30 | # 安装 cargo-thanku 31 | cargo install cargo-thanku 32 | 33 | # 生成 shell 补全脚本(可选) 34 | cargo thanku completions bash > ~/.local/share/bash-completion/completions/cargo-thanku 35 | ``` 36 | 37 | ## 使用方法 38 | 39 | ### 基本用法 40 | 41 | ```bash 42 | # 为你的项目生成致谢文档 43 | cargo thanku 44 | 45 | # 指定输出格式 46 | cargo thanku -f markdown-table # 可选:mt(markdown-table), ml(markdown-list), json, csv, yaml, toml 47 | 48 | # 设置 GitHub 令牌以获取更多信息并自动点赞 49 | cargo thanku -t YOUR_GITHUB_TOKEN 50 | 51 | # 切换语言 52 | cargo thanku -l zh # 支持 zh/en/ja/ko/es/fr/de/it 53 | ``` 54 | 55 | ### 高级选项 56 | 57 | ```bash 58 | # 配置并发请求数 59 | cargo thanku -j 10 # 设置最大并发请求数为 10 60 | 61 | # 调整重试次数 62 | cargo thanku -r 5 # 设置最大重试次数为 5 63 | 64 | # 自定义输出文件 65 | cargo thanku -o custom_thanks.md 66 | 67 | # 启用详细日志 68 | cargo thanku -v 69 | 70 | # 过滤掉相对路径导入的 libs 71 | cargo thanku --no-relative-libs 72 | ``` 73 | 74 | ### 格式转换 75 | 76 | 在不同的输出格式之间进行转换: 77 | 78 | ```bash 79 | # 不支持 cargo thanku convert 模式语法调用 80 | # 将单个文件转换为多种格式 81 | cargo-thanku convert input.md -o markdown-table,json,yaml,toml 82 | 83 | # 简短的命令别名 84 | # Short command aliases 85 | cargo-thanku cvt input.csv -o mt,yaml 86 | cargo-thanku conv input.md -o json 87 | cargo-thanku convt input.yaml -o markdown-list 88 | ``` 89 | 90 | 转换器将: 91 | - 在与输入文件相同的目录下创建一个 `converted` 目录 92 | - 生成带有适当扩展名的输出文件 93 | - 支持所有受支持格式之间的转换 (markdown-table, markdown-list, json, yaml, csv, toml) 94 | 95 | #### 命令行参数 96 | 97 | | 参数             | 描述                                       | 默认值    | 98 | |--------------------|--------------------------------------------|-----------| 99 | | `-i, --input`     | 输入 Cargo.toml 文件路径                   | -         | 100 | | `-o, --outputs`   | 输出文件格式                               | -         | 101 | | `-l, --language` | 语言 (zh/en/ja/ko/es/fr/de/it)             | `zh`      | 102 | | `-v, --verbose`   | 启用详细日志记录                           | `false`   | 103 | 104 | ### 命令行补全 105 | 106 | 为不同的 shell 生成命令行补全脚本: 107 | 108 | ```bash 109 | # Bash 110 | cargo thanku completions bash > ~/.local/share/bash-completion/completions/cargo-thanku 111 | 112 | # Zsh 113 | cargo thanku completions zsh > ~/.zsh/_cargo-thanku 114 | 115 | # Fish 116 | cargo thanku completions fish > ~/.config/fish/completions/cargo-thanku.fish 117 | 118 | # PowerShell 119 | mkdir -p $PROFILE\..\Completions 120 | cargo thanku completions powershell > $PROFILE\..\Completions\cargo-thanku.ps1 121 | 122 | # Elvish 123 | cargo thanku completions elvish > ~/.elvish/lib/cargo-thanku.elv 124 | ``` 125 | 126 | ## 测试 127 | 128 | ```bash 129 | cargo test 130 | ``` 131 | 132 | 默认会跳过依赖外网的测试,可通过 `cargo test -- --ignored` 单独运行。 133 | 134 | ## 命令行参数 135 | 136 | | 参数 | 描述 | 默认值 | 137 | |---------------------|----------------------------------------------------|-----------------| 138 | | `-i, --input` | 输入的 Cargo.toml 文件路径 | `Cargo.toml` | 139 | | `-o, --output` | 输出文件路径 | `thanks.md` | 140 | | `-f, --format` | 输出格式 | `markdown-table`| 141 | | `-t, --token` | GitHub API 令牌 | - | 142 | | `-l, --language` | 语言 (zh/en/ja/ko/es/fr/de/it) | `zh` | 143 | | `-v, --verbose` | 启用详细日志 | `false` | 144 | | `-j, --concurrent` | 最大并发请求数 | `5` | 145 | | `-r, --retries` | 最大重试次数 | `3` | 146 | | `--no-relative-libs`| 过滤掉相对路径导入的库 | `false` | 147 | 148 | ## 输出格式 149 | 150 | ### Markdown 表格 151 | 152 | ```markdown 153 | | 名称 | 描述 | 来源 | 统计 | 状态 | 154 | |------|------|------|------|------| 155 | | [serde](https://crates.io/crates/serde) | 序列化框架 | [GitHub](https://github.com/serde-rs/serde) | 🌟 3.5k | ✅ | 156 | ``` 157 | 158 | ### Markdown 列表 159 | 160 | ```markdown 161 | # 依赖项 162 | 163 | - [serde](https://crates.io/crates/serde) [序列化框架](https://github.com/serde-rs/serde) (🌟 3.5k) ✅ 164 | ``` 165 | 166 | ### MARKDOWN/CSV/JSON/TOML/YAML 167 | 同时支持结构化输出格式,方便程序化使用。 168 | 169 | ## 重要说明 170 | 171 | 1. 设置 GitHub 令牌(通过 `-t` 或环境变量 `GITHUB_TOKEN`)可以: 172 | - 获取更多仓库信息 173 | - 自动获取依赖仓库 stars 174 | - 提高 API 访问限制 175 | 176 | 2. 依赖处理失败时: 177 | - 不会中断整体处理过程 178 | - 在输出中会标记为 ❌ 179 | - 显示错误信息以便调试 180 | 181 | 3. 语言代码支持: 182 | - 支持灵活的格式(如 "zh"、"zh_CN"、"zh_CN.UTF-8") 183 | - 自动提取主要语言代码 184 | - 对于拼写错误会提供相似代码建议 185 | 186 | ## 致谢 187 | 188 | 本项目本身也使用了许多优秀的 Rust crate。以下是一些主要依赖: 189 | 190 | > [!TIP] 191 | > 由 `cargo-thanku` 工具生成 192 | 193 | | 名称 | 描述 | Crates.io | 来源 | 统计 | 状态 | 194 | |------|--------|--------|-------|-------|--------| 195 | |🔍|Normal| | | | | 196 | | anyhow | Flexible concrete Error type built on std::error::Error | [anyhow](https://crates.io/crates/anyhow) | [GitHub](https://github.com/dtolnay/anyhow) | ❓ | ✅ | 197 | | cargo_metadata | structured access to the output of `cargo metadata` | [cargo_metadata](https://crates.io/crates/cargo_metadata) | [GitHub](https://github.com/oli-obk/cargo_metadata) | ❓ | ✅ | 198 | | clap | A simple to use, efficient, and full-featured Command Line Argument Parser | [clap](https://crates.io/crates/clap) | [GitHub](https://github.com/clap-rs/clap) | ❓ | ✅ | 199 | | clap_complete | Generate shell completion scripts for your clap::Command | [clap_complete](https://crates.io/crates/clap_complete) | [GitHub](https://github.com/clap-rs/clap) | ❓ | ✅ | 200 | | futures | An implementation of futures and streams featuring zero allocations, composability, and iterator-like interfaces. | [futures](https://crates.io/crates/futures) | [GitHub](https://github.com/rust-lang/futures-rs) | ❓ | ✅ | 201 | | reqwest | higher level HTTP client library | [reqwest](https://crates.io/crates/reqwest) | [GitHub](https://github.com/seanmonstar/reqwest) | ❓ | ✅ | 202 | | rust-i18n | Rust I18n is use Rust codegen for load YAML file storage translations on compile time, and give you a t! macro for simply get translation texts. | [rust-i18n](https://crates.io/crates/rust-i18n) | [GitHub](https://github.com/longbridge/rust-i18n) | ❓ | ✅ | 203 | | serde | A generic serialization/deserialization framework | [serde](https://crates.io/crates/serde) | [GitHub](https://github.com/serde-rs/serde) | ❓ | ✅ | 204 | | serde_json | A JSON serialization file format | [serde_json](https://crates.io/crates/serde_json) | [GitHub](https://github.com/serde-rs/json) | ❓ | ✅ | 205 | | serde_yaml | YAML data format for Serde | [serde_yaml](https://crates.io/crates/serde_yaml) | [GitHub](https://github.com/dtolnay/serde-yaml) | ❓ | ✅ | 206 | | strsim | Implementations of string similarity metrics. Includes Hamming, Levenshtein, OSA, Damerau-Levenshtein, Jaro, Jaro-Winkler, and Sørensen-Dice. | [strsim](https://crates.io/crates/strsim) | [GitHub](https://github.com/rapidfuzz/strsim-rs) | ❓ | ✅ | 207 | | thiserror | derive(Error) | [thiserror](https://crates.io/crates/thiserror) | [GitHub](https://github.com/dtolnay/thiserror) | ❓ | ✅ | 208 | | tokio | An event-driven, non-blocking I/O platform for writing asynchronous I/O backed applications. | [tokio](https://crates.io/crates/tokio) | [GitHub](https://github.com/tokio-rs/tokio) | ❓ | ✅ | 209 | | toml | A native Rust encoder and decoder of TOML-formatted files and streams. Provides implementations of the standard Serialize/Deserialize traits for TOML data to facilitate deserializing and serializing Rust structures. | [toml](https://crates.io/crates/toml) | [GitHub](https://github.com/toml-rs/toml) | ❓ | ✅ | 210 | | tracing | Application-level tracing for Rust. | [tracing](https://crates.io/crates/tracing) | [GitHub](https://github.com/tokio-rs/tracing) | ❓ | ✅ | 211 | | tracing-subscriber | Utilities for implementing and composing `tracing` subscribers. | [tracing-subscriber](https://crates.io/crates/tracing-subscriber) | [GitHub](https://github.com/tokio-rs/tracing) | ❓ | ✅ | 212 | | url | URL library for Rust, based on the WHATWG URL Standard | [url](https://crates.io/crates/url) | [GitHub](https://github.com/servo/rust-url) | ❓ | ✅ | 213 | |🔧|Development| | | | | 214 | | assert_fs | Filesystem fixtures and assertions for testing. | [assert_fs](https://crates.io/crates/assert_fs) | [GitHub](https://github.com/assert-rs/assert_fs.git) | ❓ | ✅ | 215 | | pretty_assertions | Overwrite `assert_eq!` and `assert_ne!` with drop-in replacements, adding colorful diffs. | [pretty_assertions](https://crates.io/crates/pretty_assertions) | [GitHub](https://github.com/rust-pretty-assertions/rust-pretty-assertions) | ❓ | ✅ | 216 | | tokio-test | Testing utilities for Tokio- and futures-based code | [tokio-test](https://crates.io/crates/tokio-test) | [GitHub](https://github.com/tokio-rs/tokio) | ❓ | ✅ | 217 | 218 | 219 | 要查看完整的依赖列表和致谢,请运行: 220 | ```bash 221 | cargo thanku 222 | ``` 223 | 224 | ## 许可证 225 | 226 | 本项目采用 MIT 许可证 - 详见 [LICENSE](./LICENSE.md) 文件。 227 | -------------------------------------------------------------------------------- /src/app/pipeline.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, sync::Arc, time::Duration}; 2 | 3 | use anyhow::Result; 4 | use cargo_metadata::MetadataCommand; 5 | use futures::stream::{FuturesUnordered, StreamExt}; 6 | use tokio::sync::Semaphore; 7 | use tracing::{debug, info, instrument}; 8 | use url::Url; 9 | 10 | use crate::{ 11 | config::Config, 12 | errors::AppError, 13 | output::{DependencyInfo, DependencyKind, DependencyStats, OutputFormat, OutputManager}, 14 | sources::{CratesioClient, GitHubClient}, 15 | }; 16 | 17 | #[instrument(skip_all)] 18 | pub async fn process_dependencies() -> Result<()> { 19 | let config = Config::global()?; 20 | 21 | let mut deps = get_dependencies(&config.get_cargo_toml_path()?)?; 22 | debug!("{}", t!("main.found_dependencies", count = deps.len())); 23 | 24 | if config.no_relative_libs { 25 | debug!("{}", t!("main.filtering_relative_libs")); 26 | deps.retain(|_, dep| dep.path.is_none()); 27 | } 28 | 29 | let crates_io_client = Arc::new(CratesioClient::new()); 30 | let github_client = match &config.github_token { 31 | Some(token) => Some(Arc::new(GitHubClient::new(token)?)), 32 | None => None, 33 | }; 34 | 35 | let semaphore = Arc::new(Semaphore::new(config.max_concurrent_requests)); 36 | 37 | let mut tasks = FuturesUnordered::new(); 38 | for (name, dep) in deps { 39 | let dependency_name = name.clone(); 40 | let dependency_kind = dep.kind.into(); 41 | let crates_io = Arc::clone(&crates_io_client); 42 | let github = github_client.as_ref().map(Arc::clone); 43 | let semaphore = Arc::clone(&semaphore); 44 | let max_retries = config.max_retries; 45 | 46 | tasks.push(async move { 47 | let _permit = semaphore.acquire_owned().await.unwrap(); 48 | process_with_retries( 49 | dependency_name, 50 | dependency_kind, 51 | crates_io, 52 | github, 53 | max_retries, 54 | ) 55 | .await 56 | }); 57 | } 58 | 59 | let mut collected = Vec::new(); 60 | while let Some(dep) = tasks.next().await { 61 | collected.push(dep); 62 | } 63 | 64 | generate_output(&collected, config.format)?; 65 | Ok(()) 66 | } 67 | 68 | #[instrument(skip_all)] 69 | async fn process_with_retries( 70 | name: String, 71 | dep_kind: DependencyKind, 72 | crates_io_client: Arc, 73 | github_client: Option>, 74 | max_retries: u32, 75 | ) -> DependencyInfo { 76 | let mut last_error = None; 77 | 78 | for retry in 0..=max_retries { 79 | match process_dependency(&name, dep_kind, &crates_io_client, github_client.as_deref()).await 80 | { 81 | Ok(info) => { 82 | if retry > 0 { 83 | debug!( 84 | "{}", 85 | t!("main.retry_succeeded", name = name, attempt = retry + 1) 86 | ); 87 | } 88 | return info; 89 | } 90 | Err(err) => { 91 | last_error = Some(err); 92 | if retry < max_retries { 93 | let delay = Duration::from_secs(2u64.pow(retry)); 94 | debug!( 95 | "{}", 96 | t!( 97 | "main.retry_attempt", 98 | name = name, 99 | attempt = retry + 1, 100 | max_retries = max_retries, 101 | delay = delay.as_secs() 102 | ) 103 | ); 104 | tokio::time::sleep(delay).await; 105 | } 106 | } 107 | } 108 | } 109 | 110 | let error_msg = last_error.map(|err| err.to_string()).unwrap_or_else(|| { 111 | t!("main.max_retries_exceeded", name = &name, error = "unknown").to_string() 112 | }); 113 | debug!( 114 | "{}", 115 | t!( 116 | "main.max_retries_exceeded", 117 | name = &name, 118 | error = &error_msg 119 | ) 120 | ); 121 | DependencyInfo::failure(&name, dep_kind, error_msg) 122 | } 123 | 124 | #[instrument(skip_all)] 125 | async fn process_dependency( 126 | name: &str, 127 | dep_kind: DependencyKind, 128 | crates_io_client: &CratesioClient, 129 | github_client: Option<&GitHubClient>, 130 | ) -> Result { 131 | let crate_info = crates_io_client 132 | .get_crate_info(name) 133 | .await 134 | .map_err(|err| AppError::Unknown(err.to_string()))?; 135 | 136 | let (source_type, source_url, stats) = if let Some(repo) = crate_info.repository.as_ref() { 137 | if let Ok(url) = Url::parse(repo) { 138 | if url.host_str() == Some("github.com") { 139 | let path_segments: Vec<&str> = url 140 | .path_segments() 141 | .map(|segments| segments.collect()) 142 | .unwrap_or_default(); 143 | 144 | if path_segments.len() >= 2 { 145 | let owner = path_segments[0]; 146 | let repo = path_segments[1]; 147 | 148 | if let Some(client) = github_client { 149 | match client.get_repository_info(owner, repo).await { 150 | Ok(repo_info) => { 151 | let _ = client.star_repository(owner, repo).await; 152 | info!("💖 {} {}", name, repo_info.html_url); 153 | 154 | ( 155 | "GitHub".to_string(), 156 | Some(url.to_string()), 157 | DependencyStats { 158 | stars: Some(repo_info.stargazers_count), 159 | downloads: None, 160 | }, 161 | ) 162 | } 163 | Err(e) => { 164 | debug!("{}", t!("main.github_api_error", error = e.to_string())); 165 | ( 166 | "GitHub".to_string(), 167 | Some(url.to_string()), 168 | DependencyStats { 169 | stars: None, 170 | downloads: None, 171 | }, 172 | ) 173 | } 174 | } 175 | } else { 176 | ( 177 | "GitHub".to_string(), 178 | Some(url.to_string()), 179 | DependencyStats { 180 | stars: None, 181 | downloads: None, 182 | }, 183 | ) 184 | } 185 | } else { 186 | ( 187 | "Source".to_string(), 188 | Some(url.to_string()), 189 | DependencyStats { 190 | stars: None, 191 | downloads: None, 192 | }, 193 | ) 194 | } 195 | } else { 196 | ( 197 | "Source".to_string(), 198 | Some(url.to_string()), 199 | DependencyStats { 200 | stars: None, 201 | downloads: None, 202 | }, 203 | ) 204 | } 205 | } else { 206 | debug!("{}", t!("main.invalid_repo_url", url = repo)); 207 | ( 208 | "crates.io".to_string(), 209 | Some(format!("https://crates.io/crates/{}", name)), 210 | DependencyStats { 211 | stars: None, 212 | downloads: Some(crate_info.downloads), 213 | }, 214 | ) 215 | } 216 | } else { 217 | ( 218 | "crates.io".to_string(), 219 | Some(format!("https://crates.io/crates/{}", name)), 220 | DependencyStats { 221 | stars: None, 222 | downloads: Some(crate_info.downloads), 223 | }, 224 | ) 225 | }; 226 | 227 | Ok(DependencyInfo { 228 | name: name.to_string(), 229 | dependency_kind: dep_kind, 230 | description: crate_info.description, 231 | crate_url: Some(CratesioClient::get_crate_url(name)), 232 | source_type, 233 | source_url, 234 | stats, 235 | failed: false, 236 | error_message: None, 237 | }) 238 | } 239 | 240 | #[instrument(skip_all)] 241 | fn get_dependencies

(cargo_toml_path: P) -> Result> 242 | where 243 | P: AsRef, 244 | { 245 | let metadata = MetadataCommand::new() 246 | .manifest_path(cargo_toml_path.as_ref()) 247 | .no_deps() 248 | .exec() 249 | .map_err(AppError::MetadataError)?; 250 | 251 | let mut deps = HashMap::new(); 252 | for pkg in &metadata.packages { 253 | for dep in &pkg.dependencies { 254 | deps.entry(dep.name.clone()).or_insert_with(|| dep.clone()); 255 | } 256 | } 257 | 258 | debug!("{}", t!("main.found_dependencies", count = deps.len())); 259 | Ok(deps) 260 | } 261 | 262 | #[instrument(skip(deps))] 263 | fn generate_output(deps: &[DependencyInfo], format: OutputFormat) -> Result<()> { 264 | let config = Config::global()?; 265 | let output = config.get_output_writer()?; 266 | let mut manager = OutputManager::new(format, output); 267 | manager.write(deps) 268 | } 269 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use clap::{Arg, ArgAction, ArgGroup, Command}; 2 | use clap_complete::Shell; 3 | use rust_i18n::t; 4 | use std::path::PathBuf; 5 | use std::str::FromStr; 6 | use tracing::instrument; 7 | 8 | use crate::output::OutputFormat; 9 | 10 | // Arg::new("language") 11 | // .short('l') 12 | // .long("language") 13 | // .help(format!("{}", t!("cli.language_help"))) 14 | // .global(true) 15 | // .env("LANG") 16 | // .value_parser(|s: &str| { 17 | // let tag = LanguageTag::parse(s) 18 | // .map_err(|_| format!("Invalid language tag: {}", s))?; 19 | // let lang = tag.primary_language(); 20 | // match lang { 21 | // "zh" | "en" | "ja" | "ko" | "es" | "fr" | "de" | "it" => Ok(lang.to_string()), 22 | // _ => Err(format!("Unsupported language: {}", lang)) 23 | // } 24 | // }) 25 | // .default_value("zh"), 26 | 27 | /// 定义语言解析器 28 | #[allow(unused)] 29 | #[derive(Clone, Debug)] 30 | struct LanguageParser; 31 | 32 | impl clap::builder::TypedValueParser for LanguageParser { 33 | type Value = String; 34 | 35 | #[instrument] 36 | fn parse_ref( 37 | &self, 38 | cmd: &Command, 39 | arg: Option<&Arg>, 40 | value: &std::ffi::OsStr, 41 | ) -> Result { 42 | let input = value.to_string_lossy().to_lowercase(); 43 | 44 | // 解析语言代码 45 | let lang_code = if input.contains('_') || input.contains('.') { 46 | // 处理形如 "en_US.UTF-8" 的格式 47 | input 48 | .split(['_', '.', '-']) 49 | .next() 50 | .unwrap_or("zh") 51 | .to_string() 52 | } else { 53 | input 54 | }; 55 | 56 | // 验证是否是支持的语言 57 | match lang_code.as_str() { 58 | "zh" | "en" | "ja" | "ko" | "es" | "fr" | "de" | "it" => Ok(lang_code), 59 | _ => { 60 | // 尝试找到最相似的语言代码 61 | let supported = ["zh", "en", "ja", "ko", "es", "fr", "de", "it"]; 62 | if let Some(similar) = supported 63 | .iter() 64 | .min_by_key(|&x| strsim::levenshtein(x, &lang_code)) 65 | { 66 | Err(clap::Error::raw( 67 | clap::error::ErrorKind::InvalidValue, 68 | format!( 69 | "Invalid language '{}'. Did you mean '{}'?", 70 | lang_code, similar 71 | ), 72 | )) 73 | } else { 74 | Err(clap::Error::raw( 75 | clap::error::ErrorKind::InvalidValue, 76 | format!("Unsupported language: {}", lang_code), 77 | )) 78 | } 79 | } 80 | } 81 | } 82 | } 83 | 84 | /// 定义输出格式解析器 85 | #[allow(unused)] 86 | #[derive(Clone, Debug)] 87 | struct OutputFormatParser; 88 | 89 | impl clap::builder::TypedValueParser for OutputFormatParser { 90 | type Value = OutputFormat; 91 | 92 | #[instrument] 93 | fn parse_ref( 94 | &self, 95 | cmd: &Command, 96 | arg: Option<&Arg>, 97 | value: &std::ffi::OsStr, 98 | ) -> Result { 99 | let input = value.to_string_lossy().to_lowercase(); 100 | OutputFormat::from_str(&input).map_err(|_| { 101 | clap::Error::raw( 102 | clap::error::ErrorKind::InvalidValue, 103 | format!("{}", t!("cli.invalid_output_format", format = input)), 104 | ) 105 | }) 106 | } 107 | } 108 | 109 | fn build_global_args() -> [Arg; 2] { 110 | [ 111 | Arg::new("verbose") 112 | .short('v') 113 | .long("verbose") 114 | .help(format!("{}", t!("cli.verbose_help"))) 115 | .global(true) 116 | .display_order(99) 117 | .env("VERBOSE") 118 | .default_value("false") 119 | .action(ArgAction::SetTrue), 120 | Arg::new("language") 121 | .short('l') 122 | .long("language") 123 | .help(format!("{}", t!("cli.language_help"))) 124 | .global(true) 125 | .display_order(98) 126 | // .env("LANG") 127 | .value_parser(["zh", "en", "ja", "ko", "es", "fr", "de", "it"]) 128 | // .value_parser(LanguageParser) // Assuming LanguageParser is handled later or is simple 129 | .default_value("zh"), 130 | ] 131 | } 132 | 133 | fn build_thanku_args() -> [Arg; 8] { 134 | [ 135 | Arg::new("input") 136 | .short('i') 137 | .long("input") 138 | .aliases(["in"]) 139 | .help(format!("{}", t!("cli.input_help"))) 140 | .display_order(0) 141 | // .global(true) 142 | .group("thanku") 143 | .value_hint(clap::ValueHint::FilePath) 144 | .value_parser(clap::value_parser!(PathBuf)) 145 | .default_value("Cargo.toml"), 146 | Arg::new("output") 147 | .short('o') 148 | .long("output") 149 | .aliases(["out"]) 150 | .help(format!("{}", t!("cli.output_help"))) 151 | .display_order(1) 152 | // .global(true) 153 | .group("thanku") 154 | .value_hint(clap::ValueHint::FilePath) 155 | .value_parser(clap::value_parser!(PathBuf)) 156 | .default_value("thanks.md"), 157 | Arg::new("format") 158 | .short('f') 159 | .long("format") 160 | .aliases(["fmt", "type"]) 161 | .help(format!("{}", t!("cli.format_help"))) 162 | .display_order(2) 163 | // .global(true) 164 | .group("thanku") 165 | // .value_parser(OutputFormatParser) 166 | .value_parser([ 167 | "mt", 168 | "ml", 169 | "csv", 170 | "json", 171 | "yaml", 172 | "toml", 173 | "yml", 174 | "markdown-list", 175 | "markdown-table", 176 | ]) 177 | .default_value("markdown-table"), 178 | Arg::new("source") 179 | .short('s') 180 | .long("source") 181 | .aliases(["src"]) 182 | .help(format!("{}", t!("cli.source_help"))) 183 | .display_order(3) 184 | // .global(true) 185 | .group("thanku") 186 | .value_parser(["github", "crates-io", "link-empty", "other"]) 187 | .default_value("github"), 188 | Arg::new("token") 189 | .short('t') 190 | .long("token") 191 | // .global(true) 192 | .group("thanku") 193 | .env("GITHUB_TOKEN") 194 | .help(format!("{}", t!("cli.token_help"))) 195 | .display_order(4) 196 | .action(ArgAction::Set), 197 | Arg::new("no-relative-libs") 198 | .long("no-relative-libs") 199 | .aliases(["no-rel-libs", "no-rel"]) 200 | // .global(true) 201 | .group("thanku") 202 | .help(format!("{}", t!("cli.no_relative_libs_help"))) 203 | .display_order(5) 204 | .action(ArgAction::SetTrue), 205 | Arg::new("concurrent") 206 | .short('j') 207 | .long("concurrent") 208 | .aliases(["con", "conc"]) 209 | .help(format!("{}", t!("cli.concurrent_help"))) 210 | .display_order(6) 211 | // .global(true) 212 | .group("thanku") 213 | .value_parser(clap::value_parser!(usize)) 214 | .default_value("5"), 215 | Arg::new("retries") 216 | .short('r') 217 | .long("retries") 218 | .aliases(["retry"]) 219 | .help(format!("{}", t!("cli.retries_help"))) 220 | .display_order(7) 221 | // .global(true) 222 | .group("thanku") 223 | .value_parser(clap::value_parser!(u32)) 224 | .default_value("3"), 225 | ] 226 | } 227 | 228 | fn build_convert_args() -> [Arg; 2] { 229 | [ 230 | Arg::new("input") 231 | .short('i') 232 | .long("input") 233 | .aliases(["in"]) 234 | .group("convert") 235 | .help(format!("{}", t!("cli.convert_input_help"))) 236 | .display_order(0) 237 | .value_hint(clap::ValueHint::FilePath) 238 | .value_parser(clap::value_parser!(PathBuf)), 239 | Arg::new("outputs") // 支持多选,使用逗号分隔 240 | .short('o') 241 | .aliases(["out", "output"]) 242 | .long("outputs") 243 | .group("convert") 244 | .help(format!("{}", t!("cli.convert_outputs_help"))) 245 | .value_delimiter(',') 246 | .display_order(1) 247 | // .value_parser(OutputFormatParser) 248 | .value_parser([ 249 | "mt", 250 | "ml", 251 | "csv", 252 | "json", 253 | "yaml", 254 | "toml", 255 | "yml", 256 | "markdown-list", 257 | "markdown-table", 258 | ]), 259 | ] 260 | } 261 | 262 | pub fn build_cli() -> Command { 263 | let global_args = build_global_args(); 264 | let thanku_args = build_thanku_args(); 265 | let convert_args = build_convert_args(); 266 | 267 | let thanku_args_ids = thanku_args 268 | .iter() 269 | .map(|arg| arg.get_id()) 270 | .collect::>(); 271 | let convert_args_ids = convert_args 272 | .iter() 273 | .map(|arg| arg.get_id()) 274 | .collect::>(); 275 | 276 | let thanku_group = ArgGroup::new("thanku").args(thanku_args_ids).multiple(true); 277 | let convert_group = ArgGroup::new("convert") 278 | .args(convert_args_ids) 279 | .multiple(true) 280 | .required(true); 281 | 282 | let mut cmd = Command::new("cargo-thanku") // Use "cargo-thanku" as the command name for `cargo thanku` 283 | .bin_name("cargo-thanku") // This tells cargo how to invoke it 284 | .aliases(["thx", "thxu"]) 285 | .groups([&thanku_group]) 286 | .version(env!("CARGO_PKG_VERSION")) 287 | .about(format!("{}", t!("cli.about"))) 288 | .args(&global_args) 289 | .args(&thanku_args) 290 | .subcommands([ 291 | Command::new("thanku") 292 | .aliases(["thx", "thxu"]) 293 | .group(&thanku_group) 294 | .about(format!("{}", t!("cli.thanku_about"))) 295 | .hide(true) 296 | .args(&thanku_args), 297 | Command::new("convert") 298 | .group(&convert_group) 299 | .aliases(["cvt", "conv", "convt"]) 300 | .about(format!("{}", t!("cli.convert_help"))) 301 | .args(&convert_args), 302 | Command::new("completions") 303 | .aliases(["comp", "completion"]) 304 | .about(format!("{}", t!("cli.completions_about"))) 305 | .arg( 306 | Arg::new("shell") 307 | .help(format!("{}", t!("cli.completions_args.shell_help"))) 308 | .required(true) 309 | .value_parser(["bash", "fish", "zsh", "powershell", "elvish"]), 310 | ), 311 | ]); 312 | 313 | #[cfg(debug_assertions)] 314 | { 315 | cmd = cmd.subcommand( 316 | Command::new("test") 317 | .about("test") 318 | .args(&global_args) 319 | // .args(&thanku_args) 320 | .args(&convert_args), // .group(&convert_group) 321 | // .group(&thanku_group), 322 | ); 323 | } 324 | 325 | cmd 326 | } 327 | 328 | #[instrument] 329 | pub fn generate_completions(shell: &str) -> anyhow::Result<()> { 330 | let shell = Shell::from_str(shell) 331 | .map_err(|_| anyhow::anyhow!(t!("cli.invalid_shell_type", shell = shell)))?; 332 | let mut cmd = build_cli(); 333 | clap_complete::generate(shell, &mut cmd, "cargo-thanku", &mut std::io::stdout()); 334 | Ok(()) 335 | } 336 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cargo Thanku 2 | 3 | [中文 README.md](./README_CN.md) 4 | 5 | A command-line tool for generating acknowledgments for your Rust project dependencies. 6 | 7 | ## Key Features 8 | 9 | - Generates acknowledgments in multiple formats (Markdown table/list, JSON, TOML, CSV, YAML) 10 | - Fetches dependency information from crates.io and GitHub 11 | - Supports concurrent processing with configurable limits 12 | - Implements retry mechanism for failed requests 13 | - Offers command-line completion for Bash, Zsh, Fish, PowerShell, and Elvish 14 | - Provides internationalization support (zh/en/ja/ko/es/fr/de/it) 15 | 16 | ## Architecture Overview 17 | 18 | - `src/app` owns CLI bootstrapping, command dispatch (`convert`, `completions`, `test`), logging initialization, and the asynchronous dependency-processing pipeline built on `tokio` semaphores plus `FuturesUnordered` for smoother bounded concurrency. 19 | - `src/output` is split into focused modules: 20 | - `output/dependency.rs` defines the domain entities (`DependencyInfo`, `DependencyKind`, parsing helpers, failure helpers). 21 | - `output/markdown/tokenizer.rs` exposes a lightweight tokenizer for Markdown list headers/items so parsing no longer relies on brittle string splits. 22 | - `output/format/` hosts Markdown, CSV, and structured (JSON/TOML/YAML) formatters behind a shared `Formatter` trait and `OutputFormat` enum. 23 | - `output/manager.rs` centralizes writer orchestration so CLI and the converter can stream results to stdout/files uniformly. 24 | - `sources.rs` keeps HTTP clients (crates.io + GitHub) isolated; integration-style tests that hit the network are flagged with `#[ignore]` so the default `cargo test` run stays deterministic/offline-friendly. 25 | - `travert::Converter` parses once using a streaming `BufReader` and writes each target file via `BufWriter`, which reduces peak memory and improves throughput for large conversion batches. 26 | - `tests/` mirrors the module layout (`config_tests.rs`, `markdown_format_tests.rs`, etc.) and runs entirely through the public API; ignored cases (like live crates.io calls) are clearly tagged so CI remains deterministic. 27 | 28 | A quick glance at `app/pipeline.rs` + `output/` is usually all you need to understand how new data sources or formats plug into the tool. 29 | 30 | ## Installation 31 | 32 | Ensure you have the Rust toolchain installed on your system, then execute: 33 | 34 | ```bash 35 | # Install cargo-thanku 36 | cargo install cargo-thanku 37 | 38 | # Generate shell completions (optional) 39 | cargo thanku completions bash > ~/.local/share/bash-completion/completions/cargo-thanku 40 | ``` 41 | 42 | ## Usage 43 | 44 | ### Basic Usage 45 | 46 | ```bash 47 | # Generate acknowledgments for your project 48 | cargo thanku 49 | 50 | # Specify output format 51 | cargo thanku -f markdown-table # or markdown-list, json, csv, yaml, toml 52 | 53 | # Set GitHub token for more information and automatic starring 54 | cargo thanku -t YOUR_GITHUB_TOKEN 55 | 56 | # Change language 57 | cargo thanku -l en # supports zh/en/ja/ko/es/fr/de/it 58 | ``` 59 | 60 | ### Advanced Options 61 | 62 | ```bash 63 | # Configure concurrent requests 64 | cargo thanku -j 10 # Set maximum concurrent requests to 10 65 | 66 | # Adjust retry attempts 67 | cargo thanku -r 5 # Set maximum retry attempts to 5 68 | 69 | # Customize output file 70 | cargo thanku -o custom_thanks.md 71 | 72 | # Enable verbose logging 73 | cargo thanku -v 74 | 75 | # Filter out libraries imported with relative paths 76 | cargo thanku --no-relative-libs 77 | ``` 78 | 79 | ### Format Conversion 80 | 81 | Convert between different output formats: 82 | 83 | ```bash 84 | # Do support `cargo thanku convert` syntax to invoke converter 85 | # Convert a single file to multiple formats 86 | cargo-thanku convert input.md -o markdown-table,json,yaml,toml 87 | 88 | # Short command aliases 89 | cargo-thanku cvt input.csv -o mt,yaml 90 | cargo-thanku conv input.md -o json,csv 91 | cargo-thanku convt input.yaml -o markdown-list 92 | ``` 93 | 94 | The converter will: 95 | - Create a `converted` directory in the same location as the input file 96 | - Generate output files with appropriate extensions 97 | - Support conversion between all supported formats (mt[markdown-table], ml[markdown-list], json, toml, yaml, csv) 98 | 99 | #### Command-Line Arguments 100 | 101 | | Argument | Description | Default Value | 102 | |---------------------|----------------------------------------------------|-------------------| 103 | | `-i, --input` | Input Cargo.toml file path | - | 104 | | `-o, --outputs` | Output file formats | - | 105 | | `-l, --language` | Language (zh/en/ja/ko/es/fr/de/it) | `zh` | 106 | | `-v, --verbose` | Enable verbose logging | `false` | 107 | 108 | ### Command-Line Completion 109 | 110 | Generate command-line completion scripts for various shells: 111 | 112 | ```bash 113 | # Bash 114 | cargo thanku completions bash > ~/.local/share/bash-completion/completions/cargo-thanku 115 | 116 | # Zsh 117 | cargo thanku completions zsh > ~/.zsh/_cargo-thanku 118 | 119 | # Fish 120 | cargo thanku completions fish > ~/.config/fish/completions/cargo-thanku.fish 121 | 122 | # PowerShell 123 | mkdir -p $PROFILE\..\Completions 124 | cargo thanku completions powershell > $PROFILE\..\Completions\cargo-thanku.ps1 125 | 126 | # Elvish 127 | cargo thanku completions elvish > ~/.elvish/lib/cargo-thanku.elv 128 | ``` 129 | 130 | ## Testing 131 | 132 | Run the entire suite (unit-style and integration) with: 133 | 134 | ```bash 135 | cargo test 136 | ``` 137 | 138 | Network-dependent checks are ignored by default; include them with `cargo test -- --ignored` when you have connectivity and credentials. 139 | 140 | ## Command-Line Arguments 141 | 142 | | Argument | Description | Default Value | 143 | |---------------------|----------------------------------------------------|-------------------| 144 | | `-i, --input` | Input Cargo.toml file path | `Cargo.toml` | 145 | | `-o, --output` | Output file path | `thanks.md` | 146 | | `-f, --format` | Output format | `markdown-table` | 147 | | `-t, --token` | GitHub API token | - | 148 | | `-l, --language` | Language (zh/en/ja/ko/es/fr/de/it) | `zh` | 149 | | `-v, --verbose` | Enable verbose logging | `false` | 150 | | `-j, --concurrent` | Maximum concurrent requests | `5` | 151 | | `-r, --retries` | Maximum retry attempts | `3` | 152 | | `--no-relative-libs`| Filter out libraries imported with relative paths | `false` | 153 | 154 | ## Output Formats 155 | 156 | ### Markdown Table 157 | ```markdown 158 | | Name | Description | Source | Stats | Status | 159 | |------|-------------|--------|-------|--------| 160 | |🔍 | Normal | | | | 161 | |[serde](https://crates.io/crates/serde) | Serialization framework | [GitHub](https://github.com/serde-rs/serde) | 🌟 3.5k | ✅ | 162 | ``` 163 | 164 | ### Markdown List 165 | ```markdown 166 | # Dependencies 167 | 168 | - [serde](https://crates.io/crates/serde) [Serialization framework](https://github.com/serde-rs/serde) (🌟 3.5k) ✅ 169 | ``` 170 | 171 | ### MARKDOWN/JSON/TOML/YAML/CSV 172 | Also supports structured output formats for programmatic use. 173 | 174 | ## Important Notes 175 | 176 | 1. Setting a GitHub token (`-t` or `GITHUB_TOKEN` env) enables: 177 | - Fetching additional repository information 178 | - Automatic fetching stars of dependency repositories 179 | - Higher API rate limits 180 | 181 | 2. Failed dependency processing: 182 | - Won't interrupt the overall process 183 | - Will be marked with ❌ in the output 184 | - Shows error messages for debugging 185 | 186 | 3. Language codes: 187 | - Supports flexible formats (e.g., "en", "en_US", "en_US.UTF-8") 188 | - Falls back to primary language code 189 | - Suggests similar codes for typos 190 | 191 | ## Acknowledgments 192 | 193 | This project itself is built with many excellent Rust crates. Here are some key dependencies: 194 | 195 | > [!TIP] 196 | > Generated by `cargo-thanku` tool 197 | 198 | | Name | Description | Crates.io | Source | Stats | Status | 199 | |------|--------|--------|-------|-------|--------| 200 | |🔍|Normal| | | | | 201 | | anyhow | Flexible concrete Error type built on std::error::Error | [anyhow](https://crates.io/crates/anyhow) | [GitHub](https://github.com/dtolnay/anyhow) | ❓ | ✅ | 202 | | cargo_metadata | structured access to the output of `cargo metadata` | [cargo_metadata](https://crates.io/crates/cargo_metadata) | [GitHub](https://github.com/oli-obk/cargo_metadata) | ❓ | ✅ | 203 | | clap | A simple to use, efficient, and full-featured Command Line Argument Parser | [clap](https://crates.io/crates/clap) | [GitHub](https://github.com/clap-rs/clap) | ❓ | ✅ | 204 | | clap_complete | Generate shell completion scripts for your clap::Command | [clap_complete](https://crates.io/crates/clap_complete) | [GitHub](https://github.com/clap-rs/clap) | ❓ | ✅ | 205 | | futures | An implementation of futures and streams featuring zero allocations, composability, and iterator-like interfaces. | [futures](https://crates.io/crates/futures) | [GitHub](https://github.com/rust-lang/futures-rs) | ❓ | ✅ | 206 | | reqwest | higher level HTTP client library | [reqwest](https://crates.io/crates/reqwest) | [GitHub](https://github.com/seanmonstar/reqwest) | ❓ | ✅ | 207 | | rust-i18n | Rust I18n is use Rust codegen for load YAML file storage translations on compile time, and give you a t! macro for simply get translation texts. | [rust-i18n](https://crates.io/crates/rust-i18n) | [GitHub](https://github.com/longbridge/rust-i18n) | ❓ | ✅ | 208 | | serde | A generic serialization/deserialization framework | [serde](https://crates.io/crates/serde) | [GitHub](https://github.com/serde-rs/serde) | ❓ | ✅ | 209 | | serde_json | A JSON serialization file format | [serde_json](https://crates.io/crates/serde_json) | [GitHub](https://github.com/serde-rs/json) | ❓ | ✅ | 210 | | serde_yaml | YAML data format for Serde | [serde_yaml](https://crates.io/crates/serde_yaml) | [GitHub](https://github.com/dtolnay/serde-yaml) | ❓ | ✅ | 211 | | strsim | Implementations of string similarity metrics. Includes Hamming, Levenshtein, OSA, Damerau-Levenshtein, Jaro, Jaro-Winkler, and Sørensen-Dice. | [strsim](https://crates.io/crates/strsim) | [GitHub](https://github.com/rapidfuzz/strsim-rs) | ❓ | ✅ | 212 | | thiserror | derive(Error) | [thiserror](https://crates.io/crates/thiserror) | [GitHub](https://github.com/dtolnay/thiserror) | ❓ | ✅ | 213 | | tokio | An event-driven, non-blocking I/O platform for writing asynchronous I/O backed applications. | [tokio](https://crates.io/crates/tokio) | [GitHub](https://github.com/tokio-rs/tokio) | ❓ | ✅ | 214 | | toml | A native Rust encoder and decoder of TOML-formatted files and streams. Provides implementations of the standard Serialize/Deserialize traits for TOML data to facilitate deserializing and serializing Rust structures. | [toml](https://crates.io/crates/toml) | [GitHub](https://github.com/toml-rs/toml) | ❓ | ✅ | 215 | | tracing | Application-level tracing for Rust. | [tracing](https://crates.io/crates/tracing) | [GitHub](https://github.com/tokio-rs/tracing) | ❓ | ✅ | 216 | | tracing-subscriber | Utilities for implementing and composing `tracing` subscribers. | [tracing-subscriber](https://crates.io/crates/tracing-subscriber) | [GitHub](https://github.com/tokio-rs/tracing) | ❓ | ✅ | 217 | | url | URL library for Rust, based on the WHATWG URL Standard | [url](https://crates.io/crates/url) | [GitHub](https://github.com/servo/rust-url) | ❓ | ✅ | 218 | |🔧|Development| | | | | 219 | | assert_fs | Filesystem fixtures and assertions for testing. | [assert_fs](https://crates.io/crates/assert_fs) | [GitHub](https://github.com/assert-rs/assert_fs.git) | ❓ | ✅ | 220 | | pretty_assertions | Overwrite `assert_eq!` and `assert_ne!` with drop-in replacements, adding colorful diffs. | [pretty_assertions](https://crates.io/crates/pretty_assertions) | [GitHub](https://github.com/rust-pretty-assertions/rust-pretty-assertions) | ❓ | ✅ | 221 | | tokio-test | Testing utilities for Tokio- and futures-based code | [tokio-test](https://crates.io/crates/tokio-test) | [GitHub](https://github.com/tokio-rs/tokio) | ❓ | ✅ | 222 | 223 | For a complete list of dependencies and their acknowledgments, run: 224 | 225 | ```bash 226 | cargo thanku 227 | ``` 228 | 229 | ## License 230 | 231 | This project is licensed under the MIT License - see the [LICENSE](./LICENSE.md) file for details. 232 | -------------------------------------------------------------------------------- /src/output/format/markdown.rs: -------------------------------------------------------------------------------- 1 | use std::io::BufRead; 2 | 3 | use anyhow::Result; 4 | use regex::Regex; 5 | use rust_i18n::t; 6 | use tracing::warn; 7 | 8 | use crate::output::{ 9 | dependency::{DependencyInfo, DependencyKind}, 10 | markdown::tokenizer::{MarkdownListTokenizer, MarkdownSection, section_kind_from_header}, 11 | }; 12 | 13 | use super::Formatter; 14 | 15 | pub struct MarkdownTableFormatter; 16 | 17 | impl MarkdownTableFormatter { 18 | pub(crate) fn column_count() -> usize { 19 | MarkdownTableFormatter::header().as_ref().split('|').count() - 2 20 | } 21 | 22 | fn header() -> impl AsRef { 23 | format!( 24 | "| {} | {} | {} | {} | {} | {} |", 25 | t!("output.name"), 26 | t!("output.description"), 27 | t!("output.crates_link"), 28 | t!("output.source_link"), 29 | t!("output.stats"), 30 | t!("output.status") 31 | ) 32 | } 33 | 34 | fn separator() -> impl AsRef { 35 | let column_num = MarkdownTableFormatter::column_count(); 36 | format!("|{}", "---|".repeat(column_num)) 37 | } 38 | 39 | fn take_sort_dependencies<'a>( 40 | deps: &'a [DependencyInfo], 41 | kind: &DependencyKind, 42 | ) -> Vec<&'a DependencyInfo> { 43 | let mut filtered = deps 44 | .iter() 45 | .filter(|dep| dep.dependency_kind == *kind) 46 | .collect::>(); 47 | filtered.sort_by(|a, b| a.name.cmp(&b.name)); 48 | filtered 49 | } 50 | 51 | fn first_table(content: &str) -> Option<&str> { 52 | let lines: Vec<&str> = content.lines().collect(); 53 | if lines.len() < 2 { 54 | return None; 55 | } 56 | 57 | for i in 0..lines.len() - 1 { 58 | let header_line = lines[i].trim(); 59 | if !header_line.starts_with('|') && !header_line.contains('|') { 60 | continue; 61 | } 62 | 63 | let separator_line = lines[i + 1].trim(); 64 | if !Self::is_valid_separator(separator_line) { 65 | continue; 66 | } 67 | 68 | let header_columns = Self::count_columns(header_line); 69 | if header_columns != Self::count_columns(separator_line) { 70 | continue; 71 | } 72 | 73 | let mut end_idx = i + 2; 74 | while end_idx < lines.len() { 75 | let row = lines[end_idx].trim(); 76 | if row.is_empty() || (!row.starts_with('|') && !row.contains('|')) { 77 | break; 78 | } 79 | if Self::count_columns(row) != header_columns { 80 | break; 81 | } 82 | end_idx += 1; 83 | } 84 | 85 | if end_idx >= i + 2 { 86 | let start_pos = content.find(lines[i])?; 87 | let end_line_start = content.find(lines[end_idx - 1])?; 88 | let end_pos = end_line_start + lines[end_idx - 1].len(); 89 | return Some(&content[start_pos..end_pos]); 90 | } 91 | } 92 | 93 | None 94 | } 95 | 96 | fn is_valid_separator(line: &str) -> bool { 97 | if !line.contains('|') { 98 | return false; 99 | } 100 | 101 | for cell in Self::split_row(line) { 102 | let trimmed = cell.trim(); 103 | if trimmed.is_empty() { 104 | continue; 105 | } 106 | if !trimmed.chars().all(|c| c == '-' || c == ':' || c == ' ') { 107 | return false; 108 | } 109 | if !trimmed.contains('-') { 110 | return false; 111 | } 112 | } 113 | 114 | true 115 | } 116 | 117 | fn count_columns(line: &str) -> usize { 118 | Self::split_row(line).len() 119 | } 120 | 121 | fn split_row(line: &str) -> Vec<&str> { 122 | let trimmed = line.trim(); 123 | let processed = if trimmed.starts_with('|') && trimmed.ends_with('|') { 124 | trimmed 125 | .strip_prefix('|') 126 | .and_then(|s| s.strip_suffix('|')) 127 | .unwrap_or(trimmed) 128 | } else if let Some(s) = trimmed.strip_prefix('|') { 129 | s 130 | } else if let Some(s) = trimmed.strip_suffix('|') { 131 | s 132 | } else { 133 | trimmed 134 | }; 135 | 136 | processed.split('|').collect() 137 | } 138 | } 139 | 140 | impl Formatter for MarkdownTableFormatter { 141 | fn format(&self, deps: &[DependencyInfo]) -> Result { 142 | let mut output = String::new(); 143 | output.push_str(&format!("\n{}\n", Self::header().as_ref())); 144 | output.push_str(&format!("{}\n", Self::separator().as_ref())); 145 | 146 | for kind in DependencyKind::ordered() { 147 | let mut show_header = true; 148 | let deps = Self::take_sort_dependencies(deps, &kind); 149 | let header = kind.to_md_table_header(); 150 | 151 | for dep in deps { 152 | if show_header { 153 | output.push_str(&format!("{}\n", header.as_ref())); 154 | show_header = false; 155 | } 156 | let (name, description, crates_link, source_link, stats, status) = dep.to_strings(); 157 | output.push_str(&format!( 158 | "| {} | {} | {} | {} | {} | {} |\n", 159 | name, description, crates_link, source_link, stats, status 160 | )); 161 | } 162 | } 163 | 164 | Ok(output) 165 | } 166 | 167 | fn parse(&self, content: &str) -> Result> { 168 | let Some(table) = Self::first_table(content) else { 169 | return Ok(vec![]); 170 | }; 171 | 172 | let mut deps = Vec::new(); 173 | let mut dependency_kind = DependencyKind::Unknown; 174 | for line in table.lines().skip(2) { 175 | let trimmed = line.trim(); 176 | if trimmed.contains(DependencyKind::Normal.to_md_table_header().as_ref()) { 177 | dependency_kind = DependencyKind::Normal; 178 | continue; 179 | } else if trimmed.contains(DependencyKind::Development.to_md_table_header().as_ref()) { 180 | dependency_kind = DependencyKind::Development; 181 | continue; 182 | } else if trimmed.contains(DependencyKind::Build.to_md_table_header().as_ref()) { 183 | dependency_kind = DependencyKind::Build; 184 | continue; 185 | } else if trimmed.contains(DependencyKind::Unknown.to_md_table_header().as_ref()) { 186 | dependency_kind = DependencyKind::Unknown; 187 | continue; 188 | } 189 | 190 | let dep = DependencyInfo::try_from_md_table_line(trimmed, &dependency_kind)?; 191 | deps.push(dep); 192 | } 193 | 194 | Ok(deps) 195 | } 196 | 197 | fn parse_reader(&self, reader: &mut dyn BufRead) -> Result> { 198 | let mut buffer = String::new(); 199 | let mut header_candidate: Option = None; 200 | let mut table_lines: Vec = Vec::new(); 201 | let mut header_columns = 0usize; 202 | let mut collecting = false; 203 | 204 | loop { 205 | buffer.clear(); 206 | let bytes = reader.read_line(&mut buffer)?; 207 | if bytes == 0 { 208 | break; 209 | } 210 | let line = buffer.trim_end_matches(['\r', '\n']).to_string(); 211 | 212 | if collecting { 213 | if line.is_empty() 214 | || (!line.starts_with('|') && !line.contains('|')) 215 | || MarkdownTableFormatter::count_columns(&line) != header_columns 216 | { 217 | break; 218 | } 219 | table_lines.push(line); 220 | continue; 221 | } 222 | 223 | if let Some(header) = header_candidate.take() { 224 | if MarkdownTableFormatter::is_valid_separator(&line) 225 | && MarkdownTableFormatter::count_columns(&line) 226 | == MarkdownTableFormatter::count_columns(&header) 227 | { 228 | header_columns = MarkdownTableFormatter::count_columns(&header); 229 | table_lines.push(header); 230 | table_lines.push(line.clone()); 231 | collecting = true; 232 | continue; 233 | } else { 234 | if line.starts_with('|') || line.contains('|') { 235 | header_candidate = Some(line.clone()); 236 | } 237 | continue; 238 | } 239 | } 240 | 241 | if line.starts_with('|') || line.contains('|') { 242 | header_candidate = Some(line); 243 | } 244 | } 245 | 246 | if table_lines.is_empty() { 247 | return Ok(vec![]); 248 | } 249 | 250 | let table_content = table_lines.join("\n"); 251 | self.parse(&table_content) 252 | } 253 | } 254 | 255 | pub struct MarkdownListFormatter; 256 | 257 | impl MarkdownListFormatter { 258 | fn header() -> impl AsRef { 259 | format!("# {}", t!("output.dependencies")) 260 | } 261 | 262 | fn first_list(content: &str) -> Option<&str> { 263 | let regex = Regex::new(r"(?m)^(#|##) .+$").ok()?; 264 | let headers: Vec<_> = regex.find_iter(content).collect(); 265 | 266 | if headers.len() < 2 { 267 | warn!( 268 | "{}", 269 | t!("output.invalid_list_header_num", num = headers.len()) 270 | ); 271 | return None; 272 | } 273 | 274 | let start_idx = headers 275 | .iter() 276 | .position(|m| content[m.start()..].starts_with("# "))?; 277 | let start_header = headers[start_idx]; 278 | let start_pos = start_header.start(); 279 | 280 | let end_pos = headers 281 | .iter() 282 | .skip(start_idx + 1) 283 | .find(|m| content[m.start()..].starts_with("# ")) 284 | .map(|m| m.start()) 285 | .unwrap_or_else(|| { 286 | content[start_pos..] 287 | .find("\n### ") 288 | .map(|pos| start_pos + pos) 289 | .unwrap_or_else(|| content.len()) 290 | }); 291 | 292 | let list_content = &content[start_pos..end_pos]; 293 | let lines_after_header = list_content 294 | .lines() 295 | .skip(1) 296 | .filter(|line| !line.trim().is_empty()) 297 | .collect::>(); 298 | 299 | if lines_after_header 300 | .iter() 301 | .any(|line| line.starts_with("## ")) 302 | && lines_after_header.iter().any(|line| { 303 | DependencyInfo::try_from_md_list_line(line, &DependencyKind::Unknown).is_ok() 304 | }) 305 | { 306 | Some(list_content) 307 | } else { 308 | warn!("{}", t!("output.no_valid_list_items_found")); 309 | None 310 | } 311 | } 312 | } 313 | 314 | impl Formatter for MarkdownListFormatter { 315 | fn format(&self, deps: &[DependencyInfo]) -> Result { 316 | let mut output = String::new(); 317 | output.push_str(&format!("\n{}\n", Self::header().as_ref())); 318 | 319 | for kind in DependencyKind::ordered() { 320 | let mut show_header = true; 321 | let deps = MarkdownTableFormatter::take_sort_dependencies(deps, &kind); 322 | let header = kind.to_md_list_header(); 323 | 324 | for dep in deps { 325 | if show_header { 326 | output.push_str(&format!("\n{}\n", header.as_ref())); 327 | show_header = false; 328 | } 329 | let (name, description, crates_link, source_link, stats, status) = dep.to_strings(); 330 | output.push_str(&format!( 331 | "- {} : {} - {} {} ({}) {}\n", 332 | name, description, crates_link, source_link, stats, status 333 | )); 334 | } 335 | } 336 | 337 | Ok(output) 338 | } 339 | 340 | fn parse(&self, content: &str) -> Result> { 341 | let Some(list) = Self::first_list(content) else { 342 | return Ok(vec![]); 343 | }; 344 | 345 | let mut deps = Vec::new(); 346 | let mut dependency_kind = DependencyKind::Unknown; 347 | for token in MarkdownListTokenizer::new(list) { 348 | match token { 349 | Ok(MarkdownSection::Header(header)) => { 350 | if let Some(kind) = section_kind_from_header(header) { 351 | dependency_kind = kind; 352 | } 353 | } 354 | Ok(MarkdownSection::Item(entry)) => { 355 | match DependencyInfo::from_list_entry(entry, &dependency_kind) { 356 | Ok(dep) => deps.push(dep), 357 | Err(_) => warn!( 358 | "{}", 359 | t!("output.failed_to_parse_list_line", line = entry.raw_line) 360 | ), 361 | } 362 | } 363 | Err(err) => warn!( 364 | "{}", 365 | t!("output.failed_to_parse_list_line", line = err.to_string()) 366 | ), 367 | } 368 | } 369 | 370 | Ok(deps) 371 | } 372 | 373 | fn parse_reader(&self, reader: &mut dyn BufRead) -> Result> { 374 | let mut buffer = String::new(); 375 | let mut collected = String::new(); 376 | let mut collecting = false; 377 | 378 | loop { 379 | buffer.clear(); 380 | let bytes = reader.read_line(&mut buffer)?; 381 | if bytes == 0 { 382 | break; 383 | } 384 | let line = buffer.trim_end_matches(['\r', '\n']).to_string(); 385 | if !collecting && line.starts_with("# ") { 386 | collecting = true; 387 | } 388 | 389 | if collecting { 390 | if line.starts_with("# ") && !collected.is_empty() { 391 | break; 392 | } 393 | collected.push_str(&line); 394 | collected.push('\n'); 395 | } 396 | } 397 | 398 | if collected.is_empty() { 399 | return Ok(vec![]); 400 | } 401 | 402 | self.parse(&collected) 403 | } 404 | } 405 | -------------------------------------------------------------------------------- /src/output/dependency.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use anyhow::Result; 4 | use rust_i18n::t; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use crate::{ 8 | errors::AppError, 9 | output::markdown::tokenizer::ListEntry, 10 | sources::{CratesioClient, Source}, 11 | }; 12 | 13 | #[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default)] 14 | pub enum DependencyKind { 15 | #[default] 16 | Normal, 17 | Development, 18 | Build, 19 | Unknown, 20 | } 21 | 22 | impl DependencyKind { 23 | pub(crate) fn ordered() -> [DependencyKind; 4] { 24 | [ 25 | DependencyKind::Normal, 26 | DependencyKind::Development, 27 | DependencyKind::Build, 28 | DependencyKind::Unknown, 29 | ] 30 | } 31 | 32 | pub fn to_md_table_header(&self) -> impl AsRef { 33 | match self { 34 | DependencyKind::Normal => format!("| 🔍 | {} | | | | |", t!("output.normal")), 35 | DependencyKind::Development => { 36 | format!("| 🔧 | {} | | | | |", t!("output.development")) 37 | } 38 | DependencyKind::Build => format!("| 🔨 | {} | | | | |", t!("output.build")), 39 | DependencyKind::Unknown => format!("| ❓ | {} | | | | |", t!("output.unknown")), 40 | } 41 | } 42 | 43 | pub fn to_md_list_header(&self) -> impl AsRef { 44 | let label = match self { 45 | DependencyKind::Normal => t!("output.normal"), 46 | DependencyKind::Development => t!("output.development"), 47 | DependencyKind::Build => t!("output.build"), 48 | DependencyKind::Unknown => t!("output.unknown"), 49 | }; 50 | 51 | format!("## {}", label) 52 | } 53 | 54 | pub(crate) fn try_from_list_header_line(header: &str) -> Result { 55 | let token = header.trim_start_matches("## ").trim(); 56 | Self::from_str(token) 57 | } 58 | } 59 | 60 | impl FromStr for DependencyKind { 61 | type Err = AppError; 62 | 63 | fn from_str(s: &str) -> Result { 64 | let normalized = s.trim().to_lowercase(); 65 | if normalized.is_empty() { 66 | return Err(AppError::InvalidDependencyKind(normalized)); 67 | } 68 | 69 | match normalized.as_str() { 70 | kind if kind == t!("output.normal").to_lowercase() => Ok(Self::Normal), 71 | kind if kind == t!("output.development").to_lowercase() => Ok(Self::Development), 72 | kind if kind == t!("output.build").to_lowercase() => Ok(Self::Build), 73 | kind if kind == t!("output.unknown").to_lowercase() => Ok(Self::Unknown), 74 | _ => Err(AppError::InvalidDependencyKind( 75 | t!("output.invalid_dependency_kind", kind = normalized).to_string(), 76 | )), 77 | } 78 | } 79 | } 80 | 81 | impl std::fmt::Display for DependencyKind { 82 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 83 | let value = match self { 84 | DependencyKind::Normal => t!("output.normal"), 85 | DependencyKind::Development => t!("output.development"), 86 | DependencyKind::Build => t!("output.build"), 87 | DependencyKind::Unknown => t!("output.unknown"), 88 | }; 89 | write!(f, "{}", value) 90 | } 91 | } 92 | 93 | impl From for DependencyKind { 94 | fn from(kind: cargo_metadata::DependencyKind) -> Self { 95 | match kind { 96 | cargo_metadata::DependencyKind::Normal => Self::Normal, 97 | cargo_metadata::DependencyKind::Development => Self::Development, 98 | cargo_metadata::DependencyKind::Build => Self::Build, 99 | _ => Self::Unknown, 100 | } 101 | } 102 | } 103 | 104 | #[derive(Debug, Serialize, Deserialize, Clone, Default)] 105 | pub struct DependencyStats { 106 | pub stars: Option, 107 | pub downloads: Option, 108 | } 109 | 110 | #[derive(Debug, Serialize, Deserialize, Clone, Default)] 111 | pub struct DependencyInfo { 112 | pub name: String, 113 | pub description: Option, 114 | pub dependency_kind: DependencyKind, 115 | pub crate_url: Option, 116 | pub source_type: String, 117 | pub source_url: Option, 118 | pub stats: DependencyStats, 119 | pub failed: bool, 120 | pub error_message: Option, 121 | } 122 | 123 | impl DependencyInfo { 124 | const TRIM_PATTERN: [char; 4] = ['[', '(', ' ', ')']; 125 | const MARKDOWN_COLUMNS: usize = 6; 126 | 127 | pub fn failure( 128 | name: &str, 129 | dependency_kind: DependencyKind, 130 | error_message: impl Into, 131 | ) -> Self { 132 | Self { 133 | name: name.to_string(), 134 | dependency_kind, 135 | description: None, 136 | crate_url: Some(CratesioClient::get_crate_url(name)), 137 | source_type: "Unknown".to_string(), 138 | source_url: None, 139 | stats: DependencyStats { 140 | stars: None, 141 | downloads: None, 142 | }, 143 | failed: true, 144 | error_message: Some(error_message.into()), 145 | } 146 | } 147 | 148 | pub fn to_strings(&self) -> (String, String, String, String, String, String) { 149 | let description = self 150 | .description 151 | .as_ref() 152 | .map(|desc| desc.replace('\n', " ")) 153 | .unwrap_or_else(|| "unknown".to_string()); 154 | 155 | let stats = match (self.stats.stars, self.stats.downloads) { 156 | (Some(stars), _) => format!("🌟 {}", stars), 157 | (None, Some(downloads)) => format!("📦 {}", downloads), 158 | _ => "❓".to_string(), 159 | }; 160 | 161 | let status = if self.failed { 162 | format!("❌ {}", self.error_message.as_deref().unwrap_or("Failed")) 163 | } else { 164 | "✅".to_string() 165 | }; 166 | 167 | let crates_link = self 168 | .crate_url 169 | .as_ref() 170 | .map(|url| format!("[{}]({})", self.name, url)) 171 | .unwrap_or_else(|| self.name.clone()); 172 | 173 | let source_link = self 174 | .source_url 175 | .as_ref() 176 | .map(|url| format!("[{}]({})", self.source_type, url)) 177 | .unwrap_or_else(|| self.source_type.clone()); 178 | 179 | ( 180 | self.name.clone(), 181 | description, 182 | crates_link, 183 | source_link, 184 | stats, 185 | status, 186 | ) 187 | } 188 | 189 | pub fn try_from_csv_line(line: &str, header_num: usize) -> Result { 190 | let columns: Vec<&str> = line.split(',').map(|s| s.trim()).collect(); 191 | if columns.len() != header_num { 192 | return Err(AppError::InvalidCsvContent(line.to_string()).into()); 193 | } 194 | 195 | let name = columns[0].to_string(); 196 | let description = Self::option_from_str::(columns[1])?.map(|s| s.replace(';', ",")); 197 | let dependency_kind = DependencyKind::from_str(columns[2])?; 198 | let (_, crate_url) = Self::parse_md_link(columns[3])?; 199 | let (source_type, source_url) = Self::parse_md_link(columns[4])?; 200 | let (stars, downloads) = Self::parse_stats(columns[5])?; 201 | let (failed, error_message) = Self::parse_status(columns[6])?; 202 | 203 | Ok(Self { 204 | name, 205 | description, 206 | dependency_kind, 207 | crate_url, 208 | source_type, 209 | source_url, 210 | stats: DependencyStats { stars, downloads }, 211 | failed, 212 | error_message, 213 | }) 214 | } 215 | 216 | pub fn try_from_md_table_line(line: &str, dependency_kind: &DependencyKind) -> Result { 217 | let columns: Vec<&str> = line 218 | .trim_matches(['|', ' ', '\n']) 219 | .split('|') 220 | .map(|s| s.trim()) 221 | .collect(); 222 | 223 | if columns.len() != Self::MARKDOWN_COLUMNS { 224 | return Err(AppError::InvalidTableLine(line.to_string()).into()); 225 | } 226 | 227 | let name = columns[0].to_string(); 228 | let description = Self::option_from_str(columns[1])?; 229 | let (_, crate_url) = Self::parse_md_link(columns[2])?; 230 | let (source_type, source_url) = Self::parse_md_link(columns[3])?; 231 | let (stars, downloads) = Self::parse_stats(columns[4])?; 232 | let (failed, error_message) = Self::parse_status(columns[5])?; 233 | 234 | Ok(Self { 235 | name, 236 | description, 237 | dependency_kind: *dependency_kind, 238 | crate_url, 239 | source_type, 240 | source_url, 241 | stats: DependencyStats { stars, downloads }, 242 | failed, 243 | error_message, 244 | }) 245 | } 246 | 247 | pub fn try_from_md_list_line(line: &str, dependency_kind: &DependencyKind) -> Result { 248 | let entry = ListEntry::from_line(line)?; 249 | Self::from_list_entry(entry, dependency_kind) 250 | } 251 | 252 | pub(crate) fn from_list_entry( 253 | entry: ListEntry<'_>, 254 | dependency_kind: &DependencyKind, 255 | ) -> Result { 256 | let description = entry.description.map(|text| text.to_string()); 257 | let (_, crate_url) = Self::parse_md_link(entry.crate_segment)?; 258 | let (source_type, source_url) = Self::parse_md_link(entry.source_segment)?; 259 | let (stars, downloads) = Self::parse_stats(entry.stats_segment)?; 260 | let (failed, error_message) = Self::parse_status(entry.status_segment)?; 261 | 262 | Ok(Self { 263 | name: entry.name.to_string(), 264 | description, 265 | dependency_kind: *dependency_kind, 266 | crate_url, 267 | source_type, 268 | source_url, 269 | stats: DependencyStats { stars, downloads }, 270 | failed, 271 | error_message, 272 | }) 273 | } 274 | 275 | fn option_from_str(s: &str) -> Result> 276 | where 277 | ::Err: std::error::Error + Send + Sync + 'static, 278 | { 279 | let trimmed = s.trim(); 280 | if trimmed.is_empty() { 281 | Ok(None) 282 | } else { 283 | trimmed 284 | .parse::() 285 | .map(Some) 286 | .map_err(|e| AppError::InvalidListLine(e.to_string()).into()) 287 | } 288 | } 289 | 290 | pub fn parse_md_link(s: &str) -> Result<(String, Option)> { 291 | let parts: Vec<&str> = s.split("](").collect(); 292 | let source_type = parts[0] 293 | .trim_start_matches(Self::TRIM_PATTERN) 294 | .trim_end_matches(Self::TRIM_PATTERN); 295 | let source_url = if parts.len() > 1 { 296 | Some( 297 | parts[1] 298 | .trim_start_matches(Self::TRIM_PATTERN) 299 | .trim_end_matches(Self::TRIM_PATTERN) 300 | .to_string(), 301 | ) 302 | } else { 303 | None 304 | }; 305 | Ok((source_type.to_string(), source_url)) 306 | } 307 | 308 | pub fn parse_stats(s: &str) -> Result<(Option, Option)> { 309 | let cleaned = s 310 | .trim_start_matches(Self::TRIM_PATTERN) 311 | .trim_end_matches(Self::TRIM_PATTERN); 312 | 313 | match cleaned { 314 | text if text.contains('🌟') && text.contains('📦') => { 315 | let normalized = text.replace('🌟', "").replace('📦', "|"); 316 | let parts: Vec<&str> = normalized.split('|').collect(); 317 | if parts.len() != 2 { 318 | return Err(AppError::InvalidStats(cleaned.to_string()).into()); 319 | } 320 | let stars = parts[0] 321 | .trim() 322 | .parse::() 323 | .map_err(|_| AppError::InvalidStats(cleaned.to_string()))?; 324 | let downloads = parts[1] 325 | .trim() 326 | .parse::() 327 | .map_err(|_| AppError::InvalidStats(cleaned.to_string()))?; 328 | Ok((Some(stars), Some(downloads))) 329 | } 330 | text if text.contains('🌟') => { 331 | let parts: Vec<&str> = text.split('🌟').collect(); 332 | let stars = parts[1] 333 | .trim() 334 | .parse::() 335 | .map_err(|_| AppError::InvalidStats(cleaned.to_string()))?; 336 | Ok((Some(stars), None)) 337 | } 338 | text if text.contains('📦') => { 339 | let parts: Vec<&str> = text.split('📦').collect(); 340 | let downloads = parts[1] 341 | .trim() 342 | .parse::() 343 | .map_err(|_| AppError::InvalidStats(cleaned.to_string()))?; 344 | Ok((None, Some(downloads))) 345 | } 346 | _ => Ok((None, None)), 347 | } 348 | } 349 | 350 | pub fn parse_status(s: &str) -> Result<(bool, Option)> { 351 | let cleaned = s 352 | .trim_start_matches(Self::TRIM_PATTERN) 353 | .trim_end_matches(Self::TRIM_PATTERN); 354 | 355 | if cleaned.contains('✅') { 356 | Ok((false, None)) 357 | } else if cleaned.contains('❌') { 358 | let parts: Vec<&str> = cleaned.split('❌').collect(); 359 | let message = parts.get(1).map(|msg| msg.trim()).unwrap_or(""); 360 | if message.is_empty() { 361 | Ok((true, None)) 362 | } else { 363 | Ok((true, Some(message.to_string()))) 364 | } 365 | } else { 366 | Err(AppError::InvalidStatus(cleaned.to_string()).into()) 367 | } 368 | } 369 | } 370 | 371 | impl From<(&str, &Source)> for DependencyInfo { 372 | fn from((name, source): (&str, &Source)) -> Self { 373 | match source { 374 | Source::GitHub { owner, repo, stars } => Self { 375 | name: name.to_string(), 376 | description: None, 377 | crate_url: Some(format!("https://crates.io/crates/{}", name)), 378 | source_type: "GitHub".to_string(), 379 | source_url: Some(format!("https://github.com/{}/{}", owner, repo)), 380 | stats: DependencyStats { 381 | stars: *stars, 382 | downloads: None, 383 | }, 384 | failed: false, 385 | error_message: None, 386 | dependency_kind: DependencyKind::Normal, 387 | }, 388 | Source::CratesIo { downloads, .. } => Self { 389 | name: name.to_string(), 390 | description: None, 391 | crate_url: Some(format!("https://crates.io/crates/{}", name)), 392 | source_type: "crates.io".to_string(), 393 | source_url: None, 394 | stats: DependencyStats { 395 | stars: None, 396 | downloads: *downloads, 397 | }, 398 | failed: false, 399 | error_message: None, 400 | dependency_kind: DependencyKind::Normal, 401 | }, 402 | Source::Link { url } => Self { 403 | name: name.to_string(), 404 | description: None, 405 | crate_url: Some(format!("https://crates.io/crates/{}", name)), 406 | source_type: "Source".to_string(), 407 | source_url: Some(url.clone()), 408 | stats: DependencyStats { 409 | stars: None, 410 | downloads: None, 411 | }, 412 | failed: false, 413 | error_message: None, 414 | dependency_kind: DependencyKind::Normal, 415 | }, 416 | Source::Other { description } => Self { 417 | name: name.to_string(), 418 | description: Some(description.clone()), 419 | crate_url: Some(format!("https://crates.io/crates/{}", name)), 420 | source_type: description.clone(), 421 | source_url: None, 422 | stats: DependencyStats { 423 | stars: None, 424 | downloads: None, 425 | }, 426 | failed: false, 427 | error_message: None, 428 | dependency_kind: DependencyKind::Normal, 429 | }, 430 | } 431 | } 432 | } 433 | --------------------------------------------------------------------------------