mirror of
https://github.com/nushell/nushell
synced 2024-12-27 05:23:11 +00:00
To html
and to md
(#453)
* MathEval Variance and Stddev * Fix tests and linting * Typo * Deal with streams when they are not tables * First draft of these commands * To MD * To md and to html * Fixed cargo and to_md * `into_abbreviated_string` instead of `into_string` * Changed how inner tables are displayed
This commit is contained in:
parent
865906e450
commit
7a892ec5d7
12 changed files with 1302 additions and 15 deletions
101
Cargo.lock
generated
101
Cargo.lock
generated
|
@ -342,6 +342,27 @@ version = "1.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c58ec36aac5066d5ca17df51b3e70279f5670a72102f5752cb7e7c856adfc70"
|
||||
|
||||
[[package]]
|
||||
name = "bzip2"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6afcd980b5f3a45017c57e57a2fcccbb351cc43a356ce117ef760ef8052b89b0"
|
||||
dependencies = [
|
||||
"bzip2-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bzip2-sys"
|
||||
version = "0.1.11+1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "calamine"
|
||||
version = "0.18.0"
|
||||
|
@ -1064,6 +1085,12 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "htmlescape"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163"
|
||||
|
||||
[[package]]
|
||||
name = "ical"
|
||||
version = "0.7.0"
|
||||
|
@ -1578,6 +1605,7 @@ dependencies = [
|
|||
"dtparse",
|
||||
"eml-parser",
|
||||
"glob",
|
||||
"htmlescape",
|
||||
"ical",
|
||||
"indexmap",
|
||||
"itertools",
|
||||
|
@ -1594,10 +1622,12 @@ dependencies = [
|
|||
"nu-term-grid",
|
||||
"num 0.4.0",
|
||||
"polars",
|
||||
"pretty-hex",
|
||||
"rand",
|
||||
"rayon",
|
||||
"regex",
|
||||
"roxmltree",
|
||||
"rust-embed",
|
||||
"serde",
|
||||
"serde_ini",
|
||||
"serde_urlencoded",
|
||||
|
@ -1612,6 +1642,7 @@ dependencies = [
|
|||
"unicode-segmentation",
|
||||
"url",
|
||||
"uuid",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2196,6 +2227,12 @@ dependencies = [
|
|||
"termtree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pretty-hex"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc5c99d529f0d30937f6f4b8a86d988047327bb88d04d2c4afc356de74722131"
|
||||
|
||||
[[package]]
|
||||
name = "pretty_assertions"
|
||||
version = "0.7.2"
|
||||
|
@ -2439,6 +2476,39 @@ dependencies = [
|
|||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed"
|
||||
version = "5.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2fe1fe6aac5d6bb9e1ffd81002340363272a7648234ec7bdfac5ee202cb65523"
|
||||
dependencies = [
|
||||
"rust-embed-impl",
|
||||
"rust-embed-utils",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-impl"
|
||||
version = "5.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ed91c41c42ef7bf687384439c312e75e0da9c149b0390889b94de3c7d9d9e66"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rust-embed-utils",
|
||||
"syn",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-utils"
|
||||
version = "5.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a512219132473ab0a77b52077059f1c47ce4af7fbdc94503e9862a34422876d"
|
||||
dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust_decimal"
|
||||
version = "0.10.2"
|
||||
|
@ -2463,6 +2533,15 @@ version = "1.0.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.1.0"
|
||||
|
@ -3064,6 +3143,17 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.9.0+wasi-snapshot-preview1"
|
||||
|
@ -3156,6 +3246,15 @@ version = "0.4.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
|
@ -3190,9 +3289,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "93ab48844d61251bb3835145c521d88aa4031d7139e8485990f60ca911fa0815"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"bzip2",
|
||||
"crc32fast",
|
||||
"flate2",
|
||||
"thiserror",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -78,6 +78,8 @@ example = ["nu_plugin_example"]
|
|||
# Extra
|
||||
gstat = ["nu_plugin_gstat"]
|
||||
|
||||
zip-support = ["nu-command/zip"]
|
||||
|
||||
# Dataframe feature for nushell
|
||||
dataframe = ["nu-command/dataframe"]
|
||||
|
||||
|
|
|
@ -46,9 +46,13 @@ ical = "0.7.0"
|
|||
calamine = "0.18.0"
|
||||
roxmltree = "0.14.0"
|
||||
rand = "0.8"
|
||||
rust-embed = "5.9.0"
|
||||
trash = { version = "1.3.0", optional = true }
|
||||
unicode-segmentation = "1.8.0"
|
||||
uuid = { version = "0.8.2", features = ["v4"] }
|
||||
htmlescape = "0.3.1"
|
||||
pretty-hex = "0.2.1"
|
||||
zip = { version="0.5.9", optional=true }
|
||||
lazy_static = "1.4.0"
|
||||
strip-ansi-escapes = "0.1.1"
|
||||
crossterm = "0.22.1"
|
||||
|
|
BIN
crates/nu-command/assets/228_themes.zip
Normal file
BIN
crates/nu-command/assets/228_themes.zip
Normal file
Binary file not shown.
|
@ -164,6 +164,8 @@ pub fn create_default_context() -> EngineState {
|
|||
ToToml,
|
||||
ToTsv,
|
||||
ToCsv,
|
||||
ToHtml,
|
||||
ToMd,
|
||||
Touch,
|
||||
Uniq,
|
||||
Use,
|
||||
|
|
|
@ -22,7 +22,7 @@ mod uniq;
|
|||
mod update;
|
||||
mod where_;
|
||||
mod wrap;
|
||||
mod zip;
|
||||
mod zip_;
|
||||
|
||||
pub use all::All;
|
||||
pub use any::Any;
|
||||
|
@ -48,4 +48,4 @@ pub use uniq::*;
|
|||
pub use update::Update;
|
||||
pub use where_::Where;
|
||||
pub use wrap::Wrap;
|
||||
pub use zip::Zip;
|
||||
pub use zip_::Zip;
|
||||
|
|
|
@ -87,14 +87,11 @@ fn to_string_tagged_value(v: &Value, config: &Config) -> Result<String, ShellErr
|
|||
| Value::Error { .. }
|
||||
| Value::Filesize { .. }
|
||||
| Value::CellPath { .. }
|
||||
| Value::Float { .. } => Ok(v.clone().into_string("", config)),
|
||||
| Value::List { .. }
|
||||
| Value::Record { .. }
|
||||
| Value::Float { .. } => Ok(v.clone().into_abbreviated_string(config)),
|
||||
Value::Date { val, .. } => Ok(val.to_string()),
|
||||
Value::Nothing { .. } => Ok(String::new()),
|
||||
Value::List { ref vals, .. } => match &vals[..] {
|
||||
[Value::Record { .. }, _end @ ..] => Ok(String::from("[Table]")),
|
||||
_ => Ok(String::from("[List]")),
|
||||
},
|
||||
Value::Record { .. } => Ok(String::from("[Row]")),
|
||||
_ => Err(ShellError::UnsupportedInput(
|
||||
"Unexpected value".to_string(),
|
||||
v.span().unwrap_or_else(|_| Span::unknown()),
|
||||
|
@ -102,13 +99,13 @@ fn to_string_tagged_value(v: &Value, config: &Config) -> Result<String, ShellErr
|
|||
}
|
||||
}
|
||||
|
||||
fn merge_descriptors(values: &[Value]) -> Vec<String> {
|
||||
pub fn merge_descriptors(values: &[Value]) -> Vec<String> {
|
||||
let mut ret: Vec<String> = vec![];
|
||||
let mut seen: IndexSet<String> = indexset! {};
|
||||
for value in values {
|
||||
let data_descriptors = match value {
|
||||
Value::Record { cols, .. } => cols.to_owned(),
|
||||
_ => vec![],
|
||||
_ => vec!["".to_string()],
|
||||
};
|
||||
for desc in data_descriptors {
|
||||
if !seen.contains(&desc) {
|
||||
|
|
727
crates/nu-command/src/formats/to/html.rs
Normal file
727
crates/nu-command/src/formats/to/html.rs
Normal file
|
@ -0,0 +1,727 @@
|
|||
use crate::formats::to::delimited::merge_descriptors;
|
||||
use nu_engine::CallExt;
|
||||
use nu_protocol::ast::Call;
|
||||
use nu_protocol::engine::{Command, EngineState, Stack};
|
||||
use nu_protocol::{
|
||||
Category, Config, Example, IntoPipelineData, PipelineData, ShellError, Signature, Spanned,
|
||||
SyntaxShape, Value,
|
||||
};
|
||||
use regex::Regex;
|
||||
use rust_embed::RustEmbed;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
use std::fmt::Write;
|
||||
|
||||
#[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;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ToHtml;
|
||||
|
||||
impl Command 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'))
|
||||
.category(Category::Formats)
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
description: "Outputs an HTML string representing the contents of this table",
|
||||
example: "[[foo bar]; [1 2]] | to html",
|
||||
result: Some(Value::test_string(
|
||||
r#"<html><style>body { background-color:white;color:black; }</style><body><table><tr><th>foo</th><th>bar</th></tr><tr><td>1</td><td>2</td></tr></table></body></html>"#,
|
||||
)),
|
||||
},
|
||||
Example {
|
||||
description: "Optionally, only output the html for the content itself",
|
||||
example: "[[foo bar]; [1 2]] | to html --partial",
|
||||
result: Some(Value::test_string(
|
||||
r#"<div style="background-color:white;color:black;"><table><tr><th>foo</th><th>bar</th></tr><tr><td>1</td><td>2</td></tr></table></div>"#,
|
||||
)),
|
||||
},
|
||||
Example {
|
||||
description: "Optionally, output the string with a dark background",
|
||||
example: "[[foo bar]; [1 2]] | to html --dark",
|
||||
result: Some(Value::test_string(
|
||||
r#"<html><style>body { background-color:black;color:white; }</style><body><table><tr><th>foo</th><th>bar</th></tr><tr><td>1</td><td>2</td></tr></table></body></html>"#,
|
||||
)),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Convert table into simple HTML"
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
) -> Result<nu_protocol::PipelineData, ShellError> {
|
||||
to_html(input, call, engine_state, stack)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_theme_from_asset_file(
|
||||
is_dark: bool,
|
||||
theme: &Option<Spanned<String>>,
|
||||
) -> Result<HashMap<&'static str, String>, ShellError> {
|
||||
let theme_name = match theme {
|
||||
Some(s) => s.item.clone(),
|
||||
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()); // 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::NotFound(
|
||||
theme.as_ref().expect("this should never trigger").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(nu_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.clone()).collect();
|
||||
|
||||
theme_names
|
||||
}
|
||||
|
||||
fn to_html(
|
||||
input: PipelineData,
|
||||
call: &Call,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let head = call.head;
|
||||
let html_color = call.has_flag("html_color");
|
||||
let no_color = call.has_flag("no_color");
|
||||
let dark = call.has_flag("dark");
|
||||
let partial = call.has_flag("partial");
|
||||
let list = call.has_flag("list");
|
||||
let theme: Option<Spanned<String>> = call.get_flag(engine_state, stack, "theme")?;
|
||||
let config = stack.get_config()?;
|
||||
|
||||
let vec_of_values = input.into_iter().collect::<Vec<Value>>();
|
||||
let headers = merge_descriptors(&vec_of_values);
|
||||
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 {
|
||||
writeln!(&mut output_string, "{}", s).unwrap();
|
||||
}
|
||||
|
||||
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_span = match &theme {
|
||||
Some(v) => v.span,
|
||||
None => head,
|
||||
};
|
||||
|
||||
let color_hm = get_theme_from_asset_file(dark, &theme);
|
||||
let color_hm = match color_hm {
|
||||
Ok(c) => c,
|
||||
_ => {
|
||||
return Err(ShellError::SpannedLabeledError(
|
||||
"Error finding theme name".to_string(),
|
||||
"Error finding theme name".to_string(),
|
||||
theme_span,
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
// change the color of the page
|
||||
if !partial {
|
||||
write!(
|
||||
&mut output_string,
|
||||
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")
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
write!(
|
||||
&mut output_string,
|
||||
"<div style=\"background-color:{};color:{};\">",
|
||||
color_hm
|
||||
.get("background")
|
||||
.expect("Error getting background color"),
|
||||
color_hm
|
||||
.get("foreground")
|
||||
.expect("Error getting foreground color")
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let inner_value = match vec_of_values.len() {
|
||||
0 => String::default(),
|
||||
1 => match headers {
|
||||
Some(headers) => html_table(vec_of_values, headers, &config),
|
||||
None => {
|
||||
let value = &vec_of_values[0];
|
||||
html_value(value.clone(), &config)
|
||||
}
|
||||
},
|
||||
_ => match headers {
|
||||
Some(headers) => html_table(vec_of_values, headers, &config),
|
||||
None => html_list(vec_of_values, &config),
|
||||
},
|
||||
};
|
||||
|
||||
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(Value::string(output_string, head).into_pipeline_data())
|
||||
}
|
||||
|
||||
fn html_list(list: Vec<Value>, config: &Config) -> 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, config));
|
||||
output_string.push_str("</li>");
|
||||
}
|
||||
output_string.push_str("</ol>");
|
||||
output_string
|
||||
}
|
||||
|
||||
fn html_table(table: Vec<Value>, headers: Vec<String>, config: &Config) -> String {
|
||||
let mut output_string = String::new();
|
||||
|
||||
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 Value::Record { span, .. } = row {
|
||||
output_string.push_str("<tr>");
|
||||
for header in &headers {
|
||||
let data = row.get_data_by_key(header);
|
||||
output_string.push_str("<td>");
|
||||
output_string.push_str(&html_value(
|
||||
data.unwrap_or_else(|| Value::nothing(span)),
|
||||
config,
|
||||
));
|
||||
output_string.push_str("</td>");
|
||||
}
|
||||
output_string.push_str("</tr>");
|
||||
}
|
||||
}
|
||||
output_string.push_str("</table>");
|
||||
|
||||
output_string
|
||||
}
|
||||
|
||||
fn html_value(value: Value, config: &Config) -> String {
|
||||
let mut output_string = String::new();
|
||||
match value {
|
||||
Value::Binary { val, .. } => {
|
||||
let output = pretty_hex::pretty_hex(&val);
|
||||
output_string.push_str("<pre>");
|
||||
output_string.push_str(&output);
|
||||
output_string.push_str("</pre>");
|
||||
}
|
||||
other => output_string.push_str(
|
||||
&htmlescape::encode_minimal(&other.into_abbreviated_string(config))
|
||||
.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::*;
|
||||
|
||||
#[test]
|
||||
fn test_examples() {
|
||||
use crate::test_examples;
|
||||
|
||||
test_examples(ToHtml {})
|
||||
}
|
||||
}
|
443
crates/nu-command/src/formats/to/md.rs
Normal file
443
crates/nu-command/src/formats/to/md.rs
Normal file
|
@ -0,0 +1,443 @@
|
|||
use crate::formats::to::delimited::merge_descriptors;
|
||||
use indexmap::map::IndexMap;
|
||||
use nu_protocol::ast::Call;
|
||||
use nu_protocol::engine::{Command, EngineState, Stack};
|
||||
use nu_protocol::{
|
||||
Category, Config, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, Value,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ToMd;
|
||||
|
||||
impl Command for ToMd {
|
||||
fn name(&self) -> &str {
|
||||
"to md"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("to md")
|
||||
.switch(
|
||||
"pretty",
|
||||
"Formats the Markdown table to vertically align items",
|
||||
Some('p'),
|
||||
)
|
||||
.switch(
|
||||
"per-element",
|
||||
"treat each row as markdown syntax element",
|
||||
Some('e'),
|
||||
)
|
||||
.category(Category::Formats)
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Convert table into simple Markdown"
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
description: "Outputs an MD string representing the contents of this table",
|
||||
example: "[[foo bar]; [1 2]] | to md",
|
||||
result: Some(Value::test_string("|foo|bar|\n|-|-|\n|1|2|\n")),
|
||||
},
|
||||
Example {
|
||||
description: "Optionally, output a formatted markdown string",
|
||||
example: "[[foo bar]; [1 2]] | to md --pretty",
|
||||
result: Some(Value::test_string(
|
||||
"| foo | bar |\n| --- | --- |\n| 1 | 2 |\n",
|
||||
)),
|
||||
},
|
||||
Example {
|
||||
description: "Treat each row as a markdown element",
|
||||
example: r#"[{"H1": "Welcome to Nushell" } [[foo bar]; [1 2]]] | to md --per-element --pretty"#,
|
||||
result: Some(Value::test_string(
|
||||
"# Welcome to Nushell\n| foo | bar |\n| --- | --- |\n| 1 | 2 |",
|
||||
)),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
) -> Result<nu_protocol::PipelineData, ShellError> {
|
||||
let head = call.head;
|
||||
let pretty = call.has_flag("pretty");
|
||||
let per_element = call.has_flag("per-element");
|
||||
let config = stack.get_config()?;
|
||||
to_md(input, pretty, per_element, config, head)
|
||||
}
|
||||
}
|
||||
|
||||
fn to_md(
|
||||
input: PipelineData,
|
||||
pretty: bool,
|
||||
per_element: bool,
|
||||
config: Config,
|
||||
head: Span,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let (grouped_input, single_list) = group_by(input, head, &config);
|
||||
if per_element || single_list {
|
||||
return Ok(Value::string(
|
||||
grouped_input
|
||||
.into_iter()
|
||||
.map(move |val| match val {
|
||||
Value::List { .. } => table(val.into_pipeline_data(), pretty, &config),
|
||||
other => fragment(other, pretty, &config),
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join(""),
|
||||
head,
|
||||
)
|
||||
.into_pipeline_data());
|
||||
}
|
||||
Ok(Value::string(table(grouped_input, pretty, &config), head).into_pipeline_data())
|
||||
}
|
||||
|
||||
fn fragment(input: Value, pretty: bool, config: &Config) -> String {
|
||||
let headers = match input {
|
||||
Value::Record { ref cols, .. } => cols.to_owned(),
|
||||
_ => vec![],
|
||||
};
|
||||
let mut out = String::new();
|
||||
|
||||
if headers.len() == 1 {
|
||||
let markup = match (&headers[0]).to_ascii_lowercase().as_ref() {
|
||||
"h1" => "# ".to_string(),
|
||||
"h2" => "## ".to_string(),
|
||||
"h3" => "### ".to_string(),
|
||||
"blockquote" => "> ".to_string(),
|
||||
|
||||
_ => return table(input.into_pipeline_data(), pretty, config),
|
||||
};
|
||||
|
||||
out.push_str(&markup);
|
||||
let data = match input.get_data_by_key(&headers[0]) {
|
||||
Some(v) => v,
|
||||
None => input,
|
||||
};
|
||||
out.push_str(&data.into_string("|", config));
|
||||
} else if let Value::Record { .. } = input {
|
||||
out = table(input.into_pipeline_data(), pretty, config)
|
||||
} else {
|
||||
out = input.into_string("|", config)
|
||||
}
|
||||
|
||||
out.push('\n');
|
||||
out
|
||||
}
|
||||
|
||||
fn collect_headers(headers: &[String]) -> (Vec<String>, Vec<usize>) {
|
||||
let mut escaped_headers: Vec<String> = Vec::new();
|
||||
let mut column_widths: Vec<usize> = Vec::new();
|
||||
|
||||
if !headers.is_empty() && (headers.len() > 1 || !headers[0].is_empty()) {
|
||||
for header in headers {
|
||||
let escaped_header_string = htmlescape::encode_minimal(header);
|
||||
column_widths.push(escaped_header_string.len());
|
||||
escaped_headers.push(escaped_header_string);
|
||||
}
|
||||
} else {
|
||||
column_widths = vec![0; headers.len()]
|
||||
}
|
||||
|
||||
(escaped_headers, column_widths)
|
||||
}
|
||||
|
||||
fn table(input: PipelineData, pretty: bool, config: &Config) -> String {
|
||||
let vec_of_values = input.into_iter().collect::<Vec<Value>>();
|
||||
let headers = merge_descriptors(&vec_of_values);
|
||||
|
||||
let (escaped_headers, mut column_widths) = collect_headers(&headers);
|
||||
|
||||
let mut escaped_rows: Vec<Vec<String>> = Vec::new();
|
||||
|
||||
for row in vec_of_values {
|
||||
let mut escaped_row: Vec<String> = Vec::new();
|
||||
|
||||
match row.to_owned() {
|
||||
Value::Record { span, .. } => {
|
||||
for i in 0..headers.len() {
|
||||
let data = row.get_data_by_key(&headers[i]);
|
||||
let value_string = data
|
||||
.unwrap_or_else(|| Value::nothing(span))
|
||||
.into_string("|", config);
|
||||
let new_column_width = value_string.len();
|
||||
|
||||
escaped_row.push(value_string);
|
||||
|
||||
if column_widths[i] < new_column_width {
|
||||
column_widths[i] = new_column_width;
|
||||
}
|
||||
}
|
||||
}
|
||||
p => {
|
||||
let value_string = htmlescape::encode_minimal(&p.into_abbreviated_string(config));
|
||||
escaped_row.push(value_string);
|
||||
}
|
||||
}
|
||||
|
||||
escaped_rows.push(escaped_row);
|
||||
}
|
||||
|
||||
let output_string = if (column_widths.is_empty() || column_widths.iter().all(|x| *x == 0))
|
||||
&& escaped_rows.is_empty()
|
||||
{
|
||||
String::from("")
|
||||
} else {
|
||||
get_output_string(&escaped_headers, &escaped_rows, &column_widths, pretty)
|
||||
.trim()
|
||||
.to_string()
|
||||
};
|
||||
|
||||
output_string
|
||||
}
|
||||
|
||||
pub fn group_by(values: PipelineData, head: Span, config: &Config) -> (PipelineData, bool) {
|
||||
let mut lists = IndexMap::new();
|
||||
let mut single_list = false;
|
||||
for val in values {
|
||||
if let Value::Record { ref cols, .. } = val {
|
||||
lists
|
||||
.entry(cols.concat())
|
||||
.and_modify(|v: &mut Vec<Value>| v.push(val.clone()))
|
||||
.or_insert_with(|| vec![val.clone()]);
|
||||
} else {
|
||||
lists
|
||||
.entry(val.clone().into_string(",", config))
|
||||
.and_modify(|v: &mut Vec<Value>| v.push(val.clone()))
|
||||
.or_insert_with(|| vec![val.clone()]);
|
||||
}
|
||||
}
|
||||
let mut output = vec![];
|
||||
for (_, mut value) in lists {
|
||||
if value.len() == 1 {
|
||||
output.push(value.pop().unwrap_or_else(|| Value::nothing(head)))
|
||||
} else {
|
||||
output.push(Value::List {
|
||||
vals: value.to_vec(),
|
||||
span: head,
|
||||
})
|
||||
}
|
||||
}
|
||||
if output.len() == 1 {
|
||||
single_list = true;
|
||||
}
|
||||
(
|
||||
Value::List {
|
||||
vals: output,
|
||||
span: head,
|
||||
}
|
||||
.into_pipeline_data(),
|
||||
single_list,
|
||||
)
|
||||
}
|
||||
|
||||
fn get_output_string(
|
||||
headers: &[String],
|
||||
rows: &[Vec<String>],
|
||||
column_widths: &[usize],
|
||||
pretty: bool,
|
||||
) -> String {
|
||||
let mut output_string = String::new();
|
||||
|
||||
if !headers.is_empty() {
|
||||
output_string.push('|');
|
||||
|
||||
for i in 0..headers.len() {
|
||||
if pretty {
|
||||
output_string.push(' ');
|
||||
output_string.push_str(&get_padded_string(
|
||||
headers[i].clone(),
|
||||
column_widths[i],
|
||||
' ',
|
||||
));
|
||||
output_string.push(' ');
|
||||
} else {
|
||||
output_string.push_str(&headers[i]);
|
||||
}
|
||||
|
||||
output_string.push('|');
|
||||
}
|
||||
|
||||
output_string.push_str("\n|");
|
||||
|
||||
#[allow(clippy::needless_range_loop)]
|
||||
for i in 0..headers.len() {
|
||||
if pretty {
|
||||
output_string.push(' ');
|
||||
output_string.push_str(&get_padded_string(
|
||||
String::from("-"),
|
||||
column_widths[i],
|
||||
'-',
|
||||
));
|
||||
output_string.push(' ');
|
||||
} else {
|
||||
output_string.push('-');
|
||||
}
|
||||
|
||||
output_string.push('|');
|
||||
}
|
||||
|
||||
output_string.push('\n');
|
||||
}
|
||||
|
||||
for row in rows {
|
||||
if !headers.is_empty() {
|
||||
output_string.push('|');
|
||||
}
|
||||
|
||||
for i in 0..row.len() {
|
||||
if pretty {
|
||||
output_string.push(' ');
|
||||
output_string.push_str(&get_padded_string(row[i].clone(), column_widths[i], ' '));
|
||||
output_string.push(' ');
|
||||
} else {
|
||||
output_string.push_str(&row[i]);
|
||||
}
|
||||
|
||||
if !headers.is_empty() {
|
||||
output_string.push('|');
|
||||
}
|
||||
}
|
||||
|
||||
output_string.push('\n');
|
||||
}
|
||||
|
||||
output_string
|
||||
}
|
||||
|
||||
fn get_padded_string(text: String, desired_length: usize, padding_character: char) -> String {
|
||||
let repeat_length = if text.len() > desired_length {
|
||||
0
|
||||
} else {
|
||||
desired_length - text.len()
|
||||
};
|
||||
|
||||
format!(
|
||||
"{}{}",
|
||||
text,
|
||||
padding_character.to_string().repeat(repeat_length)
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use nu_protocol::{Config, IntoPipelineData, Span, Value};
|
||||
|
||||
fn one(string: &str) -> String {
|
||||
string
|
||||
.lines()
|
||||
.skip(1)
|
||||
.map(|line| line.trim())
|
||||
.collect::<Vec<&str>>()
|
||||
.join("\n")
|
||||
.trim_end()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_examples() {
|
||||
use crate::test_examples;
|
||||
|
||||
test_examples(ToMd {})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_h1() {
|
||||
let value = Value::Record {
|
||||
cols: vec!["H1".to_string()],
|
||||
vals: vec![Value::test_string("Ecuador")],
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
assert_eq!(fragment(value, false, &Config::default()), "# Ecuador\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_h2() {
|
||||
let value = Value::Record {
|
||||
cols: vec!["H2".to_string()],
|
||||
vals: vec![Value::test_string("Ecuador")],
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
assert_eq!(fragment(value, false, &Config::default()), "## Ecuador\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_h3() {
|
||||
let value = Value::Record {
|
||||
cols: vec!["H3".to_string()],
|
||||
vals: vec![Value::test_string("Ecuador")],
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
assert_eq!(fragment(value, false, &Config::default()), "### Ecuador\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_blockquote() {
|
||||
let value = Value::Record {
|
||||
cols: vec!["BLOCKQUOTE".to_string()],
|
||||
vals: vec![Value::test_string("Ecuador")],
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
assert_eq!(fragment(value, false, &Config::default()), "> Ecuador\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_table() {
|
||||
let value = Value::List {
|
||||
vals: vec![
|
||||
Value::Record {
|
||||
cols: vec!["country".to_string()],
|
||||
vals: vec![Value::test_string("Ecuador")],
|
||||
span: Span::unknown(),
|
||||
},
|
||||
Value::Record {
|
||||
cols: vec!["country".to_string()],
|
||||
vals: vec![Value::test_string("New Zealand")],
|
||||
span: Span::unknown(),
|
||||
},
|
||||
Value::Record {
|
||||
cols: vec!["country".to_string()],
|
||||
vals: vec![Value::test_string("USA")],
|
||||
span: Span::unknown(),
|
||||
},
|
||||
],
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
table(
|
||||
value.clone().into_pipeline_data(),
|
||||
false,
|
||||
&Config::default()
|
||||
),
|
||||
one(r#"
|
||||
|country|
|
||||
|-|
|
||||
|Ecuador|
|
||||
|New Zealand|
|
||||
|USA|
|
||||
"#)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
table(value.clone().into_pipeline_data(), true, &Config::default()),
|
||||
one(r#"
|
||||
| country |
|
||||
| ----------- |
|
||||
| Ecuador |
|
||||
| New Zealand |
|
||||
| USA |
|
||||
"#)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
mod command;
|
||||
mod csv;
|
||||
mod delimited;
|
||||
mod html;
|
||||
mod json;
|
||||
mod md;
|
||||
mod toml;
|
||||
mod tsv;
|
||||
mod url;
|
||||
|
@ -10,5 +12,7 @@ pub use self::csv::ToCsv;
|
|||
pub use self::toml::ToToml;
|
||||
pub use self::url::ToUrl;
|
||||
pub use command::To;
|
||||
pub use html::ToHtml;
|
||||
pub use json::ToJson;
|
||||
pub use md::ToMd;
|
||||
pub use tsv::ToTsv;
|
||||
|
|
|
@ -391,11 +391,18 @@ impl Value {
|
|||
)
|
||||
}
|
||||
Value::String { val, .. } => val,
|
||||
Value::List { vals: val, .. } => format!(
|
||||
"[list {} item{}]",
|
||||
val.len(),
|
||||
if val.len() == 1 { "" } else { "s" }
|
||||
Value::List { ref vals, .. } => match &vals[..] {
|
||||
[Value::Record { .. }, _end @ ..] => format!(
|
||||
"[table {} row{}]",
|
||||
vals.len(),
|
||||
if vals.len() == 1 { "" } else { "s" }
|
||||
),
|
||||
_ => format!(
|
||||
"[list {} item{}]",
|
||||
vals.len(),
|
||||
if vals.len() == 1 { "" } else { "s" }
|
||||
),
|
||||
},
|
||||
Value::Record { cols, .. } => format!(
|
||||
"{{record {} field{}}}",
|
||||
cols.len(),
|
||||
|
|
Loading…
Reference in a new issue