1use super::error::RenderError;
4use super::schema::*;
5use serde_json::Value;
6use std::fmt::Write;
7
8impl Schema {
9 pub fn scaffold(&self) -> String {
13 let mut out = String::new();
14
15 if !self.frontmatter.is_empty() {
16 out.push_str("---\n");
17 for f in &self.frontmatter {
18 if f.optional {
19 continue;
20 }
21 let _ = writeln!(out, "{}: {}", f.key, fm_placeholder(&f.ty));
22 }
23 out.push_str("---\n\n");
24 }
25
26 scaffold_nodes(&self.body, &mut out, 0);
27 while out.ends_with("\n\n") {
29 out.pop();
30 }
31 out
32 }
33
34 pub fn render(&self, data: &Value) -> Result<String, RenderError> {
42 let mut out = String::new();
43
44 if !self.frontmatter.is_empty() {
45 out.push_str("---\n");
46 if let Value::Object(obj) = data {
47 for f in &self.frontmatter {
48 match obj.get(&f.alias) {
49 Some(v) if !v.is_null() => {
50 let _ = writeln!(out, "{}: {}", f.key, render_fm_value(v));
51 }
52 None if !f.optional => {
53 return Err(RenderError::MissingField(f.alias.clone()));
54 }
55 _ => {} }
57 }
58 } else {
59 if let Some(f) = self.frontmatter.iter().find(|f| !f.optional) {
61 return Err(RenderError::MissingField(f.alias.clone()));
62 }
63 }
64 out.push_str("---\n\n");
65 }
66
67 render_nodes(&self.body, data, &mut out, 0)?;
68
69 while out.ends_with("\n\n") {
71 out.pop();
72 }
73
74 Ok(out)
75 }
76}
77
78fn render_nodes(
81 nodes: &[Node],
82 scope: &Value,
83 out: &mut String,
84 list_indent: usize,
85) -> Result<(), RenderError> {
86 for node in nodes {
87 let Some(key) = node_alias(node) else {
88 continue;
89 };
90 let value = scope.get(&key).unwrap_or(&Value::Null);
91 render_node(node, &key, value, out, list_indent)?;
92 }
93 Ok(())
94}
95
96fn render_node(
97 node: &Node,
98 key: &str,
99 value: &Value,
100 out: &mut String,
101 list_indent: usize,
102) -> Result<(), RenderError> {
103 match node {
104 Node::Heading {
105 level,
106 title,
107 head,
108 children,
109 } => {
110 if value.is_null() {
111 if !omitted(head.card) {
112 return Err(RenderError::MissingField(key.to_string()));
113 }
114 return Ok(());
115 }
116 let hashes = "#".repeat(*level as usize);
117 if is_repeated(head.card) {
118 let arr = value.as_array().ok_or_else(|| RenderError::WrongType {
119 field: key.to_string(),
120 expected: "array",
121 })?;
122 for item in arr {
123 let t = heading_title(title, item);
124 let _ = writeln!(out, "{hashes} {t}");
125 render_nodes(children, item, out, 0)?;
126 out.push('\n');
127 }
128 } else {
129 let t = heading_title(title, value);
130 let _ = writeln!(out, "{hashes} {t}");
131 render_nodes(children, value, out, 0)?;
132 out.push('\n');
133 }
134 }
135
136 Node::List {
137 style,
138 item: label,
139 head,
140 children,
141 } => {
142 if value.is_null() {
143 if !omitted(head.card) {
144 return Err(RenderError::MissingField(key.to_string()));
145 }
146 return Ok(());
147 }
148 let pad = " ".repeat(list_indent);
149 let marker = match style {
150 ListStyle::Bullet => "- ",
151 ListStyle::Ordered => "1. ",
152 ListStyle::Checklist => "- [ ] ",
153 };
154 let label_prefix = match label {
155 Some(Match::Literal(l)) => format!("{l} "),
156 _ => String::new(),
157 };
158
159 let single = matches!(head.card, Card::Required | Card::Optional);
160 if single && children.is_empty() {
161 let text = esc_inline(value.as_str().unwrap_or(""));
163 let _ = writeln!(out, "{pad}{marker}{label_prefix}{text}");
164 } else {
165 match value {
166 Value::Array(arr) => {
167 for item_val in arr {
168 if children.is_empty() {
169 let text = esc_inline(item_val.as_str().unwrap_or(""));
170 let _ = writeln!(out, "{pad}{marker}{label_prefix}{text}");
171 } else {
172 let text = esc_inline(
173 item_val
174 .get("text")
175 .and_then(Value::as_str)
176 .or_else(|| item_val.as_str())
177 .unwrap_or(""),
178 );
179 let _ = writeln!(out, "{pad}{marker}{label_prefix}{text}");
180 render_nodes(children, item_val, out, list_indent + 2)?;
181 }
182 }
183 }
184 Value::String(s) => {
185 let text = esc_inline(s);
187 let _ = writeln!(out, "{pad}{marker}{label_prefix}{text}");
188 }
189 _ => {
190 return Err(RenderError::WrongType {
191 field: key.to_string(),
192 expected: "array or string",
193 });
194 }
195 }
196 }
197 }
198
199 Node::Prose { head, .. } => {
200 if value.is_null() {
201 if !omitted(head.card) {
202 return Err(RenderError::MissingField(key.to_string()));
203 }
204 return Ok(());
205 }
206 let text = esc_inline(value.as_str().unwrap_or(""));
207 let _ = writeln!(out, "{text}");
208 out.push('\n');
209 }
210 }
211 Ok(())
212}
213
214fn esc_inline(s: &str) -> String {
221 s.replace('\\', "\\\\")
222}
223
224fn heading_title(title: &Match, value: &Value) -> String {
227 match title {
228 Match::Literal(s) => s.clone(),
229 Match::Regex(_) => value
230 .get("title")
231 .and_then(Value::as_str)
232 .unwrap_or("")
233 .to_string(),
234 }
235}
236
237fn node_alias(node: &Node) -> Option<String> {
239 match node {
240 Node::Heading { head, title, .. } => head.name.clone().or_else(|| match title {
241 Match::Literal(t) => Some(slug(t)),
242 Match::Regex(_) => None,
243 }),
244 Node::List { head, .. } | Node::Prose { head, .. } => head.name.clone(),
245 }
246}
247
248fn slug(s: &str) -> String {
249 s.chars()
250 .map(|c| {
251 if c.is_ascii_alphanumeric() {
252 c.to_ascii_lowercase()
253 } else {
254 '_'
255 }
256 })
257 .collect::<String>()
258 .trim_matches('_')
259 .to_string()
260}
261
262fn omitted(card: Card) -> bool {
264 matches!(card, Card::Optional | Card::Star | Card::Range(0, _))
265}
266
267fn is_repeated(card: Card) -> bool {
269 matches!(card, Card::Plus | Card::Star | Card::Range(..))
270}
271
272fn fm_placeholder(ty: &FieldType) -> String {
275 match ty {
276 FieldType::Enum(vals) => vals.first().cloned().unwrap_or_default(),
277 FieldType::List(_) => "[]".to_string(),
278 _ => String::new(),
279 }
280}
281
282fn scaffold_nodes(nodes: &[Node], out: &mut String, indent: usize) {
283 for node in nodes {
284 match node {
285 Node::Heading {
286 level,
287 title,
288 head,
289 children,
290 } => {
291 if omitted(head.card) {
292 continue;
293 }
294 let hashes = "#".repeat(*level as usize);
295 let _ = writeln!(out, "{hashes} {}", literal_of(title)).map(|_| ());
296 scaffold_nodes(children, out, 0);
297 out.push('\n');
298 }
299 Node::List {
300 style,
301 item,
302 head,
303 children,
304 } => {
305 if omitted(head.card) {
306 continue;
307 }
308 let pad = " ".repeat(indent);
309 let marker = match style {
310 ListStyle::Bullet => "- ",
311 ListStyle::Ordered => "1. ",
312 ListStyle::Checklist => "- [ ] ",
313 };
314 let lbl = item.as_ref().map(literal_of).unwrap_or_default();
315 let _ = writeln!(out, "{pad}{marker}{lbl}");
316 scaffold_nodes(children, out, indent + 2);
317 }
318 Node::Prose { head, text } => {
319 if omitted(head.card) {
320 continue;
321 }
322 let lbl = text.as_ref().map(literal_of).unwrap_or_default();
323 let _ = writeln!(out, "{lbl}");
324 }
325 }
326 }
327}
328
329fn render_fm_value(v: &Value) -> String {
330 match v {
331 Value::String(s) => s.clone(),
332 Value::Bool(b) => b.to_string(),
333 Value::Number(n) => n.to_string(),
334 Value::Array(arr) => {
335 let items: Vec<String> = arr.iter().map(render_fm_value).collect();
336 format!("[{}]", items.join(", "))
337 }
338 Value::Null => String::new(),
339 Value::Object(_) => v.to_string(),
340 }
341}
342
343fn literal_of(m: &Match) -> String {
345 match m {
346 Match::Literal(s) => s.clone(),
347 Match::Regex(_) => String::new(),
348 }
349}