nu_command/formats/to/
md.rs

1use indexmap::IndexMap;
2use nu_cmd_base::formats::to::delimited::merge_descriptors;
3use nu_engine::command_prelude::*;
4use nu_protocol::{Config, ast::PathMember};
5use std::collections::HashSet;
6
7#[derive(Clone)]
8pub struct ToMd;
9
10impl Command for ToMd {
11    fn name(&self) -> &str {
12        "to md"
13    }
14
15    fn signature(&self) -> Signature {
16        Signature::build("to md")
17            .input_output_types(vec![(Type::Any, Type::String)])
18            .switch(
19                "pretty",
20                "Formats the Markdown table to vertically align items",
21                Some('p'),
22            )
23            .switch(
24                "per-element",
25                "treat each row as markdown syntax element",
26                Some('e'),
27            )
28            .named(
29                "center",
30                SyntaxShape::List(Box::new(SyntaxShape::CellPath)),
31                "Formats the Markdown table to center given columns",
32                Some('c'),
33            )
34            .category(Category::Formats)
35    }
36
37    fn description(&self) -> &str {
38        "Convert table into simple Markdown."
39    }
40
41    fn examples(&self) -> Vec<Example> {
42        vec![
43            Example {
44                description: "Outputs an MD string representing the contents of this table",
45                example: "[[foo bar]; [1 2]] | to md",
46                result: Some(Value::test_string("|foo|bar|\n|-|-|\n|1|2|")),
47            },
48            Example {
49                description: "Optionally, output a formatted markdown string",
50                example: "[[foo bar]; [1 2]] | to md --pretty",
51                result: Some(Value::test_string(
52                    "| foo | bar |\n| --- | --- |\n| 1   | 2   |",
53                )),
54            },
55            Example {
56                description: "Treat each row as a markdown element",
57                example: r#"[{"H1": "Welcome to Nushell" } [[foo bar]; [1 2]]] | to md --per-element --pretty"#,
58                result: Some(Value::test_string(
59                    "# Welcome to Nushell\n| foo | bar |\n| --- | --- |\n| 1   | 2   |",
60                )),
61            },
62            Example {
63                description: "Render a list",
64                example: "[0 1 2] | to md --pretty",
65                result: Some(Value::test_string("0\n1\n2")),
66            },
67            Example {
68                description: "Separate list into markdown tables",
69                example: "[ {foo: 1, bar: 2} {foo: 3, bar: 4} {foo: 5}] | to md --per-element",
70                result: Some(Value::test_string(
71                    "|foo|bar|\n|-|-|\n|1|2|\n|3|4|\n|foo|\n|-|\n|5|",
72                )),
73            },
74            Example {
75                description: "Center a column of a markdown table",
76                example: "[ {foo: 1, bar: 2} {foo: 3, bar: 4}] | to md --pretty --center [bar]",
77                result: Some(Value::test_string(
78                    "| foo | bar |\n| --- |:---:|\n| 1   |  2  |\n| 3   |  4  |",
79                )),
80            },
81        ]
82    }
83
84    fn run(
85        &self,
86        engine_state: &EngineState,
87        stack: &mut Stack,
88        call: &Call,
89        input: PipelineData,
90    ) -> Result<PipelineData, ShellError> {
91        let head = call.head;
92        let pretty = call.has_flag(engine_state, stack, "pretty")?;
93        let per_element = call.has_flag(engine_state, stack, "per-element")?;
94        let center: Option<Vec<CellPath>> = call.get_flag(engine_state, stack, "center")?;
95        let config = stack.get_config(engine_state);
96        to_md(input, pretty, per_element, &center, &config, head)
97    }
98}
99
100fn to_md(
101    input: PipelineData,
102    pretty: bool,
103    per_element: bool,
104    center: &Option<Vec<CellPath>>,
105    config: &Config,
106    head: Span,
107) -> Result<PipelineData, ShellError> {
108    // text/markdown became a valid mimetype with rfc7763
109    let metadata = input
110        .metadata()
111        .unwrap_or_default()
112        .with_content_type(Some("text/markdown".into()));
113
114    let (grouped_input, single_list) = group_by(input, head, config);
115    if per_element || single_list {
116        return Ok(Value::string(
117            grouped_input
118                .into_iter()
119                .map(move |val| match val {
120                    Value::List { .. } => {
121                        format!(
122                            "{}\n",
123                            table(val.into_pipeline_data(), pretty, center, config)
124                        )
125                    }
126                    other => fragment(other, pretty, center, config),
127                })
128                .collect::<Vec<String>>()
129                .join("")
130                .trim(),
131            head,
132        )
133        .into_pipeline_data_with_metadata(Some(metadata)));
134    }
135    Ok(
136        Value::string(table(grouped_input, pretty, center, config), head)
137            .into_pipeline_data_with_metadata(Some(metadata)),
138    )
139}
140
141fn fragment(input: Value, pretty: bool, center: &Option<Vec<CellPath>>, config: &Config) -> String {
142    let mut out = String::new();
143
144    if let Value::Record { val, .. } = &input {
145        match val.get_index(0) {
146            Some((header, data)) if val.len() == 1 => {
147                let markup = match header.to_ascii_lowercase().as_ref() {
148                    "h1" => "# ".to_string(),
149                    "h2" => "## ".to_string(),
150                    "h3" => "### ".to_string(),
151                    "blockquote" => "> ".to_string(),
152                    _ => return table(input.into_pipeline_data(), pretty, center, config),
153                };
154
155                out.push_str(&markup);
156                out.push_str(&data.to_expanded_string("|", config));
157            }
158            _ => out = table(input.into_pipeline_data(), pretty, center, config),
159        }
160    } else {
161        out = input.to_expanded_string("|", config)
162    }
163
164    out.push('\n');
165    out
166}
167
168fn collect_headers(headers: &[String]) -> (Vec<String>, Vec<usize>) {
169    let mut escaped_headers: Vec<String> = Vec::new();
170    let mut column_widths: Vec<usize> = Vec::new();
171
172    if !headers.is_empty() && (headers.len() > 1 || !headers[0].is_empty()) {
173        for header in headers {
174            let escaped_header_string = v_htmlescape::escape(header).to_string();
175            column_widths.push(escaped_header_string.len());
176            escaped_headers.push(escaped_header_string);
177        }
178    } else {
179        column_widths = vec![0; headers.len()]
180    }
181
182    (escaped_headers, column_widths)
183}
184
185fn table(
186    input: PipelineData,
187    pretty: bool,
188    center: &Option<Vec<CellPath>>,
189    config: &Config,
190) -> String {
191    let vec_of_values = input
192        .into_iter()
193        .flat_map(|val| match val {
194            Value::List { vals, .. } => vals,
195            other => vec![other],
196        })
197        .collect::<Vec<Value>>();
198    let mut headers = merge_descriptors(&vec_of_values);
199
200    let mut empty_header_index = 0;
201    for value in &vec_of_values {
202        if let Value::Record { val, .. } = value {
203            for column in val.columns() {
204                if column.is_empty() && !headers.contains(&String::new()) {
205                    headers.insert(empty_header_index, String::new());
206                    empty_header_index += 1;
207                    break;
208                }
209                empty_header_index += 1;
210            }
211        }
212    }
213
214    let (escaped_headers, mut column_widths) = collect_headers(&headers);
215
216    let mut escaped_rows: Vec<Vec<String>> = Vec::new();
217
218    for row in vec_of_values {
219        let mut escaped_row: Vec<String> = Vec::new();
220        let span = row.span();
221
222        match row.to_owned() {
223            Value::Record { val: row, .. } => {
224                for i in 0..headers.len() {
225                    let value_string = row
226                        .get(&headers[i])
227                        .cloned()
228                        .unwrap_or_else(|| Value::nothing(span))
229                        .to_expanded_string(", ", config);
230                    let new_column_width = value_string.len();
231
232                    escaped_row.push(value_string);
233
234                    if column_widths[i] < new_column_width {
235                        column_widths[i] = new_column_width;
236                    }
237                }
238            }
239            p => {
240                let value_string =
241                    v_htmlescape::escape(&p.to_abbreviated_string(config)).to_string();
242                escaped_row.push(value_string);
243            }
244        }
245
246        escaped_rows.push(escaped_row);
247    }
248
249    if (column_widths.is_empty() || column_widths.iter().all(|x| *x == 0))
250        && escaped_rows.is_empty()
251    {
252        String::from("")
253    } else {
254        get_output_string(
255            &escaped_headers,
256            &escaped_rows,
257            &column_widths,
258            pretty,
259            center,
260        )
261        .trim()
262        .to_string()
263    }
264}
265
266pub fn group_by(values: PipelineData, head: Span, config: &Config) -> (PipelineData, bool) {
267    let mut lists = IndexMap::new();
268    let mut single_list = false;
269    for val in values {
270        if let Value::Record {
271            val: ref record, ..
272        } = val
273        {
274            lists
275                .entry(record.columns().map(|c| c.as_str()).collect::<String>())
276                .and_modify(|v: &mut Vec<Value>| v.push(val.clone()))
277                .or_insert_with(|| vec![val.clone()]);
278        } else {
279            lists
280                .entry(val.to_expanded_string(",", config))
281                .and_modify(|v: &mut Vec<Value>| v.push(val.clone()))
282                .or_insert_with(|| vec![val.clone()]);
283        }
284    }
285    let mut output = vec![];
286    for (_, mut value) in lists {
287        if value.len() == 1 {
288            output.push(value.pop().unwrap_or_else(|| Value::nothing(head)))
289        } else {
290            output.push(Value::list(value.to_vec(), head))
291        }
292    }
293    if output.len() == 1 {
294        single_list = true;
295    }
296    (Value::list(output, head).into_pipeline_data(), single_list)
297}
298
299fn get_output_string(
300    headers: &[String],
301    rows: &[Vec<String>],
302    column_widths: &[usize],
303    pretty: bool,
304    center: &Option<Vec<CellPath>>,
305) -> String {
306    let mut output_string = String::new();
307
308    let mut to_center: HashSet<String> = HashSet::new();
309    if let Some(center_vec) = center.as_ref() {
310        for cell_path in center_vec {
311            if let Some(PathMember::String { val, .. }) = cell_path
312                .members
313                .iter()
314                .find(|member| matches!(member, PathMember::String { .. }))
315            {
316                to_center.insert(val.clone());
317            }
318        }
319    }
320
321    if !headers.is_empty() {
322        output_string.push('|');
323
324        for i in 0..headers.len() {
325            if pretty {
326                output_string.push(' ');
327                if center.is_some() && to_center.contains(&headers[i]) {
328                    output_string.push_str(&get_centered_string(
329                        headers[i].clone(),
330                        column_widths[i],
331                        ' ',
332                    ));
333                } else {
334                    output_string.push_str(&get_padded_string(
335                        headers[i].clone(),
336                        column_widths[i],
337                        ' ',
338                    ));
339                }
340                output_string.push(' ');
341            } else {
342                output_string.push_str(&headers[i]);
343            }
344
345            output_string.push('|');
346        }
347
348        output_string.push_str("\n|");
349
350        for i in 0..headers.len() {
351            let centered_column = center.is_some() && to_center.contains(&headers[i]);
352            let border_char = if centered_column { ':' } else { ' ' };
353            if pretty {
354                output_string.push(border_char);
355                output_string.push_str(&get_padded_string(
356                    String::from("-"),
357                    column_widths[i],
358                    '-',
359                ));
360                output_string.push(border_char);
361            } else if centered_column {
362                output_string.push(':');
363                output_string.push('-');
364                output_string.push(':');
365            } else {
366                output_string.push('-');
367            }
368
369            output_string.push('|');
370        }
371
372        output_string.push('\n');
373    }
374
375    for row in rows {
376        if !headers.is_empty() {
377            output_string.push('|');
378        }
379
380        for i in 0..row.len() {
381            if pretty && column_widths.get(i).is_some() {
382                output_string.push(' ');
383                if center.is_some() && to_center.contains(&headers[i]) {
384                    output_string.push_str(&get_centered_string(
385                        row[i].clone(),
386                        column_widths[i],
387                        ' ',
388                    ));
389                } else {
390                    output_string.push_str(&get_padded_string(
391                        row[i].clone(),
392                        column_widths[i],
393                        ' ',
394                    ));
395                }
396                output_string.push(' ');
397            } else {
398                output_string.push_str(&row[i]);
399            }
400
401            if !headers.is_empty() {
402                output_string.push('|');
403            }
404        }
405
406        output_string.push('\n');
407    }
408
409    output_string
410}
411
412fn get_centered_string(text: String, desired_length: usize, padding_character: char) -> String {
413    let total_padding = if text.len() > desired_length {
414        0
415    } else {
416        desired_length - text.len()
417    };
418
419    let repeat_left = total_padding / 2;
420    let repeat_right = total_padding - repeat_left;
421
422    format!(
423        "{}{}{}",
424        padding_character.to_string().repeat(repeat_left),
425        text,
426        padding_character.to_string().repeat(repeat_right)
427    )
428}
429
430fn get_padded_string(text: String, desired_length: usize, padding_character: char) -> String {
431    let repeat_length = if text.len() > desired_length {
432        0
433    } else {
434        desired_length - text.len()
435    };
436
437    format!(
438        "{}{}",
439        text,
440        padding_character.to_string().repeat(repeat_length)
441    )
442}
443
444#[cfg(test)]
445mod tests {
446    use crate::{Get, Metadata};
447
448    use super::*;
449    use nu_cmd_lang::eval_pipeline_without_terminal_expression;
450    use nu_protocol::{Config, IntoPipelineData, Value, casing::Casing, record};
451
452    fn one(string: &str) -> String {
453        string
454            .lines()
455            .skip(1)
456            .map(|line| line.trim())
457            .collect::<Vec<&str>>()
458            .join("\n")
459            .trim_end()
460            .to_string()
461    }
462
463    #[test]
464    fn test_examples() {
465        use crate::test_examples;
466
467        test_examples(ToMd {})
468    }
469
470    #[test]
471    fn render_h1() {
472        let value = Value::test_record(record! {
473            "H1" => Value::test_string("Ecuador"),
474        });
475
476        assert_eq!(
477            fragment(value, false, &None, &Config::default()),
478            "# Ecuador\n"
479        );
480    }
481
482    #[test]
483    fn render_h2() {
484        let value = Value::test_record(record! {
485            "H2" => Value::test_string("Ecuador"),
486        });
487
488        assert_eq!(
489            fragment(value, false, &None, &Config::default()),
490            "## Ecuador\n"
491        );
492    }
493
494    #[test]
495    fn render_h3() {
496        let value = Value::test_record(record! {
497            "H3" => Value::test_string("Ecuador"),
498        });
499
500        assert_eq!(
501            fragment(value, false, &None, &Config::default()),
502            "### Ecuador\n"
503        );
504    }
505
506    #[test]
507    fn render_blockquote() {
508        let value = Value::test_record(record! {
509            "BLOCKQUOTE" => Value::test_string("Ecuador"),
510        });
511
512        assert_eq!(
513            fragment(value, false, &None, &Config::default()),
514            "> Ecuador\n"
515        );
516    }
517
518    #[test]
519    fn render_table() {
520        let value = Value::test_list(vec![
521            Value::test_record(record! {
522                "country" => Value::test_string("Ecuador"),
523            }),
524            Value::test_record(record! {
525                "country" => Value::test_string("New Zealand"),
526            }),
527            Value::test_record(record! {
528                "country" => Value::test_string("USA"),
529            }),
530        ]);
531
532        assert_eq!(
533            table(
534                value.clone().into_pipeline_data(),
535                false,
536                &None,
537                &Config::default()
538            ),
539            one(r#"
540            |country|
541            |-|
542            |Ecuador|
543            |New Zealand|
544            |USA|
545        "#)
546        );
547
548        assert_eq!(
549            table(value.into_pipeline_data(), true, &None, &Config::default()),
550            one(r#"
551            | country     |
552            | ----------- |
553            | Ecuador     |
554            | New Zealand |
555            | USA         |
556        "#)
557        );
558    }
559
560    #[test]
561    fn test_empty_column_header() {
562        let value = Value::test_list(vec![
563            Value::test_record(record! {
564                "" => Value::test_string("1"),
565                "foo" => Value::test_string("2"),
566            }),
567            Value::test_record(record! {
568                "" => Value::test_string("3"),
569                "foo" => Value::test_string("4"),
570            }),
571        ]);
572
573        assert_eq!(
574            table(
575                value.clone().into_pipeline_data(),
576                false,
577                &None,
578                &Config::default()
579            ),
580            one(r#"
581            ||foo|
582            |-|-|
583            |1|2|
584            |3|4|
585        "#)
586        );
587    }
588
589    #[test]
590    fn test_empty_row_value() {
591        let value = Value::test_list(vec![
592            Value::test_record(record! {
593                "foo" => Value::test_string("1"),
594                "bar" => Value::test_string("2"),
595            }),
596            Value::test_record(record! {
597                "foo" => Value::test_string("3"),
598                "bar" => Value::test_string("4"),
599            }),
600            Value::test_record(record! {
601                "foo" => Value::test_string("5"),
602                "bar" => Value::test_string(""),
603            }),
604        ]);
605
606        assert_eq!(
607            table(
608                value.clone().into_pipeline_data(),
609                false,
610                &None,
611                &Config::default()
612            ),
613            one(r#"
614            |foo|bar|
615            |-|-|
616            |1|2|
617            |3|4|
618            |5||
619        "#)
620        );
621    }
622
623    #[test]
624    fn test_center_column() {
625        let value = Value::test_list(vec![
626            Value::test_record(record! {
627                "foo" => Value::test_string("1"),
628                "bar" => Value::test_string("2"),
629            }),
630            Value::test_record(record! {
631                "foo" => Value::test_string("3"),
632                "bar" => Value::test_string("4"),
633            }),
634            Value::test_record(record! {
635                "foo" => Value::test_string("5"),
636                "bar" => Value::test_string("6"),
637            }),
638        ]);
639
640        let center_columns = Value::test_list(vec![Value::test_cell_path(CellPath {
641            members: vec![PathMember::test_string(
642                "bar".into(),
643                false,
644                Casing::Sensitive,
645            )],
646        })]);
647
648        let cell_path: Vec<CellPath> = center_columns
649            .into_list()
650            .unwrap()
651            .into_iter()
652            .map(|v| v.into_cell_path().unwrap())
653            .collect();
654
655        let center: Option<Vec<CellPath>> = Some(cell_path);
656
657        // With pretty
658        assert_eq!(
659            table(
660                value.clone().into_pipeline_data(),
661                true,
662                &center,
663                &Config::default()
664            ),
665            one(r#"
666            | foo | bar |
667            | --- |:---:|
668            | 1   |  2  |
669            | 3   |  4  |
670            | 5   |  6  |
671        "#)
672        );
673
674        // Without pretty
675        assert_eq!(
676            table(
677                value.clone().into_pipeline_data(),
678                false,
679                &center,
680                &Config::default()
681            ),
682            one(r#"
683            |foo|bar|
684            |-|:-:|
685            |1|2|
686            |3|4|
687            |5|6|
688        "#)
689        );
690    }
691
692    #[test]
693    fn test_empty_center_column() {
694        let value = Value::test_list(vec![
695            Value::test_record(record! {
696                "foo" => Value::test_string("1"),
697                "bar" => Value::test_string("2"),
698            }),
699            Value::test_record(record! {
700                "foo" => Value::test_string("3"),
701                "bar" => Value::test_string("4"),
702            }),
703            Value::test_record(record! {
704                "foo" => Value::test_string("5"),
705                "bar" => Value::test_string("6"),
706            }),
707        ]);
708
709        let center: Option<Vec<CellPath>> = Some(vec![]);
710
711        assert_eq!(
712            table(
713                value.clone().into_pipeline_data(),
714                true,
715                &center,
716                &Config::default()
717            ),
718            one(r#"
719            | foo | bar |
720            | --- | --- |
721            | 1   | 2   |
722            | 3   | 4   |
723            | 5   | 6   |
724        "#)
725        );
726    }
727
728    #[test]
729    fn test_center_multiple_columns() {
730        let value = Value::test_list(vec![
731            Value::test_record(record! {
732                "command" => Value::test_string("ls"),
733                "input" => Value::test_string("."),
734                "output" => Value::test_string("file.txt"),
735            }),
736            Value::test_record(record! {
737                "command" => Value::test_string("echo"),
738                "input" => Value::test_string("'hi'"),
739                "output" => Value::test_string("hi"),
740            }),
741            Value::test_record(record! {
742                "command" => Value::test_string("cp"),
743                "input" => Value::test_string("a.txt"),
744                "output" => Value::test_string("b.txt"),
745            }),
746        ]);
747
748        let center_columns = Value::test_list(vec![
749            Value::test_cell_path(CellPath {
750                members: vec![PathMember::test_string(
751                    "command".into(),
752                    false,
753                    Casing::Sensitive,
754                )],
755            }),
756            Value::test_cell_path(CellPath {
757                members: vec![PathMember::test_string(
758                    "output".into(),
759                    false,
760                    Casing::Sensitive,
761                )],
762            }),
763        ]);
764
765        let cell_path: Vec<CellPath> = center_columns
766            .into_list()
767            .unwrap()
768            .into_iter()
769            .map(|v| v.into_cell_path().unwrap())
770            .collect();
771
772        let center: Option<Vec<CellPath>> = Some(cell_path);
773
774        assert_eq!(
775            table(
776                value.clone().into_pipeline_data(),
777                true,
778                &center,
779                &Config::default()
780            ),
781            one(r#"
782            | command | input |  output  |
783            |:-------:| ----- |:--------:|
784            |   ls    | .     | file.txt |
785            |  echo   | 'hi'  |    hi    |
786            |   cp    | a.txt |  b.txt   |
787        "#)
788        );
789    }
790
791    #[test]
792    fn test_center_non_existing_column() {
793        let value = Value::test_list(vec![
794            Value::test_record(record! {
795                "name" => Value::test_string("Alice"),
796                "age" => Value::test_string("30"),
797            }),
798            Value::test_record(record! {
799                "name" => Value::test_string("Bob"),
800                "age" => Value::test_string("5"),
801            }),
802            Value::test_record(record! {
803                "name" => Value::test_string("Charlie"),
804                "age" => Value::test_string("20"),
805            }),
806        ]);
807
808        let center_columns = Value::test_list(vec![Value::test_cell_path(CellPath {
809            members: vec![PathMember::test_string(
810                "none".into(),
811                false,
812                Casing::Sensitive,
813            )],
814        })]);
815
816        let cell_path: Vec<CellPath> = center_columns
817            .into_list()
818            .unwrap()
819            .into_iter()
820            .map(|v| v.into_cell_path().unwrap())
821            .collect();
822
823        let center: Option<Vec<CellPath>> = Some(cell_path);
824
825        assert_eq!(
826            table(
827                value.clone().into_pipeline_data(),
828                true,
829                &center,
830                &Config::default()
831            ),
832            one(r#"
833            | name    | age |
834            | ------- | --- |
835            | Alice   | 30  |
836            | Bob     | 5   |
837            | Charlie | 20  |
838        "#)
839        );
840    }
841
842    #[test]
843    fn test_center_complex_cell_path() {
844        let value = Value::test_list(vec![
845            Value::test_record(record! {
846                "k" => Value::test_string("version"),
847                "v" => Value::test_string("0.104.1"),
848            }),
849            Value::test_record(record! {
850                "k" => Value::test_string("build_time"),
851                "v" => Value::test_string("2025-05-28 11:00:45 +01:00"),
852            }),
853        ]);
854
855        let center_columns = Value::test_list(vec![Value::test_cell_path(CellPath {
856            members: vec![
857                PathMember::test_int(1, false),
858                PathMember::test_string("v".into(), false, Casing::Sensitive),
859            ],
860        })]);
861
862        let cell_path: Vec<CellPath> = center_columns
863            .into_list()
864            .unwrap()
865            .into_iter()
866            .map(|v| v.into_cell_path().unwrap())
867            .collect();
868
869        let center: Option<Vec<CellPath>> = Some(cell_path);
870
871        assert_eq!(
872            table(
873                value.clone().into_pipeline_data(),
874                true,
875                &center,
876                &Config::default()
877            ),
878            one(r#"
879            | k          |             v              |
880            | ---------- |:--------------------------:|
881            | version    |          0.104.1           |
882            | build_time | 2025-05-28 11:00:45 +01:00 |
883        "#)
884        );
885    }
886
887    #[test]
888    fn test_content_type_metadata() {
889        let mut engine_state = Box::new(EngineState::new());
890        let state_delta = {
891            // Base functions that are needed for testing
892            // Try to keep this working set small to keep tests running as fast as possible
893            let mut working_set = StateWorkingSet::new(&engine_state);
894
895            working_set.add_decl(Box::new(ToMd {}));
896            working_set.add_decl(Box::new(Metadata {}));
897            working_set.add_decl(Box::new(Get {}));
898
899            working_set.render()
900        };
901        let delta = state_delta;
902
903        engine_state
904            .merge_delta(delta)
905            .expect("Error merging delta");
906
907        let cmd = "{a: 1 b: 2} | to md  | metadata | get content_type | $in";
908        let result = eval_pipeline_without_terminal_expression(
909            cmd,
910            std::env::temp_dir().as_ref(),
911            &mut engine_state,
912        );
913        assert_eq!(
914            Value::test_string("text/markdown"),
915            result.expect("There should be a result")
916        );
917    }
918}