1use crate::ir::*;
21use std::collections::HashMap;
22
23const INDENT: &str = " ";
24
25pub fn format(module: &Module, style: Style) -> String {
28 let style = match style {
29 Style::Preserve => module.source_style,
30 other => other,
31 };
32 let mut f = Formatter {
33 module,
34 style,
35 names: build_names(module),
36 leading: HashMap::new(),
37 trailing: HashMap::new(),
38 trailing_block: HashMap::new(),
39 trailing_document: Vec::new(),
40 out: String::new(),
41 };
42 f.index_trivia();
43 f.emit_module();
44 f.out
45}
46
47struct Formatter<'a> {
48 module: &'a Module,
49 style: Style,
50 names: HashMap<NodeId, String>,
52 leading: HashMap<String, Vec<String>>,
53 trailing: HashMap<String, Vec<String>>,
54 trailing_block: HashMap<String, Vec<String>>,
55 trailing_document: Vec<String>,
56 out: String,
57}
58
59fn build_names(module: &Module) -> HashMap<NodeId, String> {
62 fn walk(module: &Module, id: NodeId, names: &mut HashMap<NodeId, String>) {
63 if let Some(Node::Table { fields }) = module.get(id) {
64 for (key, child) in fields {
65 names.entry(*child).or_insert_with(|| key.clone());
66 walk(module, *child, names);
67 }
68 }
69 }
70 let mut names = HashMap::new();
71 for (name, id) in &module.bindings {
72 names.entry(*id).or_insert_with(|| name.clone());
73 }
74 if let Some(root) = module.root {
75 walk(module, root, &mut names);
76 }
77 for id in module.profiles.values() {
78 walk(module, *id, &mut names);
79 }
80 names
81}
82
83impl<'a> Formatter<'a> {
84 fn index_trivia(&mut self) {
89 for c in &self.module.trivia {
90 match &c.attachment {
91 Attachment::Leading(key) => self
92 .leading
93 .entry(key.clone())
94 .or_default()
95 .push(c.text.clone()),
96 Attachment::Trailing(key) => {
97 self.trailing
100 .entry(key.clone())
101 .or_default()
102 .push(c.text.clone());
103 }
104 Attachment::TrailingBlock(path) => self
105 .trailing_block
106 .entry(path.clone())
107 .or_default()
108 .push(c.text.clone()),
109 Attachment::TrailingDocument => self.trailing_document.push(c.text.clone()),
110 }
111 }
112 }
113
114 fn emit_module(&mut self) {
115 let mut lets: Vec<(&String, &NodeId)> = self.module.bindings.iter().collect();
117 lets.sort_by_key(|(_, id)| {
118 self.module
119 .node_spans
120 .get(id)
121 .map(|s| (s.line, s.column))
122 .unwrap_or((u32::MAX, 0))
123 });
124 for (name, id) in lets {
125 let id = *id;
126 self.emit_leading(name);
127 let value = self.render_expr(id);
128 self.out.push_str(&format!("let {name} = {value}"));
129 self.emit_trailing(name);
130 self.out.push('\n');
131 }
132
133 if let Some(schema) = self.module.schema {
136 self.emit_leading("schema");
137 self.emit_schema_table(schema, 0);
138 }
139
140 let profiles: Vec<(String, NodeId)> = self
143 .module
144 .profiles
145 .iter()
146 .map(|(k, v)| (k.clone(), *v))
147 .collect();
148 for (name, id) in &profiles {
149 self.emit_leading(name);
150 self.emit_profile(*id, name, 0);
151 }
152
153 let mut root_items: Vec<(u32, bool, NodeId, Option<IncludeStmt>)> = Vec::new();
161 for id in &self.module.patches {
162 let line = self
163 .module
164 .node_spans
165 .get(id)
166 .map(|s| s.line)
167 .unwrap_or(u32::MAX);
168 root_items.push((line, false, *id, None));
169 }
170 for inc in self
171 .module
172 .includes
173 .iter()
174 .filter(|inc| inc.scope_path.is_empty())
175 {
176 root_items.push((inc.line, true, NodeId(u32::MAX), Some(inc.clone())));
177 }
178 root_items.sort_by_key(|(line, is_include, id, _)| (*line, *is_include, id.0));
179 for (_, is_include, id, inc) in &root_items {
180 if *is_include {
181 let inc = inc.as_ref().expect("include entry");
182 self.emit_include(inc, 0);
183 } else {
184 self.emit_patch(*id);
185 }
186 }
187
188 if let Some(root) = self.module.root {
189 let has_entries = !self.out.is_empty()
190 && matches!(self.module.get(root), Some(Node::Table { fields }) if !fields.is_empty());
191 if has_entries && !self.out.ends_with("\n\n") {
192 self.out.push('\n');
193 }
194 self.emit_depends_on(root, 0);
197 self.emit_table_body(root, 0, "", true);
198 }
199 for text in self.trailing_document.clone() {
200 self.out.push_str(&text);
201 self.out.push('\n');
202 }
203 }
204
205 fn emit_table_body(&mut self, id: NodeId, depth: usize, path: &str, is_root: bool) {
210 let fields = match self.module.get(id) {
211 Some(Node::Table { fields }) => fields.clone(),
212 _ => return,
213 };
214 if !path.is_empty() {
220 let mut includes: Vec<&IncludeStmt> = self
221 .module
222 .includes
223 .iter()
224 .filter(|inc| inc.scope_path.join(".") == path)
225 .collect();
226 includes.sort_by_key(|inc| inc.line);
227 for inc in includes {
228 self.emit_include(inc, depth);
229 }
230 if let Some(schema) = self.schema_for_path(path) {
235 self.emit_schema_table(schema, depth);
236 }
237 self.emit_depends_on(id, depth);
240 }
241 let is_table_node =
242 |f: &Formatter, n: &NodeId| matches!(f.module.get(*n), Some(Node::Table { .. }));
243 let ordered: Vec<(String, NodeId)> = if is_root {
248 let mut scalars: Vec<(String, NodeId)> = Vec::new();
249 let mut tables: Vec<(String, NodeId)> = Vec::new();
250 for (k, v) in &fields {
251 if is_table_node(self, v) {
252 tables.push((k.clone(), *v));
253 } else {
254 scalars.push((k.clone(), *v));
255 }
256 }
257 scalars.into_iter().chain(tables).collect()
258 } else {
259 fields.iter().map(|(k, v)| (k.clone(), *v)).collect()
260 };
261 let tables: Vec<(String, NodeId)> = ordered
262 .iter()
263 .filter(|(_, v)| is_table_node(self, v))
264 .cloned()
265 .collect();
266
267 let mut first = true;
269 for (key, node) in ordered.iter() {
270 let child_path = if path.is_empty() {
271 key.clone()
272 } else {
273 format!("{path}.{key}")
274 };
275 let is_table = tables.iter().any(|(k, _)| k == key);
276 if is_root && !first {
277 self.out.push('\n');
278 }
279 first = false;
280
281 let qkey = child_path.clone();
282 self.emit_leading(&qkey);
283 if is_table {
284 self.emit_table_block(*node, &child_path, key, depth, is_root);
285 } else {
286 let value = self.render_expr(*node);
287 let indent = INDENT.repeat(depth);
288 self.out.push_str(&format!("{indent}{key} = {value}"));
289 self.emit_trailing(&qkey);
290 self.out.push('\n');
291 }
292 }
293
294 if let Some(comments) = self.trailing_block.get(path).cloned() {
295 for text in comments {
296 self.out.push_str(&INDENT.repeat(depth));
297 self.out.push_str(&text);
298 self.out.push('\n');
299 }
300 }
301 }
302
303 fn emit_depends_on(&mut self, id: NodeId, depth: usize) {
308 let Some(dep) = self.module.depends_on.get(&id).cloned() else {
309 return;
310 };
311 let quoted: Vec<std::string::String> = dep
312 .paths
313 .iter()
314 .map(|p| format!("\"{}\"", escape(p)))
315 .collect();
316 self.out.push_str(&format!(
317 "{}depends_on = [{}]",
318 INDENT.repeat(depth),
319 quoted.join(", ")
320 ));
321 self.out.push('\n');
322 }
323
324 fn schema_for_path(&self, path: &str) -> Option<NodeId> {
328 let mut id = self.module.schema?;
329 if path.is_empty() {
330 return Some(id);
331 }
332 for segment in path.split('.') {
333 match self.module.get(id) {
334 Some(Node::SchemaTable { fields, .. }) => {
335 id = *fields.get(segment)?;
336 }
337 _ => return None,
338 }
339 }
340 Some(id)
341 }
342
343 fn emit_schema_table(&mut self, id: NodeId, depth: usize) {
350 let (fields, closed) = match self.module.get(id) {
351 Some(Node::SchemaTable { fields, closed }) => (fields.clone(), *closed),
352 _ => return,
353 };
354 if fields.is_empty() && !closed {
355 let indent = INDENT.repeat(depth);
356 self.out.push_str(&format!("{indent}schema = {{}}"));
357 self.emit_trailing("schema");
358 self.out.push('\n');
359 return;
360 }
361 let indent = INDENT.repeat(depth);
362 match self.style {
363 Style::Indent => {
364 self.out.push_str(&format!("{indent}schema:"));
365 self.emit_trailing("schema");
366 self.out.push('\n');
367 let inner = INDENT.repeat(depth + 1);
368 if closed {
369 self.out.push_str(&format!("{inner}closed = true\n"));
370 }
371 for (name, entry) in &fields {
372 let typ = self.render_schema_type(*entry);
373 self.out.push_str(&format!("{inner}{name} = {typ}\n"));
374 }
375 }
376 _ => {
377 self.out.push_str(&format!("{indent}schema {{"));
378 self.emit_trailing("schema");
379 self.out.push('\n');
380 let inner = INDENT.repeat(depth + 1);
381 if closed {
382 self.out.push_str(&format!("{inner}closed = true\n"));
383 }
384 for (name, entry) in &fields {
385 let typ = self.render_schema_type(*entry);
386 self.out.push_str(&format!("{inner}{name} = {typ}\n"));
387 }
388 self.out.push_str(&format!("{indent}}}\n"));
389 }
390 }
391 }
392
393 fn render_schema_type(&self, id: NodeId) -> String {
397 match self.module.get(id).cloned() {
398 Some(Node::SchemaField { field }) => self.render_schema_field(&field),
399 Some(Node::SchemaArray { element }) => {
400 format!("Array<{}>", self.render_schema_type(element))
401 }
402 Some(Node::SchemaTable { fields, closed }) => {
403 let mut parts: Vec<std::string::String> = Vec::new();
404 if closed {
405 parts.push("closed = true".to_string());
406 }
407 for (name, entry) in fields {
408 parts.push(format!("{name} = {}", self.render_schema_type(entry)));
409 }
410 if parts.is_empty() {
411 "Table {}".to_string()
412 } else {
413 format!("Table {{ {} }}", parts.join(", "))
414 }
415 }
416 _ => "Any".to_string(),
417 }
418 }
419
420 fn render_schema_field(&self, field: &crate::ir::SchemaField) -> String {
423 use crate::types::ActiveType::*;
424 let deprecated = match &field.deprecated {
427 None => std::string::String::new(),
428 Some(reason) if reason.is_empty() => ", deprecated = true".to_string(),
429 Some(reason) => format!(", deprecated = \"{}\"", escape(reason)),
430 };
431 if let Enum(values) = &field.typ {
432 let quoted: Vec<std::string::String> = values
433 .iter()
434 .map(|v| format!("\"{}\"", escape(v)))
435 .collect();
436 return format!("Enum({}{})", quoted.join(", "), deprecated);
437 }
438 let mut args: Vec<std::string::String> = Vec::new();
439 for (name, slot) in [
440 ("min", field.min),
441 ("max", field.max),
442 ("min_len", field.min_len),
443 ("max_len", field.max_len),
444 ("pattern", field.pattern),
445 ("default", field.default),
446 ] {
447 if let Some(arg) = slot {
448 args.push(format!("{name} = {}", self.render_expr(arg)));
449 }
450 }
451 match &field.deprecated {
452 None => {}
453 Some(reason) if reason.is_empty() => args.push("deprecated = true".to_string()),
454 Some(reason) => args.push(format!("deprecated = \"{}\"", escape(reason))),
455 }
456 let base = match &field.typ {
457 Table { closed: true } => "Table(closed)".to_string(),
458 other => other.name(),
459 };
460 if args.is_empty() {
461 base
462 } else {
463 format!("{base}({})", args.join(", "))
464 }
465 }
466
467 fn emit_table_block(&mut self, id: NodeId, path: &str, key: &str, depth: usize, is_root: bool) {
468 match self.style {
469 Style::Indent => {
470 let empty = matches!(
473 self.module.get(id),
474 Some(Node::Table { fields }) if fields.is_empty()
475 );
476 if empty {
477 self.out
478 .push_str(&format!("{}{key} = {{}}", INDENT.repeat(depth)));
479 self.emit_trailing(path);
480 self.out.push('\n');
481 if let Some(comments) = self.trailing_block.get(path).cloned() {
483 for text in comments {
484 self.out.push_str(&INDENT.repeat(depth));
485 self.out.push_str(&text);
486 self.out.push('\n');
487 }
488 }
489 return;
490 }
491 self.out
492 .push_str(&format!("{}{key}:", INDENT.repeat(depth)));
493 self.emit_trailing(path);
494 self.out.push('\n');
495 self.emit_table_body(id, depth + 1, path, false);
496 }
497 _ => {
498 if is_root && depth == 0 {
499 self.out.push_str(&format!("[{key}]"));
502 self.emit_trailing(path);
503 self.out.push('\n');
504 self.emit_table_body(id, 0, path, false);
505 } else {
506 self.out
507 .push_str(&format!("{}{key} {{", INDENT.repeat(depth)));
508 self.emit_trailing(path);
509 self.out.push('\n');
510 self.emit_table_body(id, depth + 1, path, false);
511 self.out.push_str(&format!("{}}}\n", INDENT.repeat(depth)));
512 }
513 }
514 }
515 }
516
517 fn emit_profile(&mut self, id: NodeId, name: &str, depth: usize) {
524 let qname = escape_interp_literal(name);
525 let empty = matches!(
526 self.module.get(id),
527 Some(Node::Table { fields }) if fields.is_empty()
528 );
529 match self.style {
530 Style::Indent if !empty => {
531 self.out
532 .push_str(&format!("{}profile \"{qname}\":", INDENT.repeat(depth)));
533 self.emit_trailing(name);
534 self.out.push('\n');
535 self.emit_table_body(id, depth + 1, name, false);
536 }
537 _ => {
538 if empty {
539 self.out
540 .push_str(&format!("{}profile \"{qname}\" {{}}", INDENT.repeat(depth)));
541 self.emit_trailing(name);
542 self.out.push('\n');
543 } else {
544 self.out
545 .push_str(&format!("{}profile \"{qname}\" {{", INDENT.repeat(depth)));
546 self.emit_trailing(name);
547 self.out.push('\n');
548 self.emit_table_body(id, depth + 1, name, false);
549 self.out.push_str(&format!("{}}}\n", INDENT.repeat(depth)));
550 }
551 }
552 }
553 }
554
555 fn emit_include(&mut self, inc: &IncludeStmt, depth: usize) {
557 self.out.push_str(&INDENT.repeat(depth));
558 self.out.push_str(&format!("include {:?}", inc.path));
559 if let Some(alias) = &inc.alias {
560 self.out.push_str(&format!(" as {alias}"));
561 }
562 if inc.private {
563 self.out.push_str(" private");
564 }
565 self.out.push('\n');
566 }
567
568 fn emit_patch(&mut self, id: NodeId) {
573 fn q(s: &str) -> String {
574 format!("\"{}\"", escape(s))
575 }
576 let (text, key) = match self.module.get(id) {
577 Some(Node::PatchDirective { op }) => match op {
578 PatchOp::Set { path, value } => (
579 format!("@patch({}, {})", q(path), self.render_expr(*value)),
580 format!("@patch({path})"),
581 ),
582 PatchOp::Remove { path } => {
583 (format!("@remove({})", q(path)), format!("@remove({path})"))
584 }
585 PatchOp::Rename { from, to } => (
586 format!("@rename({}, {})", q(from), q(to)),
587 format!("@rename({from})"),
588 ),
589 },
590 _ => return,
591 };
592 self.emit_leading(&key);
593 self.out.push_str(&text);
594 self.emit_trailing(&key);
595 self.out.push('\n');
596 }
597
598 fn emit_leading(&mut self, key: &str) {
599 if let Some(comments) = self.leading.get(key).cloned() {
600 for text in comments {
601 self.out.push_str(&text);
602 self.out.push('\n');
603 }
604 }
605 }
606
607 fn emit_trailing(&mut self, key: &str) {
608 if let Some(texts) = self.trailing.get(key).cloned() {
609 for text in texts {
610 self.out.push(' ');
611 self.out.push_str(&text);
612 }
613 }
614 }
615
616 fn render_expr(&self, id: NodeId) -> String {
621 match self.module.get(id) {
622 Some(Node::Literal(v)) => render_literal(v),
623 Some(Node::Env { name, default }) => match default {
624 Some(d) => {
625 let d = self.render_expr(*d);
626 format!("env.{name} ?: {d}")
627 }
628 None => format!("env.{name}"),
629 },
630 Some(Node::Secret { locator }) => {
631 format!("secret({})", self.render_expr(*locator))
632 }
633 Some(Node::Ref(target)) => self
634 .names
635 .get(target)
636 .cloned()
637 .unwrap_or_else(|| "<?>".into()),
638 Some(Node::Ident(name)) => name.clone(),
639 Some(Node::Interpolate { parts }) => {
640 let mut s = String::from("\"");
641 for part in parts {
642 match part {
643 InterpPart::Literal(t) => s.push_str(&escape_interp_literal(t)),
646 InterpPart::Expr(e) => {
647 s.push_str("${");
648 let inner = self.render_expr(*e);
649 s.push_str(&escape_embedded(&inner));
650 s.push('}');
651 }
652 }
653 }
654 s.push('"');
655 s
656 }
657 Some(Node::Binary { .. }) => self.render_binary(id),
658 Some(Node::Unary { op, expr }) => {
659 let symbol = match op {
660 UnOp::Neg => "-",
661 UnOp::Not => "!",
662 };
663 format!("{symbol}{}", self.render_child(*expr, 7, false))
664 }
665 Some(Node::Array { elements }) => {
666 let parts: Vec<String> = elements.iter().map(|e| self.render_expr(*e)).collect();
667 format!("[{}]", parts.join(", "))
668 }
669 Some(Node::Table { fields }) => {
670 let parts: Vec<String> = fields
671 .iter()
672 .map(|(k, v)| format!("{k} = {}", self.render_expr(*v)))
673 .collect();
674 format!("{{ {} }}", parts.join(", "))
675 }
676 Some(Node::Quantity { value, unit }) => {
677 format!("{}{}", self.render_expr(*value), unit.suffix())
678 }
679 Some(Node::Semantic { tag, inner }) => {
680 format!("@{} {}", tag.as_str(), self.render_ml(*inner))
681 }
682 _ => "<?>".into(),
683 }
684 }
685
686 fn render_ml(&self, id: NodeId) -> String {
691 match self.module.get(id) {
692 Some(Node::Interpolate { parts }) => {
693 let mut s = String::from("\"\"\"");
694 for part in parts {
695 match part {
696 InterpPart::Literal(t) => s.push_str(&escape_ml_literal(t)),
697 InterpPart::Expr(e) => {
698 s.push_str("${");
699 let inner = self.render_expr(*e);
700 s.push_str(&escape_embedded(&inner));
701 s.push('}');
702 }
703 }
704 }
705 s.push_str("\"\"\"");
706 s
707 }
708 Some(Node::Literal(Value::String(text))) if !text.contains("'''") => {
709 format!("'''{text}'''")
710 }
711 Some(Node::Literal(Value::String(text))) => {
712 format!("\"\"\"{}\"\"\"", escape_ml_literal(text))
713 }
714 _ => self.render_expr(id),
715 }
716 }
717
718 fn render_binary(&self, id: NodeId) -> String {
723 let top_prec = match self.module.get(id) {
724 Some(Node::Binary { op, .. }) => prec(*op),
725 _ => 0,
726 };
727 let mut rights: Vec<String> = Vec::new();
728 let mut ops: Vec<BinOp> = Vec::new();
729 let mut cur = id;
730 loop {
731 match self.module.get(cur) {
732 Some(Node::Binary { op, lhs, rhs }) => {
733 ops.push(*op);
734 rights.push(self.render_child(*rhs, prec(*op), true));
735 let next = *lhs;
740 match self.module.get(next) {
741 Some(Node::Binary { op: next_op, .. }) if prec(*next_op) == top_prec => {
742 cur = next;
743 }
744 _ => {
745 let leftmost = self.render_child(next, top_prec, false);
746 rights.push(leftmost);
747 break;
748 }
749 }
750 }
751 _ => {
752 let leftmost = self.render_child(cur, top_prec, false);
753 rights.push(leftmost);
754 break;
755 }
756 }
757 }
758 rights.reverse();
760 ops.reverse();
761 let mut out = rights[0].clone();
762 for (i, op) in ops.iter().enumerate() {
763 out.push_str(&format!(" {} {}", op_symbol(*op), rights[i + 1]));
764 }
765 out
766 }
767
768 fn render_child(&self, id: NodeId, parent_prec: u8, is_right: bool) -> String {
772 let text = self.render_expr(id);
773 match self.module.get(id) {
774 Some(Node::Binary { op, .. }) => {
775 let child = prec(*op);
776 if child < parent_prec || (child == parent_prec && is_right) {
777 return format!("({text})");
778 }
779 }
780 Some(Node::Env {
781 default: Some(_), ..
782 }) => {
783 return format!("({text})");
784 }
785 _ => {}
786 }
787 text
788 }
789}
790
791fn prec(op: BinOp) -> u8 {
792 match op {
793 BinOp::Or => 1,
794 BinOp::And => 2,
795 BinOp::Eq | BinOp::Ne => 3,
796 BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge => 4,
797 BinOp::Add | BinOp::Sub => 5,
798 BinOp::Mul | BinOp::Div => 6,
799 }
800}
801
802fn op_symbol(op: BinOp) -> &'static str {
803 match op {
804 BinOp::Add => "+",
805 BinOp::Sub => "-",
806 BinOp::Mul => "*",
807 BinOp::Div => "/",
808 BinOp::Eq => "==",
809 BinOp::Ne => "!=",
810 BinOp::Lt => "<",
811 BinOp::Le => "<=",
812 BinOp::Gt => ">",
813 BinOp::Ge => ">=",
814 BinOp::And => "&&",
815 BinOp::Or => "||",
816 }
817}
818
819fn render_literal(v: &Value) -> String {
820 match v {
821 Value::Null => "null".into(),
822 Value::Bool(b) => b.to_string(),
823 Value::Int(i) => i.to_string(),
824 Value::Float(f) => {
825 let mut s = format!("{f}");
829 if !s.contains('.') {
830 s.push_str(".0");
831 }
832 s
833 }
834 Value::String(s) => {
835 if s.contains("${") {
838 format!("'{}'", escape_single(s))
839 } else {
840 format!("\"{}\"", escape(s))
841 }
842 }
843 Value::DurationMs(ms) => format!("{ms}ms"),
844 Value::Quantity(n, unit) => unit.format_amount(*n),
845 Value::Array(a) => {
846 let parts: Vec<String> = a.iter().map(render_literal).collect();
847 format!("[{}]", parts.join(", "))
848 }
849 Value::Table(t) => {
850 let parts: Vec<String> = t
851 .iter()
852 .map(|(k, v)| format!("{k} = {}", render_literal(v)))
853 .collect();
854 format!("{{ {} }}", parts.join(", "))
855 }
856 Value::Secret(_) => "\"***SECRET***\"".into(),
858 }
859}
860
861fn escape(s: &str) -> String {
862 let mut out = String::with_capacity(s.len());
863 for ch in s.chars() {
864 match ch {
865 '"' => out.push_str("\\\""),
866 '\\' => out.push_str("\\\\"),
867 '\n' => out.push_str("\\n"),
868 '\t' => out.push_str("\\t"),
869 '\r' => out.push_str("\\r"),
870 _ => out.push(ch),
871 }
872 }
873 out
874}
875
876fn escape_single(s: &str) -> String {
880 let mut out = String::with_capacity(s.len());
881 for ch in s.chars() {
882 match ch {
883 '\'' => out.push_str("\\'"),
884 '\\' => out.push_str("\\\\"),
885 _ => out.push(ch),
886 }
887 }
888 out
889}
890
891fn escape_interp_literal(s: &str) -> String {
894 escape(s).replace("${", "\\${")
895}
896
897fn escape_ml_literal(s: &str) -> String {
903 escape_interp_literal(s)
904}
905
906fn escape_embedded(s: &str) -> String {
909 let mut out = String::with_capacity(s.len());
910 for ch in s.chars() {
911 match ch {
912 '"' => out.push_str("\\\""),
913 '\\' => out.push_str("\\\\"),
914 _ => out.push(ch),
915 }
916 }
917 out
918}