Skip to main content

xyz_rust/cli/
format.rs

1//! `--format` 的渲染器与取值(spec §10.7)。
2//!
3//! json 复用既有 --json 路径(pretty),text 走 §9.1 人类渲染;本模块提供
4//! jsonl 与 markdown 两个渲染器,以及合法的取值枚举。优先级:显式
5//! json/jsonl/markdown 绕过 §9.5 自定义输出,仅 text 进入默认链。
6
7use std::io::Write;
8
9use serde_json::Value;
10
11use crate::errors;
12
13/// 输出格式(spec §10.7)。
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15pub enum Format {
16    /// TTY 感知默认(spec §10.7):按 writer 是否交互解析为
17    /// format_interactive(默认 text)或 format_piped(默认 jsonl)。
18    /// resolve 之后不再出现在渲染路径上。
19    #[default]
20    Auto,
21    /// §9.1 人类渲染。
22    Text,
23    /// pretty JSON(两空格缩进),裸值。
24    Json,
25    /// JSON Lines:数组逐元素紧凑一行,其余整体一行。
26    JsonL,
27    /// Markdown:表格/列表渲染。
28    Markdown,
29}
30
31impl Format {
32    /// 解析 `--format` 取值("" 视为 text)。
33    pub fn parse(s: &str) -> Option<Format> {
34        match s {
35            "" | "auto" => Some(Format::Auto),
36            "text" => Some(Format::Text),
37            "json" => Some(Format::Json),
38            "jsonl" => Some(Format::JsonL),
39            "markdown" => Some(Format::Markdown),
40            _ => None,
41        }
42    }
43
44    pub fn as_str(&self) -> &'static str {
45        match self {
46            Format::Auto => "auto",
47            Format::Text => "text",
48            Format::Json => "json",
49            Format::JsonL => "jsonl",
50            Format::Markdown => "markdown",
51        }
52    }
53
54    /// 机器/备用格式(错误体走 §8.6 对象、绕过自定义渲染的判定用)。
55    /// auto 未解析时按非机器处理(解析后不会以 Auto 到达渲染层)。
56    pub fn is_machine(&self) -> bool {
57        matches!(self, Format::Json | Format::JsonL)
58    }
59
60    /// 解析 auto(spec §10.7):interactive 时取 ifmt(空→text),否则取
61    /// pfmt(空→jsonl);具体值直通。
62    pub fn resolve(self, interactive: bool, ifmt: &str, pfmt: &str) -> Format {
63        if !matches!(self, Format::Auto) {
64            return self;
65        }
66        let pick = if interactive {
67            if ifmt.is_empty() { "text" } else { ifmt }
68        } else if pfmt.is_empty() {
69            "jsonl"
70        } else {
71            pfmt
72        };
73        Format::parse(pick).unwrap_or(if interactive {
74            Format::Text
75        } else {
76            Format::JsonL
77        })
78    }
79}
80
81/// JSON Lines:数组/切片逐元素紧凑一行;其余值整体紧凑一行;null 无输出。
82pub fn render_jsonl(w: &mut dyn Write, v: &Value) -> errors::Result<()> {
83    match v {
84        Value::Null => Ok(()),
85        Value::Array(items) => {
86            for item in items {
87                writeln!(
88                    w,
89                    "{}",
90                    serde_json::to_string(item).unwrap_or_else(|_| "null".into())
91                )?;
92            }
93            Ok(())
94        }
95        other => {
96            writeln!(
97                w,
98                "{}",
99                serde_json::to_string(other).unwrap_or_else(|_| "null".into())
100            )?;
101            Ok(())
102        }
103    }
104}
105
106/// Markdown(spec §10.7):
107/// null→无输出;标量→裸值;对象→`| Field | Value |` 两列表(插入序,与
108/// text 渲染同源;Go 对 map 用 `| Key | Value |` 表头,Rust 侧 struct/map
109/// 序列化后同形,统一 Field 表头——见 README 差异节);
110/// 对象数组→以首元素键为列的表;标量数组→`- item` 子弹;单元格转义
111/// `|`→`\|`、换行→`<br>`。
112pub fn render_markdown(w: &mut dyn Write, v: &Value) -> errors::Result<()> {
113    match v {
114        Value::Null => Ok(()),
115        Value::String(_) | Value::Bool(_) | Value::Number(_) => {
116            writeln!(w, "{}", md_escape(&cell(v)))?;
117            Ok(())
118        }
119        Value::Object(o) => md_kv(w, o),
120        Value::Array(items) => {
121            if items.is_empty() {
122                return Ok(());
123            }
124            if items[0].is_object() {
125                md_table(w, items)
126            } else {
127                for item in items {
128                    writeln!(w, "- {}", md_escape(&cell(item)))?;
129                }
130                Ok(())
131            }
132        }
133    }
134}
135
136fn md_kv(w: &mut dyn Write, o: &serde_json::Map<String, Value>) -> errors::Result<()> {
137    if o.is_empty() {
138        return Ok(());
139    }
140    writeln!(w, "| Field | Value |")?;
141    writeln!(w, "| --- | --- |")?;
142    for (k, val) in o {
143        writeln!(w, "| {} | {} |", md_escape(k), md_escape(&cell(val)))?;
144    }
145    Ok(())
146}
147
148fn md_table(w: &mut dyn Write, items: &[Value]) -> errors::Result<()> {
149    let Some(first) = items[0].as_object() else {
150        return Ok(());
151    };
152    let keys: Vec<&String> = first.keys().collect();
153    if keys.is_empty() {
154        return Ok(());
155    }
156    write!(w, "|")?;
157    for k in &keys {
158        write!(w, " {} |", md_escape(k))?;
159    }
160    writeln!(w)?;
161    write!(w, "|")?;
162    for _ in &keys {
163        write!(w, " --- |")?;
164    }
165    writeln!(w)?;
166    for item in items {
167        write!(w, "|")?;
168        let row = item.as_object();
169        for k in &keys {
170            let val = row.and_then(|r| r.get(k.as_str()));
171            let text = match val {
172                Some(v) => md_escape(&cell(v)),
173                None => String::new(),
174            };
175            write!(w, " {text} |")?;
176        }
177        writeln!(w)?;
178    }
179    Ok(())
180}
181
182/// 单元格字符串:复用 §9.1 的单值渲染(数组 → `[a b]`、对象 → JSON 等)。
183fn cell(v: &Value) -> String {
184    crate::cli::render::format_cell(v)
185}
186
187/// 转义会破坏 Markdown 表格的字符:竖线与换行。
188fn md_escape(s: &str) -> String {
189    s.replace("\r\n", "<br>")
190        .replace('\n', "<br>")
191        .replace('|', "\\|")
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197    use serde_json::json;
198
199    fn md(v: &Value) -> String {
200        let mut buf = Vec::new();
201        render_markdown(&mut buf, v).unwrap();
202        String::from_utf8(buf).unwrap()
203    }
204
205    fn jsonl(v: &Value) -> String {
206        let mut buf = Vec::new();
207        render_jsonl(&mut buf, v).unwrap();
208        String::from_utf8(buf).unwrap()
209    }
210
211    #[test]
212    fn jsonl_shapes() {
213        assert_eq!(jsonl(&json!(null)), "");
214        assert_eq!(jsonl(&json!([1, 2])), "1\n2\n");
215        assert_eq!(jsonl(&json!({"a": 1})), "{\"a\":1}\n");
216        assert_eq!(jsonl(&json!("x")), "\"x\"\n");
217    }
218
219    #[test]
220    fn markdown_shapes() {
221        assert_eq!(md(&json!(null)), "");
222        assert_eq!(md(&json!("hi")), "hi\n");
223        assert_eq!(
224            md(&json!({"k": "v", "long": 1})),
225            "| Field | Value |\n| --- | --- |\n| k | v |\n| long | 1 |\n"
226        );
227        assert_eq!(md(&json!([1, "a"])), "- 1\n- a\n");
228        assert_eq!(
229            md(&json!([{"a": 1, "b": 2}, {"a": 3, "b": 4}])),
230            "| a | b |\n| --- | --- |\n| 1 | 2 |\n| 3 | 4 |\n"
231        );
232        // 转义:竖线与换行不破表。
233        assert_eq!(
234            md(&json!({"k": "a|b\nc"})),
235            "| Field | Value |\n| --- | --- |\n| k | a\\|b<br>c |\n"
236        );
237        // 空对象/空数组无输出。
238        assert_eq!(md(&json!({})), "");
239        assert_eq!(md(&json!([])), "");
240    }
241
242    #[test]
243    fn format_parsing() {
244        assert_eq!(Format::parse(""), Some(Format::Auto));
245        assert_eq!(Format::parse("auto"), Some(Format::Auto));
246        assert_eq!(Format::parse("jsonl"), Some(Format::JsonL));
247        assert_eq!(Format::parse("MARKDOWN"), None);
248        assert!(Format::Json.is_machine() && !Format::Markdown.is_machine());
249    }
250}