use crate::commands::WholeStreamCommand; use crate::data::value::format_leaf; use crate::prelude::*; use futures::StreamExt; use nu_errors::ShellError; use nu_protocol::{Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value}; use nu_source::{AnchorLocation, Tagged}; use regex::Regex; use std::collections::HashMap; pub struct ToHTML; #[derive(Deserialize)] pub struct ToHTMLArgs { html_color: bool, no_color: bool, dark: bool, theme: Option>, } #[async_trait] impl WholeStreamCommand for ToHTML { fn name(&self) -> &str { "to html" } fn signature(&self) -> Signature { Signature::build("to html") .switch("html_color", "change ansi colors to html colors", Some('c')) .switch("no_color", "remove all ansi colors in output", Some('n')) .switch( "dark", "indicate your background color is a darker color", Some('d'), ) .named( "theme", SyntaxShape::String, "the name of the theme to use (default, campbell, github, blulocolight)", Some('t'), ) } fn usage(&self) -> &str { "Convert table into simple HTML" } async fn run( &self, args: CommandArgs, registry: &CommandRegistry, ) -> Result { to_html(args, registry).await } } fn get_campbell_theme(is_dark: bool) -> HashMap<&'static str, String> { // for reference here is Microsoft's Campbell Theme // taken from here // https://docs.microsoft.com/en-us/windows/terminal/customize-settings/color-schemes let mut hm: HashMap<&str, String> = HashMap::new(); hm.insert("bold_black", "#767676".to_string()); hm.insert("bold_red", "#E74856".to_string()); hm.insert("bold_green", "#16C60C".to_string()); hm.insert("bold_yellow", "#F9F1A5".to_string()); hm.insert("bold_blue", "#3B78FF".to_string()); hm.insert("bold_magenta", "#B4009E".to_string()); hm.insert("bold_cyan", "#61D6D6".to_string()); hm.insert("bold_white", "#F2F2F2".to_string()); hm.insert("black", "#0C0C0C".to_string()); hm.insert("red", "#C50F1F".to_string()); hm.insert("green", "#13A10E".to_string()); hm.insert("yellow", "#C19C00".to_string()); hm.insert("blue", "#0037DA".to_string()); hm.insert("magenta", "#881798".to_string()); hm.insert("cyan", "#3A96DD".to_string()); hm.insert("white", "#CCCCCC".to_string()); // Try to make theme work with light or dark but // flipping the foreground and background but leave // the other colors the same. if is_dark { hm.insert("background", "#0C0C0C".to_string()); hm.insert("foreground", "#CCCCCC".to_string()); } else { hm.insert("background", "#CCCCCC".to_string()); hm.insert("foreground", "#0C0C0C".to_string()); } hm } fn get_default_theme(is_dark: bool) -> HashMap<&'static str, String> { let mut hm: HashMap<&str, String> = HashMap::new(); // This theme has different colors for dark and light // so we can't just swap the background colors. if is_dark { hm.insert("bold_black", "black".to_string()); hm.insert("bold_red", "red".to_string()); hm.insert("bold_green", "green".to_string()); hm.insert("bold_yellow", "yellow".to_string()); hm.insert("bold_blue", "blue".to_string()); hm.insert("bold_magenta", "magenta".to_string()); hm.insert("bold_cyan", "cyan".to_string()); hm.insert("bold_white", "white".to_string()); hm.insert("black", "black".to_string()); hm.insert("red", "red".to_string()); hm.insert("green", "green".to_string()); hm.insert("yellow", "yellow".to_string()); hm.insert("blue", "blue".to_string()); hm.insert("magenta", "magenta".to_string()); hm.insert("cyan", "cyan".to_string()); hm.insert("white", "white".to_string()); hm.insert("background", "black".to_string()); hm.insert("foreground", "white".to_string()); } else { hm.insert("bold_black", "black".to_string()); hm.insert("bold_red", "red".to_string()); hm.insert("bold_green", "green".to_string()); hm.insert("bold_yellow", "#717100".to_string()); hm.insert("bold_blue", "blue".to_string()); hm.insert("bold_magenta", "#c800c8".to_string()); hm.insert("bold_cyan", "#037979".to_string()); hm.insert("bold_white", "white".to_string()); hm.insert("black", "black".to_string()); hm.insert("red", "red".to_string()); hm.insert("green", "green".to_string()); hm.insert("yellow", "#717100".to_string()); hm.insert("blue", "blue".to_string()); hm.insert("magenta", "#c800c8".to_string()); hm.insert("cyan", "#037979".to_string()); hm.insert("white", "white".to_string()); hm.insert("background", "white".to_string()); hm.insert("foreground", "black".to_string()); } hm } fn get_github_theme(is_dark: bool) -> HashMap<&'static str, String> { // Suggested by Jörn for use with demo site // Taken from here https://github.com/mbadolato/iTerm2-Color-Schemes/blob/master/windowsterminal/Github.json // This is a light theme named github, intended for a white background // The next step will be to load these json themes if we ever get to that point let mut hm: HashMap<&str, String> = HashMap::new(); hm.insert("bold_black", "#666666".to_string()); hm.insert("bold_red", "#de0000".to_string()); hm.insert("bold_green", "#87d5a2".to_string()); hm.insert("bold_yellow", "#f1d007".to_string()); hm.insert("bold_blue", "#2e6cba".to_string()); hm.insert("bold_magenta", "#ffa29f".to_string()); hm.insert("bold_cyan", "#1cfafe".to_string()); hm.insert("bold_white", "#ffffff".to_string()); hm.insert("black", "#3e3e3e".to_string()); hm.insert("red", "#970b16".to_string()); hm.insert("green", "#07962a".to_string()); hm.insert("yellow", "#f8eec7".to_string()); hm.insert("blue", "#003e8a".to_string()); hm.insert("magenta", "#e94691".to_string()); hm.insert("cyan", "#89d1ec".to_string()); hm.insert("white", "#ffffff".to_string()); // Try to make theme work with light or dark but // flipping the foreground and background but leave // the other colors the same. if is_dark { hm.insert("background", "#3e3e3e".to_string()); hm.insert("foreground", "#f4f4f4".to_string()); } else { hm.insert("background", "#f4f4f4".to_string()); hm.insert("foreground", "#3e3e3e".to_string()); } hm } fn get_blulocolight_theme(is_dark: bool) -> HashMap<&'static str, String> { let mut hm: HashMap<&str, String> = HashMap::new(); hm.insert("bold_black", "#dedfe8".to_string()); hm.insert("bold_red", "#fc4a6d".to_string()); hm.insert("bold_green", "#34b354".to_string()); hm.insert("bold_yellow", "#b89427".to_string()); hm.insert("bold_blue", "#1085d9".to_string()); hm.insert("bold_magenta", "#c00db3".to_string()); hm.insert("bold_cyan", "#5b80ad".to_string()); hm.insert("bold_white", "#1d1d22".to_string()); hm.insert("black", "#cbccd5".to_string()); hm.insert("red", "#c90e42".to_string()); hm.insert("green", "#21883a".to_string()); hm.insert("yellow", "#d54d17".to_string()); hm.insert("blue", "#1e44dd".to_string()); hm.insert("magenta", "#6d1bed".to_string()); hm.insert("cyan", "#1f4d7a".to_string()); hm.insert("white", "#000000".to_string()); // Try to make theme work with light or dark but // flipping the foreground and background but leave // the other colors the same. if is_dark { hm.insert("background", "#2a2c33".to_string()); hm.insert("foreground", "#f7f7f7".to_string()); } else { hm.insert("background", "#f7f7f7".to_string()); hm.insert("foreground", "#2a2c33".to_string()); } hm } fn get_colors(is_dark: bool, theme: &Option>) -> HashMap<&'static str, String> { let theme_name = match theme { Some(s) => s.to_string(), None => "default".to_string(), }; match theme_name.as_ref() { "default" => get_default_theme(is_dark), "campbell" => get_campbell_theme(is_dark), "github" => get_github_theme(is_dark), "blulocolight" => get_blulocolight_theme(is_dark), _ => get_default_theme(is_dark), } } async fn to_html( args: CommandArgs, registry: &CommandRegistry, ) -> Result { let registry = registry.clone(); let name_tag = args.call_info.name_tag.clone(); let ( ToHTMLArgs { html_color, no_color, dark, theme, }, input, ) = args.process(®istry).await?; let input: Vec = input.collect().await; let headers = nu_protocol::merge_descriptors(&input); let headers = Some(headers) .filter(|headers| !headers.is_empty() && (headers.len() > 1 || headers[0] != "")); let mut output_string = "".to_string(); let mut regex_hm: HashMap = HashMap::new(); let color_hm = get_colors(dark, &theme); // change the color of the page output_string.push_str(&format!( // r"", r"", color_hm .get("background") .expect("Error getting background color"), color_hm .get("foreground") .expect("Error getting foreground color") )); let inner_value = match input.len() { 0 => String::default(), 1 => match headers { Some(headers) => html_table(input, headers, color_hm), None => { let value = &input[0]; html_value(value) } }, _ => match headers { Some(headers) => html_table(input, headers, color_hm), None => html_list(input), }, }; output_string.push_str(&inner_value); output_string.push_str(""); // Check to see if we want to remove all color or change ansi to html colors if html_color { setup_html_color_regexes(&mut regex_hm, dark, &theme); output_string = run_regexes(®ex_hm, &output_string); } else if no_color { setup_no_color_regexes(&mut regex_hm); output_string = run_regexes(®ex_hm, &output_string); } Ok(OutputStream::one(ReturnSuccess::value( UntaggedValue::string(output_string).into_value(name_tag), ))) } fn html_list(list: Vec) -> String { let mut output_string = String::new(); output_string.push_str("
    "); for value in list { output_string.push_str("
  1. "); output_string.push_str(&html_value(&value)); output_string.push_str("
  2. "); } output_string.push_str("
"); output_string } fn html_table(table: Vec, headers: Vec, color_hm: HashMap<&str, String>) -> String { let mut output_string = String::new(); // Add grid lines to html // let mut output_string = ""); // output_string.push_str(""); // change the color of tables output_string.push_str(&format!( r"
", color_hm .get("background") .expect("Error getting background color"), color_hm .get("foreground") .expect("Error getting foreground color") )); output_string.push_str(""); for header in &headers { output_string.push_str(""); } output_string.push_str(""); for row in table { if let UntaggedValue::Row(row) = row.value { output_string.push_str(""); for header in &headers { let data = row.get_data(header); output_string.push_str(""); } output_string.push_str(""); } } output_string.push_str("
"); output_string.push_str(&htmlescape::encode_minimal(&header)); output_string.push_str("
"); output_string.push_str(&html_value(data.borrow())); output_string.push_str("
"); output_string } fn html_value(value: &Value) -> String { let mut output_string = String::new(); match &value.value { UntaggedValue::Primitive(Primitive::Binary(b)) => { // This might be a bit much, but it's fun :) match &value.tag.anchor { Some(AnchorLocation::Url(f)) | Some(AnchorLocation::File(f)) => { let extension = f.split('.').last().map(String::from); match extension { Some(s) if ["png", "jpg", "bmp", "gif", "tiff", "jpeg"] .contains(&s.to_lowercase().as_str()) => { output_string.push_str(""); } _ => { let output = pretty_hex::pretty_hex(&b); output_string.push_str("
");
                            output_string.push_str(&output);
                            output_string.push_str("
"); } } } _ => { let output = pretty_hex::pretty_hex(&b); output_string.push_str("
");
                    output_string.push_str(&output);
                    output_string.push_str("
"); } } } UntaggedValue::Primitive(Primitive::String(ref b)) => { // This might be a bit much, but it's fun :) match &value.tag.anchor { Some(AnchorLocation::Url(f)) | Some(AnchorLocation::File(f)) => { let extension = f.split('.').last().map(String::from); match extension { Some(s) if s.to_lowercase() == "svg" => { output_string.push_str(""); return output_string; } _ => {} } } _ => {} } output_string.push_str( &htmlescape::encode_minimal(&format_leaf(&value.value).plain_string(100_000)) .replace("\n", "
"), ); } other => output_string.push_str( &htmlescape::encode_minimal(&format_leaf(other).plain_string(100_000)) .replace("\n", "
"), ), } output_string } fn setup_html_color_regexes( hash: &mut HashMap, is_dark: bool, theme: &Option>, ) { let color_hm = get_colors(is_dark, theme); // All the bold colors hash.insert( 0, ( r"(?P\[0m)(?P[[:alnum:][:space:][:punct:]]*)", // Reset the text color, normal weight font format!( r"$word", color_hm .get("foreground") .expect("Error getting reset text color") ), ), ); hash.insert( 1, ( // Bold Black r"(?P\[1;30m)(?P[[:alnum:][:space:][:punct:]]*)", format!( r"$word", color_hm .get("foreground") .expect("Error getting bold black text color") ), ), ); hash.insert( 2, ( // Bold Red r"(?P
\[1;31m)(?P[[:alnum:][:space:][:punct:]]*)", format!( r"$word", color_hm .get("bold_red") .expect("Error getting bold red text color"), ), ), ); hash.insert( 3, ( // Bold Green r"(?P\[1;32m)(?P[[:alnum:][:space:][:punct:]]*)", format!( r"$word", color_hm .get("bold_green") .expect("Error getting bold green text color"), ), ), ); hash.insert( 4, ( // Bold Yellow r"(?P\[1;33m)(?P[[:alnum:][:space:][:punct:]]*)", format!( r"$word", color_hm .get("bold_yellow") .expect("Error getting bold yellow text color"), ), ), ); hash.insert( 5, ( // Bold Blue r"(?P\[1;34m)(?P[[:alnum:][:space:][:punct:]]*)", format!( r"$word", color_hm .get("bold_blue") .expect("Error getting bold blue text color"), ), ), ); hash.insert( 6, ( // Bold Magenta r"(?P\[1;35m)(?P[[:alnum:][:space:][:punct:]]*)", format!( r"$word", color_hm .get("bold_magenta") .expect("Error getting bold magenta text color"), ), ), ); hash.insert( 7, ( // Bold Cyan r"(?P\[1;36m)(?P[[:alnum:][:space:][:punct:]]*)", format!( r"$word", color_hm .get("bold_cyan") .expect("Error getting bold cyan text color"), ), ), ); hash.insert( 8, ( // Bold White // Let's change this to black since the html background // is white. White on white = no bueno. r"(?P\[1;37m)(?P[[:alnum:][:space:][:punct:]]*)", format!( r"$word", color_hm .get("foreground") .expect("Error getting bold bold white text color"), ), ), ); // All the normal colors hash.insert( 9, ( // Black r"(?P\[30m)(?P[[:alnum:][:space:][:punct:]]*)", format!( r"$word", color_hm .get("foreground") .expect("Error getting black text color"), ), ), ); hash.insert( 10, ( // Red r"(?P\[31m)(?P[[:alnum:][:space:][:punct:]]*)", format!( r"$word", color_hm.get("red").expect("Error getting red text color"), ), ), ); hash.insert( 11, ( // Green r"(?P\[32m)(?P[[:alnum:][:space:][:punct:]]*)", format!( r"$word", color_hm .get("green") .expect("Error getting green text color"), ), ), ); hash.insert( 12, ( // Yellow r"(?P\[33m)(?P[[:alnum:][:space:][:punct:]]*)", format!( r"$word", color_hm .get("yellow") .expect("Error getting yellow text color"), ), ), ); hash.insert( 13, ( // Blue r"(?P\[34m)(?P[[:alnum:][:space:][:punct:]]*)", format!( r"$word", color_hm.get("blue").expect("Error getting blue text color"), ), ), ); hash.insert( 14, ( // Magenta r"(?P\[35m)(?P[[:alnum:][:space:][:punct:]]*)", format!( r"$word", color_hm .get("magenta") .expect("Error getting magenta text color"), ), ), ); hash.insert( 15, ( // Cyan r"(?P\[36m)(?P[[:alnum:][:space:][:punct:]]*)", format!( r"$word", color_hm.get("cyan").expect("Error getting cyan text color"), ), ), ); hash.insert( 16, ( // White // Let's change this to black since the html background // is white. White on white = no bueno. r"(?P\[37m)(?P[[:alnum:][:space:][:punct:]]*)", format!( r"$word", color_hm .get("foreground") .expect("Error getting white text color"), ), ), ); } fn setup_no_color_regexes(hash: &mut HashMap) { // We can just use one regex here because we're just removing ansi sequences // and not replacing them with html colors. // attribution: https://stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python hash.insert( 0, ( r"(?:\x1B[@-Z\\-_]|[\x80-\x9A\x9C-\x9F]|(?:\x1B\[|\x9B)[0-?]*[ -/]*[@-~])", r"$name_group_doesnt_exist".to_string(), ), ); } fn run_regexes(hash: &HashMap, contents: &str) -> String { let mut working_string = contents.to_owned(); let hash_count: u32 = hash.len() as u32; for n in 0..hash_count { let value = hash.get(&n).expect("error getting hash at index"); //println!("{},{}", value.0, value.1); let re = Regex::new(value.0).expect("problem with color regex"); let after = re.replace_all(&working_string, &value.1[..]).to_string(); working_string = after.clone(); } working_string } #[cfg(test)] mod tests { use super::*; #[test] fn examples_work_as_expected() { use crate::examples::test as test_examples; test_examples(ToHTML {}) } }