diff --git a/spec/fluent.ebnf b/spec/fluent.ebnf index 6a3682b..46789b0 100644 --- a/spec/fluent.ebnf +++ b/spec/fluent.ebnf @@ -1,31 +1,35 @@ /* An FTL file defines a Resource. */ Resource ::= (Entry | blank_block | junk_line)* -Entry ::= (Message line_end) - | (Term line_end) - | (ResourceComment | GroupComment | Comment) -Message ::= Comment? Identifier blank_inline? "=" blank_inline? ((Pattern Attribute*) | (Attribute+)) -Term ::= Comment? TermIdentifier blank_inline? "=" blank_inline? Value Attribute* -Comment ::= ("#" ("\u0020" /.*/)? line_end)+ -GroupComment ::= ("##" ("\u0020" /.*/)? line_end)+ -ResourceComment ::= ("###" ("\u0020" /.*/)? line_end)+ +Entry ::= Message + | Term + | CommentLine +Message ::= Identifier blank_inline? "=" ((Pattern Attribute*) | (Attribute+)) +Term ::= TermIdentifier blank_inline? "=" Value Attribute* +CommentLine ::= ("###" | "##" | "#") ("\u0020" /.*/)? line_end /* Adjacent junk_lines should be joined into FTL.Junk during the AST construction. */ junk_line ::= /.*/ line_end /* Attributes of Messages and Terms. */ -Attribute ::= line_end blank? "." Identifier blank_inline? "=" blank_inline? Pattern +Attribute ::= blank? "." Identifier blank_inline? "=" Pattern /* Value types: Pattern and VariantList. */ -Value ::= Pattern - | VariantList -Pattern ::= PatternElement+ +Value ::= VariantList + | Pattern VariantList ::= blank? "{" variant_list blank? "}" +Pattern ::= (PatternInline PatternBlock) + | PatternInline + | PatternBlock +PatternInline ::= blank_inline? PatternElement+ line_end +PatternBlock ::= blank_block* pattern_line (pattern_line | blank_block)* +pattern_line ::= PatternStartElement PatternElement* line_end PatternElement ::= TextElement | Placeable - | (blank_block blank_inline? Placeable) -TextElement ::= (text_char | text_cont)+ +PatternStartElement ::= (blank_inline text_start) + | (blank_inline? Placeable) +TextElement ::= text_char+ Placeable ::= "{" blank? (SelectExpression | InlineExpression) blank? "}" /* Rules for validating expressions in Placeables and as selectors of @@ -58,10 +62,10 @@ AttributeExpression ::= (MessageReference | TermReference) "." Identifier VariantExpression ::= TermReference VariantKey /* Block Expressions */ -SelectExpression ::= InlineExpression blank? "->" blank_inline? variant_list -variant_list ::= Variant* DefaultVariant Variant* line_end -Variant ::= line_end blank? VariantKey blank_inline? Value -DefaultVariant ::= line_end blank? "*" VariantKey blank_inline? Value +SelectExpression ::= InlineExpression blank? "->" variant_list +variant_list ::= blank_block Variant* DefaultVariant Variant* +Variant ::= blank? VariantKey Value +DefaultVariant ::= blank? "*" VariantKey Value VariantKey ::= "[" blank? (NumberLiteral | VariantName) blank? "]" VariantName ::= word (blank word)* @@ -89,7 +93,7 @@ text_char ::= blank_inline | (backslash backslash) | (backslash "{") | (regular_char - "{" - backslash) -text_cont ::= blank_block blank_inline (text_char - "}" - "[" - "*" - ".") +text_start ::= text_char - "}" - "[" - "*" - "." quoted_text_char ::= (text_char - quote) | (backslash quote) digit ::= [0-9] diff --git a/syntax/abstract.mjs b/syntax/abstract.mjs index b73dad6..ed993e9 100644 --- a/syntax/abstract.mjs +++ b/syntax/abstract.mjs @@ -10,6 +10,19 @@ import {always, never} from "../lib/combinators.mjs"; export function list_into(Type) { switch (Type) { + case FTL.BaseComment: + return ([sigil, content = ""]) => { + switch (sigil) { + case "#": + return always(new FTL.Comment(content)); + case "##": + return always(new FTL.GroupComment(content)); + case "###": + return always(new FTL.ResourceComment(content)); + default: + return never(`Unknown comment sigil: ${sigil}`); + } + }; case FTL.CallExpression: return ([callee, args]) => { let positional_args = []; @@ -32,9 +45,6 @@ export function list_into(Type) { let named_args = Array.from(named_map.values()); return always(new Type(callee, positional_args, named_args)); }; - case FTL.Message: - return ([comment, ...args]) => - always(new Type(...args, comment)); case FTL.Pattern: return elements => always(new FTL.Pattern( @@ -46,7 +56,12 @@ export function list_into(Type) { return entries => always(new FTL.Resource( entries - .reduce(join_adjacent(FTL.Junk), []) + .reduce(join_adjacent( + FTL.Junk, + FTL.Comment, + FTL.GroupComment, + FTL.ResourceComment), []) + .reduce(attach_comments, []) .filter(remove_blank_lines))); case FTL.SelectExpression: return ([selector, variants]) => { @@ -68,9 +83,6 @@ export function list_into(Type) { } return always(new Type(selector, variants)); }; - case FTL.Term: - return ([comment, ...args]) => - always(new Type(...args, comment)); case FTL.VariantList: return ([variants]) => always(new Type(variants)); @@ -82,21 +94,6 @@ export function list_into(Type) { export function into(Type) { switch (Type) { - case FTL.Comment: - case FTL.GroupComment: - case FTL.ResourceComment: - return content => { - if (!content.endsWith("\n")) { - // The comment ended with the EOF; don't trim it. - return always(new Type(content)); - } - if (content.endsWith("\r\n")) { - // Trim the CRLF from the end of the comment. - return always(new Type(content.slice(0, -2))); - } - // Trim the LF from the end of the comment. - return always(new Type(content.slice(0, -1))); - }; case FTL.Placeable: return expression => { let invalid_expression_found = @@ -108,21 +105,29 @@ export function into(Type) { } return always(new Type(expression)); }; + case FTL.TextElement: + return value => { + if (value === Symbol.for("eof")) { + return always(new Type("")); + } + return always(new Type(value)); + }; default: return (...args) => always(new Type(...args)); } } -function join_adjacent(Type) { +function join_adjacent(...types) { return function(acc, cur) { let prev = acc[acc.length - 1]; - if (prev instanceof Type && cur instanceof Type) { - join_of_type(Type, prev, cur); - return acc; - } else { - return acc.concat(cur); + for (let Type of types) { + if (prev instanceof Type && cur instanceof Type) { + join_of_type(Type, prev, cur); + return acc; + } } + return acc.concat(cur); }; } @@ -132,12 +137,30 @@ function join_of_type(Type, ...elements) { case FTL.TextElement: return elements.reduce((a, b) => (a.value += b.value, a)); + case FTL.Comment: + case FTL.GroupComment: + case FTL.ResourceComment: + return elements.reduce((a, b) => + (a.content += `\n${b.content}`, a)); case FTL.Junk: return elements.reduce((a, b) => (a.content += b.content, a)); } } +function attach_comments(acc, cur) { + let prev = acc[acc.length - 1]; + if (prev instanceof FTL.Comment + && (cur instanceof FTL.Message + || cur instanceof FTL.Term)) { + cur.comment = prev; + acc[acc.length - 1] = cur; + return acc; + } else { + return acc.concat(cur); + } +} + function trim_text_at_extremes(element, index, array) { if (element instanceof FTL.TextElement) { if (index === 0) { @@ -151,6 +174,7 @@ function trim_text_at_extremes(element, index, array) { } function remove_empty_text(element) { + // Keep Placeables and non-empty TextElements. return !(element instanceof FTL.TextElement) || element.value !== ""; } diff --git a/syntax/grammar.mjs b/syntax/grammar.mjs index dcbbc84..99281ab 100644 --- a/syntax/grammar.mjs +++ b/syntax/grammar.mjs @@ -22,24 +22,15 @@ let Resource = defer(() => export let Entry = defer(() => either( - sequence( - Message, - line_end).map(element_at(0)), - sequence( - Term, - line_end).map(element_at(0)), - either( - ResourceComment, - GroupComment, - Comment))); + Message, + Term, + CommentLine)); let Message = defer(() => sequence( - maybe(Comment).abstract, Identifier.abstract, maybe(blank_inline), string("="), - maybe(blank_inline), either( sequence( Pattern.abstract, @@ -53,57 +44,28 @@ let Message = defer(() => let Term = defer(() => sequence( - maybe(Comment).abstract, TermIdentifier.abstract, maybe(blank_inline), string("="), - maybe(blank_inline), Value.abstract, repeat(Attribute).abstract) .map(keep_abstract) .chain(list_into(FTL.Term))); -let Comment = defer(() => - repeat1( - sequence( - string("#"), - maybe( - sequence( - string(" "), - regex(/.*/).abstract)), - line_end.abstract)) - .map(flatten(2)) - .map(keep_abstract) - .map(join) - .chain(into(FTL.Comment))); - -let GroupComment = defer(() => - repeat1( - sequence( - string("##"), - maybe( - sequence( - string(" "), - regex(/.*/).abstract)), - line_end.abstract)) - .map(flatten(2)) - .map(keep_abstract) - .map(join) - .chain(into(FTL.GroupComment))); - -let ResourceComment = defer(() => - repeat1( - sequence( +let CommentLine = defer(() => + sequence( + either( string("###"), - maybe( - sequence( - string(" "), - regex(/.*/).abstract)), - line_end.abstract)) - .map(flatten(2)) + string("##"), + string("#")).abstract, + maybe( + sequence( + string(" "), + regex(/.*/).abstract)), + line_end) + .map(flatten(1)) .map(keep_abstract) - .map(join) - .chain(into(FTL.ResourceComment))); + .chain(list_into(FTL.BaseComment))); /* ----------------------------------------------------------------- */ /* Adjacent junk_lines should be joined into FTL.Junk during the AST @@ -119,13 +81,11 @@ let junk_line = defer(() => /* Attributes of Messages and Terms. */ let Attribute = defer(() => sequence( - line_end, maybe(blank), string("."), Identifier.abstract, maybe(blank_inline), string("="), - maybe(blank_inline), Pattern.abstract) .map(keep_abstract) .chain(list_into(FTL.Attribute))); @@ -134,15 +94,8 @@ let Attribute = defer(() => /* Value types: Pattern and VariantList. */ let Value = defer(() => either( - Pattern, - VariantList)); - -let Pattern = defer(() => - repeat1( - PatternElement) - // Flatten indented Placeables. - .map(flatten(1)) - .chain(list_into(FTL.Pattern))); + VariantList, + Pattern)); let VariantList = defer(() => sequence( @@ -154,22 +107,60 @@ let VariantList = defer(() => .map(keep_abstract) .chain(list_into(FTL.VariantList))); +let Pattern = defer(() => + either( + sequence( + PatternInline, + PatternBlock).map(flatten(1)), + PatternInline, + PatternBlock) + .chain(list_into(FTL.Pattern))); + +let PatternInline = defer(() => + sequence( + maybe(blank_inline), + repeat1(PatternElement.abstract), + line_end.chain(into(FTL.TextElement)).abstract) + .map(flatten(1)) + .map(keep_abstract)); + +let PatternBlock = defer(() => + sequence( + repeat(blank_block), + pattern_line.abstract, + repeat( + either( + pattern_line, + blank_block.chain(into(FTL.TextElement))).abstract)) + .map(flatten(1)) + .map(keep_abstract) + .map(flatten(1))); + +let pattern_line = defer(() => + sequence( + PatternStartElement, + repeat(PatternElement), + line_end.chain(into(FTL.TextElement))) + .map(flatten(1))); + let PatternElement = defer(() => either( TextElement, - Placeable, + Placeable)); + +let PatternStartElement = defer(() => + either( + sequence( + blank_inline, + text_start.chain(into(FTL.TextElement))), sequence( - // Joined with preceding TextElements during AST construction. - blank_block.chain(into(FTL.TextElement)).abstract, maybe(blank_inline), - Placeable.abstract) - .map(keep_abstract))); + Placeable)) + .map(element_at(1))); let TextElement = defer(() => repeat1( - either( - text_char, - text_cont)) + text_char) .map(join) .chain(into(FTL.TextElement))); @@ -301,37 +292,32 @@ let SelectExpression = defer(() => InlineExpression.abstract, maybe(blank), string("->"), - maybe(blank_inline), variant_list.abstract) .map(keep_abstract) .chain(list_into(FTL.SelectExpression))); let variant_list = defer(() => sequence( + blank_block, repeat(Variant).abstract, DefaultVariant.abstract, - repeat(Variant).abstract, - line_end) + repeat(Variant).abstract) .map(keep_abstract) .map(flatten(1))); let Variant = defer(() => sequence( - line_end, maybe(blank), VariantKey.abstract, - maybe(blank_inline), Value.abstract) .map(keep_abstract) .chain(list_into(FTL.Variant))); let DefaultVariant = defer(() => sequence( - line_end, maybe(blank), string("*"), VariantKey.abstract, - maybe(blank_inline), Value.abstract) .map(keep_abstract) .chain(list_into(FTL.Variant)) @@ -439,18 +425,13 @@ let text_char = defer(() => not(string("{")), regular_char))); -let text_cont = defer(() => - sequence( - blank_block.abstract, - blank_inline, - and( - not(string(".")), - not(string("*")), - not(string("[")), - not(string("}")), - text_char).abstract) - .map(keep_abstract) - .map(join)); +let text_start = defer(() => + and( + not(string(".")), + not(string("*")), + not(string("[")), + not(string("}")), + text_char)); let quoted_text_char = either(