Skip to main content

dkit_core/format/
markdown.rs

1use std::io::Write;
2
3use crate::format::FormatWriter;
4use crate::value::Value;
5
6/// Markdown 테이블 포맷 Writer (GFM 호환)
7///
8/// - Array<Object> → 컬럼 헤더 + 데이터 행
9/// - Array<Primitive> → 단일 "value" 컬럼
10/// - Single Object → key | value 2-컬럼 테이블
11/// - Primitive → 단일 값 출력
12pub struct MarkdownWriter;
13
14impl FormatWriter for MarkdownWriter {
15    fn write(&self, value: &Value) -> anyhow::Result<String> {
16        Ok(render_markdown(value))
17    }
18
19    fn write_to_writer(&self, value: &Value, mut writer: impl Write) -> anyhow::Result<()> {
20        let output = render_markdown(value);
21        writer.write_all(output.as_bytes())?;
22        Ok(())
23    }
24}
25
26fn render_markdown(value: &Value) -> String {
27    match value {
28        Value::Array(arr) if !arr.is_empty() && arr[0].as_object().is_some() => {
29            render_array_of_objects(arr)
30        }
31        Value::Array(arr) => render_array_of_primitives(arr),
32        Value::Object(_) => render_single_object(value),
33        _ => format_cell_value(value),
34    }
35}
36
37/// Array<Object> → Markdown 테이블
38fn render_array_of_objects(arr: &[Value]) -> String {
39    let headers = collect_keys(arr);
40    if headers.is_empty() {
41        return String::new();
42    }
43
44    let mut lines = Vec::new();
45
46    // 헤더 행
47    let header_line = format!(
48        "| {} |",
49        headers
50            .iter()
51            .map(|h| escape_pipe(h))
52            .collect::<Vec<_>>()
53            .join(" | ")
54    );
55    lines.push(header_line);
56
57    // 구분자 행 (숫자 컬럼은 우측 정렬)
58    let alignments: Vec<Alignment> = headers
59        .iter()
60        .map(|key| detect_column_alignment(arr, key))
61        .collect();
62    let separator_line = format!(
63        "| {} |",
64        alignments
65            .iter()
66            .map(|a| match a {
67                Alignment::Right => "---:".to_string(),
68                Alignment::Left => "---".to_string(),
69            })
70            .collect::<Vec<_>>()
71            .join(" | ")
72    );
73    lines.push(separator_line);
74
75    // 데이터 행
76    for item in arr {
77        if let Value::Object(obj) = item {
78            let row = headers
79                .iter()
80                .map(|key| match obj.get(key) {
81                    Some(Value::Null) => "null".to_string(),
82                    Some(v) => escape_pipe(&format_cell_value(v)),
83                    None => String::new(),
84                })
85                .collect::<Vec<_>>();
86            lines.push(format!("| {} |", row.join(" | ")));
87        }
88    }
89
90    lines.join("\n") + "\n"
91}
92
93/// Array<Primitive> → 단일 컬럼 Markdown 테이블
94fn render_array_of_primitives(arr: &[Value]) -> String {
95    let mut lines = Vec::new();
96    lines.push("| value |".to_string());
97    lines.push("| --- |".to_string());
98
99    for item in arr {
100        lines.push(format!("| {} |", escape_pipe(&format_cell_value(item))));
101    }
102
103    lines.join("\n") + "\n"
104}
105
106/// 단일 Object → key | value 테이블
107fn render_single_object(value: &Value) -> String {
108    let mut lines = Vec::new();
109    lines.push("| key | value |".to_string());
110    lines.push("| --- | --- |".to_string());
111
112    if let Value::Object(obj) = value {
113        for (k, v) in obj {
114            lines.push(format!(
115                "| {} | {} |",
116                escape_pipe(k),
117                escape_pipe(&format_cell_value(v))
118            ));
119        }
120    }
121
122    lines.join("\n") + "\n"
123}
124
125/// 모든 object에서 키를 순서 보존하며 수집
126fn collect_keys(arr: &[Value]) -> Vec<String> {
127    let mut keys: Vec<String> = Vec::new();
128    for item in arr {
129        if let Value::Object(obj) = item {
130            for k in obj.keys() {
131                if !keys.contains(k) {
132                    keys.push(k.clone());
133                }
134            }
135        }
136    }
137    keys
138}
139
140#[derive(Debug)]
141enum Alignment {
142    Left,
143    Right,
144}
145
146/// 컬럼의 값이 모두 숫자이면 우측 정렬, 아니면 좌측 정렬
147fn detect_column_alignment(arr: &[Value], key: &str) -> Alignment {
148    let mut has_value = false;
149    for item in arr {
150        if let Value::Object(obj) = item {
151            match obj.get(key) {
152                Some(Value::Integer(_)) | Some(Value::Float(_)) => {
153                    has_value = true;
154                }
155                Some(Value::Null) | None => {
156                    // null/missing은 무시
157                }
158                _ => return Alignment::Left,
159            }
160        }
161    }
162    if has_value {
163        Alignment::Right
164    } else {
165        Alignment::Left
166    }
167}
168
169/// Value를 셀 표시용 문자열로 변환
170fn format_cell_value(v: &Value) -> String {
171    match v {
172        Value::Null => "null".to_string(),
173        Value::Bool(b) => b.to_string(),
174        Value::Integer(n) => n.to_string(),
175        Value::Float(f) => f.to_string(),
176        Value::String(s) => s.clone(),
177        Value::Array(_) | Value::Object(_) => {
178            // 중첩 객체/배열은 JSON 문자열로 inline 표시
179            serde_json::to_string(&value_to_json(v)).unwrap_or_else(|_| "{...}".to_string())
180        }
181    }
182}
183
184/// Value를 serde_json::Value로 변환 (중첩 표시용)
185fn value_to_json(v: &Value) -> serde_json::Value {
186    match v {
187        Value::Null => serde_json::Value::Null,
188        Value::Bool(b) => serde_json::Value::Bool(*b),
189        Value::Integer(n) => serde_json::json!(n),
190        Value::Float(f) => serde_json::json!(f),
191        Value::String(s) => serde_json::Value::String(s.clone()),
192        Value::Array(arr) => serde_json::Value::Array(arr.iter().map(value_to_json).collect()),
193        Value::Object(obj) => {
194            let map: serde_json::Map<String, serde_json::Value> = obj
195                .iter()
196                .map(|(k, v)| (k.clone(), value_to_json(v)))
197                .collect();
198            serde_json::Value::Object(map)
199        }
200    }
201}
202
203/// 파이프 문자(`|`) 이스케이프 처리
204fn escape_pipe(s: &str) -> String {
205    s.replace('|', "\\|")
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211    use crate::format::FormatWriter;
212    use indexmap::IndexMap;
213
214    fn make_user(name: &str, age: i64) -> Value {
215        let mut m = IndexMap::new();
216        m.insert("name".to_string(), Value::String(name.to_string()));
217        m.insert("age".to_string(), Value::Integer(age));
218        Value::Object(m)
219    }
220
221    #[test]
222    fn test_array_of_objects() {
223        let data = Value::Array(vec![make_user("Alice", 30), make_user("Bob", 25)]);
224        let output = MarkdownWriter.write(&data).unwrap();
225        assert!(output.contains("| name | age |"));
226        assert!(output.contains("| --- | ---: |")); // age는 숫자이므로 우측 정렬
227        assert!(output.contains("| Alice | 30 |"));
228        assert!(output.contains("| Bob | 25 |"));
229    }
230
231    #[test]
232    fn test_numeric_right_alignment() {
233        let mut m1 = IndexMap::new();
234        m1.insert("label".to_string(), Value::String("x".to_string()));
235        m1.insert("count".to_string(), Value::Integer(10));
236        let mut m2 = IndexMap::new();
237        m2.insert("label".to_string(), Value::String("y".to_string()));
238        m2.insert("count".to_string(), Value::Integer(20));
239        let data = Value::Array(vec![Value::Object(m1), Value::Object(m2)]);
240        let output = MarkdownWriter.write(&data).unwrap();
241        // label은 좌측, count는 우측 정렬
242        assert!(output.contains("| --- | ---: |"));
243    }
244
245    #[test]
246    fn test_mixed_column_left_alignment() {
247        let mut m1 = IndexMap::new();
248        m1.insert("val".to_string(), Value::Integer(10));
249        let mut m2 = IndexMap::new();
250        m2.insert("val".to_string(), Value::String("text".to_string()));
251        let data = Value::Array(vec![Value::Object(m1), Value::Object(m2)]);
252        let output = MarkdownWriter.write(&data).unwrap();
253        // 혼합 타입이므로 좌측 정렬
254        assert!(output.contains("| --- |"));
255        assert!(!output.contains("---:"));
256    }
257
258    #[test]
259    fn test_single_object() {
260        let mut m = IndexMap::new();
261        m.insert("host".to_string(), Value::String("localhost".to_string()));
262        m.insert("port".to_string(), Value::Integer(8080));
263        let data = Value::Object(m);
264        let output = MarkdownWriter.write(&data).unwrap();
265        assert!(output.contains("| key | value |"));
266        assert!(output.contains("| host | localhost |"));
267        assert!(output.contains("| port | 8080 |"));
268    }
269
270    #[test]
271    fn test_array_of_primitives() {
272        let data = Value::Array(vec![
273            Value::Integer(1),
274            Value::Integer(2),
275            Value::Integer(3),
276        ]);
277        let output = MarkdownWriter.write(&data).unwrap();
278        assert!(output.contains("| value |"));
279        assert!(output.contains("| 1 |"));
280        assert!(output.contains("| 2 |"));
281        assert!(output.contains("| 3 |"));
282    }
283
284    #[test]
285    fn test_primitive_value() {
286        let data = Value::String("hello".to_string());
287        let output = MarkdownWriter.write(&data).unwrap();
288        assert_eq!(output, "hello");
289    }
290
291    #[test]
292    fn test_null_value_in_cell() {
293        let mut m = IndexMap::new();
294        m.insert("name".to_string(), Value::String("Alice".to_string()));
295        m.insert("email".to_string(), Value::Null);
296        let data = Value::Array(vec![Value::Object(m)]);
297        let output = MarkdownWriter.write(&data).unwrap();
298        assert!(output.contains("null"));
299    }
300
301    #[test]
302    fn test_nested_value_json_inline() {
303        let mut m = IndexMap::new();
304        m.insert(
305            "tags".to_string(),
306            Value::Array(vec![
307                Value::String("a".to_string()),
308                Value::String("b".to_string()),
309            ]),
310        );
311        let data = Value::Array(vec![Value::Object(m)]);
312        let output = MarkdownWriter.write(&data).unwrap();
313        // 중첩 배열은 JSON 문자열로 표시
314        assert!(output.contains(r#"["a","b"]"#));
315    }
316
317    #[test]
318    fn test_pipe_escape() {
319        let mut m = IndexMap::new();
320        m.insert("formula".to_string(), Value::String("a | b".to_string()));
321        let data = Value::Array(vec![Value::Object(m)]);
322        let output = MarkdownWriter.write(&data).unwrap();
323        assert!(output.contains(r"a \| b"));
324    }
325
326    #[test]
327    fn test_empty_array() {
328        let data = Value::Array(vec![]);
329        let output = MarkdownWriter.write(&data).unwrap();
330        assert!(output.contains("| value |"));
331    }
332
333    #[test]
334    fn test_missing_fields() {
335        let mut m1 = IndexMap::new();
336        m1.insert("a".to_string(), Value::Integer(1));
337        m1.insert("b".to_string(), Value::Integer(2));
338        let mut m2 = IndexMap::new();
339        m2.insert("a".to_string(), Value::Integer(3));
340        // m2 has no "b" field
341        let data = Value::Array(vec![Value::Object(m1), Value::Object(m2)]);
342        let output = MarkdownWriter.write(&data).unwrap();
343        assert!(output.contains("| a | b |"));
344        assert!(output.contains("| 3 |  |")); // missing field → empty
345    }
346}