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 | |
24 | use crate::{ast::*, parser::matches_fluent_ws, parser::Slice}; |
25 | use 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 | /// ``` |
51 | pub 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. |
57 | pub fn serialize_with_options<'s, S: Slice<'s>>( |
58 | resource: &Resource<S>, |
59 | options: Options, |
60 | ) -> String { |
61 | let mut ser: Serializer = Serializer::new(options); |
62 | ser.serialize_resource(res:resource); |
63 | ser.into_serialized_text() |
64 | } |
65 | |
66 | #[derive (Debug)] |
67 | struct Serializer { |
68 | writer: TextWriter, |
69 | options: Options, |
70 | state: State, |
71 | } |
72 | |
73 | impl 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 | |
357 | impl<'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: &PatternElement| match elem { |
364 | PatternElement::TextElement { value: &S } => value.as_ref().contains(' \n' ), |
365 | PatternElement::Placeable { expression: &Expression } => is_select_expr(expression), |
366 | }) |
367 | } |
368 | |
369 | fn has_leading_text_dot(&self) -> bool { |
370 | if let Some(PatternElement::TextElement { value: &S }) = self.elements.get(index:0) { |
371 | value.as_ref().starts_with('.' ) |
372 | } else { |
373 | false |
374 | } |
375 | } |
376 | } |
377 | |
378 | fn is_select_expr<'s, S: Slice<'s>>(expr: &Expression<S>) -> bool { |
379 | match expr { |
380 | Expression::Select { .. } => true, |
381 | Expression::Inline(InlineExpression::Placeable { expression: &Box> }) => { |
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)] |
390 | pub struct Options { |
391 | /// Whether invalid text fragments should be serialized, too. |
392 | pub with_junk: bool, |
393 | } |
394 | |
395 | #[derive (Debug, Default, PartialEq)] |
396 | struct State { |
397 | wrote_non_junk_entry: bool, |
398 | } |
399 | |
400 | #[derive (Debug, Clone, Default)] |
401 | struct TextWriter { |
402 | buffer: String, |
403 | indent_level: usize, |
404 | } |
405 | |
406 | impl 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)] |
452 | mod 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 | } |
656 | |