diff --git a/Cargo.lock b/Cargo.lock index 3f8a7d9..13e8dfd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -136,7 +136,7 @@ dependencies = [ "semver 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", - "toml 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", + "toml 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-normalization 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "url 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -672,6 +672,7 @@ dependencies = [ "tar 0.4.14 (registry+https://github.com/rust-lang/crates.io-index)", "tempdir 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "time 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)", + "toml 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "walkdir 2.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "xdg 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -724,7 +725,7 @@ dependencies = [ [[package]] name = "toml" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "serde 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", @@ -937,7 +938,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096" "checksum thread_local 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "279ef31c19ededf577bfd12dfae728040a21f635b06a24cd670ff510edd38963" "checksum time 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "a15375f1df02096fb3317256ce2cee6a1f42fc84ea5ad5fc8c421cfe40c73098" -"checksum toml 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "a7540f4ffc193e0d3c94121edb19b055670d369f77d5804db11ae053a45b6e7e" +"checksum toml 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "a0263c6c02c4db6c8f7681f9fd35e90de799ebd4cfdeab77a38f4ff6b3d8c0d9" "checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" "checksum unicode-normalization 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "51ccda9ef9efa3f7ef5d91e8f9b83bbe6955f9bf86aec89d5cce2c874625920f" "checksum unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" diff --git a/Cargo.toml b/Cargo.toml index 619d888..f2c142b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ serde = "1.0.21" serde_derive = "1.0.21" tar = "0.4.14" time = "0.1.38" +toml = "0.4.6" walkdir = "2.0.1" xdg = "2.1.0" diff --git a/README.md b/README.md index 4c3a527..4e959a7 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,14 @@ A very fast implementation of [tldr](https://github.com/tldr-pages/tldr) in Rust: Simplified, example based and community-driven man pages. -![screenshot](screenshot.png) +Screenshot of tldr command If you pronounce "tldr" in English, it sounds somewhat like "tealdeer". Hence the project name :) In case you're in a hurry and just want to quickly try tealdeer, you can find static binaries on the [GitHub releases page](https://github.com/dbrgn/tealdeer/releases/)! + ## Goals High level project goals: @@ -54,6 +55,7 @@ These are the clients I tried but failed to compile or run: [Go client by anoopengineer](https://github.com/anoopengineer/tldr/), [PHP client](https://github.com/BrainMaestro/tldr-php). + ## Usage tldr [options] @@ -61,13 +63,15 @@ These are the clients I tried but failed to compile or run: Options: - -h --help Show this screen - -v --version Show version information - -l --list List all commands in the cache - -f --render Render a specific markdown file - -o --os Override the operating system [linux, osx, sunos] - -u --update Update the local cache - -c --clear-cache Clear the local cache + -h --help Show this screen + -v --version Show version information + -l --list List all commands in the cache + -f --render Render a specific markdown file + -o --os Override the operating system [linux, osx, sunos] + -u --update Update the local cache + -c --clear-cache Clear the local cache + --config-path Show config file path + --seed-config Create a basic config Examples: @@ -116,14 +120,58 @@ To enable the log output, set the `RUST_LOG` env variable: $ export RUST_LOG=tldr=debug - ### From AUR (Arch Linux) If you're an Arch Linux user, you can also install the package from the AUR: - $ yaourt -S tealdeer +- [tealdeer](https://aur.archlinux.org/packages/tealdeer/) +- [tealdeer-git](https://aur.archlinux.org/packages/tealdeer-git/) -(Or `tealdeer-git` if you prefer the current development version.) + +## Configuration + +The tldr command can be customized with a config file called `config.toml`. +Creating the config file can be done manually or with the help of tldr: + + $ tldr --seed-config + +The configuration file path follows OS conventions. It can be queried with the following command: + + $ tldr --config-path + +### Style + +Using the config file, the style (e.g. colors or underlines) can be customized. + +Possible styles: + +- `description`: The initial description text +- `command_name`: The command name as part of the example code +- `example_text`: The text that describes an example +- `example_code`: The example itself, except the `command_name` and `example_variable` +- `example_variable`: The variables in the example + +Currently supported attributes: + +- `foreground` (color string, see below) +- `background` (color string, see below) +- `underline` (`true` or `false`) +- `bold` (`true` or `false`) + +The currently supported colors are: + +- `black` +- `red` +- `green` +- `yellow` +- `blue` +- `purple` +- `cyan` +- `white` + +Example customization: + +Screenshot of customized version ## Bash Autocompletion diff --git a/bash_tealdeer b/bash_tealdeer index 315aee5..05d298e 100644 --- a/bash_tealdeer +++ b/bash_tealdeer @@ -6,7 +6,7 @@ _tealdeer() _init_completion || return case $prev in - -h|--help|-v|--version|-l|--list|-u|--update|-c|--clear-cache) + -h|--help|-v|--version|-l|--list|-u|--update|-c|--clear-cache|--config-path|--seed-config) return ;; -f|--render) diff --git a/screenshot-custom.png b/screenshot-custom.png new file mode 100644 index 0000000..ff0c78d Binary files /dev/null and b/screenshot-custom.png differ diff --git a/screenshot-default.png b/screenshot-default.png new file mode 100644 index 0000000..8a73de7 Binary files /dev/null and b/screenshot-default.png differ diff --git a/screenshot.png b/screenshot.png deleted file mode 100644 index 2aa51e8..0000000 Binary files a/screenshot.png and /dev/null differ diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..bbfc2d7 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,255 @@ +use std::env; +use std::fs; +use std::io::{Error as IoError, Read, Write}; +use std::path::PathBuf; + +use ansi_term::{Color, Style}; +use toml; +use xdg::BaseDirectories; + +use error::TealdeerError::{self, ConfigError}; + +pub const CONFIG_FILE_NAME: &'static str = "config.toml"; + +fn default_underline() -> bool { + false +} + +fn default_bold() -> bool { + false +} + +#[serde(rename_all = "lowercase")] +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub enum RawColor { + Black, + Red, + Green, + Yellow, + Blue, + Purple, + Cyan, + White, +} + +impl From for Color { + fn from(raw_color: RawColor) -> Color { + match raw_color { + RawColor::Black => Color::Black, + RawColor::Red => Color::Red, + RawColor::Green => Color::Green, + RawColor::Yellow => Color::Yellow, + RawColor::Blue => Color::Blue, + RawColor::Purple => Color::Purple, + RawColor::Cyan => Color::Cyan, + RawColor::White => Color::White, + } + } +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +struct RawStyle { + pub foreground: Option, + pub background: Option, + #[serde(default = "default_underline")] + pub underline: bool, + #[serde(default = "default_bold")] + pub bold: bool, +} + +impl Default for RawStyle { + fn default() -> RawStyle { + RawStyle{ + foreground: None, + background: None, + underline: false, + bold: false, + } + } +} // impl RawStyle + +impl From for Style { + fn from(raw_style: RawStyle) -> Style { + let mut style = Style::default(); + + if let Some(foreground) = raw_style.foreground { + style = style.fg(Color::from(foreground)); + } + + if let Some(background) = raw_style.background { + style = style.on(Color::from(background)); + } + + if raw_style.underline { + style = style.underline(); + } + + if raw_style.bold { + style = style.bold(); + } + + style + } +} + +#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +struct RawStyleConfig { + #[serde(default)] + pub description: RawStyle, + #[serde(default)] + pub command_name: RawStyle, + #[serde(default)] + pub example_text: RawStyle, + #[serde(default)] + pub example_code: RawStyle, + #[serde(default)] + pub example_variable: RawStyle, +} + + +#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +struct RawConfig { + style: RawStyleConfig, +} + +impl RawConfig { + fn new() -> RawConfig { + let mut raw_config = RawConfig::default(); + + // Set default config + raw_config.style.example_text.foreground = Some(RawColor::Green); + raw_config.style.command_name.foreground = Some(RawColor::Cyan); + raw_config.style.example_code.foreground = Some(RawColor::Cyan); + raw_config.style.example_variable.foreground = Some(RawColor::Cyan); + raw_config.style.example_variable.underline = true; + + raw_config + } +} // impl RawConfig + +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct StyleConfig { + pub description: Style, + pub command_name: Style, + pub example_text: Style, + pub example_code: Style, + pub example_variable: Style, +} + +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct Config { + pub style: StyleConfig, +} + +impl From for Config { + fn from(raw_config: RawConfig) -> Config { + Config{ + style: StyleConfig{ + command_name: raw_config.style.command_name.into(), + description: raw_config.style.description.into(), + example_text: raw_config.style.example_text.into(), + example_code: raw_config.style.example_code.into(), + example_variable: raw_config.style.example_variable.into(), + } + } + } +} + +fn map_io_err_to_config_err(e: IoError) -> TealdeerError { + ConfigError(format!("Io Error: {}", e)) +} + +impl Config { + pub fn load() -> Result { + let raw_config = match get_config_path() { + Ok(config_file_path) => { + let mut config_file = fs::File::open(config_file_path) + .map_err(map_io_err_to_config_err)?; + let mut contents = String::new(); + let _rc = config_file.read_to_string(&mut contents) + .map_err(map_io_err_to_config_err)?; + + toml::from_str(&contents).map_err(|err| ConfigError(format!("Failed to parse config file: {}", err)))? + } + Err(ConfigError(_)) => RawConfig::new(), + Err(_) => { + return Err(ConfigError("Unknown error while looking up config path".into())); + } + }; + + Ok(Config::from(raw_config)) + } +} // impl Config + +/// Return the path to the config directory. +pub fn get_config_dir() -> Result { + // Allow overriding the config directory by setting the + // $TEALDEER_CONFIG_DIR env variable. + if let Ok(value) = env::var("TEALDEER_CONFIG_DIR") { + let path = PathBuf::from(value); + + if path.exists() && path.is_dir() { + return Ok(path) + } else { + return Err(ConfigError( + "Path specified by $TEALDEER_CONFIG_DIR \ + does not exist or is not a directory.".into() + )); + } + }; + + // Otherwise, fall back to $XDG_CONFIG_HOME/tealdeer. + let xdg_dirs = match BaseDirectories::with_prefix(::NAME) { + Ok(dirs) => dirs, + Err(_) => return Err(ConfigError("Could not determine XDG base directory.".into())), + }; + Ok(xdg_dirs.get_config_home()) +} + +/// Return the path to the config file. +pub fn get_config_path() -> Result { + let config_dir = get_config_dir()?; + let config_file_path = config_dir.join(CONFIG_FILE_NAME); + + if config_file_path.is_file() { + Ok(config_file_path) + } else { + Err(ConfigError(format!("{} is not a file path", config_file_path.to_str().unwrap()))) + } +} + +/// Create default config file. +pub fn make_default_config() -> Result { + let config_dir = get_config_dir()?; + if !config_dir.is_dir() { + if let Err(e) = fs::create_dir_all(&config_dir) { + return Err(ConfigError(format!("Could not create config directory: {}", e))); + } + } + + let config_file_path = config_dir.join(CONFIG_FILE_NAME); + if config_file_path.is_file() { + return Err(ConfigError(format!( + "A configuration file already exists at {}, no action was taken.", + config_file_path.to_str().unwrap() + ))); + } + + let serialized_config = toml::to_string(&RawConfig::new()) + .map_err(|err| ConfigError(format!("Failed to serialize default config: {}", err)))?; + + let mut config_file = fs::File::create(&config_file_path) + .map_err(map_io_err_to_config_err)?; + let _wc = config_file.write(serialized_config.as_bytes()) + .map_err(map_io_err_to_config_err)?; + + Ok(config_file_path) +} + +#[test] +fn test_serialize_deserialize() { + let raw_config = RawConfig::new(); + let serialized = toml::to_string(&raw_config).unwrap(); + let deserialized: RawConfig = toml::from_str(&serialized).unwrap(); + assert_eq!(raw_config, deserialized); +} diff --git a/src/error.rs b/src/error.rs index f099eb2..8274008 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,6 +3,7 @@ use curl::Error as CurlError; #[derive(Debug)] pub enum TealdeerError { CacheError(String), + ConfigError(String), UpdateError(String), } diff --git a/src/formatter.rs b/src/formatter.rs index 4031132..2be62d1 100644 --- a/src/formatter.rs +++ b/src/formatter.rs @@ -2,35 +2,64 @@ use std::io::BufRead; -use ansi_term::{Colour, ANSIStrings}; +use ansi_term::{ANSIString, ANSIStrings}; +use config::Config; use tokenizer::Tokenizer; use types::LineType; -/// Provide formatting for {{ curly braces }} in ExampleCode lines -fn format_braces(text: &str) -> String { - let parts = text.split("{{").flat_map(|s| s.split("}}")) - .enumerate() - .map(|(i, v)| { - if i % 2 == 0 { - Colour::Cyan.paint(v) - } else { - Colour::Cyan.underline().paint(v) - } - }) - .collect::>(); +fn highlight_command<'a>( + command: &'a str, + example_code: &'a str, + config: &Config, + parts: &mut Vec> +) { + let mut code_part_end_pos = 0; + while let Some(command_start) = example_code[code_part_end_pos..].find(&command) { + let code_part = &example_code[code_part_end_pos..code_part_end_pos + command_start]; + parts.push(config.style.example_code.paint(code_part)); + parts.push(config.style.command_name.paint(command)); + + code_part_end_pos += command_start + command.len(); + } + parts.push(config.style.example_code.paint(&example_code[code_part_end_pos..])); +} + +/// Format and highlight code examples including variables in {{ curly braces }}. +fn format_code(command: &str, text: &str, config: &Config) -> String { + let mut parts = Vec::new(); + for between_variables in text.split("}}") { + if let Some(variable_start) = between_variables.find("{{") { + let example_code = &between_variables[..variable_start]; + let example_variable = &between_variables[variable_start + 2..]; + + highlight_command(&command, &example_code, &config, &mut parts); + parts.push(config.style.example_variable.paint(example_variable)); + } else { + highlight_command(&command, &between_variables, &config, &mut parts); + } + } + ANSIStrings(&parts).to_string() } /// Print a token stream to an ANSI terminal. -pub fn print_lines(tokenizer: &mut Tokenizer) where R: BufRead { +pub fn print_lines(tokenizer: &mut Tokenizer, config: &Config) where R: BufRead { + let mut command = String::new(); while let Some(token) = tokenizer.next_token() { match token { LineType::Empty => println!(""), - LineType::Title(_) => debug!("Ignoring title"), - LineType::Description(text) => println!(" {}", text), - LineType::ExampleText(text) => println!(" {}", Colour::Green.paint(text)), - LineType::ExampleCode(text) => println!(" {}", &format_braces(&text)), + LineType::Title(title) => { + debug!("Ignoring title"); + + // This is safe as long as the parsed title is only the command, + // and tokenizer yields values in order of appearance. + command = title; + debug!("Detected command name: {}", &command); + }, + LineType::Description(text) => println!(" {}", config.style.description.paint(text)), + LineType::ExampleText(text) => println!(" {}", config.style.example_text.paint(text)), + LineType::ExampleCode(text) => println!(" {}", &format_code(&command, &text, &config)), LineType::Other(text) => debug!("Unknown line type: {:?}", text), } } diff --git a/src/main.rs b/src/main.rs index c40e62f..9ce2d32 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,6 +31,7 @@ extern crate tar; extern crate xdg; extern crate curl; extern crate time; +extern crate toml; extern crate walkdir; #[macro_use] extern crate serde_derive; @@ -41,17 +42,19 @@ use std::path::{Path, PathBuf}; use std::process; use docopt::Docopt; -use ansi_term::Colour; +use ansi_term::Color; mod types; mod tokenizer; mod formatter; mod cache; +mod config; mod error; use tokenizer::Tokenizer; use cache::Cache; -use error::TealdeerError::{UpdateError, CacheError}; +use config::{get_config_path, make_default_config, Config}; +use error::TealdeerError::{CacheError, ConfigError, UpdateError}; use formatter::print_lines; use types::OsType; @@ -72,6 +75,8 @@ Options: -o --os Override the operating system [linux, osx, sunos] -u --update Update the local cache -c --clear-cache Clear the local cache + --config-path Show config file path + --seed-config Create a basic config Examples: @@ -100,6 +105,8 @@ struct Args { flag_os: Option, flag_update: bool, flag_clear_cache: bool, + flag_config_path: bool, + flag_seed_config: bool, } /// Print page by path @@ -110,9 +117,22 @@ fn print_page(path: &Path) -> Result<(), String> { ); let reader = BufReader::new(file); + // Look up config file, if none is found fall back to default config. + let config = match Config::load() { + Ok(config) => config, + Err(ConfigError(msg)) => { + eprintln!("Failed to create config: {}", msg); + process::exit(1); + } + Err(_) => { + eprintln!("Unknown error while creating config"); + process::exit(1); + } + }; + // Create tokenizer and print output let mut tokenizer = Tokenizer::new(reader); - print_lines(&mut tokenizer); + print_lines(&mut tokenizer, &config); Ok(()) } @@ -122,14 +142,14 @@ fn check_cache(args: &Args, cache: &Cache) { if !args.flag_update { match cache.last_update() { Some(ago) if ago > MAX_CACHE_AGE => { - println!("{}", Colour::Red.paint(format!( + println!("{}", Color::Red.paint(format!( "Cache wasn't updated in {} days.\n\ - You should probably run `tldr --update` soon.\n", + You should probably run `tldr --update` soon.", MAX_CACHE_AGE / 24 / 3600 ))); }, None => { - println!("Cache not found. Please run `tldr --update`."); + eprintln!("Cache not found. Please run `tldr --update`."); process::exit(1); }, _ => {}, @@ -182,7 +202,8 @@ fn main() { if args.flag_clear_cache { cache.clear().unwrap_or_else(|e| { match e { - UpdateError(msg) | CacheError(msg) => println!("Could not delete cache: {}", msg), + CacheError(msg) | ConfigError(msg) | UpdateError(msg) => + eprintln!("Could not delete cache: {}", msg), }; process::exit(1); }); @@ -193,7 +214,8 @@ fn main() { if args.flag_update { cache.update().unwrap_or_else(|e| { match e { - UpdateError(msg) | CacheError(msg) => println!("Could not update cache: {}", msg), + CacheError(msg) | ConfigError(msg) | UpdateError(msg) => + eprintln!("Could not update cache: {}", msg), }; process::exit(1); }); @@ -204,7 +226,7 @@ fn main() { if let Some(ref file) = args.flag_render { let path = PathBuf::from(file); if let Err(msg) = print_page(&path) { - println!("{}", msg); + eprintln!("{}", msg); process::exit(1); } else { process::exit(0); @@ -219,7 +241,8 @@ fn main() { // Get list of pages let pages = cache.list_pages().unwrap_or_else(|e| { match e { - UpdateError(msg) | CacheError(msg) => println!("Could not get list of pages: {}", msg), + CacheError(msg) | ConfigError(msg) | UpdateError(msg) => + eprintln!("Could not get list of pages: {}", msg), } process::exit(1); }); @@ -237,7 +260,7 @@ fn main() { // Search for command in cache if let Some(path) = cache.find_page(&command) { if let Err(msg) = print_page(&path) { - println!("{}", msg); + eprintln!("{}", msg); process::exit(1); } else { process::exit(0); @@ -245,14 +268,50 @@ fn main() { } else { println!("Page {} not found in cache", &command); println!("Try updating with `tldr --update`, or submit a pull request to:"); - println!("https://github.com/tldr-pages/tldr"); + eprintln!("https://github.com/tldr-pages/tldr"); process::exit(1); } } + // Show config file and path and exit + if args.flag_config_path { + match get_config_path() { + Ok(config_file_path) => { + println!("Config path is: {}", config_file_path.to_str().unwrap()); + process::exit(0); + }, + Err(ConfigError(msg)) => { + eprintln!("Could not look up config_path: {}", msg); + process::exit(1); + }, + Err(_) => { + eprintln!("Unknown error"); + process::exit(1); + }, + } + } + + // Create a basic config and exit + if args.flag_seed_config { + match make_default_config() { + Ok(config_file_path) => { + println!("Successfully created seed config file here: {}", config_file_path.to_str().unwrap()); + process::exit(0); + }, + Err(ConfigError(msg)) => { + eprintln!("Could not create seed config: {}", msg); + process::exit(1); + }, + Err(_) => { + eprintln!("Unkown error"); + process::exit(1); + }, + } + } + // Some flags can be run without a command. if !(args.flag_update || args.flag_clear_cache) { - println!("{}", USAGE); + eprintln!("{}", USAGE); process::exit(1); } } diff --git a/tests/config.toml b/tests/config.toml new file mode 100644 index 0000000..7440a82 --- /dev/null +++ b/tests/config.toml @@ -0,0 +1,17 @@ +[style.highlight] +foreground = "green" +underline = false +bold = false + +[style.description] +underline = false +bold = false + +[style.example_text] +foreground = "black" +background = "blue" +underline = false + +[style.example_variable] +underline = true +bold = false diff --git a/tests/inkscape.expected b/tests/inkscape-default.expected similarity index 87% rename from tests/inkscape.expected rename to tests/inkscape-default.expected index 7aed257..c7061b3 100644 --- a/tests/inkscape.expected +++ b/tests/inkscape-default.expected @@ -20,7 +20,7 @@ Export an SVG document to PDF, converting all texts to paths: - inkscape filename.svg --export-pdf=filename.pdf --export-text-to-path + inkscape filename.svg | inkscape | inkscape --export-pdf=inkscape.pdf | inkscape | inkscape --export-text-to-path Duplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape: diff --git a/tests/inkscape-v1.md b/tests/inkscape-v1.md index dfa30dc..4071196 100644 --- a/tests/inkscape-v1.md +++ b/tests/inkscape-v1.md @@ -21,7 +21,7 @@ - Export an SVG document to PDF, converting all texts to paths: -`inkscape {{filename.svg}} --export-pdf={{filename.pdf}} --export-text-to-path` +`inkscape {{filename.svg}} | inkscape | inkscape --export-pdf={{inkscape.pdf}} | inkscape | inkscape --export-text-to-path` - Duplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape: diff --git a/tests/inkscape-v2.md b/tests/inkscape-v2.md index 5772c89..3051867 100644 --- a/tests/inkscape-v2.md +++ b/tests/inkscape-v2.md @@ -22,7 +22,7 @@ Export a single object, given its ID, into a bitmap: Export an SVG document to PDF, converting all texts to paths: - inkscape {{filename.svg}} --export-pdf={{filename.pdf}} --export-text-to-path + inkscape {{filename.svg}} | inkscape | inkscape --export-pdf={{inkscape.pdf}} | inkscape | inkscape --export-text-to-path Duplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape: diff --git a/tests/inkscape-with-config.expected b/tests/inkscape-with-config.expected new file mode 100644 index 0000000..313e140 --- /dev/null +++ b/tests/inkscape-with-config.expected @@ -0,0 +1,28 @@ + + An SVG (Scalable Vector Graphics) editing program. + Use -z to not open the GUI and only process files in the console. + + Open an SVG file in the Inkscape GUI: + + inkscape filename.svg + + Export an SVG file into a bitmap with the default format (PNG) and the default resolution (90 DPI): + + inkscape filename.svg -e filename.png + + Export an SVG file into a bitmap of 600x400 pixels (aspect ratio distortion may occur): + + inkscape filename.svg -e filename.png -w 600 -h 400 + + Export a single object, given its ID, into a bitmap: + + inkscape filename.svg -i id -e object.png + + Export an SVG document to PDF, converting all texts to paths: + + inkscape filename.svg | inkscape | inkscape --export-pdf=inkscape.pdf | inkscape | inkscape --export-text-to-path + + Duplicate the object with id="path123", rotate the duplicate 90 degrees, save the file, and quit Inkscape: + + inkscape filename.svg --select=path123 --verb=EditDuplicate --verb=ObjectRotate90 --verb=FileSave --verb=FileQuit + diff --git a/tests/lib.rs b/tests/lib.rs index 79a12ec..6c3f51b 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -11,21 +11,25 @@ use tempdir::TempDir; struct TestEnv { pub cache_dir: TempDir, + pub config_dir: TempDir, pub input_dir: TempDir, } impl TestEnv { fn new() -> Self { - let cache_dir = TempDir::new(".tldr.test.cache").unwrap(); - let input_dir = TempDir::new(".tldr.test.input").unwrap(); - TestEnv { cache_dir, input_dir } + TestEnv { + cache_dir: TempDir::new(".tldr.test.cache").unwrap(), + config_dir: TempDir::new(".tldr.test.config").unwrap(), + input_dir: TempDir::new(".tldr.test.input").unwrap(), + } } /// Return a new [`Assert`](../assert_cli/struct.Assert.html) instance for /// the main binary with env vars set. fn assert(&self) -> Assert { let env = Environment::inherit() - .insert("TEALDEER_CACHE_DIR", self.cache_dir.path().to_str().unwrap()); + .insert("TEALDEER_CACHE_DIR", self.cache_dir.path().to_str().unwrap()) + .insert("TEALDEER_CONFIG_DIR", self.config_dir.path().to_str().unwrap()); Assert::main_binary() .with_env(env) } @@ -37,7 +41,7 @@ fn test_missing_cache() { .assert() .with_args(&["sl"]) .fails() - .stdout().contains("Cache not found. Please run `tldr --update`.") + .stderr().contains("Cache not found. Please run `tldr --update`.") .unwrap(); } @@ -48,7 +52,7 @@ fn test_update_cache() { testenv.assert() .with_args(&["sl"]) .fails() - .stdout().contains("Cache not found. Please run `tldr --update`.") + .stderr().contains("Cache not found. Please run `tldr --update`.") .unwrap(); testenv.assert() @@ -63,6 +67,17 @@ fn test_update_cache() { .unwrap(); } +#[test] +fn test_setup_seed_config() { + let testenv = TestEnv::new(); + + testenv.assert() + .with_args(&["--seed-config"]) + .succeeds() + .stdout().contains("Successfully created seed config file") + .unwrap(); +} + fn _test_correct_rendering(input_file: &str, filename: &str) { let testenv = TestEnv::new(); @@ -73,7 +88,7 @@ fn _test_correct_rendering(input_file: &str, filename: &str) { file.write_all(input_file.as_bytes()).unwrap(); // Load expected output - let expected = include_str!("inkscape.expected"); + let expected = include_str!("inkscape-default.expected"); testenv.assert() .with_args(&["-f", &file_path.to_str().unwrap()]) @@ -93,3 +108,33 @@ fn test_correct_rendering_v1() { fn test_correct_rendering_v2() { _test_correct_rendering(include_str!("inkscape-v2.md"), "inkscape-v2.md"); } + +/// An end-to-end integration test for rendering with constom syntax config. +#[test] +fn test_correct_rendering_with_config() { + let testenv = TestEnv::new(); + + // Setup config file + // TODO should be config::CONFIG_FILE_NAME + let config_file_path = testenv.config_dir.path().join("config.toml"); + println!("Config path: {:?}", &config_file_path); + + let mut config_file = File::create(&config_file_path).unwrap(); + config_file.write(include_str!("config.toml").as_bytes()).unwrap(); + + // Create input file + let file_path = testenv.input_dir.path().join("inkscape-v2.md"); + println!("Testfile path: {:?}", &file_path); + + let mut file = File::create(&file_path).unwrap(); + file.write_all(include_str!("inkscape-v2.md").as_bytes()).unwrap(); + + // Load expected output + let expected = include_str!("inkscape-with-config.expected"); + + testenv.assert() + .with_args(&["-f", &file_path.to_str().unwrap()]) + .succeeds() + .stdout().is(expected) + .unwrap(); +}