1use crate::{IcuMessage, IcuNode, IcuPluralKind};
2
3#[must_use]
10pub fn stringify_icu(message: &IcuMessage) -> String {
11 let mut out = String::new();
12 render_nodes(&message.nodes, &mut out);
13 out
14}
15
16fn render_nodes(nodes: &[IcuNode], out: &mut String) {
17 for node in nodes {
18 render_node(node, out);
19 }
20}
21
22fn render_node(node: &IcuNode, out: &mut String) {
23 match node {
24 IcuNode::Literal(value) => append_escaped_literal(out, value),
25 IcuNode::Argument { name } => {
26 out.push('{');
27 out.push_str(name);
28 out.push('}');
29 }
30 IcuNode::Number { name, style } => render_formatter("number", name, style.as_deref(), out),
31 IcuNode::Date { name, style } => render_formatter("date", name, style.as_deref(), out),
32 IcuNode::Time { name, style } => render_formatter("time", name, style.as_deref(), out),
33 IcuNode::List { name, style } => render_formatter("list", name, style.as_deref(), out),
34 IcuNode::Duration { name, style } => {
35 render_formatter("duration", name, style.as_deref(), out);
36 }
37 IcuNode::Ago { name, style } => render_formatter("ago", name, style.as_deref(), out),
38 IcuNode::Name { name, style } => render_formatter("name", name, style.as_deref(), out),
39 IcuNode::Select { name, options } => {
40 out.push('{');
41 out.push_str(name);
42 out.push_str(", select, ");
43 render_options(options, out);
44 out.push('}');
45 }
46 IcuNode::Plural {
47 name,
48 kind,
49 offset,
50 options,
51 } => {
52 out.push('{');
53 out.push_str(name);
54 out.push_str(match kind {
55 IcuPluralKind::Cardinal => ", plural, ",
56 IcuPluralKind::Ordinal => ", selectordinal, ",
57 });
58 if *offset > 0 {
59 out.push_str("offset:");
60 out.push_str(&offset.to_string());
61 out.push(' ');
62 }
63 render_options(options, out);
64 out.push('}');
65 }
66 IcuNode::Pound => out.push('#'),
67 IcuNode::Tag { name, children } => {
68 out.push('<');
69 out.push_str(name);
70 if children.is_empty() {
71 out.push_str("/>");
72 } else {
73 out.push('>');
74 render_nodes(children, out);
75 out.push_str("</");
76 out.push_str(name);
77 out.push('>');
78 }
79 }
80 }
81}
82
83fn render_options(options: &[crate::IcuOption], out: &mut String) {
84 for (index, option) in options.iter().enumerate() {
85 if index > 0 {
86 out.push(' ');
87 }
88 out.push_str(&option.selector);
89 out.push_str(" {");
90 render_nodes(&option.value, out);
91 out.push('}');
92 }
93}
94
95fn render_formatter(kind: &str, name: &str, style: Option<&str>, out: &mut String) {
96 out.push('{');
97 out.push_str(name);
98 out.push_str(", ");
99 out.push_str(kind);
100 if let Some(style) = style {
101 out.push_str(", ");
102 out.push_str(style);
103 }
104 out.push('}');
105}
106
107fn append_escaped_literal(out: &mut String, value: &str) {
108 if !value
109 .as_bytes()
110 .iter()
111 .any(|byte| matches!(byte, b'\'' | b'{' | b'}' | b'#' | b'<' | b'>'))
112 {
113 out.push_str(value);
114 return;
115 }
116
117 for ch in value.chars() {
118 match ch {
119 '\'' => out.push_str("''"),
120 '{' | '}' | '#' | '<' | '>' => {
121 out.push('\'');
122 out.push(ch);
123 out.push('\'');
124 }
125 _ => out.push(ch),
126 }
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use crate::{parse_icu, stringify_icu};
133
134 fn assert_roundtrip(input: &str) {
135 let parsed = parse_icu(input).expect("parse source");
136 let rendered = stringify_icu(&parsed);
137 let reparsed = parse_icu(&rendered).expect("parse rendered");
138 assert_eq!(reparsed, parsed, "rendered: {rendered}");
139 }
140
141 #[test]
142 fn roundtrips_literal_escaping_arguments_and_formatters() {
143 assert_roundtrip("Use '<'tag'>' with '{'braces'}', ''quotes'', and '#'.");
144 assert_roundtrip(
145 "{name} {count, number, integer} {created, date, short} {created, time, ::HHmm}",
146 );
147 assert_roundtrip("{items, list, conjunction} {elapsed, duration} {age, ago} {owner, name}");
148 }
149
150 #[test]
151 fn roundtrips_select_plural_pound_and_tags() {
152 assert_roundtrip(
153 "{gender, select, female {She liked <b>{thing}</b>} male {He liked <b>{thing}</b>} other {They liked <b>{thing}</b>}}",
154 );
155 assert_roundtrip(
156 "{count, plural, offset:1 =0 {No items} one {# item} other {# items and '#'}}",
157 );
158 assert_roundtrip(
159 "{rank, selectordinal, one {#st place} two {#nd place} few {#rd place} other {#th place}}",
160 );
161 }
162
163 #[test]
164 fn uses_canonical_spacing_for_rendered_messages() {
165 let parsed = parse_icu("{count,plural,one{# item} other{# items}}").expect("parse");
166
167 assert_eq!(
168 stringify_icu(&parsed),
169 "{count, plural, one {# item} other {# items}}"
170 );
171 }
172}