1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
//! Render a `key: value` (or `key:: value` / `key: { ... }` / `key: [ ... ]`)
//! line. Under spec 0.5.0, integers and floats are emitted with plain `:`
//! (no `:i` / `:f` markers).
use crate::error::Result;
use crate::value::Value;
use super::array_item::render_array_item;
use super::helpers::{needs_raw_marker, push_escaped_key_segment, push_indent};
use super::object::render_object_body;
pub(super) fn render_pair(
key: &str,
value: &Value,
indent: usize,
root_first_key: bool,
out: &mut String,
) -> Result<()> {
push_indent(out, indent);
// Spec 0.7 § 5.9.10 — bare/quoted form selection + re-escape per
// segment. The Object value stores the *decoded* leaf key as a
// single byte string; the emitted form must escape (or quote
// around) any literal structural byte. `root_first_key` is set
// only for the document-root Object's first-serialized key (the
// § 5.9.10 rule (c) U+FEFF guard, § 5.9.12).
push_escaped_key_segment(key, root_first_key, out);
match value {
Value::Null => {
out.push_str(": null\n");
}
Value::Bool(b) => {
out.push_str(": ");
out.push_str(if *b { "true" } else { "false" });
out.push('\n');
}
Value::Integer(s) => {
out.push_str(": ");
out.push_str(s);
out.push('\n');
}
Value::Float(s) => {
out.push_str(": ");
out.push_str(s);
out.push('\n');
}
Value::String(s) => {
if s.contains('\r') {
return Err(crate::render::helpers::cr_error());
}
if crate::render::helpers::string_needs_multiline(s) {
// Pick the form whose terminator doesn't clash with the
// content (spec § 5.6.1). Prefer **stripped** (`(` ... `)`)
// because indented output is much more readable; fall back
// to **verbatim** (`((` ... `))`) when stripped can't
// round-trip the content losslessly. The shared chooser
// errors when neither form can hold the body.
let form = crate::render::helpers::choose_multiline_form(s, true)?;
if matches!(form, crate::render::helpers::MultilineForm::Stripped) {
// Stripped form (default). Each content line gets a
// `content_indent` prefix; the dedent on parse strips
// it back off, so the round-trip is byte-exact (blank
// lines inside `s` remain blank: spec § 5.6 replaces
// them with the empty string).
out.push_str(": (\n");
let content_indent = indent + 1;
for line in s.split('\n') {
if !line.is_empty() {
push_indent(out, content_indent);
out.push_str(line);
}
out.push('\n');
}
push_indent(out, indent);
out.push_str(")\n");
} else {
// Verbatim form (fallback). Exactly one `\n` is pushed
// after `s`: if `s` already ends with `\n`, the result
// is `...\n\n` before `))`, i.e. a blank content line
// that preserves the trailing newline through the
// verbatim-join round-trip.
out.push_str(": ((\n");
out.push_str(s);
out.push('\n');
push_indent(out, indent);
out.push_str("))\n");
}
} else {
if needs_raw_marker(s) {
out.push_str(":: ");
} else {
out.push_str(": ");
}
out.push_str(s);
out.push('\n');
}
}
Value::Array(items) => {
if items.is_empty() {
out.push_str(": []\n");
} else {
out.push_str(": [\n");
for item in items {
// Nested items are never root-detected (§ 5.9.6).
render_array_item(item, indent + 1, false, out)?;
}
push_indent(out, indent);
out.push_str("]\n");
}
}
Value::Object(obj) => {
if obj.is_empty() {
out.push_str(": {}\n");
} else {
out.push_str(": {\n");
render_object_body(obj, indent + 1, false, out)?;
push_indent(out, indent);
out.push_str("}\n");
}
}
}
Ok(())
}