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 out.push('>');
71 render_nodes(children, out);
72 out.push_str("</");
73 out.push_str(name);
74 out.push('>');
75 }
76 }
77}
78
79fn render_options(options: &[crate::IcuOption], out: &mut String) {
80 for (index, option) in options.iter().enumerate() {
81 if index > 0 {
82 out.push(' ');
83 }
84 out.push_str(&option.selector);
85 out.push_str(" {");
86 render_nodes(&option.value, out);
87 out.push('}');
88 }
89}
90
91fn render_formatter(kind: &str, name: &str, style: Option<&str>, out: &mut String) {
92 out.push('{');
93 out.push_str(name);
94 out.push_str(", ");
95 out.push_str(kind);
96 if let Some(style) = style {
97 out.push_str(", ");
98 out.push_str(style);
99 }
100 out.push('}');
101}
102
103fn append_escaped_literal(out: &mut String, value: &str) {
104 if !value
105 .as_bytes()
106 .iter()
107 .any(|byte| matches!(byte, b'\'' | b'{' | b'}' | b'#' | b'<' | b'>'))
108 {
109 out.push_str(value);
110 return;
111 }
112
113 for ch in value.chars() {
114 match ch {
115 '\'' => out.push_str("''"),
116 '{' | '}' | '#' | '<' | '>' => {
117 out.push('\'');
118 out.push(ch);
119 out.push('\'');
120 }
121 _ => out.push(ch),
122 }
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use crate::{parse_icu, stringify_icu};
129
130 fn assert_roundtrip(input: &str) {
131 let parsed = parse_icu(input).expect("parse source");
132 let rendered = stringify_icu(&parsed);
133 let reparsed = parse_icu(&rendered).expect("parse rendered");
134 assert_eq!(reparsed, parsed, "rendered: {rendered}");
135 }
136
137 #[test]
138 fn roundtrips_literal_escaping_arguments_and_formatters() {
139 assert_roundtrip("Use '<'tag'>' with '{'braces'}', ''quotes'', and '#'.");
140 assert_roundtrip(
141 "{name} {count, number, integer} {created, date, short} {created, time, ::HHmm}",
142 );
143 assert_roundtrip("{items, list, conjunction} {elapsed, duration} {age, ago} {owner, name}");
144 }
145
146 #[test]
147 fn roundtrips_select_plural_pound_and_tags() {
148 assert_roundtrip(
149 "{gender, select, female {She liked <b>{thing}</b>} male {He liked <b>{thing}</b>} other {They liked <b>{thing}</b>}}",
150 );
151 assert_roundtrip(
152 "{count, plural, offset:1 =0 {No items} one {# item} other {# items and '#'}}",
153 );
154 assert_roundtrip(
155 "{rank, selectordinal, one {#st place} two {#nd place} few {#rd place} other {#th place}}",
156 );
157 }
158
159 #[test]
160 fn uses_canonical_spacing_for_rendered_messages() {
161 let parsed = parse_icu("{count,plural,one{# item} other{# items}}").expect("parse");
162
163 assert_eq!(
164 stringify_icu(&parsed),
165 "{count, plural, one {# item} other {# items}}"
166 );
167 }
168}