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    let output_string = 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    output_string
266}
267
268pub fn group_by(values: PipelineData, head: Span, config: &Config) -> (PipelineData, bool) {
269    let mut lists = IndexMap::new();
270    let mut single_list = false;
271    for val in values {
272        if let Value::Record {
273            val: ref record, ..
274        } = val
275        {
276            lists
277                .entry(record.columns().map(|c| c.as_str()).collect::<String>())
278                .and_modify(|v: &mut Vec<Value>| v.push(val.clone()))
279                .or_insert_with(|| vec![val.clone()]);
280        } else {
281            lists
282                .entry(val.to_expanded_string(",", config))
283                .and_modify(|v: &mut Vec<Value>| v.push(val.clone()))
284                .or_insert_with(|| vec![val.clone()]);
285        }
286    }
287    let mut output = vec![];
288    for (_, mut value) in lists {
289        if value.len() == 1 {
290            output.push(value.pop().unwrap_or_else(|| Value::nothing(head)))
291        } else {
292            output.push(Value::list(value.to_vec(), head))
293        }
294    }
295    if output.len() == 1 {
296        single_list = true;
297    }
298    (Value::list(output, head).into_pipeline_data(), single_list)
299}
300
301fn get_output_string(
302    headers: &[String],
303    rows: &[Vec<String>],
304    column_widths: &[usize],
305    pretty: bool,
306    center: &Option<Vec<CellPath>>,
307) -> String {
308    let mut output_string = String::new();
309
310    let mut to_center: HashSet<String> = HashSet::new();
311    if let Some(center_vec) = center.as_ref() {
312        for cell_path in center_vec {
313            if let Some(PathMember::String { val, .. }) = cell_path
314                .members
315                .iter()
316                .find(|member| matches!(member, PathMember::String { .. }))
317            {
318                to_center.insert(val.clone());
319            }
320        }
321    }
322
323    if !headers.is_empty() {
324        output_string.push('|');
325
326        for i in 0..headers.len() {
327            if pretty {
328                output_string.push(' ');
329                if center.is_some() && to_center.contains(&headers[i]) {
330                    output_string.push_str(&get_centered_string(
331                        headers[i].clone(),
332                        column_widths[i],
333                        ' ',
334                    ));
335                } else {
336                    output_string.push_str(&get_padded_string(
337                        headers[i].clone(),
338                        column_widths[i],
339                        ' ',
340                    ));
341                }
342                output_string.push(' ');
343            } else {
344                output_string.push_str(&headers[i]);
345            }
346
347            output_string.push('|');
348        }
349
350        output_string.push_str("\n|");
351
352        for i in 0..headers.len() {
353            let centered_column = center.is_some() && to_center.contains(&headers[i]);
354            let border_char = if centered_column { ':' } else { ' ' };
355            if pretty {
356                output_string.push(border_char);
357                output_string.push_str(&get_padded_string(
358                    String::from("-"),
359                    column_widths[i],
360                    '-',
361                ));
362                output_string.push(border_char);
363            } else if centered_column {
364                output_string.push(':');
365                output_string.push('-');
366                output_string.push(':');
367            } else {
368                output_string.push('-');
369            }
370
371            output_string.push('|');
372        }
373
374        output_string.push('\n');
375    }
376
377    for row in rows {
378        if !headers.is_empty() {
379            output_string.push('|');
380        }
381
382        for i in 0..row.len() {
383            if pretty && column_widths.get(i).is_some() {
384                output_string.push(' ');
385                if center.is_some() && to_center.contains(&headers[i]) {
386                    output_string.push_str(&get_centered_string(
387                        row[i].clone(),
388                        column_widths[i],
389                        ' ',
390                    ));
391                } else {
392                    output_string.push_str(&get_padded_string(
393                        row[i].clone(),
394                        column_widths[i],
395                        ' ',
396                    ));
397                }
398                output_string.push(' ');
399            } else {
400                output_string.push_str(&row[i]);
401            }
402
403            if !headers.is_empty() {
404                output_string.push('|');
405            }
406        }
407
408        output_string.push('\n');
409    }
410
411    output_string
412}
413
414fn get_centered_string(text: String, desired_length: usize, padding_character: char) -> String {
415    let total_padding = if text.len() > desired_length {
416        0
417    } else {
418        desired_length - text.len()
419    };
420
421    let repeat_left = total_padding / 2;
422    let repeat_right = total_padding - repeat_left;
423
424    format!(
425        "{}{}{}",
426        padding_character.to_string().repeat(repeat_left),
427        text,
428        padding_character.to_string().repeat(repeat_right)
429    )
430}
431
432fn get_padded_string(text: String, desired_length: usize, padding_character: char) -> String {
433    let repeat_length = if text.len() > desired_length {
434        0
435    } else {
436        desired_length - text.len()
437    };
438
439    format!(
440        "{}{}",
441        text,
442        padding_character.to_string().repeat(repeat_length)
443    )
444}
445
446#[cfg(test)]
447mod tests {
448    use crate::{Get, Metadata};
449
450    use super::*;
451    use nu_cmd_lang::eval_pipeline_without_terminal_expression;
452    use nu_protocol::{Config, IntoPipelineData, Value, casing::Casing, record};
453
454    fn one(string: &str) -> String {
455        string
456            .lines()
457            .skip(1)
458            .map(|line| line.trim())
459            .collect::<Vec<&str>>()
460            .join("\n")
461            .trim_end()
462            .to_string()
463    }
464
465    #[test]
466    fn test_examples() {
467        use crate::test_examples;
468
469        test_examples(ToMd {})
470    }
471
472    #[test]
473    fn render_h1() {
474        let value = Value::test_record(record! {
475            "H1" => Value::test_string("Ecuador"),
476        });
477
478        assert_eq!(
479            fragment(value, false, &None, &Config::default()),
480            "# Ecuador\n"
481        );
482    }
483
484    #[test]
485    fn render_h2() {
486        let value = Value::test_record(record! {
487            "H2" => Value::test_string("Ecuador"),
488        });
489
490        assert_eq!(
491            fragment(value, false, &None, &Config::default()),
492            "## Ecuador\n"
493        );
494    }
495
496    #[test]
497    fn render_h3() {
498        let value = Value::test_record(record! {
499            "H3" => Value::test_string("Ecuador"),
500        });
501
502        assert_eq!(
503            fragment(value, false, &None, &Config::default()),
504            "### Ecuador\n"
505        );
506    }
507
508    #[test]
509    fn render_blockquote() {
510        let value = Value::test_record(record! {
511            "BLOCKQUOTE" => Value::test_string("Ecuador"),
512        });
513
514        assert_eq!(
515            fragment(value, false, &None, &Config::default()),
516            "> Ecuador\n"
517        );
518    }
519
520    #[test]
521    fn render_table() {
522        let value = Value::test_list(vec![
523            Value::test_record(record! {
524                "country" => Value::test_string("Ecuador"),
525            }),
526            Value::test_record(record! {
527                "country" => Value::test_string("New Zealand"),
528            }),
529            Value::test_record(record! {
530                "country" => Value::test_string("USA"),
531            }),
532        ]);
533
534        assert_eq!(
535            table(
536                value.clone().into_pipeline_data(),
537                false,
538                &None,
539                &Config::default()
540            ),
541            one(r#"
542            |country|
543            |-|
544            |Ecuador|
545            |New Zealand|
546            |USA|
547        "#)
548        );
549
550        assert_eq!(
551            table(value.into_pipeline_data(), true, &None, &Config::default()),
552            one(r#"
553            | country     |
554            | ----------- |
555            | Ecuador     |
556            | New Zealand |
557            | USA         |
558        "#)
559        );
560    }
561
562    #[test]
563    fn test_empty_column_header() {
564        let value = Value::test_list(vec![
565            Value::test_record(record! {
566                "" => Value::test_string("1"),
567                "foo" => Value::test_string("2"),
568            }),
569            Value::test_record(record! {
570                "" => Value::test_string("3"),
571                "foo" => Value::test_string("4"),
572            }),
573        ]);
574
575        assert_eq!(
576            table(
577                value.clone().into_pipeline_data(),
578                false,
579                &None,
580                &Config::default()
581            ),
582            one(r#"
583            ||foo|
584            |-|-|
585            |1|2|
586            |3|4|
587        "#)
588        );
589    }
590
591    #[test]
592    fn test_empty_row_value() {
593        let value = Value::test_list(vec![
594            Value::test_record(record! {
595                "foo" => Value::test_string("1"),
596                "bar" => Value::test_string("2"),
597            }),
598            Value::test_record(record! {
599                "foo" => Value::test_string("3"),
600                "bar" => Value::test_string("4"),
601            }),
602            Value::test_record(record! {
603                "foo" => Value::test_string("5"),
604                "bar" => Value::test_string(""),
605            }),
606        ]);
607
608        assert_eq!(
609            table(
610                value.clone().into_pipeline_data(),
611                false,
612                &None,
613                &Config::default()
614            ),
615            one(r#"
616            |foo|bar|
617            |-|-|
618            |1|2|
619            |3|4|
620            |5||
621        "#)
622        );
623    }
624
625    #[test]
626    fn test_center_column() {
627        let value = Value::test_list(vec![
628            Value::test_record(record! {
629                "foo" => Value::test_string("1"),
630                "bar" => Value::test_string("2"),
631            }),
632            Value::test_record(record! {
633                "foo" => Value::test_string("3"),
634                "bar" => Value::test_string("4"),
635            }),
636            Value::test_record(record! {
637                "foo" => Value::test_string("5"),
638                "bar" => Value::test_string("6"),
639            }),
640        ]);
641
642        let center_columns = Value::test_list(vec![Value::test_cell_path(CellPath {
643            members: vec![PathMember::test_string(
644                "bar".into(),
645                false,
646                Casing::Sensitive,
647            )],
648        })]);
649
650        let cell_path: Vec<CellPath> = center_columns
651            .into_list()
652            .unwrap()
653            .into_iter()
654            .map(|v| v.into_cell_path().unwrap())
655            .collect();
656
657        let center: Option<Vec<CellPath>> = Some(cell_path);
658
659        // With pretty
660        assert_eq!(
661            table(
662                value.clone().into_pipeline_data(),
663                true,
664                &center,
665                &Config::default()
666            ),
667            one(r#"
668            | foo | bar |
669            | --- |:---:|
670            | 1   |  2  |
671            | 3   |  4  |
672            | 5   |  6  |
673        "#)
674        );
675
676        // Without pretty
677        assert_eq!(
678            table(
679                value.clone().into_pipeline_data(),
680                false,
681                &center,
682                &Config::default()
683            ),
684            one(r#"
685            |foo|bar|
686            |-|:-:|
687            |1|2|
688            |3|4|
689            |5|6|
690        "#)
691        );
692    }
693
694    #[test]
695    fn test_empty_center_column() {
696        let value = Value::test_list(vec![
697            Value::test_record(record! {
698                "foo" => Value::test_string("1"),
699                "bar" => Value::test_string("2"),
700            }),
701            Value::test_record(record! {
702                "foo" => Value::test_string("3"),
703                "bar" => Value::test_string("4"),
704            }),
705            Value::test_record(record! {
706                "foo" => Value::test_string("5"),
707                "bar" => Value::test_string("6"),
708            }),
709        ]);
710
711        let center: Option<Vec<CellPath>> = Some(vec![]);
712
713        assert_eq!(
714            table(
715                value.clone().into_pipeline_data(),
716                true,
717                &center,
718                &Config::default()
719            ),
720            one(r#"
721            | foo | bar |
722            | --- | --- |
723            | 1   | 2   |
724            | 3   | 4   |
725            | 5   | 6   |
726        "#)
727        );
728    }
729
730    #[test]
731    fn test_center_multiple_columns() {
732        let value = Value::test_list(vec![
733            Value::test_record(record! {
734                "command" => Value::test_string("ls"),
735                "input" => Value::test_string("."),
736                "output" => Value::test_string("file.txt"),
737            }),
738            Value::test_record(record! {
739                "command" => Value::test_string("echo"),
740                "input" => Value::test_string("'hi'"),
741                "output" => Value::test_string("hi"),
742            }),
743            Value::test_record(record! {
744                "command" => Value::test_string("cp"),
745                "input" => Value::test_string("a.txt"),
746                "output" => Value::test_string("b.txt"),
747            }),
748        ]);
749
750        let center_columns = Value::test_list(vec![
751            Value::test_cell_path(CellPath {
752                members: vec![PathMember::test_string(
753                    "command".into(),
754                    false,
755                    Casing::Sensitive,
756                )],
757            }),
758            Value::test_cell_path(CellPath {
759                members: vec![PathMember::test_string(
760                    "output".into(),
761                    false,
762                    Casing::Sensitive,
763                )],
764            }),
765        ]);
766
767        let cell_path: Vec<CellPath> = center_columns
768            .into_list()
769            .unwrap()
770            .into_iter()
771            .map(|v| v.into_cell_path().unwrap())
772            .collect();
773
774        let center: Option<Vec<CellPath>> = Some(cell_path);
775
776        assert_eq!(
777            table(
778                value.clone().into_pipeline_data(),
779                true,
780                &center,
781                &Config::default()
782            ),
783            one(r#"
784            | command | input |  output  |
785            |:-------:| ----- |:--------:|
786            |   ls    | .     | file.txt |
787            |  echo   | 'hi'  |    hi    |
788            |   cp    | a.txt |  b.txt   |
789        "#)
790        );
791    }
792
793    #[test]
794    fn test_center_non_existing_column() {
795        let value = Value::test_list(vec![
796            Value::test_record(record! {
797                "name" => Value::test_string("Alice"),
798                "age" => Value::test_string("30"),
799            }),
800            Value::test_record(record! {
801                "name" => Value::test_string("Bob"),
802                "age" => Value::test_string("5"),
803            }),
804            Value::test_record(record! {
805                "name" => Value::test_string("Charlie"),
806                "age" => Value::test_string("20"),
807            }),
808        ]);
809
810        let center_columns = Value::test_list(vec![Value::test_cell_path(CellPath {
811            members: vec![PathMember::test_string(
812                "none".into(),
813                false,
814                Casing::Sensitive,
815            )],
816        })]);
817
818        let cell_path: Vec<CellPath> = center_columns
819            .into_list()
820            .unwrap()
821            .into_iter()
822            .map(|v| v.into_cell_path().unwrap())
823            .collect();
824
825        let center: Option<Vec<CellPath>> = Some(cell_path);
826
827        assert_eq!(
828            table(
829                value.clone().into_pipeline_data(),
830                true,
831                &center,
832                &Config::default()
833            ),
834            one(r#"
835            | name    | age |
836            | ------- | --- |
837            | Alice   | 30  |
838            | Bob     | 5   |
839            | Charlie | 20  |
840        "#)
841        );
842    }
843
844    #[test]
845    fn test_center_complex_cell_path() {
846        let value = Value::test_list(vec![
847            Value::test_record(record! {
848                "k" => Value::test_string("version"),
849                "v" => Value::test_string("0.104.1"),
850            }),
851            Value::test_record(record! {
852                "k" => Value::test_string("build_time"),
853                "v" => Value::test_string("2025-05-28 11:00:45 +01:00"),
854            }),
855        ]);
856
857        let center_columns = Value::test_list(vec![Value::test_cell_path(CellPath {
858            members: vec![
859                PathMember::test_int(1, false),
860                PathMember::test_string("v".into(), false, Casing::Sensitive),
861            ],
862        })]);
863
864        let cell_path: Vec<CellPath> = center_columns
865            .into_list()
866            .unwrap()
867            .into_iter()
868            .map(|v| v.into_cell_path().unwrap())
869            .collect();
870
871        let center: Option<Vec<CellPath>> = Some(cell_path);
872
873        assert_eq!(
874            table(
875                value.clone().into_pipeline_data(),
876                true,
877                &center,
878                &Config::default()
879            ),
880            one(r#"
881            | k          |             v              |
882            | ---------- |:--------------------------:|
883            | version    |          0.104.1           |
884            | build_time | 2025-05-28 11:00:45 +01:00 |
885        "#)
886        );
887    }
888
889    #[test]
890    fn test_content_type_metadata() {
891        let mut engine_state = Box::new(EngineState::new());
892        let state_delta = {
893            // Base functions that are needed for testing
894            // Try to keep this working set small to keep tests running as fast as possible
895            let mut working_set = StateWorkingSet::new(&engine_state);
896
897            working_set.add_decl(Box::new(ToMd {}));
898            working_set.add_decl(Box::new(Metadata {}));
899            working_set.add_decl(Box::new(Get {}));
900
901            working_set.render()
902        };
903        let delta = state_delta;
904
905        engine_state
906            .merge_delta(delta)
907            .expect("Error merging delta");
908
909        let cmd = "{a: 1 b: 2} | to md  | metadata | get content_type";
910        let result = eval_pipeline_without_terminal_expression(
911            cmd,
912            std::env::temp_dir().as_ref(),
913            &mut engine_state,
914        );
915        assert_eq!(
916            Value::test_record(record!("content_type" => Value::test_string("text/markdown"))),
917            result.expect("There should be a result")
918        );
919    }
920}