use crate::prelude::*; use futures::StreamExt; use nu_data::value::format_leaf; use nu_engine::WholeStreamCommand; use nu_errors::ShellError; use nu_protocol::{Primitive, ReturnSuccess, 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<HtmlTheme>, } #[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; #[derive(Deserialize)] pub struct ToHTMLArgs { html_color: bool, no_color: bool, dark: bool, partial: bool, theme: Option<Tagged<String>>, list: bool, } #[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'), ) .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" } async fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> { to_html(args).await } } fn get_theme_from_asset_file( is_dark: bool, theme: &Option<Tagged<String>>, theme_tag: &Tag, ) -> Result<HashMap<&'static str, String>, 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<HtmlThemes, Box<dyn Error>> { match Assets::get(zip_name) { Some(content) => { let asset: Vec<u8> = 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<String> { 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<String> = html_themes .themes .iter() .map(|n| n.name[..].to_string()) .collect(); theme_names } async fn to_html(args: CommandArgs) -> Result<OutputStream, ShellError> { let name_tag = args.call_info.name_tag.clone(); let ( ToHTMLArgs { html_color, no_color, dark, partial, theme, list, }, input, ) = args.process().await?; let input: Vec<Value> = input.collect().await; 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<u32, (&str, String)> = 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"); // Short circuit and return the output_string Ok(OutputStream::one(ReturnSuccess::value( UntaggedValue::string(output_string).into_value(name_tag), ))) } 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"<html><style>body {{ background-color:{};color:{}; }}</style><body>", color_hm .get("background") .expect("Error getting background color"), color_hm .get("foreground") .expect("Error getting foreground color") )); } else { output_string.push_str(&format!( "<div style=\"background-color:{};color:{};\">", 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("</body></html>"); } else { output_string.push_str("</div>") } // 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(ReturnSuccess::value( UntaggedValue::string(output_string).into_value(name_tag), ))) } } fn html_list(list: Vec<Value>) -> String { let mut output_string = String::new(); output_string.push_str("<ol>"); for value in list { output_string.push_str("<li>"); output_string.push_str(&html_value(&value)); output_string.push_str("</li>"); } output_string.push_str("</ol>"); output_string } fn html_table(table: Vec<Value>, headers: Vec<String>) -> String { let mut output_string = String::new(); // Add grid lines to html // let mut output_string = "<html><head><style>".to_string(); // output_string.push_str("table, th, td { border: 2px solid black; border-collapse: collapse; padding: 10px; }"); // output_string.push_str("</style></head><body>"); output_string.push_str("<table>"); output_string.push_str("<tr>"); for header in &headers { output_string.push_str("<th>"); output_string.push_str(&htmlescape::encode_minimal(&header)); output_string.push_str("</th>"); } output_string.push_str("</tr>"); for row in table { if let UntaggedValue::Row(row) = row.value { output_string.push_str("<tr>"); for header in &headers { let data = row.get_data(header); output_string.push_str("<td>"); output_string.push_str(&html_value(data.borrow())); output_string.push_str("</td>"); } output_string.push_str("</tr>"); } } output_string.push_str("</table>"); 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("<img src=\"data:image/"); output_string.push_str(&s); output_string.push_str(";base64,"); output_string.push_str(&base64::encode(&b)); output_string.push_str("\">"); } _ => { let output = pretty_hex::pretty_hex(&b); output_string.push_str("<pre>"); output_string.push_str(&output); output_string.push_str("</pre>"); } } } _ => { let output = pretty_hex::pretty_hex(&b); output_string.push_str("<pre>"); output_string.push_str(&output); output_string.push_str("</pre>"); } } } 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("<img src=\"data:image/svg+xml;base64,"); output_string.push_str(&base64::encode(&b.as_bytes())); output_string.push_str("\">"); return output_string; } _ => {} } } _ => {} } output_string.push_str( &htmlescape::encode_minimal(&format_leaf(&value.value).plain_string(100_000)) .replace("\n", "<br>"), ); } other => output_string.push_str( &htmlescape::encode_minimal(&format_leaf(other).plain_string(100_000)) .replace("\n", "<br>"), ), } output_string } fn setup_html_color_regexes( hash: &mut HashMap<u32, (&'static str, String)>, color_hm: &HashMap<&str, String>, ) { // All the bold colors hash.insert( 0, ( r"(?P<reset>\[0m)(?P<word>[[:alnum:][:space:][:punct:]]*)", // Reset the text color, normal weight font format!( r"<span style='color:{};font-weight:normal;'>$word</span>", color_hm .get("foreground") .expect("Error getting reset text color") ), ), ); hash.insert( 1, ( // Bold Black r"(?P<bb>\[1;30m)(?P<word>[[:alnum:][:space:][:punct:]]*)", format!( r"<span style='color:{};font-weight:bold;'>$word</span>", color_hm .get("foreground") .expect("Error getting bold black text color") ), ), ); hash.insert( 2, ( // Bold Red r"(?P<br>\[1;31m)(?P<word>[[:alnum:][:space:][:punct:]]*)", format!( r"<span style='color:{};font-weight:bold;'>$word</span>", color_hm .get("bold_red") .expect("Error getting bold red text color"), ), ), ); hash.insert( 3, ( // Bold Green r"(?P<bg>\[1;32m)(?P<word>[[:alnum:][:space:][:punct:]]*)", format!( r"<span style='color:{};font-weight:bold;'>$word</span>", color_hm .get("bold_green") .expect("Error getting bold green text color"), ), ), ); hash.insert( 4, ( // Bold Yellow r"(?P<by>\[1;33m)(?P<word>[[:alnum:][:space:][:punct:]]*)", format!( r"<span style='color:{};font-weight:bold;'>$word</span>", color_hm .get("bold_yellow") .expect("Error getting bold yellow text color"), ), ), ); hash.insert( 5, ( // Bold Blue r"(?P<bu>\[1;34m)(?P<word>[[:alnum:][:space:][:punct:]]*)", format!( r"<span style='color:{};font-weight:bold;'>$word</span>", color_hm .get("bold_blue") .expect("Error getting bold blue text color"), ), ), ); hash.insert( 6, ( // Bold Magenta r"(?P<bm>\[1;35m)(?P<word>[[:alnum:][:space:][:punct:]]*)", format!( r"<span style='color:{};font-weight:bold;'>$word</span>", color_hm .get("bold_magenta") .expect("Error getting bold magenta text color"), ), ), ); hash.insert( 7, ( // Bold Cyan r"(?P<bc>\[1;36m)(?P<word>[[:alnum:][:space:][:punct:]]*)", format!( r"<span style='color:{};font-weight:bold;'>$word</span>", 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<bw>\[1;37m)(?P<word>[[:alnum:][:space:][:punct:]]*)", format!( r"<span style='color:{};font-weight:bold;'>$word</span>", color_hm .get("foreground") .expect("Error getting bold bold white text color"), ), ), ); // All the normal colors hash.insert( 9, ( // Black r"(?P<b>\[30m)(?P<word>[[:alnum:][:space:][:punct:]]*)", format!( r"<span style='color:{};'>$word</span>", color_hm .get("foreground") .expect("Error getting black text color"), ), ), ); hash.insert( 10, ( // Red r"(?P<r>\[31m)(?P<word>[[:alnum:][:space:][:punct:]]*)", format!( r"<span style='color:{};'>$word</span>", color_hm.get("red").expect("Error getting red text color"), ), ), ); hash.insert( 11, ( // Green r"(?P<g>\[32m)(?P<word>[[:alnum:][:space:][:punct:]]*)", format!( r"<span style='color:{};'>$word</span>", color_hm .get("green") .expect("Error getting green text color"), ), ), ); hash.insert( 12, ( // Yellow r"(?P<y>\[33m)(?P<word>[[:alnum:][:space:][:punct:]]*)", format!( r"<span style='color:{};'>$word</span>", color_hm .get("yellow") .expect("Error getting yellow text color"), ), ), ); hash.insert( 13, ( // Blue r"(?P<u>\[34m)(?P<word>[[:alnum:][:space:][:punct:]]*)", format!( r"<span style='color:{};'>$word</span>", color_hm.get("blue").expect("Error getting blue text color"), ), ), ); hash.insert( 14, ( // Magenta r"(?P<m>\[35m)(?P<word>[[:alnum:][:space:][:punct:]]*)", format!( r"<span style='color:{};'>$word</span>", color_hm .get("magenta") .expect("Error getting magenta text color"), ), ), ); hash.insert( 15, ( // Cyan r"(?P<c>\[36m)(?P<word>[[:alnum:][:space:][:punct:]]*)", format!( r"<span style='color:{};'>$word</span>", 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<w>\[37m)(?P<word>[[:alnum:][:space:][:punct:]]*)", format!( r"<span style='color:{};'>$word</span>", color_hm .get("foreground") .expect("Error getting white text color"), ), ), ); } fn setup_no_color_regexes(hash: &mut HashMap<u32, (&'static str, String)>) { // 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<u32, (&'static str, String)>, 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; Ok(test_examples(ToHTML {})?) } }