use crate::prelude::*; use nu_data::value::format_leaf; use nu_engine::WholeStreamCommand; use nu_errors::ShellError; use nu_protocol::{Primitive, Signature, SyntaxShape, UntaggedValue, Value}; use nu_source::{AnchorLocation, Tagged}; use regex::Regex; use rust_embed::RustEmbed; use serde::{Deserialize, Serialize}; use std::borrow::Cow; use std::collections::HashMap; use std::error::Error; #[derive(Serialize, Deserialize, Debug)] pub struct HtmlThemes { themes: Vec, } #[allow(non_snake_case)] #[derive(Serialize, Deserialize, Debug)] pub struct HtmlTheme { name: String, black: String, red: String, green: String, yellow: String, blue: String, purple: String, cyan: String, white: String, brightBlack: String, brightRed: String, brightGreen: String, brightYellow: String, brightBlue: String, brightPurple: String, brightCyan: String, brightWhite: String, background: String, foreground: String, } impl Default for HtmlThemes { fn default() -> Self { HtmlThemes { themes: vec![HtmlTheme::default()], } } } impl Default for HtmlTheme { fn default() -> Self { HtmlTheme { name: "nu_default".to_string(), black: "black".to_string(), red: "red".to_string(), green: "green".to_string(), yellow: "#717100".to_string(), blue: "blue".to_string(), purple: "#c800c8".to_string(), cyan: "#037979".to_string(), white: "white".to_string(), brightBlack: "black".to_string(), brightRed: "red".to_string(), brightGreen: "green".to_string(), brightYellow: "#717100".to_string(), brightBlue: "blue".to_string(), brightPurple: "#c800c8".to_string(), brightCyan: "#037979".to_string(), brightWhite: "white".to_string(), background: "white".to_string(), foreground: "black".to_string(), } } } #[derive(RustEmbed)] #[folder = "assets/"] struct Assets; pub struct ToHtml; 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'), ) .switch( "partial", "only output the html for the content itself", Some('p'), ) .named( "theme", SyntaxShape::String, "the name of the theme to use (github, blulocolight, ...)", Some('t'), ) .switch("list", "list the names of all available themes", Some('l')) } fn usage(&self) -> &str { "Convert table into simple HTML" } fn run(&self, args: CommandArgs) -> Result { to_html(args) } } fn get_theme_from_asset_file( is_dark: bool, theme: &Option>, theme_tag: &Tag, ) -> Result, ShellError> { let theme_name = match theme { Some(s) => s.to_string(), None => "default".to_string(), // There is no theme named "default" so this will be HtmlTheme::default(), which is "nu_default". }; // 228 themes come from // https://github.com/mbadolato/iTerm2-Color-Schemes/tree/master/windowsterminal // we should find a hit on any name in there let asset = get_asset_by_name_as_html_themes("228_themes.zip", "228_themes.json"); // If asset doesn't work, make sure to return the default theme let asset = match asset { Ok(a) => a, _ => HtmlThemes::default(), }; // Find the theme by theme name let th = asset .themes .iter() .find(|&n| n.name.to_lowercase() == *theme_name.to_lowercase().as_str()); // case insensitive search // If no theme is found by the name provided, ensure we return the default theme let default_theme = HtmlTheme::default(); let th = match th { Some(t) => t, None => &default_theme, }; // this just means no theme was passed in if th.name.to_lowercase().eq(&"nu_default".to_string()) // this means there was a theme passed in && theme.is_some() { return Err(ShellError::labeled_error( "Error finding theme name", "Error finding theme name", theme_tag.span, )); } Ok(convert_html_theme_to_hash_map(is_dark, th)) } #[allow(unused_variables)] fn get_asset_by_name_as_html_themes( zip_name: &str, json_name: &str, ) -> Result> { match Assets::get(zip_name) { Some(content) => { let asset: Vec = match content { Cow::Borrowed(bytes) => bytes.into(), Cow::Owned(bytes) => bytes, }; let reader = std::io::Cursor::new(asset); #[cfg(feature = "zip")] { use std::io::Read; let mut archive = zip::ZipArchive::new(reader)?; let mut zip_file = archive.by_name(json_name)?; let mut contents = String::new(); zip_file.read_to_string(&mut contents)?; Ok(serde_json::from_str(&contents)?) } #[cfg(not(feature = "zip"))] { let th = HtmlThemes::default(); Ok(th) } } None => { let th = HtmlThemes::default(); Ok(th) } } } fn convert_html_theme_to_hash_map( is_dark: bool, theme: &HtmlTheme, ) -> HashMap<&'static str, String> { let mut hm: HashMap<&str, String> = HashMap::new(); hm.insert("bold_black", theme.brightBlack[..].to_string()); hm.insert("bold_red", theme.brightRed[..].to_string()); hm.insert("bold_green", theme.brightGreen[..].to_string()); hm.insert("bold_yellow", theme.brightYellow[..].to_string()); hm.insert("bold_blue", theme.brightBlue[..].to_string()); hm.insert("bold_magenta", theme.brightPurple[..].to_string()); hm.insert("bold_cyan", theme.brightCyan[..].to_string()); hm.insert("bold_white", theme.brightWhite[..].to_string()); hm.insert("black", theme.black[..].to_string()); hm.insert("red", theme.red[..].to_string()); hm.insert("green", theme.green[..].to_string()); hm.insert("yellow", theme.yellow[..].to_string()); hm.insert("blue", theme.blue[..].to_string()); hm.insert("magenta", theme.purple[..].to_string()); hm.insert("cyan", theme.cyan[..].to_string()); hm.insert("white", theme.white[..].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", theme.black[..].to_string()); hm.insert("foreground", theme.white[..].to_string()); } else { hm.insert("background", theme.white[..].to_string()); hm.insert("foreground", theme.black[..].to_string()); } hm } fn get_list_of_theme_names() -> Vec { let asset = get_asset_by_name_as_html_themes("228_themes.zip", "228_themes.json"); // If asset doesn't work, make sure to return the default theme let html_themes = match asset { Ok(a) => a, _ => HtmlThemes::default(), }; let theme_names: Vec = html_themes .themes .iter() .map(|n| n.name[..].to_string()) .collect(); theme_names } fn to_html(args: CommandArgs) -> Result { let name_tag = args.call_info.name_tag.clone(); let args = args.evaluate_once()?; let html_color = args.has_flag("html_color"); let no_color = args.has_flag("no_color"); let dark = args.has_flag("dark"); let partial = args.has_flag("partial"); let list = args.has_flag("list"); let theme: Option> = args.get_flag("theme")?; let input: Vec = args.input.collect(); let headers = nu_protocol::merge_descriptors(&input); let headers = Some(headers) .filter(|headers| !headers.is_empty() && (headers.len() > 1 || !headers[0].is_empty())); let mut output_string = String::new(); let mut regex_hm: HashMap = HashMap::new(); if list { // Get the list of theme names let theme_names = get_list_of_theme_names(); // Put that list into the output string for s in theme_names.iter() { output_string.push_str(&format!("{}\n", s)); } output_string.push_str("\nScreenshots of themes can be found here:\n"); output_string.push_str("https://github.com/mbadolato/iTerm2-Color-Schemes\n"); } else { let theme_tag = match &theme { Some(v) => &v.tag, None => &name_tag, }; let color_hm = get_theme_from_asset_file(dark, &theme, theme_tag); let color_hm = match color_hm { Ok(c) => c, _ => { return Err(ShellError::labeled_error( "Error finding theme name", "Error finding theme name", theme_tag.span, )) } }; // change the color of the page if !partial { output_string.push_str(&format!( r"", color_hm .get("background") .expect("Error getting background color"), color_hm .get("foreground") .expect("Error getting foreground color") )); } else { output_string.push_str(&format!( "
", 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), None => { let value = &input[0]; html_value(value) } }, _ => match headers { Some(headers) => html_table(input, headers), None => html_list(input), }, }; output_string.push_str(&inner_value); if !partial { output_string.push_str(""); } else { 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, &color_hm); 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( 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) -> String { let mut output_string = String::new(); // Add grid lines to html // let mut output_string = ""); output_string.push_str(""); 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 = nu_pretty_hex::pretty_hex(&b); output_string.push_str("
");
                            output_string.push_str(&output);
                            output_string.push_str("
"); } } } _ => { let output = nu_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, color_hm: &HashMap<&str, String>, ) { // 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::ShellError; use super::*; #[test] fn examples_work_as_expected() -> Result<(), ShellError> { use crate::examples::test as test_examples; test_examples(ToHtml {}) } }