feat: add no-css and no-header-css flags for #19

refactor: change to yaml configuration for the CLI

refactor: change all flags to kebab case
This commit is contained in:
Kenneth Gitere 2021-07-22 08:47:03 +03:00
parent d67169425d
commit d1d1a0f3f4
8 changed files with 133 additions and 84 deletions

7
Cargo.lock generated
View file

@ -395,6 +395,7 @@ dependencies = [
"textwrap", "textwrap",
"unicode-width", "unicode-width",
"vec_map", "vec_map",
"yaml-rust",
] ]
[[package]] [[package]]
@ -2756,6 +2757,12 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "yaml-rust"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e66366e18dc58b46801afbf2ca7661a9f59cc8c5962c29892b6039b4f86fa992"
[[package]] [[package]]
name = "yansi" name = "yansi"
version = "0.5.0" version = "0.5.0"

View file

@ -15,7 +15,7 @@ readme = "README.md"
# atty = "0.2.14" # atty = "0.2.14"
async-std = "1.9.0" async-std = "1.9.0"
chrono = "0.4.19" chrono = "0.4.19"
clap = "2.33.3" clap = {version = "2.33.3", features = ["yaml"]}
colored = "2.0.0" colored = "2.0.0"
comfy-table = "3.0.0" comfy-table = "3.0.0"
derive_builder = "0.10.2" derive_builder = "0.10.2"

7
src/assets/body.min.css vendored Normal file
View file

@ -0,0 +1,7 @@
/*!
* Writ v1.0.4
*
* Copyright © 2015, Curtis McEnroe <curtis@cmcenroe.me>
*
* 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}

7
src/assets/headers.min.css vendored Normal file
View file

@ -0,0 +1,7 @@
/*!
* Writ v1.0.4
*
* Copyright © 2015, Curtis McEnroe <curtis@cmcenroe.me>
*
* 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}

View file

@ -1,7 +0,0 @@
/*!
* Writ v1.0.4
*
* Copyright © 2015, Curtis McEnroe <curtis@cmcenroe.me>
*
* 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}

View file

@ -1,7 +1,7 @@
use std::{fs, num::NonZeroUsize, path::Path}; use std::{fs, num::NonZeroUsize, path::Path};
use chrono::{DateTime, Local}; use chrono::{DateTime, Local};
use clap::{App, AppSettings, Arg, ArgMatches}; use clap::{load_yaml, App, ArgMatches};
use flexi_logger::LevelFilter as LogLevel; use flexi_logger::LevelFilter as LogLevel;
use itertools::Itertools; use itertools::Itertools;
@ -22,80 +22,13 @@ pub struct AppConfig {
pub start_time: DateTime<Local>, pub start_time: DateTime<Local>,
pub is_logging_to_file: bool, pub is_logging_to_file: bool,
pub inline_toc: bool, pub inline_toc: bool,
pub css_config: CSSConfig,
} }
impl AppConfig { impl AppConfig {
pub fn init_with_cli() -> Result<AppConfig, Error> { pub fn init_with_cli() -> Result<AppConfig, Error> {
let app = App::new("paperoni") let yaml_config = load_yaml!("cli_config.yml");
.settings(&[ let app = App::from_yaml(yaml_config).version(clap::crate_version!());
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")
);
Self::try_from(app.get_matches()) Self::try_from(app.get_matches())
} }
@ -159,7 +92,7 @@ impl<'a> TryFrom<ArgMatches<'a>> for AppConfig {
Some(max_conn) => max_conn.parse::<NonZeroUsize>()?.get(), Some(max_conn) => max_conn.parse::<NonZeroUsize>()?.get(),
None => DEFAULT_MAX_CONN, 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") { if name.ends_with(".epub") {
name.to_owned() name.to_owned()
} else { } else {
@ -200,6 +133,16 @@ impl<'a> TryFrom<ArgMatches<'a>> for AppConfig {
.transpose()?, .transpose()?,
) )
.start_time(Local::now()) .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() .try_init()
} }
} }
@ -212,3 +155,10 @@ impl AppConfigBuilder {
.init_merge_file() .init_merge_file()
} }
} }
#[derive(Clone, Debug)]
pub enum CSSConfig {
All,
NoHeaders,
None,
}

66
src/cli_config.yml Normal file
View file

@ -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

View file

@ -38,7 +38,8 @@ pub fn generate_epubs(
enabled_bar 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<PaperoniError> = Vec::new(); let mut errors: Vec<PaperoniError> = Vec::new();
@ -72,7 +73,7 @@ pub fn generate_epubs(
epub.inline_toc(); epub.inline_toc();
} }
match epub.stylesheet(stylesheet.as_bytes()) { match add_stylesheets(&mut epub, body_stylesheet, header_stylesheet, app_config) {
Ok(_) => (), Ok(_) => (),
Err(e) => { Err(e) => {
error!("Unable to add stylesheets to epub file"); error!("Unable to add stylesheets to epub file");
@ -188,8 +189,7 @@ pub fn generate_epubs(
epub.metadata("author", replace_escaped_characters(author))?; 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()); let title = replace_escaped_characters(article.metadata().title());
epub.metadata("title", &title)?; epub.metadata("title", &title)?;
@ -250,6 +250,25 @@ fn replace_escaped_characters(value: &str) -> String {
.replace(">", "&gt;") .replace(">", "&gt;")
} }
fn add_stylesheets<T: epub_builder::Zip>(
epub: &mut EpubBuilder<T>,
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 //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 { fn generate_appendix(articles: Vec<&Extractor>) -> String {
let link_tags: String = articles let link_tags: String = articles