diff --git a/Cargo.lock b/Cargo.lock index 8489e38..8a52a90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -395,6 +395,7 @@ dependencies = [ "textwrap", "unicode-width", "vec_map", + "yaml-rust", ] [[package]] @@ -2756,6 +2757,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "yaml-rust" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e66366e18dc58b46801afbf2ca7661a9f59cc8c5962c29892b6039b4f86fa992" + [[package]] name = "yansi" version = "0.5.0" diff --git a/Cargo.toml b/Cargo.toml index 3594149..bde8df1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ readme = "README.md" # atty = "0.2.14" async-std = "1.9.0" chrono = "0.4.19" -clap = "2.33.3" +clap = {version = "2.33.3", features = ["yaml"]} colored = "2.0.0" comfy-table = "3.0.0" derive_builder = "0.10.2" diff --git a/src/assets/body.min.css b/src/assets/body.min.css new file mode 100644 index 0000000..4e17cec --- /dev/null +++ b/src/assets/body.min.css @@ -0,0 +1,7 @@ +/*! + * Writ v1.0.4 + * + * Copyright © 2015, Curtis McEnroe + * + * https://cmcenroe.me/writ/LICENSE (ISC) + */dd,hr,ol ol,ol ul,ul ol,ul ul{margin:0}pre,table{overflow-x:auto}a,ins{text-decoration:none}html{font-family:Georgia,Lucida Bright,Book Antiqua,serif;font-size:16px;line-height:1.5rem}code,kbd,pre,samp{font-family:Fira Code,Liberation Mono,Menlo,Courier,monospace;font-size:.833rem;color:#111}kbd{font-weight:700}small{font-size:.833em}th{font-weight:400}blockquote,dl,ol,p,pre,table,ul{margin:1.5rem 0 0}pre,table{margin-bottom:-1px}hr{border:none;padding:1.5rem 0 0}table{line-height:calc(1.5rem - 1px);width:100%;border-collapse:collapse}pre{margin-top:calc(1.5rem - 1px)}body{color:#222;margin:1.5rem 1ch}a,a code,header nav a:visited{color:#00e}a:visited,a:visited code{color:#60b}mark{color:inherit;background-color:#fe0}code,pre,samp,tfoot,thead{background-color:rgba(0,0,0,.05)}blockquote,ins,main aside{border:rgba(0,0,0,.05) solid}blockquote,main aside{border-width:0 0 0 .5ch}code,pre,samp{border:rgba(0,0,0,.1) solid}td,th{border:solid #dbdbdb}body>header{text-align:center}body>footer,main{display:block;max-width:78ch;margin:auto}main aside,main figure{float:right;margin:1.5rem 0 0 1ch}main aside{max-width:26ch;padding:0 0 0 .5ch}blockquote{margin-right:3ch;margin-left:1.5ch;padding:0 0 0 1ch}pre{border-width:1px;border-radius:2px;padding:0 .5ch}pre code{border:none;padding:0;background-color:transparent;white-space:inherit}code,ins,samp,td,th{border-width:1px}img{max-width:100%}dd,ol,ul{padding:0 0 0 3ch}ul>li{list-style-type:disc}li ul>li{list-style-type:circle}li li ul>li{list-style-type:square}ol>li{list-style-type:decimal}li ol>li{list-style-type:lower-roman}li li ol>li{list-style-type:lower-alpha}nav ul{padding:0;list-style-type:none}nav ul li{display:inline;padding-left:1ch;white-space:nowrap}nav ul li:first-child{padding-left:0}ins,mark{padding:1px}td,th{padding:0 .5ch}sub,sup{font-size:.75em;line-height:1em}code,samp{border-radius:2px;padding:.1em .2em;white-space:nowrap} \ No newline at end of file diff --git a/src/assets/headers.min.css b/src/assets/headers.min.css new file mode 100644 index 0000000..ca3056c --- /dev/null +++ b/src/assets/headers.min.css @@ -0,0 +1,7 @@ +/*! + * Writ v1.0.4 + * + * Copyright © 2015, Curtis McEnroe + * + * https://cmcenroe.me/writ/LICENSE (ISC) + */h1,h2,h3,h4,h5,h6,th{font-weight:400}h1{font-size:2.488em}h2{font-size:2.074em}h3{font-size:1.728em}h4{font-size:1.44em}h5{font-size:1.2em}h6{font-size:1em}h1,h2,h3{line-height:3rem}h1,h2,h3,h4,h5,h6{margin:1.5rem 0 0} \ No newline at end of file diff --git a/src/assets/writ.min.css b/src/assets/writ.min.css deleted file mode 100644 index 1c9c0b4..0000000 --- a/src/assets/writ.min.css +++ /dev/null @@ -1,7 +0,0 @@ -/*! - * Writ v1.0.4 - * - * Copyright © 2015, Curtis McEnroe - * - * https://cmcenroe.me/writ/LICENSE (ISC) - */dd,hr,ol ol,ol ul,ul ol,ul ul{margin:0}pre,table{overflow-x:auto}a,ins{text-decoration:none}html{font-family:Georgia,Lucida Bright,Book Antiqua,serif;font-size:16px;line-height:1.5rem}code,kbd,pre,samp{font-family:Fira Code,Liberation Mono,Menlo,Courier,monospace;font-size:.833rem;color:#111}kbd{font-weight:700}h1,h2,h3,h4,h5,h6,th{font-weight:400}h1{font-size:2.488em}h2{font-size:2.074em}h3{font-size:1.728em}h4{font-size:1.44em}h5{font-size:1.2em}h6{font-size:1em}small{font-size:.833em}h1,h2,h3{line-height:3rem}blockquote,dl,h1,h2,h3,h4,h5,h6,ol,p,pre,table,ul{margin:1.5rem 0 0}pre,table{margin-bottom:-1px}hr{border:none;padding:1.5rem 0 0}table{line-height:calc(1.5rem - 1px);width:100%;border-collapse:collapse}pre{margin-top:calc(1.5rem - 1px)}body{color:#222;margin:1.5rem 1ch}a,a code,header nav a:visited{color:#00e}a:visited,a:visited code{color:#60b}mark{color:inherit;background-color:#fe0}code,pre,samp,tfoot,thead{background-color:rgba(0,0,0,.05)}blockquote,ins,main aside{border:rgba(0,0,0,.05) solid}blockquote,main aside{border-width:0 0 0 .5ch}code,pre,samp{border:rgba(0,0,0,.1) solid}td,th{border:solid #dbdbdb}body>header{text-align:center}body>footer,main{display:block;max-width:78ch;margin:auto}main aside,main figure{float:right;margin:1.5rem 0 0 1ch}main aside{max-width:26ch;padding:0 0 0 .5ch}blockquote{margin-right:3ch;margin-left:1.5ch;padding:0 0 0 1ch}pre{border-width:1px;border-radius:2px;padding:0 .5ch}pre code{border:none;padding:0;background-color:transparent;white-space:inherit}code,ins,samp,td,th{border-width:1px}img{max-width:100%}dd,ol,ul{padding:0 0 0 3ch}ul>li{list-style-type:disc}li ul>li{list-style-type:circle}li li ul>li{list-style-type:square}ol>li{list-style-type:decimal}li ol>li{list-style-type:lower-roman}li li ol>li{list-style-type:lower-alpha}nav ul{padding:0;list-style-type:none}nav ul li{display:inline;padding-left:1ch;white-space:nowrap}nav ul li:first-child{padding-left:0}ins,mark{padding:1px}td,th{padding:0 .5ch}sub,sup{font-size:.75em;line-height:1em}code,samp{border-radius:2px;padding:.1em .2em;white-space:nowrap} diff --git a/src/cli.rs b/src/cli.rs index 62937e7..c66f0de 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,7 +1,7 @@ use std::{fs, num::NonZeroUsize, path::Path}; use chrono::{DateTime, Local}; -use clap::{App, AppSettings, Arg, ArgMatches}; +use clap::{load_yaml, App, ArgMatches}; use flexi_logger::LevelFilter as LogLevel; use itertools::Itertools; @@ -22,80 +22,13 @@ pub struct AppConfig { pub start_time: DateTime, pub is_logging_to_file: bool, pub inline_toc: bool, + pub css_config: CSSConfig, } impl AppConfig { pub fn init_with_cli() -> Result { - let app = App::new("paperoni") - .settings(&[ - AppSettings::ArgRequiredElseHelp, - AppSettings::UnifiedHelpMessage, - ]) - .version(clap::crate_version!()) - .about( - "Paperoni is a CLI tool made in Rust for downloading web articles as EPUBs", - ) - .arg( - Arg::with_name("urls") - .help("Urls of web articles") - .multiple(true), - ) - .arg( - Arg::with_name("file") - .short("f") - .long("file") - .help("Input file containing links") - .takes_value(true), - ) - .arg( - Arg::with_name("output_directory") - .long("output-dir") - .short("o") - .help("Directory to store output epub documents") - .conflicts_with("output_name") - .takes_value(true), - ) - .arg( - Arg::with_name("output_name") - .long("merge") - .help("Merge multiple articles into a single epub") - .long_help("Merge multiple articles into a single epub that will be given the name provided") - .conflicts_with("output_directory") - .takes_value(true), - ).arg( - Arg::with_name("max-conn") - .long("max_conn") - .help("The maximum number of concurrent HTTP connections when downloading articles. Default is 8") - .long_help("The maximum number of concurrent HTTP connections when downloading articles. Default is 8.\nNOTE: It is advised to use as few connections as needed i.e between 1 and 50. Using more connections can end up overloading your network card with too many concurrent requests.") - .takes_value(true)) - .arg( - Arg::with_name("verbosity") - .short("v") - .multiple(true) - .help("Enables logging of events and set the verbosity level. Use --help to read on its usage") - .long_help( -"This takes upto 4 levels of verbosity in the following order. - - Error (-v) - - Warn (-vv) - - Info (-vvv) - - Debug (-vvvv) - When this flag is passed, it disables the progress bars and logs to stderr. - If you would like to send the logs to a file (and enable progress bars), pass the log-to-file flag." - ) - .takes_value(false)) - .arg( - Arg::with_name("log-to-file") - .long("log-to-file") - .help("Enables logging of events to a file located in .paperoni/logs with a default log level of debug. Use -v to specify the logging level") - .takes_value(false)) - .arg( - Arg::with_name("inline-toc") - .long("inline-toc") - .requires("output_name") - .help("Add an inlined Table of Contents page at the start of the merged article.") - .long_help("Add an inlined Table of Contents page at the start of the merged article. This does not affect the Table of Contents navigation") - ); - + let yaml_config = load_yaml!("cli_config.yml"); + let app = App::from_yaml(yaml_config).version(clap::crate_version!()); Self::try_from(app.get_matches()) } @@ -159,7 +92,7 @@ impl<'a> TryFrom> for AppConfig { Some(max_conn) => max_conn.parse::()?.get(), None => DEFAULT_MAX_CONN, }) - .merged(arg_matches.value_of("output_name").map(|name| { + .merged(arg_matches.value_of("output-name").map(|name| { if name.ends_with(".epub") { name.to_owned() } else { @@ -200,6 +133,16 @@ impl<'a> TryFrom> for AppConfig { .transpose()?, ) .start_time(Local::now()) + .css_config( + match ( + arg_matches.is_present("no-css"), + arg_matches.is_present("no-header-css"), + ) { + (true, _) => CSSConfig::None, + (_, true) => CSSConfig::NoHeaders, + _ => CSSConfig::All, + }, + ) .try_init() } } @@ -212,3 +155,10 @@ impl AppConfigBuilder { .init_merge_file() } } + +#[derive(Clone, Debug)] +pub enum CSSConfig { + All, + NoHeaders, + None, +} diff --git a/src/cli_config.yml b/src/cli_config.yml new file mode 100644 index 0000000..a1d9424 --- /dev/null +++ b/src/cli_config.yml @@ -0,0 +1,66 @@ +name: paperoni +about: Paperoni is a CLI tool made in Rust for downloading web articles as EPUBs +settings: + - ArgRequiredElseHelp + - UnifiedHelpMessage +args: + - urls: + help: Urls of web articles + multiple: true + - file: + short: f + long: file + help: Input file containing links + takes_value: true + - output_directory: + short: o + long: output-dir + help: Directory to store output epub documents + conflicts_with: output-name + takes_value: true + - output-name: + long: merge + help: Merge multiple articles into a single epub + long_help: Merge multiple articles into a single epub that will be given the name provided + conflicts_with: output_directory + takes_value: true + - max-conn: + long: max-conn + help: The maximum number of concurrent HTTP connections when downloading articles. Default is 8 + long_help: "The maximum number of concurrent HTTP connections when downloading articles. Default is 8.\nNOTE: It is advised to use as few connections as needed i.e between 1 and 50. Using more connections can end up overloading your network card with too many concurrent requests." + takes_value: true + - verbosity: + short: v + multiple: true + help: Enables logging of events and set the verbosity level. Use --help to read on its usage + long_help: "This takes upto 4 levels of verbosity in the following order. + \n- Error (-v) + \n- Warn (-vv) + \n- Info (-vvv) + \n- Debug (-vvvv) + \nWhen this flag is passed, it disables the progress bars and logs to stderr. + \nIf you would like to send the logs to a file (and enable progress bars), pass the log-to-file flag." + takes_value: false + - log-to-file: + long: log-to-file + help: Enables logging of events to a file located in .paperoni/logs with a default log level of debug. Use -v to specify the logging level + takes_value: false + - inline-toc: + long: inline-toc + requires: output-name + help: Add an inlined Table of Contents page at the start of the merged article. + long_help: Add an inlined Table of Contents page at the start of the merged article. This does not affect the Table of Contents navigation" + - no-css: + long: no-css + conflicts_with: no-header-css + help: Removes the stylesheets used in the EPUB generation. Pass --help to learn more + long_help: "Removes the stylesheets used in the EPUB generation. + \nThe EPUB file will then be laid out based on your e-reader's default stylesheets. + \nImages and code blocks may overflow when this flag is set and layout of generated + \nPDFs will be affected. Use --no-header-css if you want to only disable the styling on headers." + takes_value: false + - no-header-css: + long: no-header-css + conflicts_with: no-css + help: Removes the header CSS styling but preserves styling of images and codeblocks. To remove all the default CSS, use --no-css instead. + takes_value: false diff --git a/src/epub.rs b/src/epub.rs index 79f8689..c522470 100644 --- a/src/epub.rs +++ b/src/epub.rs @@ -38,7 +38,8 @@ pub fn generate_epubs( enabled_bar }; - let stylesheet = include_bytes!("./assets/writ.min.css"); + let body_stylesheet = include_bytes!("./assets/body.min.css"); + let header_stylesheet = include_bytes!("./assets/headers.min.css"); let mut errors: Vec = Vec::new(); @@ -72,7 +73,7 @@ pub fn generate_epubs( epub.inline_toc(); } - match epub.stylesheet(stylesheet.as_bytes()) { + match add_stylesheets(&mut epub, body_stylesheet, header_stylesheet, app_config) { Ok(_) => (), Err(e) => { error!("Unable to add stylesheets to epub file"); @@ -188,8 +189,7 @@ pub fn generate_epubs( epub.metadata("author", replace_escaped_characters(author))?; } - epub.stylesheet(stylesheet.as_bytes())?; - + add_stylesheets(&mut epub, body_stylesheet, header_stylesheet, app_config)?; let title = replace_escaped_characters(article.metadata().title()); epub.metadata("title", &title)?; @@ -250,6 +250,25 @@ fn replace_escaped_characters(value: &str) -> String { .replace(">", ">") } +fn add_stylesheets( + epub: &mut EpubBuilder, + body_stylesheet: &[u8], + header_stylesheet: &[u8], + app_config: &AppConfig, +) -> Result<(), epub_builder::Error> { + match app_config.css_config { + crate::cli::CSSConfig::All => { + epub.stylesheet([header_stylesheet, body_stylesheet].concat().as_bytes())?; + Ok(()) + } + crate::cli::CSSConfig::NoHeaders => { + epub.stylesheet(body_stylesheet.as_bytes())?; + Ok(()) + } + _ => Ok(()), + } +} + //TODO: The type signature of the argument should change as it requires that merged articles create an entirely new Vec of references fn generate_appendix(articles: Vec<&Extractor>) -> String { let link_tags: String = articles