fluent_syntax/
serializer.rs

1//! Fluent Translation List serialization utilities
2//!
3//! This modules provides a way to serialize an abstract syntax tree representing a
4//! Fluent Translation List. Use cases include normalization and programmatic
5//! manipulation of a Fluent Translation List.
6//!
7//! # Example
8//!
9//! ```
10//! use fluent_syntax::parser;
11//! use fluent_syntax::serializer;
12//!
13//! let ftl = r#"# This is a message comment
14//! hello-world = Hello World!
15//! "#;
16//!
17//! let resource = parser::parse(ftl).expect("Failed to parse an FTL resource.");
18//!
19//! let serialized = serializer::serialize(&resource);
20//!
21//! assert_eq!(ftl, serialized);
22//! ```
23
24use crate::{ast::*, parser::matches_fluent_ws, parser::Slice};
25use std::fmt::Write;
26
27/// Serializes an abstract syntax tree representing a Fluent Translation List into a
28/// String.
29///
30/// # Example
31///
32/// ```
33/// use fluent_syntax::parser;
34/// use fluent_syntax::serializer;
35///
36/// let ftl = r#"
37/// unnormalized-message=This message has
38///   abnormal spacing and indentation"#;
39///
40/// let resource = parser::parse(ftl).expect("Failed to parse an FTL resource.");
41///
42/// let serialized = serializer::serialize(&resource);
43///
44/// let expected = r#"unnormalized-message =
45///     This message has
46///     abnormal spacing and indentation
47/// "#;
48///
49/// assert_eq!(expected, serialized);
50/// ```
51pub fn serialize<'s, S: Slice<'s>>(resource: &Resource<S>) -> String {
52    serialize_with_options(resource, Options::default())
53}
54
55/// Serializes an abstract syntax tree representing a Fluent Translation List into a
56/// String accepting custom options.
57pub 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                    // A placeable inside a placeable is a special case because we
210                    // don't want the braces to look silly (e.g. "{ { Foo() } }").
211                    self.writer.write_literal("{{ ");
212                    self.serialize_expression(expression);
213                    self.writer.write_literal(" }}");
214                }
215                Expression::Select { .. } => {
216                    // select adds its own newline and indent, emit the brace
217                    // *without* a space so we don't get 5 spaces instead of 4
218                    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/// Options for serializing an abstract syntax tree.
389#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
390pub struct Options {
391    /// Whether invalid text fragments should be serialized, too.
392    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            // handle rare edge case, where the trailing `\r` would get confused
427            // as part of the line ending
428            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            // we've just added a newline, make sure it's properly indented
436            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}