use serde_json::Value as JsonValue;
use super::frontmatter::FrontmatterItem;
use super::{Card, Document, Sentinel};
impl Document {
pub fn to_markdown(&self) -> String {
let mut out = String::new();
emit_card_fence(&mut out, self.main());
out.push_str(self.main().body());
for card in self.cards() {
ensure_f2_before_fence(&mut out);
emit_card_fence(&mut out, card);
if !card.body().is_empty() {
out.push_str(card.body());
}
}
out
}
}
fn emit_card_fence(out: &mut String, card: &Card) {
out.push_str("---\n");
match card.sentinel() {
Sentinel::Main(r) => {
out.push_str("QUILL: ");
out.push_str(&r.to_string());
out.push('\n');
}
Sentinel::Card(tag) => {
out.push_str("CARD: ");
out.push_str(tag);
out.push('\n');
}
}
for item in card.frontmatter().items() {
match item {
FrontmatterItem::Field { key, value, fill } => {
emit_field(out, key, value.as_json(), 0, *fill);
}
FrontmatterItem::Comment { text } => {
out.push_str("# ");
out.push_str(text);
out.push('\n');
}
}
}
out.push_str("---\n");
}
fn ensure_f2_before_fence(out: &mut String) {
if out.is_empty() {
return;
}
if !out.ends_with('\n') {
out.push('\n');
}
out.push('\n');
}
fn emit_field(out: &mut String, key: &str, value: &JsonValue, indent: usize, fill: bool) {
if fill {
push_indent(out, indent);
out.push_str(key);
match value {
JsonValue::Null => out.push_str(": !fill\n"),
JsonValue::Bool(_) | JsonValue::Number(_) | JsonValue::String(_) => {
out.push_str(": !fill ");
emit_scalar(out, value);
out.push('\n');
}
JsonValue::Array(items) if items.is_empty() => {
out.push_str(": !fill []\n");
}
JsonValue::Array(items) => {
out.push_str(": !fill\n");
for item in items {
emit_sequence_item(out, item, indent);
}
}
JsonValue::Object(_) => {
out.push_str(": ");
emit_scalar(out, value);
out.push('\n');
}
}
return;
}
match value {
JsonValue::Object(map) if map.is_empty() => {
return;
}
JsonValue::Object(map) => {
push_indent(out, indent);
out.push_str(key);
out.push_str(":\n");
for (k, v) in map {
emit_field(out, k, v, indent + 2, false);
}
}
JsonValue::Array(items) if items.is_empty() => {
push_indent(out, indent);
out.push_str(key);
out.push_str(": []\n");
}
JsonValue::Array(items) => {
push_indent(out, indent);
out.push_str(key);
out.push_str(":\n");
for item in items {
emit_sequence_item(out, item, indent);
}
}
_ => {
push_indent(out, indent);
out.push_str(key);
out.push_str(": ");
emit_scalar(out, value);
out.push('\n');
}
}
}
fn emit_sequence_item(out: &mut String, value: &JsonValue, base_indent: usize) {
match value {
JsonValue::Object(map) if map.is_empty() => {
push_indent(out, base_indent);
out.push_str("- {}\n");
}
JsonValue::Object(map) => {
let mut first = true;
for (k, v) in map {
if first {
push_indent(out, base_indent);
out.push_str("- ");
emit_field_inline(out, k, v, base_indent + 2);
first = false;
} else {
emit_field(out, k, v, base_indent + 2, false);
}
}
}
JsonValue::Array(inner) if inner.is_empty() => {
push_indent(out, base_indent);
out.push_str("- []\n");
}
JsonValue::Array(inner) => {
push_indent(out, base_indent);
out.push_str("-\n");
for item in inner {
emit_sequence_item(out, item, base_indent + 2);
}
}
_ => {
push_indent(out, base_indent);
out.push_str("- ");
emit_scalar(out, value);
out.push('\n');
}
}
}
fn emit_field_inline(out: &mut String, key: &str, value: &JsonValue, child_indent: usize) {
match value {
JsonValue::Object(map) if map.is_empty() => {
out.push_str(key);
out.push_str(": {}\n");
}
JsonValue::Object(map) => {
out.push_str(key);
out.push_str(":\n");
for (k, v) in map {
emit_field(out, k, v, child_indent, false);
}
}
JsonValue::Array(items) if items.is_empty() => {
out.push_str(key);
out.push_str(": []\n");
}
JsonValue::Array(items) => {
out.push_str(key);
out.push_str(":\n");
for item in items {
emit_sequence_item(out, item, child_indent);
}
}
_ => {
out.push_str(key);
out.push_str(": ");
emit_scalar(out, value);
out.push('\n');
}
}
}
fn emit_scalar(out: &mut String, value: &JsonValue) {
match value {
JsonValue::Null => out.push_str("null"),
JsonValue::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
JsonValue::Number(n) => out.push_str(&n.to_string()),
JsonValue::String(s) => emit_double_quoted(out, s),
other => out.push_str(&other.to_string()),
}
}
fn emit_double_quoted(out: &mut String, s: &str) {
out.push('"');
for ch in s.chars() {
match ch {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 || (0x7F..=0x9F).contains(&(c as u32)) => {
let n = c as u32;
if n <= 0xFF {
out.push_str(&format!("\\u{:04X}", n));
} else {
out.push_str(&format!("\\u{:04X}", n));
}
}
c => out.push(c),
}
}
out.push('"');
}
fn push_indent(out: &mut String, spaces: usize) {
for _ in 0..spaces {
out.push(' ');
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::value::QuillValue;
#[test]
fn double_quoted_basic() {
let mut s = String::new();
emit_double_quoted(&mut s, "hello");
assert_eq!(s, r#""hello""#);
}
#[test]
fn double_quoted_ambiguous_strings() {
for ambiguous in &[
"on", "off", "yes", "no", "true", "false", "null", "~", "01234", "1e10",
] {
let mut s = String::new();
emit_double_quoted(&mut s, ambiguous);
assert!(
s.starts_with('"') && s.ends_with('"'),
"should be double-quoted: {}",
s
);
assert_eq!(&s[1..s.len() - 1], *ambiguous);
}
}
#[test]
fn double_quoted_escapes() {
let mut s = String::new();
emit_double_quoted(&mut s, "a\\b\"c\nd\te");
assert_eq!(s, r#""a\\b\"c\nd\te""#);
}
#[test]
fn double_quoted_control_chars() {
let mut s = String::new();
emit_double_quoted(&mut s, "\x01\x1F");
assert_eq!(s, "\"\\u0001\\u001F\"");
}
#[test]
fn empty_object_omitted() {
let value = QuillValue::from_json(serde_json::json!({}));
let mut out = String::new();
emit_field(&mut out, "empty_map", value.as_json(), 0, false);
assert_eq!(out, ""); }
#[test]
fn empty_array_emitted() {
let value = QuillValue::from_json(serde_json::json!([]));
let mut out = String::new();
emit_field(&mut out, "empty_seq", value.as_json(), 0, false);
assert_eq!(out, "empty_seq: []\n");
}
#[test]
fn fill_null_emits_bare_tag() {
let value = QuillValue::from_json(serde_json::Value::Null);
let mut out = String::new();
emit_field(&mut out, "recipient", value.as_json(), 0, true);
assert_eq!(out, "recipient: !fill\n");
}
#[test]
fn fill_string_emits_tag_with_value() {
let value = QuillValue::from_json(serde_json::json!("placeholder"));
let mut out = String::new();
emit_field(&mut out, "dept", value.as_json(), 0, true);
assert_eq!(out, "dept: !fill \"placeholder\"\n");
}
#[test]
fn fill_integer_emits_tag_with_value() {
let value = QuillValue::from_json(serde_json::json!(42));
let mut out = String::new();
emit_field(&mut out, "count", value.as_json(), 0, true);
assert_eq!(out, "count: !fill 42\n");
}
}