use miette::miette;
#[cfg(test)]
use mq_lang::Shared;
use mq_markdown::ColorTheme;
fn json_quote(s: &str) -> String {
serde_json::to_string(s).unwrap()
}
fn colorize_json_value(value: &serde_json::Value, indent: usize, indent_unit: &str, theme: &ColorTheme<'_>) -> String {
let reset = &theme.code.1;
match value {
serde_json::Value::Null => format!("{}null{}", theme.blockquote_marker.0, reset),
serde_json::Value::Bool(b) => format!("{}{}{}", theme.heading.0, b, reset),
serde_json::Value::Number(n) => format!("{}{}{}", theme.emphasis.0, n, reset),
serde_json::Value::String(s) => format!("{}{}{}", theme.code.0, json_quote(s), reset),
serde_json::Value::Array(arr) => {
if arr.is_empty() {
return "[]".to_string();
}
let indent_str = indent_unit.repeat(indent);
let inner_indent = indent_unit.repeat(indent + 1);
let items: Vec<String> = arr
.iter()
.map(|v| {
format!(
"{}{}",
inner_indent,
colorize_json_value(v, indent + 1, indent_unit, theme)
)
})
.collect();
format!("[\n{}\n{}]", items.join(",\n"), indent_str)
}
serde_json::Value::Object(map) => {
if map.is_empty() {
return "{}".to_string();
}
let indent_str = indent_unit.repeat(indent);
let inner_indent = indent_unit.repeat(indent + 1);
let items: Vec<String> = map
.iter()
.map(|(k, v)| {
format!(
"{}{}{}{}: {}",
inner_indent,
theme.link_url.0,
json_quote(k),
reset,
colorize_json_value(v, indent + 1, indent_unit, theme)
)
})
.collect();
format!("{{\n{}\n{}}}", items.join(",\n"), indent_str)
}
}
}
pub(crate) fn runtime_values_to_json_value(runtime_values: &[mq_lang::RuntimeValue]) -> serde_json::Value {
let filtered: Vec<&mq_lang::RuntimeValue> = runtime_values
.iter()
.filter(|v| match v {
mq_lang::RuntimeValue::Markdown(node, _) => !node.is_empty() && !node.is_empty_fragment(),
_ => true,
})
.collect();
let all_markdown = filtered
.iter()
.all(|v| matches!(v, mq_lang::RuntimeValue::Markdown(_, _)));
if !all_markdown && filtered.len() == 1 {
filtered[0].clone().to_json_value()
} else {
let json_values: Vec<serde_json::Value> = filtered
.iter()
.map(|v| match v {
mq_lang::RuntimeValue::Markdown(node, _) => {
serde_json::to_value(node.as_ref()).unwrap_or(serde_json::Value::Null)
}
_ => (*v).clone().to_json_value(),
})
.collect();
serde_json::Value::Array(json_values)
}
}
fn colorize_json_value_compact(value: &serde_json::Value, theme: &ColorTheme<'_>) -> String {
let reset = &theme.code.1;
match value {
serde_json::Value::Null => format!("{}null{}", theme.blockquote_marker.0, reset),
serde_json::Value::Bool(b) => format!("{}{}{}", theme.heading.0, b, reset),
serde_json::Value::Number(n) => format!("{}{}{}", theme.emphasis.0, n, reset),
serde_json::Value::String(s) => format!("{}{}{}", theme.code.0, json_quote(s), reset),
serde_json::Value::Array(arr) => {
let items: Vec<String> = arr.iter().map(|v| colorize_json_value_compact(v, theme)).collect();
format!("[{}]", items.join(","))
}
serde_json::Value::Object(map) => {
let items: Vec<String> = map
.iter()
.map(|(k, v)| {
format!(
"{}{}{}:{}",
theme.link_url.0,
json_quote(k),
reset,
colorize_json_value_compact(v, theme)
)
})
.collect();
format!("{{{}}}", items.join(","))
}
}
}
pub(crate) fn runtime_values_to_json(
runtime_values: &[mq_lang::RuntimeValue],
theme: Option<&ColorTheme<'_>>,
compact: bool,
indent_unit: &str,
) -> miette::Result<String> {
let result = runtime_values_to_json_value(runtime_values);
match (theme, compact) {
(Some(theme), true) => Ok(colorize_json_value_compact(&result, theme)),
(Some(theme), false) => Ok(colorize_json_value(&result, 0, indent_unit, theme)),
(None, true) => serde_json::to_string(&result).map_err(|e| miette!("Failed to serialize to JSON: {}", e)),
(None, false) => Ok(colorize_json_value(&result, 0, indent_unit, &ColorTheme::PLAIN)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use mq_lang::RuntimeValue;
use rstest::rstest;
fn plain_theme() -> ColorTheme<'static> {
ColorTheme::PLAIN
}
#[rstest]
#[case(vec![RuntimeValue::String(Shared::new("hello".to_string()))], "\"hello\"")]
#[case(vec![RuntimeValue::Boolean(true)], "true")]
#[case(vec![RuntimeValue::Boolean(false)], "false")]
#[case(vec![RuntimeValue::None], "null")]
fn test_single_non_markdown_no_theme(#[case] values: Vec<RuntimeValue>, #[case] expected: &str) {
let result = runtime_values_to_json(&values, None, false, " ").unwrap();
assert_eq!(result, expected);
}
#[test]
fn test_multiple_values_becomes_array() {
let values = vec![
RuntimeValue::String(Shared::new("a".to_string())),
RuntimeValue::String(Shared::new("b".to_string())),
];
let result = runtime_values_to_json(&values, None, false, " ").unwrap();
let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
assert!(parsed.is_array());
assert_eq!(parsed.as_array().unwrap().len(), 2);
}
#[test]
fn test_empty_markdown_filtered_out() {
let empty_node = mq_markdown::Node::Empty;
let values = vec![RuntimeValue::Markdown(Shared::new(empty_node), None)];
let result = runtime_values_to_json(&values, None, false, " ").unwrap();
let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
assert!(parsed.is_array());
assert!(parsed.as_array().unwrap().is_empty());
}
#[test]
fn test_with_plain_theme_null() {
let theme = plain_theme();
let values = vec![RuntimeValue::None];
let result = runtime_values_to_json(&values, Some(&theme), false, " ").unwrap();
assert!(result.contains("null"));
}
#[test]
fn test_with_plain_theme_bool() {
let theme = plain_theme();
let values = vec![RuntimeValue::Boolean(true)];
let result = runtime_values_to_json(&values, Some(&theme), false, " ").unwrap();
assert!(result.contains("true"));
}
#[test]
fn test_with_plain_theme_number() {
let theme = plain_theme();
let values = vec![RuntimeValue::from(42usize)];
let result = runtime_values_to_json(&values, Some(&theme), false, " ").unwrap();
assert!(result.contains("42"));
}
#[test]
fn test_with_plain_theme_string() {
let theme = plain_theme();
let values = vec![RuntimeValue::String(Shared::new("hi".to_string()))];
let result = runtime_values_to_json(&values, Some(&theme), false, " ").unwrap();
assert!(result.contains("hi"));
}
#[test]
fn test_colorize_array_empty() {
let theme = plain_theme();
let values = vec![RuntimeValue::Array(Shared::new(vec![]))];
let result = runtime_values_to_json(&values, Some(&theme), false, " ").unwrap();
assert_eq!(result, "[]");
}
#[test]
fn test_colorize_array_non_empty() {
let theme = plain_theme();
let values = vec![RuntimeValue::Array(Shared::new(vec![
RuntimeValue::String(Shared::new("x".to_string())),
RuntimeValue::String(Shared::new("y".to_string())),
]))];
let result = runtime_values_to_json(&values, Some(&theme), false, " ").unwrap();
assert!(result.contains('[') && result.contains(']'));
assert!(result.contains("\"x\"") && result.contains("\"y\""));
}
#[test]
fn test_colorize_object_empty() {
let theme = plain_theme();
let values = vec![RuntimeValue::Dict(Shared::new(mq_lang::DictMap::default()))];
let result = runtime_values_to_json(&values, Some(&theme), false, " ").unwrap();
assert_eq!(result, "{}");
}
#[test]
fn test_colorize_object_non_empty() {
let theme = plain_theme();
let mut map = mq_lang::DictMap::default();
map.insert(
mq_lang::Ident::new("key"),
RuntimeValue::String(Shared::new("val".to_string())),
);
let values = vec![RuntimeValue::Dict(Shared::new(map))];
let result = runtime_values_to_json(&values, Some(&theme), false, " ").unwrap();
assert!(result.contains("key") && result.contains("val"));
}
#[test]
fn test_compact_no_theme() {
let mut map = mq_lang::DictMap::default();
map.insert(mq_lang::Ident::new("a"), RuntimeValue::from(1usize));
map.insert(mq_lang::Ident::new("b"), RuntimeValue::from(2usize));
let values = vec![RuntimeValue::Dict(Shared::new(map))];
let result = runtime_values_to_json(&values, None, true, " ").unwrap();
assert_eq!(result, r#"{"a":1,"b":2}"#);
}
#[test]
fn test_compact_with_theme() {
let theme = plain_theme();
let mut map = mq_lang::DictMap::default();
map.insert(mq_lang::Ident::new("a"), RuntimeValue::from(1usize));
map.insert(mq_lang::Ident::new("b"), RuntimeValue::from(2usize));
let values = vec![RuntimeValue::Dict(Shared::new(map))];
let result = runtime_values_to_json(&values, Some(&theme), true, " ").unwrap();
assert_eq!(result, r#"{"a":1,"b":2}"#);
}
#[test]
fn test_custom_indent_width_no_theme() {
let values = vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::from(1usize)]))];
let result = runtime_values_to_json(&values, None, false, " ").unwrap();
assert_eq!(result, "[\n 1\n]");
}
#[test]
fn test_tab_indent_with_theme() {
let theme = plain_theme();
let values = vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::from(1usize)]))];
let result = runtime_values_to_json(&values, Some(&theme), false, "\t").unwrap();
assert_eq!(result, "[\n\t1\n]");
}
}