From b84c771a010a682a641885fccbb49483cdbd7d79 Mon Sep 17 00:00:00 2001 From: Tiago Nascimento Date: Sat, 12 Oct 2019 07:32:10 -0300 Subject: [PATCH] added cargo subcommand binary Signed-off-by: Tiago Nascimento --- Cargo.toml | 9 ++- src/cargo.rs | 188 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 89 +++++++++++++----------- src/main.rs | 131 ++++++++++++++++++++--------------- 4 files changed, 323 insertions(+), 94 deletions(-) create mode 100644 src/cargo.rs diff --git a/Cargo.toml b/Cargo.toml index 83be817..9a19f0d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,10 @@ path = "src/lib.rs" name = "dadada" path = "src/main.rs" +[[bin]] +name = "cargo-dadada" +path = "src/cargo.rs" + [badges] travis-ci = { repository = "gnunicorn/dadada" } @@ -27,9 +31,12 @@ maintenance = { status = "actively-developed" } [dependencies] clap = "2.33.0" pulldown-cmark = "0.5.3" +cargo_metadata = "0.8" +log = "0.4" +pretty_env_logger = "0.3" [dev-dependencies] tempfile = "3.1" file_diff = "1.0.0" assert_cmd = "0.10" -predicates = "1" \ No newline at end of file +predicates = "1" diff --git a/src/cargo.rs b/src/cargo.rs new file mode 100644 index 0000000..693b9ad --- /dev/null +++ b/src/cargo.rs @@ -0,0 +1,188 @@ +use clap::{App, Arg}; +use log::{error, info}; +use std::fs; + +use dadada::{build_html, extract, Block, Options}; + +fn main() { + pretty_env_logger::formatted_builder() + .filter(None, log::LevelFilter::Info) + .init(); + + let matches = App::new("cargo-dadada") + .version(clap::crate_version!()) + .author(clap::crate_authors!()) + .about(clap::crate_description!()) + .arg( + Arg::with_name("no_css") + .long("no-css") + .required(false) + .help("Do not add CSS to output"), + ) + .arg( + Arg::with_name("no_js") + .long("no-js") + .required(false) + .help("Do not add Javascript to output"), + ) + .arg( + Arg::with_name("extra_meta") + .long("meta") + .value_name("FILE") + .help("extra meta to include in html head") + .takes_value(true), + ) + .arg( + Arg::with_name("extra_header") + .long("header") + .value_name("FILE") + .help("extra html/markdown to include on top of html body") + .takes_value(true), + ) + .arg( + Arg::with_name("extra_footer") + .long("footer") + .value_name("FILE") + .help("extra html/markdown to include at the end of html body") + .takes_value(true), + ) + .arg( + clap::Arg::with_name("manifest-path") + .long("manifest-path") + .value_name("PATH") + .takes_value(true), + ) + .arg( + Arg::with_name("split_package") + .long("split-package") + .required(false) + .help("whether to split output per package, defaults to false"), + ) + .arg( + Arg::with_name("split_example") + .long("split-example") + .required(false) + .help("whether to split output per example, defaults to false"), + ) + .arg( + Arg::with_name("output_directory") + .long("out-dir") + .value_name("PATH") + .required(true) + .help("output directory"), + ) + .get_matches(); + + let options = Options { + title: matches.value_of("title").unwrap_or("").to_string(), + with_css: !matches.is_present("no_css"), + with_js: !matches.is_present("no_js"), + extra_meta: matches.value_of("extra_meta").map(|s| s.to_string()), + extra_header: matches.value_of("extra_header").map(|s| s.to_string()), + extra_footer: matches.value_of("extra_footer").map(|s| s.to_string()), + }; + + let mut cmd = cargo_metadata::MetadataCommand::new(); + + if let Some(path) = matches.value_of("manifest-path") { + cmd.manifest_path(path); + } + + let metadata = cmd.exec().unwrap(); + + let members = metadata + .packages + .iter() + .filter(|p| metadata.workspace_members.iter().any(|m| *m == p.id)); + + let members = if matches.is_present("split_package") { + members + .map(|package| { + ( + package.name.to_owned(), + package + .targets + .iter() + .filter(|t| t.kind.iter().any(|s| s == "example")) + .collect::>(), + ) + }) + .collect() + } else { + vec![( + "index".to_owned(), + members + .map(|p| &p.targets) + .flatten() + .filter(|t| t.kind.iter().any(|s| s == "example")) + .collect::>(), + )] + }; + + let output_dir = std::path::Path::new(matches.value_of("output_directory").unwrap()); + + if !output_dir.exists() { + fs::create_dir_all(output_dir).unwrap(); + } else if output_dir.is_file() { + error!("output path {:?} is a file, aborting...", output_dir); + std::process::exit(1); + } else { + info!( + "output path {:?} already exists, files may be overwritten", + output_dir + ); + } + + members + .iter() + .filter(|(_, examples)| !examples.is_empty()) + .for_each(|(member, examples)| { + let inputs = examples.iter().map(|e| { + let mut blocks = extract(e.src_path.to_str().unwrap()); + let path = e.src_path.as_path(); + + let title = path + .file_name() + .expect("Must be a file") + .to_str() + .unwrap_or(""); + + let dir = path + .parent() + .map(|i| i.to_str().unwrap_or("")) + .unwrap_or(""); + + blocks.insert(0, Block::new_file(title, dir)); + + (e.name.to_owned(), blocks) + }); + + if matches.is_present("split_example") { + let scope_dir = output_dir.join(member); + + if !scope_dir.exists() { + fs::create_dir_all(scope_dir.clone()).unwrap(); + } else if scope_dir.is_file() { + error!(" output path {:?} is a file, aborting...", scope_dir); + std::process::exit(1); + } else { + info!( + " output path {:?} already exists, files may be overwritten", + scope_dir + ); + } + + inputs.for_each(|(name, blocks)| { + let path = scope_dir.join(format!("{}.html", name)); + let output = build_html(blocks, options.clone()); + + fs::write(path, output).expect("Could not write to output file.") + }); + } else { + let path = output_dir.join(format!("{}.html", member)); + let output = + build_html(inputs.map(|(_, blocks)| blocks).flatten(), options.clone()); + fs::write(path, output).expect("Could not write to output file.") + } + }); +} diff --git a/src/lib.rs b/src/lib.rs index 441bc91..20590a5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,9 @@ - +use pulldown_cmark::{html, Parser}; +use std::cmp::PartialEq; use std::fs::File; -use std::path::Path; -use std::io::{BufRead, Read, BufReader}; +use std::io::{BufRead, BufReader, Read}; use std::iter::IntoIterator; -use std::cmp::PartialEq; -use pulldown_cmark::{Parser, html}; +use std::path::Path; // `Block` stores code sections, consisting of comments and associated code. // We initialise a new block with empty `Vec` which will later be joined. @@ -15,6 +14,7 @@ pub struct Block { } /// Rendering Options +#[derive(Clone)] pub struct Options { /// HTML title to include pub title: String, @@ -32,7 +32,7 @@ pub struct Options { impl Block { pub fn new(starting_line: usize) -> Block { - return Block { + Block { comment: Vec::new(), code: Vec::new(), starting_line, @@ -40,7 +40,7 @@ impl Block { } pub fn new_file(title: &str, path: &str) -> Block { - return Block { + Block { comment: vec![format!("**`{:}`** (in `{:}`)", title, path)], code: vec![], starting_line: 0, @@ -48,8 +48,10 @@ impl Block { } pub fn has_code(&self) -> bool { - if self.code.len() == 0 { return false } - self.code.iter().find(|i| i.trim().len() > 0).is_some() + if self.code.is_empty() { + return false; + } + self.code.iter().any(|i| !i.trim().is_empty()) } } @@ -58,20 +60,19 @@ enum CommentType { Simple, Bang, Doc, - ANY + ANY, } // We divide the source code into code/comment blocks. // A `Vec` of `Block`s is returned for further processing. -pub fn extract(path: String) -> Vec { +pub fn extract(path: &str) -> Vec { let file = File::open(path).expect("Unable to open input file"); let mut process_as_code = false; - let mut current_comment_type : CommentType = CommentType::ANY; + let mut current_comment_type: CommentType = CommentType::ANY; let mut blocks: Vec = Vec::new(); let mut current_block = Block::new(1); - for (idx, line) in BufReader::new(file).lines().into_iter().enumerate() { - + for (idx, line) in BufReader::new(file).lines().enumerate() { let line_str = line.unwrap().to_string(); let stripped = line_str.trim(); @@ -91,19 +92,18 @@ pub fn extract(path: String) -> Vec { } else { let (strip_pos, com_type) = { if stripped.starts_with("///") { - (3, CommentType::Doc) + (3, CommentType::Doc) } else if stripped.starts_with("//!") { - (3, CommentType::Bang) + (3, CommentType::Bang) } else if stripped.starts_with("// !") { - (4, CommentType::Bang) + (4, CommentType::Bang) } else { - (2, CommentType::Simple) + (2, CommentType::Simple) } }; - + let line = stripped.split_at(strip_pos).1; - if current_comment_type != CommentType::ANY && - com_type != current_comment_type { + if current_comment_type != CommentType::ANY && com_type != current_comment_type { // different type of comment, means we assume a new block blocks.push(current_block); current_block = Block::new(idx + 1); @@ -113,20 +113,20 @@ pub fn extract(path: String) -> Vec { } } blocks.push(current_block); - return blocks; + blocks } // Build a full HTML document from a vector of blocks. // This function also inlines the CSS. -pub fn build_html>(blocks: I, options: Options) -> String { +pub fn build_html>(blocks: I, options: Options) -> String { let mut html_output = String::new(); - let include_static = |file : String, mut target: &mut String| { + let include_static = |file: String, mut target: &mut String| { let path = Path::new(&file); let is_md = if let Some(ext) = path.extension() { match ext.to_str() { Some("md") | Some("mdown") | Some("markdown") => true, - _ => false + _ => false, } } else { false @@ -138,12 +138,14 @@ pub fn build_html>(blocks: I, options: Options) -> S f.read_to_string(&mut source).expect("failed to read file"); html::push_html(&mut target, Parser::new(&source)); } else { - f.read_to_string(&mut target) - .expect("failed to read file"); + f.read_to_string(&mut target).expect("failed to read file"); }; }; - html_output.push_str(&format!(include_str!("static/head.html"), title=options.title)); + html_output.push_str(&format!( + include_str!("static/head.html"), + title = options.title + )); if options.with_css { html_output.push_str("