use crate::{ast::*, parser::matches_fluent_ws, parser::Slice};
use std::fmt::Write;
pub fn serialize<'s, S: Slice<'s>>(resource: &Resource<S>) -> String {
serialize_with_options(resource, Options::default())
}
pub fn serialize_with_options<'s, S: Slice<'s>>(
resource: &Resource<S>,
options: Options,
) -> String {
let mut ser = Serializer::new(options);
ser.serialize_resource(resource);
ser.into_serialized_text()
}
#[derive(Debug)]
struct Serializer {
writer: TextWriter,
options: Options,
state: State,
}
impl Serializer {
fn new(options: Options) -> Self {
Serializer {
writer: TextWriter::default(),
options,
state: State::default(),
}
}
fn serialize_resource<'s, S: Slice<'s>>(&mut self, res: &Resource<S>) {
for entry in &res.body {
match entry {
Entry::Message(msg) => self.serialize_message(msg),
Entry::Term(term) => self.serialize_term(term),
Entry::Comment(comment) => self.serialize_free_comment(comment, "#"),
Entry::GroupComment(comment) => self.serialize_free_comment(comment, "##"),
Entry::ResourceComment(comment) => self.serialize_free_comment(comment, "###"),
Entry::Junk { content } => {
if self.options.with_junk {
self.serialize_junk(content.as_ref())
}
}
};
self.state.wrote_non_junk_entry = !matches!(entry, Entry::Junk { .. });
}
}
fn into_serialized_text(self) -> String {
self.writer.buffer
}
fn serialize_junk(&mut self, junk: &str) {
self.writer.write_literal(junk)
}
fn serialize_free_comment<'s, S: Slice<'s>>(&mut self, comment: &Comment<S>, prefix: &str) {
if self.state.wrote_non_junk_entry {
self.writer.newline();
}
self.serialize_comment(comment, prefix);
self.writer.newline();
}
fn serialize_comment<'s, S: Slice<'s>>(&mut self, comment: &Comment<S>, prefix: &str) {
for line in &comment.content {
self.writer.write_literal(prefix);
if !line.as_ref().trim_matches(matches_fluent_ws).is_empty() {
self.writer.write_literal(" ");
self.writer.write_literal(line.as_ref());
}
self.writer.newline();
}
}
fn serialize_message<'s, S: Slice<'s>>(&mut self, msg: &Message<S>) {
if let Some(comment) = msg.comment.as_ref() {
self.serialize_comment(comment, "#");
}
self.writer.write_literal(msg.id.name.as_ref());
self.writer.write_literal(" =");
if let Some(value) = msg.value.as_ref() {
self.serialize_pattern(value);
}
self.serialize_attributes(&msg.attributes);
self.writer.newline();
}
fn serialize_term<'s, S: Slice<'s>>(&mut self, term: &Term<S>) {
if let Some(comment) = term.comment.as_ref() {
self.serialize_comment(comment, "#");
}
self.writer.write_literal("-");
self.writer.write_literal(term.id.name.as_ref());
self.writer.write_literal(" =");
self.serialize_pattern(&term.value);
self.serialize_attributes(&term.attributes);
self.writer.newline();
}
fn serialize_pattern<'s, S: Slice<'s>>(&mut self, pattern: &Pattern<S>) {
let start_on_newline = pattern.starts_on_new_line();
if start_on_newline {
self.writer.newline();
self.writer.indent();
} else {
self.writer.write_literal(" ");
}
for element in &pattern.elements {
self.serialize_element(element);
}
if start_on_newline {
self.writer.dedent();
}
}
fn serialize_attributes<'s, S: Slice<'s>>(&mut self, attrs: &[Attribute<S>]) {
if attrs.is_empty() {
return;
}
self.writer.indent();
for attr in attrs {
self.writer.newline();
self.serialize_attribute(attr);
}
self.writer.dedent();
}
fn serialize_attribute<'s, S: Slice<'s>>(&mut self, attr: &Attribute<S>) {
self.writer.write_literal(".");
self.writer.write_literal(attr.id.name.as_ref());
self.writer.write_literal(" =");
self.serialize_pattern(&attr.value);
}
fn serialize_element<'s, S: Slice<'s>>(&mut self, elem: &PatternElement<S>) {
match elem {
PatternElement::TextElement { value } => self.writer.write_literal(value.as_ref()),
PatternElement::Placeable { expression } => match expression {
Expression::Inline(InlineExpression::Placeable { expression }) => {
self.writer.write_literal("{{ ");
self.serialize_expression(expression);
self.writer.write_literal(" }}");
}
Expression::Select { .. } => {
self.writer.write_literal("{ ");
self.serialize_expression(expression);
self.writer.write_literal("}");
}
Expression::Inline(_) => {
self.writer.write_literal("{ ");
self.serialize_expression(expression);
self.writer.write_literal(" }");
}
},
}
}
fn serialize_expression<'s, S: Slice<'s>>(&mut self, expr: &Expression<S>) {
match expr {
Expression::Inline(inline) => self.serialize_inline_expression(inline),
Expression::Select { selector, variants } => {
self.serialize_select_expression(selector, variants)
}
}
}
fn serialize_inline_expression<'s, S: Slice<'s>>(&mut self, expr: &InlineExpression<S>) {
match expr {
InlineExpression::StringLiteral { value } => {
self.writer.write_literal("\"");
self.writer.write_literal(value.as_ref());
self.writer.write_literal("\"");
}
InlineExpression::NumberLiteral { value } => self.writer.write_literal(value.as_ref()),
InlineExpression::VariableReference {
id: Identifier { name: value },
} => {
self.writer.write_literal("$");
self.writer.write_literal(value.as_ref());
}
InlineExpression::FunctionReference { id, arguments } => {
self.writer.write_literal(id.name.as_ref());
self.serialize_call_arguments(arguments);
}
InlineExpression::MessageReference { id, attribute } => {
self.writer.write_literal(id.name.as_ref());
if let Some(attr) = attribute.as_ref() {
self.writer.write_literal(".");
self.writer.write_literal(attr.name.as_ref());
}
}
InlineExpression::TermReference {
id,
attribute,
arguments,
} => {
self.writer.write_literal("-");
self.writer.write_literal(id.name.as_ref());
if let Some(attr) = attribute.as_ref() {
self.writer.write_literal(".");
self.writer.write_literal(attr.name.as_ref());
}
if let Some(args) = arguments.as_ref() {
self.serialize_call_arguments(args);
}
}
InlineExpression::Placeable { expression } => {
self.writer.write_literal("{");
self.serialize_expression(expression);
self.writer.write_literal("}");
}
}
}
fn serialize_select_expression<'s, S: Slice<'s>>(
&mut self,
selector: &InlineExpression<S>,
variants: &[Variant<S>],
) {
self.serialize_inline_expression(selector);
self.writer.write_literal(" ->");
self.writer.newline();
self.writer.indent();
for variant in variants {
self.serialize_variant(variant);
self.writer.newline();
}
self.writer.dedent();
}
fn serialize_variant<'s, S: Slice<'s>>(&mut self, variant: &Variant<S>) {
if variant.default {
self.writer.write_char_into_indent('*');
}
self.writer.write_literal("[");
self.serialize_variant_key(&variant.key);
self.writer.write_literal("]");
self.serialize_pattern(&variant.value);
}
fn serialize_variant_key<'s, S: Slice<'s>>(&mut self, key: &VariantKey<S>) {
match key {
VariantKey::NumberLiteral { value } | VariantKey::Identifier { name: value } => {
self.writer.write_literal(value.as_ref())
}
}
}
fn serialize_call_arguments<'s, S: Slice<'s>>(&mut self, args: &CallArguments<S>) {
let mut argument_written = false;
self.writer.write_literal("(");
for positional in &args.positional {
if argument_written {
self.writer.write_literal(", ");
}
self.serialize_inline_expression(positional);
argument_written = true;
}
for named in &args.named {
if argument_written {
self.writer.write_literal(", ");
}
self.writer.write_literal(named.name.name.as_ref());
self.writer.write_literal(": ");
self.serialize_inline_expression(&named.value);
argument_written = true;
}
self.writer.write_literal(")");
}
}
impl<'s, S: Slice<'s>> Pattern<S> {
fn starts_on_new_line(&self) -> bool {
!self.has_leading_text_dot() && self.is_multiline()
}
fn is_multiline(&self) -> bool {
self.elements.iter().any(|elem| match elem {
PatternElement::TextElement { value } => value.as_ref().contains('\n'),
PatternElement::Placeable { expression } => is_select_expr(expression),
})
}
fn has_leading_text_dot(&self) -> bool {
if let Some(PatternElement::TextElement { value }) = self.elements.get(0) {
value.as_ref().starts_with('.')
} else {
false
}
}
}
fn is_select_expr<'s, S: Slice<'s>>(expr: &Expression<S>) -> bool {
match expr {
Expression::Select { .. } => true,
Expression::Inline(InlineExpression::Placeable { expression }) => {
is_select_expr(expression)
}
Expression::Inline(_) => false,
}
}
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
pub struct Options {
pub with_junk: bool,
}
#[derive(Debug, Default, PartialEq)]
struct State {
wrote_non_junk_entry: bool,
}
#[derive(Debug, Clone, Default)]
struct TextWriter {
buffer: String,
indent_level: usize,
}
impl TextWriter {
fn indent(&mut self) {
self.indent_level += 1;
}
fn dedent(&mut self) {
self.indent_level = self
.indent_level
.checked_sub(1)
.expect("Dedenting without a corresponding indent");
}
fn write_indent(&mut self) {
for _ in 0..self.indent_level {
self.buffer.push_str(" ");
}
}
fn newline(&mut self) {
if self.buffer.ends_with('\r') {
self.buffer.push('\r');
}
self.buffer.push('\n');
}
fn write_literal(&mut self, item: &str) {
if self.buffer.ends_with('\n') {
self.write_indent();
}
write!(self.buffer, "{}", item).expect("Writing to an in-memory buffer never fails");
}
fn write_char_into_indent(&mut self, ch: char) {
if self.buffer.ends_with('\n') {
self.write_indent();
}
self.buffer.pop();
self.buffer.push(ch);
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::parser::parse;
#[test]
fn write_something_then_indent() {
let mut writer = TextWriter::default();
writer.write_literal("foo =");
writer.newline();
writer.indent();
writer.write_literal("first line");
writer.newline();
writer.write_literal("second line");
writer.newline();
writer.dedent();
writer.write_literal("not indented");
writer.newline();
let got = &writer.buffer;
assert_eq!(
got,
"foo =\n first line\n second line\nnot indented\n"
);
}
macro_rules! text_message {
($name:expr, $value:expr) => {
Entry::Message(Message {
id: Identifier { name: $name },
value: Some(Pattern {
elements: vec![PatternElement::TextElement { value: $value }],
}),
attributes: vec![],
comment: None,
})
};
}
impl<'a> Entry<&'a str> {
fn as_message(&mut self) -> &mut Message<&'a str> {
match self {
Self::Message(msg) => msg,
_ => panic!("Expected Message"),
}
}
}
impl<'a> Message<&'a str> {
fn as_pattern(&mut self) -> &mut Pattern<&'a str> {
self.value.as_mut().expect("Expected Pattern")
}
}
impl<'a> PatternElement<&'a str> {
fn as_text(&mut self) -> &mut &'a str {
match self {
Self::TextElement { value } => value,
_ => panic!("Expected TextElement"),
}
}
fn as_expression(&mut self) -> &mut Expression<&'a str> {
match self {
Self::Placeable { expression } => expression,
_ => panic!("Expected Placeable"),
}
}
}
impl<'a> Expression<&'a str> {
fn as_variants(&mut self) -> &mut Vec<Variant<&'a str>> {
match self {
Self::Select { variants, .. } => variants,
_ => panic!("Expected Select"),
}
}
fn as_inline_variable_id(&mut self) -> &mut Identifier<&'a str> {
match self {
Self::Inline(InlineExpression::VariableReference { id }) => id,
_ => panic!("Expected Inline"),
}
}
}
#[test]
fn change_id() {
let mut ast = parse("foo = bar\n").expect("failed to parse ftl resource");
ast.body[0].as_message().id.name = "baz";
assert_eq!(serialize(&ast), "baz = bar\n");
}
#[test]
fn change_value() {
let mut ast = parse("foo = bar\n").expect("failed to parse ftl resource");
*ast.body[0].as_message().as_pattern().elements[0].as_text() = "baz";
assert_eq!("foo = baz\n", serialize(&ast));
}
#[test]
fn add_expression_variant() {
let message = concat!(
"foo =\n",
" { $num ->\n",
" *[other] { $num } bars\n",
" }\n"
);
let mut ast = parse(message).expect("failed to parse ftl resource");
let one_variant = Variant {
key: VariantKey::Identifier { name: "one" },
value: Pattern {
elements: vec![
PatternElement::Placeable {
expression: Expression::Inline(InlineExpression::VariableReference {
id: Identifier { name: "num" },
}),
},
PatternElement::TextElement { value: " bar" },
],
},
default: false,
};
ast.body[0].as_message().as_pattern().elements[0]
.as_expression()
.as_variants()
.insert(0, one_variant);
let expected = concat!(
"foo =\n",
" { $num ->\n",
" [one] { $num } bar\n",
" *[other] { $num } bars\n",
" }\n"
);
assert_eq!(serialize(&ast), expected);
}
#[test]
fn change_variable_reference() {
let mut ast = parse("foo = { $bar }\n").expect("failed to parse ftl resource");
ast.body[0].as_message().as_pattern().elements[0]
.as_expression()
.as_inline_variable_id()
.name = "qux";
assert_eq!("foo = { $qux }\n", serialize(&ast));
}
#[test]
fn remove_message() {
let mut ast = parse("foo = bar\nbaz = qux\n").expect("failed to parse ftl resource");
ast.body.pop();
assert_eq!("foo = bar\n", serialize(&ast));
}
#[test]
fn add_message_at_top() {
let mut ast = parse("foo = bar\n").expect("failed to parse ftl resource");
ast.body.insert(0, text_message!("baz", "qux"));
assert_eq!("baz = qux\nfoo = bar\n", serialize(&ast));
}
#[test]
fn add_message_at_end() {
let mut ast = parse("foo = bar\n").expect("failed to parse ftl resource");
ast.body.push(text_message!("baz", "qux"));
assert_eq!("foo = bar\nbaz = qux\n", serialize(&ast));
}
#[test]
fn add_message_in_between() {
let mut ast = parse("foo = bar\nbaz = qux\n").expect("failed to parse ftl resource");
ast.body.insert(1, text_message!("hello", "there"));
assert_eq!("foo = bar\nhello = there\nbaz = qux\n", serialize(&ast));
}
#[test]
fn add_message_comment() {
let mut ast = parse("foo = bar\n").expect("failed to parse ftl resource");
ast.body[0].as_message().comment.replace(Comment {
content: vec!["great message!"],
});
assert_eq!("# great message!\nfoo = bar\n", serialize(&ast));
}
#[test]
fn remove_message_comment() {
let mut ast = parse("# great message!\nfoo = bar\n").expect("failed to parse ftl resource");
ast.body[0].as_message().comment.take();
assert_eq!("foo = bar\n", serialize(&ast));
}
#[test]
fn edit_message_comment() {
let mut ast = parse("# great message!\nfoo = bar\n").expect("failed to parse ftl resource");
ast.body[0]
.as_message()
.comment
.as_mut()
.expect("comment is missing")
.content[0] = "very original";
assert_eq!("# very original\nfoo = bar\n", serialize(&ast));
}
}