Repository: yshavit/mdq
Pull request #128 (2024-07-16 04:09:50)

change `--json` to `--output json`
Default is `--output markdown`.

This way is more flexible in the future, if I want more output formats for whatever reason.

Also fix an unrelated bug in the JSON output, which I found when I added an integ test for it.

Resolves a TODO, in service of #11.


Patch:
diff --git a/src/fmt_md.rs b/src/fmt_md.rs
index 20e120a..45074d7 100644
--- a/src/fmt_md.rs
+++ b/src/fmt_md.rs
@@ -1,11 +1,10 @@
 use crate::fmt_md_inlines::{MdInlinesWriter, MdInlinesWriterOptions};
 use clap::ValueEnum;
-use std::borrow::Borrow;
 use std::cmp::max;
 use std::fmt::Alignment;
 use std::ops::Deref;
 
-use crate::link_transform::{LinkLabel, LinkTransform};
+use crate::link_transform::LinkLabel;
 use crate::output::{Block, Output, SimpleWrite};
 use crate::str_utils::{pad_to, standard_align, CountingWriter};
 use crate::tree::*;
@@ -85,26 +84,6 @@ where
     writer_state.write_definitions(out, DefinitionsToWrite::Both, nodes_count > 1);
 }
 
-pub fn write_md_inlines<'a, I, W>(out: &mut Output<W>, nodes: I, inlines_writer: &mut MdInlinesWriter<'a>)
-where
-    I: Iterator<Item = MdElemRef<'a>>,
-    W: SimpleWrite,
-{
-    let mut writer_state = MdWriterState {
-        opts: &MdOptions {
-            link_reference_placement: ReferencePlacement::Doc, // but we won't actually write them
-            footnote_reference_placement: ReferencePlacement::Doc, // ditto
-            inline_options: MdInlinesWriterOptions {
-                link_format: LinkTransform::Keep, // unused here, but removing it is more refactoring than it's worth
-            },
-        },
-        prev_was_thematic_break: false,
-        inlines_writer,
-    };
-    // This will write everything but the references; we'll keep those in the inlines_writer
-    writer_state.write_md(out, nodes, true);
-}
-
 struct MdWriterState<'s, 'a> {
     opts: &'a MdOptions,
     prev_was_thematic_break: bool,
@@ -450,10 +429,7 @@ impl<'s, 'a> MdWriterState<'s, 'a> {
         });
     }
 
-    fn line_to_string<E>(&mut self, line: &'a [E]) -> String
-    where
-        E: Borrow<Inline>,
-    {
+    fn line_to_string(&mut self, line: &'a Line) -> String {
         let mut out = Output::new(String::with_capacity(line.len() * 10)); // rough guess
         self.inlines_writer.write_line(&mut out, line);
         out.take_underlying().unwrap()
diff --git a/src/fmt_md_inlines.rs b/src/fmt_md_inlines.rs
index 007e707..8b52326 100644
--- a/src/fmt_md_inlines.rs
+++ b/src/fmt_md_inlines.rs
@@ -5,7 +5,7 @@ use crate::tree::{
     TextVariant,
 };
 use serde::Serialize;
-use std::borrow::{Borrow, Cow};
+use std::borrow::Cow;
 use std::collections::{HashMap, HashSet};
 
 #[derive(Debug, Copy, Clone)]
@@ -102,13 +102,13 @@ impl<'a> MdInlinesWriter<'a> {
         self.pending_references.footnotes.drain().collect()
     }
 
-    pub fn write_line<E, W>(&mut self, out: &mut Output<W>, elems: &'a [E])
+    pub fn write_line<I, W>(&mut self, out: &mut Output<W>, elems: I)
     where
-        E: Borrow<Inline>,
+        I: IntoIterator<Item = &'a Inline>,
         W: SimpleWrite,
     {
         for elem in elems {
-            self.write_inline_element(out, elem.borrow());
+            self.write_inline_element(out, elem);
         }
     }
 
@@ -174,7 +174,7 @@ impl<'a> MdInlinesWriter<'a> {
         out.write_char('[');
         match &label {
             LinkLabel::Text(text) => out.write_str(text),
-            LinkLabel::Inline(text) => self.write_line(out, text),
+            LinkLabel::Inline(text) => self.write_line(out, *text),
         }
         out.write_char(']');
 
diff --git a/src/lib.rs b/src/lib.rs
index d0e3ff8..8b51bc0 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,6 +1,7 @@
-use clap::Parser;
+use clap::{Parser, ValueEnum};
 use output::Output;
 use std::borrow::Cow;
+use std::fmt::{Display, Formatter};
 use std::io;
 use std::io::{stdin, Read, Write};
 
@@ -48,8 +49,8 @@ pub struct Cli {
     pub(crate) link_format: LinkTransform,
 
     /// Output the results as a JSON object, instead of as markdown.
-    #[arg(long, short, default_value_t = false)]
-    pub(crate) json: bool, // TODO this should really be output=<json|md>
+    #[arg(long, short, default_value_t = OutputFormat::Markdown)]
+    pub(crate) output: OutputFormat,
 
     #[arg(
         short = ' ',
@@ -64,6 +65,29 @@ pub struct Cli {
     pub(crate) selectors: Option<String>,
 }
 
+#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
+pub enum OutputFormat {
+    /// Output results as Markdown.
+    Markdown,
+
+    /// Alias for markdown
+    Md,
+
+    /// Output results as JSON. Spans of inline elements (like within a single paragraph) will be rendered as a single string of
+    /// Markdown, not as separate JSON elements.
+    Json,
+}
+
+impl Display for OutputFormat {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        let self_str = match self {
+            OutputFormat::Markdown | OutputFormat::Md => "markdown",
+            OutputFormat::Json => "json",
+        };
+        f.write_str(self_str)
+    }
+}
+
 impl Cli {
     fn selector_string(&self) -> Cow<String> {
         match &self.selectors {
@@ -127,11 +151,14 @@ where
     };
 
     let mut stdout = get_out();
-    if cli.json {
-        serde_json::to_writer(&mut stdout, &SerdeDoc::new(&pipeline_nodes, md_options.inline_options)).unwrap();
-    } else {
-        let mut out = Output::new(Stream(&mut stdout));
-        fmt_md::write_md(&md_options, &mut out, pipeline_nodes.into_iter());
+    match cli.output {
+        OutputFormat::Markdown | OutputFormat::Md => {
+            let mut out = Output::new(Stream(&mut stdout));
+            fmt_md::write_md(&md_options, &mut out, pipeline_nodes.into_iter());
+        }
+        OutputFormat::Json => {
+            serde_json::to_writer(&mut stdout, &SerdeDoc::new(&pipeline_nodes, md_options.inline_options)).unwrap();
+        }
     }
 
     true
diff --git a/src/tree_ref_serde.rs b/src/tree_ref_serde.rs
index b4c9807..12599a0 100644
--- a/src/tree_ref_serde.rs
+++ b/src/tree_ref_serde.rs
@@ -1,4 +1,3 @@
-use crate::fmt_md;
 use crate::fmt_md_inlines::{MdInlinesWriter, MdInlinesWriterOptions, UrlAndTitle};
 use crate::link_transform::LinkLabel;
 use crate::output::Output;
@@ -287,9 +286,8 @@ fn inlines_to_string<'a, I>(inlines: I, writer: &mut MdInlinesWriter<'a>) -> Str
 where
     I: IntoIterator<Item = &'a Inline>,
 {
-    let md: Vec<_> = inlines.into_iter().map(|inline| MdElemRef::Inline(inline)).collect();
     let mut output = Output::new(String::with_capacity(16)); // guess
-    fmt_md::write_md_inlines(&mut output, md.into_iter().map(|e| e), writer);
+    writer.write_line(&mut output, inlines);
     output.take_underlying().unwrap()
 }
 


Test patch:
diff --git a/tests/md_cases/output_format.toml b/tests/md_cases/output_format.toml
new file mode 100644
index 0000000..81447ff
--- /dev/null
+++ b/tests/md_cases/output_format.toml
@@ -0,0 +1,39 @@
+[given]
+md = '''
+Test _one_ [two][1] three.
+
+[1]: https://example.com/1
+'''
+
+
+[expect."default"]
+cli_args = []
+output = '''
+Test _one_ [two][1] three.
+
+[1]: https://example.com/1
+'''
+
+
+[expect."md"]
+cli_args = ['-o', 'md']
+output = '''
+Test _one_ [two][1] three.
+
+[1]: https://example.com/1
+'''
+
+
+[expect."markdown"]
+cli_args = ['--output', 'markdown']
+output = '''
+Test _one_ [two][1] three.
+
+[1]: https://example.com/1
+'''
+
+
+[expect."json"]
+cli_args = ['--output', 'json']
+output = '''
+{"items":[{"document":[{"paragraph":"Test _one_ [two][1] three."}]}],"links":{"1":{"url":"https://example.com/1"}}}'''