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:
parent
d67169425d
commit
d1d1a0f3f4
8 changed files with 133 additions and 84 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -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"
|
||||||
|
|
|
@ -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
7
src/assets/body.min.css
vendored
Normal 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
7
src/assets/headers.min.css
vendored
Normal 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}
|
7
src/assets/writ.min.css
vendored
7
src/assets/writ.min.css
vendored
|
@ -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}
|
|
94
src/cli.rs
94
src/cli.rs
|
@ -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
66
src/cli_config.yml
Normal 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
|
27
src/epub.rs
27
src/epub.rs
|
@ -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(">", ">")
|
.replace(">", ">")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
Loading…
Reference in a new issue