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, ¢er, &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 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 assert_eq!(
659 table(
660 value.clone().into_pipeline_data(),
661 true,
662 ¢er,
663 &Config::default()
664 ),
665 one(r#"
666 | foo | bar |
667 | --- |:---:|
668 | 1 | 2 |
669 | 3 | 4 |
670 | 5 | 6 |
671 "#)
672 );
673
674 assert_eq!(
676 table(
677 value.clone().into_pipeline_data(),
678 false,
679 ¢er,
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 ¢er,
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 ¢er,
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 ¢er,
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 ¢er,
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 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}