1use crate::{ast::*, parser::matches_fluent_ws, parser::Slice};
25use std::fmt::Write;
26
27pub fn serialize<'s, S: Slice<'s>>(resource: &Resource<S>) -> String {
52 serialize_with_options(resource, Options::default())
53}
54
55pub fn serialize_with_options<'s, S: Slice<'s>>(
58 resource: &Resource<S>,
59 options: Options,
60) -> String {
61 let mut ser = Serializer::new(options);
62 ser.serialize_resource(resource);
63 ser.into_serialized_text()
64}
65
66#[derive(Debug)]
67struct Serializer {
68 writer: TextWriter,
69 options: Options,
70 state: State,
71}
72
73impl Serializer {
74 fn new(options: Options) -> Self {
75 Serializer {
76 writer: TextWriter::default(),
77 options,
78 state: State::default(),
79 }
80 }
81
82 fn serialize_resource<'s, S: Slice<'s>>(&mut self, res: &Resource<S>) {
83 for entry in &res.body {
84 match entry {
85 Entry::Message(msg) => self.serialize_message(msg),
86 Entry::Term(term) => self.serialize_term(term),
87 Entry::Comment(comment) => self.serialize_free_comment(comment, "#"),
88 Entry::GroupComment(comment) => self.serialize_free_comment(comment, "##"),
89 Entry::ResourceComment(comment) => self.serialize_free_comment(comment, "###"),
90 Entry::Junk { content } => {
91 if self.options.with_junk {
92 self.serialize_junk(content.as_ref())
93 }
94 }
95 };
96
97 self.state.wrote_non_junk_entry = !matches!(entry, Entry::Junk { .. });
98 }
99 }
100
101 fn into_serialized_text(self) -> String {
102 self.writer.buffer
103 }
104
105 fn serialize_junk(&mut self, junk: &str) {
106 self.writer.write_literal(junk)
107 }
108
109 fn serialize_free_comment<'s, S: Slice<'s>>(&mut self, comment: &Comment<S>, prefix: &str) {
110 if self.state.wrote_non_junk_entry {
111 self.writer.newline();
112 }
113 self.serialize_comment(comment, prefix);
114 self.writer.newline();
115 }
116
117 fn serialize_comment<'s, S: Slice<'s>>(&mut self, comment: &Comment<S>, prefix: &str) {
118 for line in &comment.content {
119 self.writer.write_literal(prefix);
120
121 if !line.as_ref().trim_matches(matches_fluent_ws).is_empty() {
122 self.writer.write_literal(" ");
123 self.writer.write_literal(line.as_ref());
124 }
125
126 self.writer.newline();
127 }
128 }
129
130 fn serialize_message<'s, S: Slice<'s>>(&mut self, msg: &Message<S>) {
131 if let Some(comment) = msg.comment.as_ref() {
132 self.serialize_comment(comment, "#");
133 }
134
135 self.writer.write_literal(msg.id.name.as_ref());
136 self.writer.write_literal(" =");
137
138 if let Some(value) = msg.value.as_ref() {
139 self.serialize_pattern(value);
140 }
141
142 self.serialize_attributes(&msg.attributes);
143
144 self.writer.newline();
145 }
146
147 fn serialize_term<'s, S: Slice<'s>>(&mut self, term: &Term<S>) {
148 if let Some(comment) = term.comment.as_ref() {
149 self.serialize_comment(comment, "#");
150 }
151
152 self.writer.write_literal("-");
153 self.writer.write_literal(term.id.name.as_ref());
154 self.writer.write_literal(" =");
155 self.serialize_pattern(&term.value);
156
157 self.serialize_attributes(&term.attributes);
158
159 self.writer.newline();
160 }
161
162 fn serialize_pattern<'s, S: Slice<'s>>(&mut self, pattern: &Pattern<S>) {
163 let start_on_newline = pattern.starts_on_new_line();
164
165 if start_on_newline {
166 self.writer.newline();
167 self.writer.indent();
168 } else {
169 self.writer.write_literal(" ");
170 }
171
172 for element in &pattern.elements {
173 self.serialize_element(element);
174 }
175
176 if start_on_newline {
177 self.writer.dedent();
178 }
179 }
180
181 fn serialize_attributes<'s, S: Slice<'s>>(&mut self, attrs: &[Attribute<S>]) {
182 if attrs.is_empty() {
183 return;
184 }
185
186 self.writer.indent();
187
188 for attr in attrs {
189 self.writer.newline();
190 self.serialize_attribute(attr);
191 }
192
193 self.writer.dedent();
194 }
195
196 fn serialize_attribute<'s, S: Slice<'s>>(&mut self, attr: &Attribute<S>) {
197 self.writer.write_literal(".");
198 self.writer.write_literal(attr.id.name.as_ref());
199 self.writer.write_literal(" =");
200
201 self.serialize_pattern(&attr.value);
202 }
203
204 fn serialize_element<'s, S: Slice<'s>>(&mut self, elem: &PatternElement<S>) {
205 match elem {
206 PatternElement::TextElement { value } => self.writer.write_literal(value.as_ref()),
207 PatternElement::Placeable { expression } => match expression {
208 Expression::Inline(InlineExpression::Placeable { expression }) => {
209 self.writer.write_literal("{{ ");
212 self.serialize_expression(expression);
213 self.writer.write_literal(" }}");
214 }
215 Expression::Select { .. } => {
216 self.writer.write_literal("{ ");
219 self.serialize_expression(expression);
220 self.writer.write_literal("}");
221 }
222 Expression::Inline(_) => {
223 self.writer.write_literal("{ ");
224 self.serialize_expression(expression);
225 self.writer.write_literal(" }");
226 }
227 },
228 }
229 }
230
231 fn serialize_expression<'s, S: Slice<'s>>(&mut self, expr: &Expression<S>) {
232 match expr {
233 Expression::Inline(inline) => self.serialize_inline_expression(inline),
234 Expression::Select { selector, variants } => {
235 self.serialize_select_expression(selector, variants)
236 }
237 }
238 }
239
240 fn serialize_inline_expression<'s, S: Slice<'s>>(&mut self, expr: &InlineExpression<S>) {
241 match expr {
242 InlineExpression::StringLiteral { value } => {
243 self.writer.write_literal("\"");
244 self.writer.write_literal(value.as_ref());
245 self.writer.write_literal("\"");
246 }
247 InlineExpression::NumberLiteral { value } => self.writer.write_literal(value.as_ref()),
248 InlineExpression::VariableReference {
249 id: Identifier { name: value },
250 } => {
251 self.writer.write_literal("$");
252 self.writer.write_literal(value.as_ref());
253 }
254 InlineExpression::FunctionReference { id, arguments } => {
255 self.writer.write_literal(id.name.as_ref());
256 self.serialize_call_arguments(arguments);
257 }
258 InlineExpression::MessageReference { id, attribute } => {
259 self.writer.write_literal(id.name.as_ref());
260
261 if let Some(attr) = attribute.as_ref() {
262 self.writer.write_literal(".");
263 self.writer.write_literal(attr.name.as_ref());
264 }
265 }
266 InlineExpression::TermReference {
267 id,
268 attribute,
269 arguments,
270 } => {
271 self.writer.write_literal("-");
272 self.writer.write_literal(id.name.as_ref());
273
274 if let Some(attr) = attribute.as_ref() {
275 self.writer.write_literal(".");
276 self.writer.write_literal(attr.name.as_ref());
277 }
278 if let Some(args) = arguments.as_ref() {
279 self.serialize_call_arguments(args);
280 }
281 }
282 InlineExpression::Placeable { expression } => {
283 self.writer.write_literal("{");
284 self.serialize_expression(expression);
285 self.writer.write_literal("}");
286 }
287 }
288 }
289
290 fn serialize_select_expression<'s, S: Slice<'s>>(
291 &mut self,
292 selector: &InlineExpression<S>,
293 variants: &[Variant<S>],
294 ) {
295 self.serialize_inline_expression(selector);
296 self.writer.write_literal(" ->");
297
298 self.writer.newline();
299 self.writer.indent();
300
301 for variant in variants {
302 self.serialize_variant(variant);
303 self.writer.newline();
304 }
305
306 self.writer.dedent();
307 }
308
309 fn serialize_variant<'s, S: Slice<'s>>(&mut self, variant: &Variant<S>) {
310 if variant.default {
311 self.writer.write_char_into_indent('*');
312 }
313
314 self.writer.write_literal("[");
315 self.serialize_variant_key(&variant.key);
316 self.writer.write_literal("]");
317 self.serialize_pattern(&variant.value);
318 }
319
320 fn serialize_variant_key<'s, S: Slice<'s>>(&mut self, key: &VariantKey<S>) {
321 match key {
322 VariantKey::NumberLiteral { value } | VariantKey::Identifier { name: value } => {
323 self.writer.write_literal(value.as_ref())
324 }
325 }
326 }
327
328 fn serialize_call_arguments<'s, S: Slice<'s>>(&mut self, args: &CallArguments<S>) {
329 let mut argument_written = false;
330
331 self.writer.write_literal("(");
332
333 for positional in &args.positional {
334 if argument_written {
335 self.writer.write_literal(", ");
336 }
337
338 self.serialize_inline_expression(positional);
339 argument_written = true;
340 }
341
342 for named in &args.named {
343 if argument_written {
344 self.writer.write_literal(", ");
345 }
346
347 self.writer.write_literal(named.name.name.as_ref());
348 self.writer.write_literal(": ");
349 self.serialize_inline_expression(&named.value);
350 argument_written = true;
351 }
352
353 self.writer.write_literal(")");
354 }
355}
356
357impl<'s, S: Slice<'s>> Pattern<S> {
358 fn starts_on_new_line(&self) -> bool {
359 !self.has_leading_text_dot() && self.is_multiline()
360 }
361
362 fn is_multiline(&self) -> bool {
363 self.elements.iter().any(|elem| match elem {
364 PatternElement::TextElement { value } => value.as_ref().contains('\n'),
365 PatternElement::Placeable { expression } => is_select_expr(expression),
366 })
367 }
368
369 fn has_leading_text_dot(&self) -> bool {
370 if let Some(PatternElement::TextElement { value }) = self.elements.get(0) {
371 value.as_ref().starts_with('.')
372 } else {
373 false
374 }
375 }
376}
377
378fn is_select_expr<'s, S: Slice<'s>>(expr: &Expression<S>) -> bool {
379 match expr {
380 Expression::Select { .. } => true,
381 Expression::Inline(InlineExpression::Placeable { expression }) => {
382 is_select_expr(expression)
383 }
384 Expression::Inline(_) => false,
385 }
386}
387
388#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
390pub struct Options {
391 pub with_junk: bool,
393}
394
395#[derive(Debug, Default, PartialEq)]
396struct State {
397 wrote_non_junk_entry: bool,
398}
399
400#[derive(Debug, Clone, Default)]
401struct TextWriter {
402 buffer: String,
403 indent_level: usize,
404}
405
406impl TextWriter {
407 fn indent(&mut self) {
408 self.indent_level += 1;
409 }
410
411 fn dedent(&mut self) {
412 self.indent_level = self
413 .indent_level
414 .checked_sub(1)
415 .expect("Dedenting without a corresponding indent");
416 }
417
418 fn write_indent(&mut self) {
419 for _ in 0..self.indent_level {
420 self.buffer.push_str(" ");
421 }
422 }
423
424 fn newline(&mut self) {
425 if self.buffer.ends_with('\r') {
426 self.buffer.push('\r');
429 }
430 self.buffer.push('\n');
431 }
432
433 fn write_literal(&mut self, item: &str) {
434 if self.buffer.ends_with('\n') {
435 self.write_indent();
437 }
438
439 write!(self.buffer, "{}", item).expect("Writing to an in-memory buffer never fails");
440 }
441
442 fn write_char_into_indent(&mut self, ch: char) {
443 if self.buffer.ends_with('\n') {
444 self.write_indent();
445 }
446 self.buffer.pop();
447 self.buffer.push(ch);
448 }
449}
450
451#[cfg(test)]
452mod test {
453 use super::*;
454 use crate::parser::parse;
455
456 #[test]
457 fn write_something_then_indent() {
458 let mut writer = TextWriter::default();
459
460 writer.write_literal("foo =");
461 writer.newline();
462 writer.indent();
463 writer.write_literal("first line");
464 writer.newline();
465 writer.write_literal("second line");
466 writer.newline();
467 writer.dedent();
468 writer.write_literal("not indented");
469 writer.newline();
470
471 let got = &writer.buffer;
472 assert_eq!(
473 got,
474 "foo =\n first line\n second line\nnot indented\n"
475 );
476 }
477
478 macro_rules! text_message {
479 ($name:expr, $value:expr) => {
480 Entry::Message(Message {
481 id: Identifier { name: $name },
482 value: Some(Pattern {
483 elements: vec![PatternElement::TextElement { value: $value }],
484 }),
485 attributes: vec![],
486 comment: None,
487 })
488 };
489 }
490
491 impl<'a> Entry<&'a str> {
492 fn as_message(&mut self) -> &mut Message<&'a str> {
493 match self {
494 Self::Message(msg) => msg,
495 _ => panic!("Expected Message"),
496 }
497 }
498 }
499
500 impl<'a> Message<&'a str> {
501 fn as_pattern(&mut self) -> &mut Pattern<&'a str> {
502 self.value.as_mut().expect("Expected Pattern")
503 }
504 }
505
506 impl<'a> PatternElement<&'a str> {
507 fn as_text(&mut self) -> &mut &'a str {
508 match self {
509 Self::TextElement { value } => value,
510 _ => panic!("Expected TextElement"),
511 }
512 }
513
514 fn as_expression(&mut self) -> &mut Expression<&'a str> {
515 match self {
516 Self::Placeable { expression } => expression,
517 _ => panic!("Expected Placeable"),
518 }
519 }
520 }
521
522 impl<'a> Expression<&'a str> {
523 fn as_variants(&mut self) -> &mut Vec<Variant<&'a str>> {
524 match self {
525 Self::Select { variants, .. } => variants,
526 _ => panic!("Expected Select"),
527 }
528 }
529 fn as_inline_variable_id(&mut self) -> &mut Identifier<&'a str> {
530 match self {
531 Self::Inline(InlineExpression::VariableReference { id }) => id,
532 _ => panic!("Expected Inline"),
533 }
534 }
535 }
536
537 #[test]
538 fn change_id() {
539 let mut ast = parse("foo = bar\n").expect("failed to parse ftl resource");
540 ast.body[0].as_message().id.name = "baz";
541 assert_eq!(serialize(&ast), "baz = bar\n");
542 }
543
544 #[test]
545 fn change_value() {
546 let mut ast = parse("foo = bar\n").expect("failed to parse ftl resource");
547 *ast.body[0].as_message().as_pattern().elements[0].as_text() = "baz";
548 assert_eq!("foo = baz\n", serialize(&ast));
549 }
550
551 #[test]
552 fn add_expression_variant() {
553 let message = concat!(
554 "foo =\n",
555 " { $num ->\n",
556 " *[other] { $num } bars\n",
557 " }\n"
558 );
559 let mut ast = parse(message).expect("failed to parse ftl resource");
560
561 let one_variant = Variant {
562 key: VariantKey::Identifier { name: "one" },
563 value: Pattern {
564 elements: vec![
565 PatternElement::Placeable {
566 expression: Expression::Inline(InlineExpression::VariableReference {
567 id: Identifier { name: "num" },
568 }),
569 },
570 PatternElement::TextElement { value: " bar" },
571 ],
572 },
573 default: false,
574 };
575 ast.body[0].as_message().as_pattern().elements[0]
576 .as_expression()
577 .as_variants()
578 .insert(0, one_variant);
579
580 let expected = concat!(
581 "foo =\n",
582 " { $num ->\n",
583 " [one] { $num } bar\n",
584 " *[other] { $num } bars\n",
585 " }\n"
586 );
587 assert_eq!(serialize(&ast), expected);
588 }
589
590 #[test]
591 fn change_variable_reference() {
592 let mut ast = parse("foo = { $bar }\n").expect("failed to parse ftl resource");
593 ast.body[0].as_message().as_pattern().elements[0]
594 .as_expression()
595 .as_inline_variable_id()
596 .name = "qux";
597 assert_eq!("foo = { $qux }\n", serialize(&ast));
598 }
599
600 #[test]
601 fn remove_message() {
602 let mut ast = parse("foo = bar\nbaz = qux\n").expect("failed to parse ftl resource");
603 ast.body.pop();
604 assert_eq!("foo = bar\n", serialize(&ast));
605 }
606
607 #[test]
608 fn add_message_at_top() {
609 let mut ast = parse("foo = bar\n").expect("failed to parse ftl resource");
610 ast.body.insert(0, text_message!("baz", "qux"));
611 assert_eq!("baz = qux\nfoo = bar\n", serialize(&ast));
612 }
613
614 #[test]
615 fn add_message_at_end() {
616 let mut ast = parse("foo = bar\n").expect("failed to parse ftl resource");
617 ast.body.push(text_message!("baz", "qux"));
618 assert_eq!("foo = bar\nbaz = qux\n", serialize(&ast));
619 }
620
621 #[test]
622 fn add_message_in_between() {
623 let mut ast = parse("foo = bar\nbaz = qux\n").expect("failed to parse ftl resource");
624 ast.body.insert(1, text_message!("hello", "there"));
625 assert_eq!("foo = bar\nhello = there\nbaz = qux\n", serialize(&ast));
626 }
627
628 #[test]
629 fn add_message_comment() {
630 let mut ast = parse("foo = bar\n").expect("failed to parse ftl resource");
631 ast.body[0].as_message().comment.replace(Comment {
632 content: vec!["great message!"],
633 });
634 assert_eq!("# great message!\nfoo = bar\n", serialize(&ast));
635 }
636
637 #[test]
638 fn remove_message_comment() {
639 let mut ast = parse("# great message!\nfoo = bar\n").expect("failed to parse ftl resource");
640 ast.body[0].as_message().comment.take();
641 assert_eq!("foo = bar\n", serialize(&ast));
642 }
643
644 #[test]
645 fn edit_message_comment() {
646 let mut ast = parse("# great message!\nfoo = bar\n").expect("failed to parse ftl resource");
647 ast.body[0]
648 .as_message()
649 .comment
650 .as_mut()
651 .expect("comment is missing")
652 .content[0] = "very original";
653 assert_eq!("# very original\nfoo = bar\n", serialize(&ast));
654 }
655}