diff --git a/crates/nu-command/src/commands.rs b/crates/nu-command/src/commands.rs index 738d1d72a..11580efd8 100644 --- a/crates/nu-command/src/commands.rs +++ b/crates/nu-command/src/commands.rs @@ -269,7 +269,7 @@ pub(crate) use to::To; pub(crate) use to_csv::ToCSV; pub(crate) use to_html::ToHTML; pub(crate) use to_json::ToJSON; -pub(crate) use to_md::ToMarkdown; +pub(crate) use to_md::Command as ToMarkdown; pub(crate) use to_toml::ToTOML; pub(crate) use to_tsv::ToTSV; pub(crate) use to_url::ToURL; @@ -327,6 +327,7 @@ mod tests { whole_stream_command(StrKebabCase), whole_stream_command(StrSnakeCase), whole_stream_command(StrScreamingSnakeCase), + whole_stream_command(ToMarkdown), ] } diff --git a/crates/nu-command/src/commands/group_by.rs b/crates/nu-command/src/commands/group_by.rs index 91fed292d..d5dd8336e 100644 --- a/crates/nu-command/src/commands/group_by.rs +++ b/crates/nu-command/src/commands/group_by.rs @@ -46,13 +46,13 @@ impl WholeStreamCommand for Command { result: Some(vec![UntaggedValue::row(indexmap! { "File".to_string() => UntaggedValue::Table(vec![ UntaggedValue::row(indexmap! { - "name".to_string() => UntaggedValue::string("Andrés.txt").into(), + "name".to_string() => UntaggedValue::string("Andres.txt").into(), "type".to_string() => UntaggedValue::string("File").into(), "chickens".to_string() => UntaggedValue::int(10).into(), "modified".to_string() => date("2019-07-23".tagged_unknown()).unwrap().into(), }).into(), UntaggedValue::row(indexmap! { - "name".to_string() => UntaggedValue::string("Andrés.txt").into(), + "name".to_string() => UntaggedValue::string("Darren.txt").into(), "type".to_string() => UntaggedValue::string("File").into(), "chickens".to_string() => UntaggedValue::int(20).into(), "modified".to_string() => date("2019-09-24".tagged_unknown()).unwrap().into(), diff --git a/crates/nu-command/src/commands/move_/command.rs b/crates/nu-command/src/commands/move_/command.rs index 865679c60..4b01b0f73 100644 --- a/crates/nu-command/src/commands/move_/command.rs +++ b/crates/nu-command/src/commands/move_/command.rs @@ -54,7 +54,7 @@ impl WholeStreamCommand for Command { example: r#"ls | move type --before name | first"#, result: Some(vec![row! { "type".into() => string("File"), - "name".into() => string("Andrés.txt"), + "name".into() => string("Andres.txt"), "chickens".into() => int(10), "modified".into() => date("2019-07-23") }]), @@ -63,7 +63,7 @@ impl WholeStreamCommand for Command { description: "or move the column \"chickens\" after \"name\"", example: r#"ls | move chickens --after name | first"#, result: Some(vec![row! { - "name".into() => string("Andrés.txt"), + "name".into() => string("Andres.txt"), "chickens".into() => int(10), "type".into() => string("File"), "modified".into() => date("2019-07-23") @@ -74,7 +74,7 @@ impl WholeStreamCommand for Command { example: r#"ls | move name chickens --after type | first"#, result: Some(vec![row! { "type".into() => string("File"), - "name".into() => string("Andrés.txt"), + "name".into() => string("Andres.txt"), "chickens".into() => int(10), "modified".into() => date("2019-07-23") }]), diff --git a/crates/nu-command/src/commands/to_md.rs b/crates/nu-command/src/commands/to_md.rs index c660f9726..2f533cde2 100644 --- a/crates/nu-command/src/commands/to_md.rs +++ b/crates/nu-command/src/commands/to_md.rs @@ -5,25 +5,33 @@ use nu_engine::WholeStreamCommand; use nu_errors::ShellError; use nu_protocol::{ReturnSuccess, Signature, UntaggedValue, Value}; -pub struct ToMarkdown; +pub struct Command; #[derive(Deserialize)] -pub struct ToMarkdownArgs { +pub struct Arguments { pretty: bool, + #[serde(rename = "per-element")] + per_element: bool, } #[async_trait] -impl WholeStreamCommand for ToMarkdown { +impl WholeStreamCommand for Command { 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'), - ) + 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'), + ) } fn usage(&self) -> &str { @@ -37,30 +45,116 @@ impl WholeStreamCommand for ToMarkdown { fn examples(&self) -> Vec { vec![ Example { - description: "Outputs an unformatted md string representing the contents of ls", + description: "Outputs an unformatted table markdown string (default)", example: "ls | to md", - result: None, + result: Some(vec![Value::from(one(r#" + |name|type|chickens|modified| + |-|-|-|-| + |Andres.txt|File|10|1 year ago| + |Jonathan|Dir|5|1 year ago| + |Darren.txt|File|20|1 year ago| + |Yehuda|Dir|4|1 year ago| + "#))]), }, Example { - description: "Outputs a formatted md string representing the contents of ls", - example: "ls | to md -p", - result: None, + description: "Optionally, output a formatted markdown string", + example: "ls | to md --pretty", + result: Some(vec![Value::from(one(r#" + | name | type | chickens | modified | + | ---------- | ---- | -------- | ---------- | + | Andres.txt | File | 10 | 1 year ago | + | Jonathan | Dir | 5 | 1 year ago | + | Darren.txt | File | 20 | 1 year ago | + | Yehuda | Dir | 4 | 1 year ago | + "#))]), }, + Example { + description: "Treat each row as a markdown element", + example: "echo [[H1]; [\"Welcome to Nushell\"]] | append $(ls | first 2) | to md --per-element --pretty", + result: Some(vec![Value::from(one(r#" + # Welcome to Nushell + | name | type | chickens | modified | + | ---------- | ---- | -------- | ---------- | + | Andres.txt | File | 10 | 1 year ago | + | Jonathan | Dir | 5 | 1 year ago | + "#))]), + } ] } } async fn to_md(args: CommandArgs) -> Result { let name_tag = args.call_info.name_tag.clone(); - let (ToMarkdownArgs { pretty }, input) = args.process().await?; - let input: Vec = input.collect().await; - let headers = nu_protocol::merge_descriptors(&input); + let (arguments, input) = args.process().await?; + let input: Vec = input.collect().await; + + Ok(OutputStream::one(ReturnSuccess::value( + UntaggedValue::string(process(&input, arguments)).into_value(if input.is_empty() { + name_tag + } else { + input[0].tag() + }), + ))) +} + +fn process( + input: &[Value], + Arguments { + pretty, + per_element, + }: Arguments, +) -> String { + if per_element { + input + .iter() + .map(|v| match &v.value { + UntaggedValue::Table(values) => table(values, pretty), + _ => fragment(v, pretty), + }) + .collect::() + } else { + table(&input, pretty) + } +} + +fn fragment(input: &Value, pretty: bool) -> String { + let headers = input.data_descriptors(); + 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.clone()], pretty), + }; + + out.push_str(&markup); + out.push_str(&format_leaf(input.get_data(&headers[0]).borrow()).plain_string(100_000)); + } else if input.is_row() { + let string = match input.row_entries().next() { + Some(value) => value.1.as_string().unwrap_or_default(), + None => String::from(""), + }; + + out = format_leaf(&UntaggedValue::from(string)).plain_string(100_000) + } else { + out = format_leaf(&input.value).plain_string(100_000) + } + + out.push('\n'); + out +} + +fn collect_headers(headers: &[String]) -> (Vec, Vec) { let mut escaped_headers: Vec = Vec::new(); let mut column_widths: Vec = Vec::new(); if !headers.is_empty() && (headers.len() > 1 || !headers[0].is_empty()) { - for header in &headers { + 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); @@ -69,9 +163,17 @@ async fn to_md(args: CommandArgs) -> Result { column_widths = vec![0; headers.len()] } + (escaped_headers, column_widths) +} + +fn table(input: &[Value], pretty: bool) -> String { + let headers = nu_protocol::merge_descriptors(&input); + + let (escaped_headers, mut column_widths) = collect_headers(&headers); + let mut escaped_rows: Vec> = Vec::new(); - for row in &input { + for row in input { let mut escaped_row: Vec = Vec::new(); match row.value.clone() { @@ -108,9 +210,7 @@ async fn to_md(args: CommandArgs) -> Result { .to_string() }; - Ok(OutputStream::one(ReturnSuccess::value( - UntaggedValue::string(output_string).into_value(name_tag), - ))) + output_string } fn get_output_string( @@ -201,15 +301,78 @@ fn get_padded_string(text: String, desired_length: usize, padding_character: cha ) } +fn one(string: &str) -> String { + string + .lines() + .skip(1) + .map(|line| line.trim()) + .collect::>() + .join("\n") + .trim_end() + .to_string() +} + #[cfg(test)] mod tests { - use super::ShellError; - use super::ToMarkdown; + use super::{fragment, one, table}; + use nu_protocol::{row, Value}; #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; + fn render_h1() { + let value = row! {"H1".into() => Value::from("Ecuador")}; - Ok(test_examples(ToMarkdown {})?) + assert_eq!(fragment(&value, false), "# Ecuador\n"); + } + + #[test] + fn render_h2() { + let value = row! {"H2".into() => Value::from("Ecuador")}; + + assert_eq!(fragment(&value, false), "## Ecuador\n"); + } + + #[test] + fn render_h3() { + let value = row! {"H3".into() => Value::from("Ecuador")}; + + assert_eq!(fragment(&value, false), "### Ecuador\n"); + } + + #[test] + fn render_blockquote() { + let value = row! {"BLOCKQUOTE".into() => Value::from("Ecuador")}; + + assert_eq!(fragment(&value, false), "> Ecuador\n"); + } + + #[test] + fn render_table() { + let value = vec![ + row! { "country".into() => Value::from("Ecuador")}, + row! { "country".into() => Value::from("New Zealand")}, + row! { "country".into() => Value::from("USA")}, + ]; + + assert_eq!( + table(&value, false), + one(r#" + |country| + |-| + |Ecuador| + |New Zealand| + |USA| + "#) + ); + + assert_eq!( + table(&value, true), + one(r#" + | country | + | ----------- | + | Ecuador | + | New Zealand | + | USA | + "#) + ); } } diff --git a/crates/nu-command/src/examples.rs b/crates/nu-command/src/examples.rs index b691b9a35..932011fa6 100644 --- a/crates/nu-command/src/examples.rs +++ b/crates/nu-command/src/examples.rs @@ -16,7 +16,7 @@ use nu_protocol::{ShellTypeName, Value}; use nu_source::AnchorLocation; use crate::commands::{ - BuildString, Each, Echo, First, Get, Keep, Last, Let, Nth, StrCollect, Wrap, + Append, BuildString, Each, Echo, First, Get, Keep, Last, Let, Nth, Select, StrCollect, Wrap, }; use nu_engine::{run_block, whole_stream_command, Command, EvaluationContext, WholeStreamCommand}; use nu_stream::InputStream; @@ -32,6 +32,7 @@ pub fn test_examples(cmd: Command) -> Result<(), ShellError> { // Command Doubles whole_stream_command(DoubleLs {}), // Minimal restricted commands to aid in testing + whole_stream_command(Append {}), whole_stream_command(Echo {}), whole_stream_command(BuildString {}), whole_stream_command(First {}), @@ -41,6 +42,7 @@ pub fn test_examples(cmd: Command) -> Result<(), ShellError> { whole_stream_command(Last {}), whole_stream_command(Nth {}), whole_stream_command(Let {}), + whole_stream_command(Select), whole_stream_command(StrCollect), whole_stream_command(Wrap), cmd, @@ -100,6 +102,7 @@ pub fn test(cmd: impl WholeStreamCommand + 'static) -> Result<(), ShellError> { whole_stream_command(Each {}), whole_stream_command(Let {}), whole_stream_command(cmd), + whole_stream_command(Select), whole_stream_command(StrCollect), whole_stream_command(Wrap), ]); @@ -153,6 +156,7 @@ pub fn test_anchors(cmd: Command) -> Result<(), ShellError> { whole_stream_command(StubOpen {}), whole_stream_command(DoubleEcho {}), whole_stream_command(DoubleLs {}), + whole_stream_command(Append {}), whole_stream_command(BuildString {}), whole_stream_command(First {}), whole_stream_command(Get {}), @@ -161,6 +165,7 @@ pub fn test_anchors(cmd: Command) -> Result<(), ShellError> { whole_stream_command(Last {}), whole_stream_command(Nth {}), whole_stream_command(Let {}), + whole_stream_command(Select), whole_stream_command(StrCollect), whole_stream_command(Wrap), cmd, @@ -172,21 +177,24 @@ pub fn test_anchors(cmd: Command) -> Result<(), ShellError> { let mut ctx = base_context.clone(); let block = parse_line(&pipeline_with_anchor, &ctx)?; - let result = block_on(evaluate_block(block, &mut ctx))?; - ctx.with_errors(|reasons| reasons.iter().cloned().take(1).next()) - .map_or(Ok(()), Err)?; + if let Some(_) = &sample_pipeline.result { + let result = block_on(evaluate_block(block, &mut ctx))?; - for actual in result.iter() { - if !is_anchor_carried(actual, mock_path()) { - let failed_call = format!("command: {}\n", pipeline_with_anchor); + ctx.with_errors(|reasons| reasons.iter().cloned().take(1).next()) + .map_or(Ok(()), Err)?; - panic!( - "example command didn't carry anchor tag correctly.\n {} {:#?} {:#?}", - failed_call, - actual, - mock_path() - ); + for actual in result.iter() { + if !is_anchor_carried(actual, mock_path()) { + let failed_call = format!("command: {}\n", pipeline_with_anchor); + + panic!( + "example command didn't carry anchor tag correctly.\n {} {:#?} {:#?}", + failed_call, + actual, + mock_path() + ); + } } } } diff --git a/crates/nu-command/src/examples/sample.rs b/crates/nu-command/src/examples/sample.rs index 903a511d5..d3a592c67 100644 --- a/crates/nu-command/src/examples/sample.rs +++ b/crates/nu-command/src/examples/sample.rs @@ -7,7 +7,7 @@ pub mod ls { pub fn file_listing() -> Vec { vec![ row! { - "name".to_string() => string("Andrés.txt"), + "name".to_string() => string("Andres.txt"), "type".to_string() => string("File"), "chickens".to_string() => int(10), "modified".to_string() => date("2019-07-23") @@ -19,7 +19,7 @@ pub mod ls { "modified".to_string() => date("2019-07-23") }, row! { - "name".to_string() => string("Andrés.txt"), + "name".to_string() => string("Darren.txt"), "type".to_string() => string("File"), "chickens".to_string() => int(20), "modified".to_string() => date("2019-09-24") diff --git a/crates/nu-command/tests/format_conversions/markdown.rs b/crates/nu-command/tests/format_conversions/markdown.rs index 5816f3bd4..189d89ab8 100644 --- a/crates/nu-command/tests/format_conversions/markdown.rs +++ b/crates/nu-command/tests/format_conversions/markdown.rs @@ -5,7 +5,7 @@ fn md_empty() { let actual = nu!( cwd: ".", pipeline( r#" - echo "{}" | from json | to md + echo [[]; []] | from json | to md "# )); @@ -53,7 +53,7 @@ fn md_table() { let actual = nu!( cwd: ".", pipeline( r#" - echo '{"name": "jason"}' | from json | to md + echo [[name]; [jason]] | to md "# )); @@ -65,9 +65,34 @@ fn md_table_pretty() { let actual = nu!( cwd: ".", pipeline( r#" - echo '{"name": "joseph"}' | from json | to md -p + echo [[name]; [joseph]] | to md -p "# )); assert_eq!(actual.out, "| name || ------ || joseph |"); } + +#[test] +fn md_combined() { + let actual = nu!( + cwd: ".", pipeline( + r#" + def title [] { + echo [[H1]; ["Nu top meals"]] + }; + + def meals [] { + echo [[dish]; [Arepa] [Taco] [Pizza]] + }; + + title + | append $(meals) + | to md --per-element --pretty + "# + )); + + assert_eq!( + actual.out, + "# Nu top meals| dish || ----- || Arepa || Taco || Pizza |" + ); +} diff --git a/crates/nu-protocol/src/value.rs b/crates/nu-protocol/src/value.rs index 56238a251..2aee7de49 100644 --- a/crates/nu-protocol/src/value.rs +++ b/crates/nu-protocol/src/value.rs @@ -920,6 +920,67 @@ impl DateTimeExt for DateTime { #[cfg(test)] mod tests { use super::*; + use indexmap::indexmap; + + #[test] + fn test_merge_descriptors() { + let value = vec![ + UntaggedValue::row(indexmap! { + "h1".into() => Value::from("Ecuador") + }) + .into_untagged_value(), + UntaggedValue::row(indexmap! { + "h2".into() => Value::from("Ecuador") + }) + .into_untagged_value(), + UntaggedValue::row(indexmap! { + "h3".into() => Value::from("Ecuador") + }) + .into_untagged_value(), + UntaggedValue::row(indexmap! { + "h1".into() => Value::from("Ecuador"), + "h4".into() => Value::from("Ecuador"), + }) + .into_untagged_value(), + ]; + + assert_eq!( + merge_descriptors(&value), + vec![ + String::from("h1"), + String::from("h2"), + String::from("h3"), + String::from("h4") + ] + ); + } + + #[test] + fn test_data_descriptors() { + let value = vec![ + UntaggedValue::row(indexmap! { + "h1".into() => Value::from("Ecuador") + }), + UntaggedValue::row(indexmap! { + "h2".into() => Value::from("Ecuador") + }), + UntaggedValue::row(indexmap! { + "h3".into() => Value::from("Ecuador") + }), + UntaggedValue::row(indexmap! { + "h1".into() => Value::from("Ecuador"), + "h4".into() => Value::from("Ecuador"), + }), + ]; + + assert_eq!( + value + .iter() + .map(|v| v.data_descriptors().len()) + .collect::>(), + vec![1, 1, 1, 2] + ); + } #[test] fn test_decimal_from_float() {