diff --git a/fluent.syntax/src/main/kotlin/org/projectfluent/syntax/serializer/Serializer.kt b/fluent.syntax/src/main/kotlin/org/projectfluent/syntax/serializer/Serializer.kt index a021851..007a4ff 100644 --- a/fluent.syntax/src/main/kotlin/org/projectfluent/syntax/serializer/Serializer.kt +++ b/fluent.syntax/src/main/kotlin/org/projectfluent/syntax/serializer/Serializer.kt @@ -2,11 +2,33 @@ package org.projectfluent.syntax.serializer import org.projectfluent.syntax.ast.* // ktlint-disable no-wildcard-imports -private fun indent(content: CharSequence) = content.split("\n").joinToString("\n ") +private fun indentExceptFirstLine(content: CharSequence) = + content.split("\n").joinToString("\n ") + +private fun PatternElement.includesLine() = + this is TextElement && value.contains("\n") + +private fun PatternElement.isSelectExpr() = + this is Placeable && expression is SelectExpression + +private fun Pattern.shouldStartOnNewLine(): Boolean { + val isMultiline = this.elements.any { it.isSelectExpr() || it.includesLine() } + if (isMultiline) { + val firstElement = this.elements.elementAtOrNull(0) + if (firstElement is TextElement) { + val firstChar = firstElement.value.elementAtOrNull(0) + // Due to the indentation requirement the following characters may not appear + // as the first character on a new line. + if (firstChar == '[' || firstChar == '.' || firstChar == '*') { + return false + } + } -private fun PatternElement.includesLine() = this is TextElement && value.contains("\n") + return true + } -private fun PatternElement.isSelectExpr() = this is Placeable && expression is SelectExpression + return false +} /** * Serialize Fluent nodes to `CharSequence`. @@ -117,16 +139,15 @@ class FluentSerializer(private val withJunk: Boolean = false) { } private fun serializeAttribute(attribute: Attribute): CharSequence { - val value = indent(serializePattern(attribute.value)) + val value = indentExceptFirstLine(serializePattern(attribute.value)) return "\n .${attribute.id.name} =$value" } private fun serializePattern(pattern: Pattern): CharSequence { - val startOnLine = pattern.elements.any { it.isSelectExpr() || it.includesLine() } val elements = pattern.elements.map(::serializeElement) - val content = indent(elements.joinToString("")) + val content = indentExceptFirstLine(elements.joinToString("")) - return if (startOnLine) { + return if (pattern.shouldStartOnNewLine()) { "\n $content" } else { " $content" @@ -187,7 +208,7 @@ class FluentSerializer(private val withJunk: Boolean = false) { private fun serializeVariant(variant: Variant): CharSequence { val key = serializeVariantKey(variant.key) - val value = indent(serializePattern(variant.value)) + val value = indentExceptFirstLine(serializePattern(variant.value)) return if (variant.default) { "\n *[$key]$value" diff --git a/fluent.syntax/src/test/kotlin/org/projectfluent/syntax/serializer/SerializeResourceTest.kt b/fluent.syntax/src/test/kotlin/org/projectfluent/syntax/serializer/SerializeResourceTest.kt index e99d42e..1135c2d 100644 --- a/fluent.syntax/src/test/kotlin/org/projectfluent/syntax/serializer/SerializeResourceTest.kt +++ b/fluent.syntax/src/test/kotlin/org/projectfluent/syntax/serializer/SerializeResourceTest.kt @@ -202,6 +202,35 @@ class SerializeResourceTest { assertEquals(input, this.pretty(input)) } + @Test + fun multiline_starting_inline() { + val input = + """ + foo = Foo + Baz + + """.trimIndent() + val output = + """ + foo = + Foo + Baz + + """.trimIndent() + assertEquals(output, this.pretty(input)) + } + + @Test + fun multiline_starting_inline_with_a_special_char() { + val input = + """ + foo = *Foo + Baz + + """.trimIndent() + assertEquals(input, this.pretty(input)) + } + @Test fun multiline_with_placeable() { val input = @@ -378,6 +407,19 @@ class SerializeResourceTest { assertEquals(expected, this.pretty(input)) } + @Test + fun select_expression_in_inline_pattern_starting_with_a_special_char() { + val input = + """ + foo = .Foo { ${'$'}sel -> + *[a] A + [b] B + } + + """.trimIndent() + assertEquals(input, this.pretty(input)) + } + @Test fun select_expression_in_multiline_pattern() { val input = diff --git a/fluent.syntax/src/test/resources/reference_fixtures/special_chars.ftl b/fluent.syntax/src/test/resources/reference_fixtures/special_chars.ftl new file mode 100644 index 0000000..5224bad --- /dev/null +++ b/fluent.syntax/src/test/resources/reference_fixtures/special_chars.ftl @@ -0,0 +1,14 @@ +## OK + +bracket-inline = [Value] +dot-inline = .Value +star-inline = *Value + +## ERRORS + +bracket-newline = + [Value] +dot-newline = + .Value +star-newline = + *Value diff --git a/fluent.syntax/src/test/resources/reference_fixtures/special_chars.json b/fluent.syntax/src/test/resources/reference_fixtures/special_chars.json new file mode 100644 index 0000000..77f2ff6 --- /dev/null +++ b/fluent.syntax/src/test/resources/reference_fixtures/special_chars.json @@ -0,0 +1,82 @@ +{ + "type": "Resource", + "body": [ + { + "type": "GroupComment", + "content": "OK" + }, + { + "type": "Message", + "id": { + "type": "Identifier", + "name": "bracket-inline" + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "TextElement", + "value": "[Value]" + } + ] + }, + "attributes": [], + "comment": null + }, + { + "type": "Message", + "id": { + "type": "Identifier", + "name": "dot-inline" + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "TextElement", + "value": ".Value" + } + ] + }, + "attributes": [], + "comment": null + }, + { + "type": "Message", + "id": { + "type": "Identifier", + "name": "star-inline" + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "TextElement", + "value": "*Value" + } + ] + }, + "attributes": [], + "comment": null + }, + { + "type": "GroupComment", + "content": "ERRORS" + }, + { + "type": "Junk", + "annotations": [], + "content": "bracket-newline =\n [Value]\n" + }, + { + "type": "Junk", + "annotations": [], + "content": "dot-newline =\n .Value\n" + }, + { + "type": "Junk", + "annotations": [], + "content": "star-newline =\n *Value\n" + } + ] +}