dkit_core/format/
markdown.rs1use std::io::Write;
2
3use crate::format::FormatWriter;
4use crate::value::Value;
5
6pub 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
37fn 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 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 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 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
93fn 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
106fn 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
125fn 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
146fn 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 }
158 _ => return Alignment::Left,
159 }
160 }
161 }
162 if has_value {
163 Alignment::Right
164 } else {
165 Alignment::Left
166 }
167}
168
169fn 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 serde_json::to_string(&value_to_json(v)).unwrap_or_else(|_| "{...}".to_string())
180 }
181 }
182}
183
184fn 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
203fn 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("| --- | ---: |")); 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 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 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 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 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 | |")); }
346}