mirror of
https://github.com/nushell/nushell.git
synced 2024-12-23 23:49:44 +01:00
Minimal markdown syntax per element support. (#2997)
This commit is contained in:
parent
c1981dfc26
commit
fa928bd25d
@ -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),
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -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(),
|
||||
|
@ -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")
|
||||
}]),
|
||||
|
@ -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<Example> {
|
||||
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<OutputStream, ShellError> {
|
||||
let name_tag = args.call_info.name_tag.clone();
|
||||
let (ToMarkdownArgs { pretty }, input) = args.process().await?;
|
||||
let input: Vec<Value> = input.collect().await;
|
||||
let headers = nu_protocol::merge_descriptors(&input);
|
||||
let (arguments, input) = args.process().await?;
|
||||
|
||||
let input: Vec<Value> = 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::<String>()
|
||||
} 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<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 {
|
||||
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<OutputStream, ShellError> {
|
||||
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<String>> = Vec::new();
|
||||
|
||||
for row in &input {
|
||||
for row in input {
|
||||
let mut escaped_row: Vec<String> = Vec::new();
|
||||
|
||||
match row.value.clone() {
|
||||
@ -108,9 +210,7 @@ async fn to_md(args: CommandArgs) -> Result<OutputStream, ShellError> {
|
||||
.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::<Vec<&str>>()
|
||||
.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 |
|
||||
"#)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ pub mod ls {
|
||||
pub fn file_listing() -> Vec<Value> {
|
||||
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")
|
||||
|
@ -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 |"
|
||||
);
|
||||
}
|
||||
|
@ -920,6 +920,67 @@ impl DateTimeExt for DateTime<FixedOffset> {
|
||||
#[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<_>>(),
|
||||
vec![1, 1, 1, 2]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decimal_from_float() {
|
||||
|
Loading…
Reference in New Issue
Block a user