diff --git a/fluent-syntax/src/errors.js b/fluent-syntax/src/errors.js index 1cc8f1c57..9b8154fc5 100644 --- a/fluent-syntax/src/errors.js +++ b/fluent-syntax/src/errors.js @@ -33,7 +33,7 @@ function getErrorMessage(code, args) { case "E0007": return "Keyword cannot end with a whitespace"; case "E0008": - return "The callee has to be a simple, upper-case identifier"; + return "The callee has to be an upper-case identifier or a term"; case "E0009": return "The key has to be a simple identifier"; case "E0010": diff --git a/fluent-syntax/src/parser.js b/fluent-syntax/src/parser.js index 61a06d7c7..5ad68c96f 100644 --- a/fluent-syntax/src/parser.js +++ b/fluent-syntax/src/parser.js @@ -678,11 +678,18 @@ export default class FluentParser { const variants = this.getVariants(ps, {allowVariantList: false}); return new AST.SelectExpression(selector, variants); - } else if (selector.type === "AttributeExpression" + } + + if (selector.type === "AttributeExpression" && selector.ref.type === "TermReference") { throw new ParseError("E0019"); } + if (selector.type === "CallExpression" + && selector.callee.type === "AttributeExpression") { + throw new ParseError("E0019"); + } + return selector; } @@ -691,7 +698,7 @@ export default class FluentParser { return this.getPlaceable(ps); } - const selector = this.getLiteral(ps); + let selector = this.getLiteral(ps); switch (selector.type) { case "StringLiteral": case "NumberLiteral": @@ -699,12 +706,6 @@ export default class FluentParser { return selector; } - if (ps.currentChar === ".") { - ps.next(); - const attr = this.getIdentifier(ps); - return new AST.AttributeExpression(selector, attr); - } - if (ps.currentChar === "[") { ps.next(); @@ -717,6 +718,12 @@ export default class FluentParser { return new AST.VariantExpression(selector, key); } + if (ps.currentChar === ".") { + ps.next(); + const attr = this.getIdentifier(ps); + selector = new AST.AttributeExpression(selector, attr); + } + if (ps.currentChar === "(") { ps.next(); @@ -733,6 +740,11 @@ export default class FluentParser { } } + if (selector.type === "AttributeExpression" + && selector.ref.type === "MessageReference") { + throw new ParseError("E0008"); + } + const args = this.getCallArgs(ps); ps.expectChar(")"); diff --git a/fluent-syntax/test/fixtures_reference/call_expressions.ftl b/fluent-syntax/test/fixtures_reference/call_expressions.ftl index a4f61da40..19e76b7e2 100644 --- a/fluent-syntax/test/fixtures_reference/call_expressions.ftl +++ b/fluent-syntax/test/fixtures_reference/call_expressions.ftl @@ -1,16 +1,3 @@ -## Callees - -function-callee = {FUNCTION()} -term-callee = {-term()} - -# ERROR Equivalent to a MessageReference callee. -mixed-case-callee = {Function()} - -# ERROR MessageReference is not a valid callee. -message-callee = {message()} -# ERROR VariableReference is not a valid callee. -variable-callee = {$variable()} - ## Arguments positional-args = {FUN(1, "a", msg)} diff --git a/fluent-syntax/test/fixtures_reference/call_expressions.json b/fluent-syntax/test/fixtures_reference/call_expressions.json index 286ea4f39..4dd734831 100644 --- a/fluent-syntax/test/fixtures_reference/call_expressions.json +++ b/fluent-syntax/test/fixtures_reference/call_expressions.json @@ -1,95 +1,6 @@ { "type": "Resource", "body": [ - { - "type": "GroupComment", - "content": "Callees" - }, - { - "type": "Message", - "id": { - "type": "Identifier", - "name": "function-callee" - }, - "value": { - "type": "Pattern", - "elements": [ - { - "type": "Placeable", - "expression": { - "type": "CallExpression", - "callee": { - "type": "FunctionReference", - "id": { - "type": "Identifier", - "name": "FUNCTION" - } - }, - "positional": [], - "named": [] - } - } - ] - }, - "attributes": [], - "comment": null - }, - { - "type": "Message", - "id": { - "type": "Identifier", - "name": "term-callee" - }, - "value": { - "type": "Pattern", - "elements": [ - { - "type": "Placeable", - "expression": { - "type": "CallExpression", - "callee": { - "type": "TermReference", - "id": { - "type": "Identifier", - "name": "term" - } - }, - "positional": [], - "named": [] - } - } - ] - }, - "attributes": [], - "comment": null - }, - { - "type": "Comment", - "content": "ERROR Equivalent to a MessageReference callee." - }, - { - "type": "Junk", - "annotations": [], - "content": "mixed-case-callee = {Function()}\n\n" - }, - { - "type": "Comment", - "content": "ERROR MessageReference is not a valid callee." - }, - { - "type": "Junk", - "annotations": [], - "content": "message-callee = {message()}\n" - }, - { - "type": "Comment", - "content": "ERROR VariableReference is not a valid callee." - }, - { - "type": "Junk", - "annotations": [], - "content": "variable-callee = {$variable()}\n\n" - }, { "type": "GroupComment", "content": "Arguments" diff --git a/fluent-syntax/test/fixtures_reference/callee_expressions.ftl b/fluent-syntax/test/fixtures_reference/callee_expressions.ftl new file mode 100644 index 000000000..637a2e4d7 --- /dev/null +++ b/fluent-syntax/test/fixtures_reference/callee_expressions.ftl @@ -0,0 +1,46 @@ +## Callees in placeables. + +function-callee-placeable = {FUNCTION()} +term-callee-placeable = {-term()} + +# ERROR Messages cannot be parameterized. +message-callee-placeable = {message()} +# ERROR Equivalent to a MessageReference callee. +mixed-case-callee-placeable = {Function()} +# ERROR Message attributes cannot be parameterized. +message-attr-callee-placeable = {message.attr()} +# ERROR Term attributes may not be used in Placeables. +term-attr-callee-placeable = {-term.attr()} +# ERROR Variables cannot be parameterized. +variable-callee-placeable = {$variable()} + + +## Callees in selectors. + +function-callee-selector = {FUNCTION() -> + *[key] Value +} +term-attr-callee-selector = {-term.attr() -> + *[key] Value +} + +# ERROR Messages cannot be parameterized. +message-callee-selector = {message() -> + *[key] Value +} +# ERROR Equivalent to a MessageReference callee. +mixed-case-callee-selector = {Function() -> + *[key] Value +} +# ERROR Message attributes cannot be parameterized. +message-attr-callee-selector = {message.attr() -> + *[key] Value +} +# ERROR Term values may not be used as selectors. +term-callee-selector = {-term() -> + *[key] Value +} +# ERROR Variables cannot be parameterized. +variable-callee-selector = {$variable() -> + *[key] Value +} diff --git a/fluent-syntax/test/fixtures_reference/callee_expressions.json b/fluent-syntax/test/fixtures_reference/callee_expressions.json new file mode 100644 index 000000000..50cdaeb41 --- /dev/null +++ b/fluent-syntax/test/fixtures_reference/callee_expressions.json @@ -0,0 +1,270 @@ +{ + "type": "Resource", + "body": [ + { + "type": "GroupComment", + "content": "Callees in placeables." + }, + { + "type": "Message", + "id": { + "type": "Identifier", + "name": "function-callee-placeable" + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "Placeable", + "expression": { + "type": "CallExpression", + "callee": { + "type": "FunctionReference", + "id": { + "type": "Identifier", + "name": "FUNCTION" + } + }, + "positional": [], + "named": [] + } + } + ] + }, + "attributes": [], + "comment": null + }, + { + "type": "Message", + "id": { + "type": "Identifier", + "name": "term-callee-placeable" + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "Placeable", + "expression": { + "type": "CallExpression", + "callee": { + "type": "TermReference", + "id": { + "type": "Identifier", + "name": "term" + } + }, + "positional": [], + "named": [] + } + } + ] + }, + "attributes": [], + "comment": null + }, + { + "type": "Comment", + "content": "ERROR Messages cannot be parameterized." + }, + { + "type": "Junk", + "annotations": [], + "content": "message-callee-placeable = {message()}\n" + }, + { + "type": "Comment", + "content": "ERROR Equivalent to a MessageReference callee." + }, + { + "type": "Junk", + "annotations": [], + "content": "mixed-case-callee-placeable = {Function()}\n" + }, + { + "type": "Comment", + "content": "ERROR Message attributes cannot be parameterized." + }, + { + "type": "Junk", + "annotations": [], + "content": "message-attr-callee-placeable = {message.attr()}\n" + }, + { + "type": "Comment", + "content": "ERROR Term attributes may not be used in Placeables." + }, + { + "type": "Junk", + "annotations": [], + "content": "term-attr-callee-placeable = {-term.attr()}\n" + }, + { + "type": "Comment", + "content": "ERROR Variables cannot be parameterized." + }, + { + "type": "Junk", + "annotations": [], + "content": "variable-callee-placeable = {$variable()}\n\n\n" + }, + { + "type": "GroupComment", + "content": "Callees in selectors." + }, + { + "type": "Message", + "id": { + "type": "Identifier", + "name": "function-callee-selector" + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "Placeable", + "expression": { + "type": "SelectExpression", + "selector": { + "type": "CallExpression", + "callee": { + "type": "FunctionReference", + "id": { + "type": "Identifier", + "name": "FUNCTION" + } + }, + "positional": [], + "named": [] + }, + "variants": [ + { + "type": "Variant", + "key": { + "type": "Identifier", + "name": "key" + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "TextElement", + "value": "Value" + } + ] + }, + "default": true + } + ] + } + } + ] + }, + "attributes": [], + "comment": null + }, + { + "type": "Message", + "id": { + "type": "Identifier", + "name": "term-attr-callee-selector" + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "Placeable", + "expression": { + "type": "SelectExpression", + "selector": { + "type": "CallExpression", + "callee": { + "type": "AttributeExpression", + "ref": { + "type": "TermReference", + "id": { + "type": "Identifier", + "name": "term" + } + }, + "name": { + "type": "Identifier", + "name": "attr" + } + }, + "positional": [], + "named": [] + }, + "variants": [ + { + "type": "Variant", + "key": { + "type": "Identifier", + "name": "key" + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "TextElement", + "value": "Value" + } + ] + }, + "default": true + } + ] + } + } + ] + }, + "attributes": [], + "comment": null + }, + { + "type": "Comment", + "content": "ERROR Messages cannot be parameterized." + }, + { + "type": "Junk", + "annotations": [], + "content": "message-callee-selector = {message() ->\n *[key] Value\n}\n" + }, + { + "type": "Comment", + "content": "ERROR Equivalent to a MessageReference callee." + }, + { + "type": "Junk", + "annotations": [], + "content": "mixed-case-callee-selector = {Function() ->\n *[key] Value\n}\n" + }, + { + "type": "Comment", + "content": "ERROR Message attributes cannot be parameterized." + }, + { + "type": "Junk", + "annotations": [], + "content": "message-attr-callee-selector = {message.attr() ->\n *[key] Value\n}\n" + }, + { + "type": "Comment", + "content": "ERROR Term values may not be used as selectors." + }, + { + "type": "Junk", + "annotations": [], + "content": "term-callee-selector = {-term() ->\n *[key] Value\n}\n" + }, + { + "type": "Comment", + "content": "ERROR Variables cannot be parameterized." + }, + { + "type": "Junk", + "annotations": [], + "content": "variable-callee-selector = {$variable() ->\n *[key] Value\n}\n" + } + ] +} diff --git a/fluent-syntax/test/fixtures_reference/member_expressions.ftl b/fluent-syntax/test/fixtures_reference/member_expressions.ftl index 09e09f3fc..b4a93922b 100644 --- a/fluent-syntax/test/fixtures_reference/member_expressions.ftl +++ b/fluent-syntax/test/fixtures_reference/member_expressions.ftl @@ -1,6 +1,28 @@ -variant-expression = {-term[case]} -attribute-expression = {msg.attr} +## Member expressions in placeables. -## Invalid syntax -variant-expression = {msg[case]} -attribute-expression = {-term.attr} +message-attribute-expression-placeable = {msg.attr} +term-variant-expression-placeable = {-term[case]} + +# ERROR Message values cannot be VariantLists +message-variant-expression-placeable = {msg[case]} +# ERROR Term attributes may not be used for interpolation. +term-attribute-expression-placeable = {-term.attr} + +## Member expressions in selectors. + +term-attribute-expression-selector = {-term.attr -> + *[key] Value +} + +# ERROR Message attributes may not be used as selector. +message-attribute-expression-selector = {msg.attr -> + *[key] Value +} +# ERROR Term values may not be used as selector. +term-variant-expression-selector = {-term[case] -> + *[key] Value +} +# ERROR Message values cannot be VariantLists +message-variant-expression-selector = {msg[case] -> + *[key] Value +} diff --git a/fluent-syntax/test/fixtures_reference/member_expressions.json b/fluent-syntax/test/fixtures_reference/member_expressions.json index 541bd795a..f6890e56f 100644 --- a/fluent-syntax/test/fixtures_reference/member_expressions.json +++ b/fluent-syntax/test/fixtures_reference/member_expressions.json @@ -1,11 +1,15 @@ { "type": "Resource", "body": [ + { + "type": "GroupComment", + "content": "Member expressions in placeables." + }, { "type": "Message", "id": { "type": "Identifier", - "name": "variant-expression" + "name": "message-attribute-expression-placeable" }, "value": { "type": "Pattern", @@ -13,17 +17,17 @@ { "type": "Placeable", "expression": { - "type": "VariantExpression", + "type": "AttributeExpression", "ref": { - "type": "TermReference", + "type": "MessageReference", "id": { "type": "Identifier", - "name": "term" + "name": "msg" } }, - "key": { + "name": { "type": "Identifier", - "name": "case" + "name": "attr" } } } @@ -36,7 +40,7 @@ "type": "Message", "id": { "type": "Identifier", - "name": "attribute-expression" + "name": "term-variant-expression-placeable" }, "value": { "type": "Pattern", @@ -44,17 +48,17 @@ { "type": "Placeable", "expression": { - "type": "AttributeExpression", + "type": "VariantExpression", "ref": { - "type": "MessageReference", + "type": "TermReference", "id": { "type": "Identifier", - "name": "msg" + "name": "term" } }, - "name": { + "key": { "type": "Identifier", - "name": "attr" + "name": "case" } } } @@ -63,19 +67,107 @@ "attributes": [], "comment": null }, + { + "type": "Comment", + "content": "ERROR Message values cannot be VariantLists" + }, + { + "type": "Junk", + "annotations": [], + "content": "message-variant-expression-placeable = {msg[case]}\n" + }, + { + "type": "Comment", + "content": "ERROR Term attributes may not be used for interpolation." + }, + { + "type": "Junk", + "annotations": [], + "content": "term-attribute-expression-placeable = {-term.attr}\n\n" + }, { "type": "GroupComment", - "content": "Invalid syntax" + "content": "Member expressions in selectors." + }, + { + "type": "Message", + "id": { + "type": "Identifier", + "name": "term-attribute-expression-selector" + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "Placeable", + "expression": { + "type": "SelectExpression", + "selector": { + "type": "AttributeExpression", + "ref": { + "type": "TermReference", + "id": { + "type": "Identifier", + "name": "term" + } + }, + "name": { + "type": "Identifier", + "name": "attr" + } + }, + "variants": [ + { + "type": "Variant", + "key": { + "type": "Identifier", + "name": "key" + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "TextElement", + "value": "Value" + } + ] + }, + "default": true + } + ] + } + } + ] + }, + "attributes": [], + "comment": null + }, + { + "type": "Comment", + "content": "ERROR Message attributes may not be used as selector." }, { "type": "Junk", "annotations": [], - "content": "variant-expression = {msg[case]}\n" + "content": "message-attribute-expression-selector = {msg.attr ->\n *[key] Value\n}\n" + }, + { + "type": "Comment", + "content": "ERROR Term values may not be used as selector." + }, + { + "type": "Junk", + "annotations": [], + "content": "term-variant-expression-selector = {-term[case] ->\n *[key] Value\n}\n" + }, + { + "type": "Comment", + "content": "ERROR Message values cannot be VariantLists" }, { "type": "Junk", "annotations": [], - "content": "attribute-expression = {-term.attr}\n" + "content": "message-variant-expression-selector = {msg[case] ->\n *[key] Value\n}\n" } ] } diff --git a/fluent-syntax/test/fixtures_reference/messages.ftl b/fluent-syntax/test/fixtures_reference/messages.ftl index 0ade3cace..00b4ab468 100644 --- a/fluent-syntax/test/fixtures_reference/messages.ftl +++ b/fluent-syntax/test/fixtures_reference/messages.ftl @@ -25,3 +25,5 @@ key07 = # JUNK Missing = key08 + +KEY09 = Value 09 diff --git a/fluent-syntax/test/fixtures_reference/messages.json b/fluent-syntax/test/fixtures_reference/messages.json index 7d779d3e2..cdbe5c93c 100644 --- a/fluent-syntax/test/fixtures_reference/messages.json +++ b/fluent-syntax/test/fixtures_reference/messages.json @@ -243,7 +243,25 @@ { "type": "Junk", "annotations": [], - "content": "key08\n" + "content": "key08\n\n" + }, + { + "type": "Message", + "id": { + "type": "Identifier", + "name": "KEY09" + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "TextElement", + "value": "Value 09" + } + ] + }, + "attributes": [], + "comment": null } ] } diff --git a/fluent-syntax/test/fixtures_reference/reference_expressions.ftl b/fluent-syntax/test/fixtures_reference/reference_expressions.ftl index ab27cbe09..9c2e9c543 100644 --- a/fluent-syntax/test/fixtures_reference/reference_expressions.ftl +++ b/fluent-syntax/test/fixtures_reference/reference_expressions.ftl @@ -1,5 +1,28 @@ -message-reference = {msg} -term-reference = {-term} -variable-reference = {$var} +## Reference expressions in placeables. -not-call-expression = {FUN} +message-reference-placeable = {msg} +term-reference-placeable = {-term} +variable-reference-placeable = {$var} + +# ERROR Function references are invalid outside of call expressions. +function-reference-placeable = {FUN} + + +## Reference expressions in selectors. + +variable-reference-selector = {$var -> + *[key] Value +} + +# ERROR Message values may not be used as selectors. +message-reference-selector = {msg -> + *[key] Value +} +# ERROR Term values may not be used as selectors. +term-reference-selector = {-term -> + *[key] Value +} +# ERROR Function references are invalid outside of call expressions. +function-expression-selector = {FUN -> + *[key] Value +} diff --git a/fluent-syntax/test/fixtures_reference/reference_expressions.json b/fluent-syntax/test/fixtures_reference/reference_expressions.json index 47e4dfab0..65c9d4cc2 100644 --- a/fluent-syntax/test/fixtures_reference/reference_expressions.json +++ b/fluent-syntax/test/fixtures_reference/reference_expressions.json @@ -1,11 +1,15 @@ { "type": "Resource", "body": [ + { + "type": "GroupComment", + "content": "Reference expressions in placeables." + }, { "type": "Message", "id": { "type": "Identifier", - "name": "message-reference" + "name": "message-reference-placeable" }, "value": { "type": "Pattern", @@ -29,7 +33,7 @@ "type": "Message", "id": { "type": "Identifier", - "name": "term-reference" + "name": "term-reference-placeable" }, "value": { "type": "Pattern", @@ -53,7 +57,7 @@ "type": "Message", "id": { "type": "Identifier", - "name": "variable-reference" + "name": "variable-reference-placeable" }, "value": { "type": "Pattern", @@ -77,7 +81,7 @@ "type": "Message", "id": { "type": "Identifier", - "name": "not-call-expression" + "name": "function-reference-placeable" }, "value": { "type": "Pattern", @@ -95,7 +99,87 @@ ] }, "attributes": [], + "comment": { + "type": "Comment", + "content": "ERROR Function references are invalid outside of call expressions." + } + }, + { + "type": "GroupComment", + "content": "Reference expressions in selectors." + }, + { + "type": "Message", + "id": { + "type": "Identifier", + "name": "variable-reference-selector" + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "Placeable", + "expression": { + "type": "SelectExpression", + "selector": { + "type": "VariableReference", + "id": { + "type": "Identifier", + "name": "var" + } + }, + "variants": [ + { + "type": "Variant", + "key": { + "type": "Identifier", + "name": "key" + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "TextElement", + "value": "Value" + } + ] + }, + "default": true + } + ] + } + } + ] + }, + "attributes": [], "comment": null + }, + { + "type": "Comment", + "content": "ERROR Message values may not be used as selectors." + }, + { + "type": "Junk", + "annotations": [], + "content": "message-reference-selector = {msg ->\n *[key] Value\n}\n" + }, + { + "type": "Comment", + "content": "ERROR Term values may not be used as selectors." + }, + { + "type": "Junk", + "annotations": [], + "content": "term-reference-selector = {-term ->\n *[key] Value\n}\n" + }, + { + "type": "Comment", + "content": "ERROR Function references are invalid outside of call expressions." + }, + { + "type": "Junk", + "annotations": [], + "content": "function-expression-selector = {FUN ->\n *[key] Value\n}\n" } ] } diff --git a/fluent-syntax/test/fixtures_structure/select_expressions.ftl b/fluent-syntax/test/fixtures_structure/select_expressions.ftl new file mode 100644 index 000000000..5a96bf906 --- /dev/null +++ b/fluent-syntax/test/fixtures_structure/select_expressions.ftl @@ -0,0 +1,9 @@ +# ERROR No blanks are allowed between * and [. +err01 = { $sel -> + * [key] Value +} + +# ERROR Missing default variant. +err02 = { $sel -> + [key] Value +} diff --git a/fluent-syntax/test/fixtures_structure/select_expressions.json b/fluent-syntax/test/fixtures_structure/select_expressions.json new file mode 100644 index 000000000..8cee7ae3b --- /dev/null +++ b/fluent-syntax/test/fixtures_structure/select_expressions.json @@ -0,0 +1,72 @@ +{ + "type": "Resource", + "body": [ + { + "type": "Comment", + "content": "ERROR No blanks are allowed between * and [.", + "span": { + "type": "Span", + "start": 0, + "end": 46 + } + }, + { + "type": "Junk", + "annotations": [ + { + "type": "Annotation", + "code": "E0011", + "args": [], + "message": "Expected at least one variant after \"->\"", + "span": { + "type": "Span", + "start": 69, + "end": 69 + } + } + ], + "content": "err01 = { $sel ->\n * [key] Value\n}\n\n", + "span": { + "type": "Span", + "start": 47, + "end": 87 + } + }, + { + "type": "Comment", + "content": "ERROR Missing default variant.", + "span": { + "type": "Span", + "start": 87, + "end": 119 + } + }, + { + "type": "Junk", + "annotations": [ + { + "type": "Annotation", + "code": "E0010", + "args": [], + "message": "Expected one of the variants to be marked as default (*)", + "span": { + "type": "Span", + "start": 154, + "end": 154 + } + } + ], + "content": "err02 = { $sel ->\n [key] Value\n}\n", + "span": { + "type": "Span", + "start": 120, + "end": 156 + } + } + ], + "span": { + "type": "Span", + "start": 0, + "end": 156 + } +} diff --git a/fluent-syntax/test/fixtures_structure/variant_keys.ftl b/fluent-syntax/test/fixtures_structure/variant_keys.ftl new file mode 100644 index 000000000..fc0f241ca --- /dev/null +++ b/fluent-syntax/test/fixtures_structure/variant_keys.ftl @@ -0,0 +1,61 @@ +key01 = { $sel -> + *[ + key + ] Value +} + +key02 = { $sel -> + *[ + key + ] + + Value +} + +err01 = { $sel -> + *["key"] Value +} + +err02 = { $sel -> + *[-key] Value +} + +err03 = { $sel -> + *[-key.attr] Value +} + +err04 = { $sel -> + *[-key()] Value +} + +err05 = { $sel -> + *[-key.attr()] Value +} + +err06 = { $sel -> + *[key.attr] Value +} + +err07 = { $sel -> + *[$key] Value +} + +err08 = { $sel -> + *[FUNC()] Value +} + +err09 = { $sel -> + *[{key}] Value +} + +err10 = { $sel -> + *[{"key"}] Value +} + +err11 = { $sel -> + *[{3.14}] Value +} + +err12 = { $sel -> + *[{$key}] Value +} diff --git a/fluent-syntax/test/fixtures_structure/variant_keys.json b/fluent-syntax/test/fixtures_structure/variant_keys.json new file mode 100644 index 000000000..0115ee51c --- /dev/null +++ b/fluent-syntax/test/fixtures_structure/variant_keys.json @@ -0,0 +1,500 @@ +{ + "type": "Resource", + "body": [ + { + "type": "Message", + "id": { + "type": "Identifier", + "name": "key01", + "span": { + "type": "Span", + "start": 0, + "end": 5 + } + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "Placeable", + "expression": { + "type": "SelectExpression", + "selector": { + "type": "VariableReference", + "id": { + "type": "Identifier", + "name": "sel", + "span": { + "type": "Span", + "start": 11, + "end": 14 + } + }, + "span": { + "type": "Span", + "start": 10, + "end": 14 + } + }, + "variants": [ + { + "type": "Variant", + "key": { + "type": "Identifier", + "name": "key", + "span": { + "type": "Span", + "start": 33, + "end": 36 + } + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "TextElement", + "value": "Value", + "span": { + "type": "Span", + "start": 43, + "end": 48 + } + } + ], + "span": { + "type": "Span", + "start": 43, + "end": 48 + } + }, + "default": true, + "span": { + "type": "Span", + "start": 22, + "end": 48 + } + } + ], + "span": { + "type": "Span", + "start": 10, + "end": 49 + } + }, + "span": { + "type": "Span", + "start": 8, + "end": 50 + } + } + ], + "span": { + "type": "Span", + "start": 8, + "end": 50 + } + }, + "attributes": [], + "comment": null, + "span": { + "type": "Span", + "start": 0, + "end": 50 + } + }, + { + "type": "Message", + "id": { + "type": "Identifier", + "name": "key02", + "span": { + "type": "Span", + "start": 52, + "end": 57 + } + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "Placeable", + "expression": { + "type": "SelectExpression", + "selector": { + "type": "VariableReference", + "id": { + "type": "Identifier", + "name": "sel", + "span": { + "type": "Span", + "start": 63, + "end": 66 + } + }, + "span": { + "type": "Span", + "start": 62, + "end": 66 + } + }, + "variants": [ + { + "type": "Variant", + "key": { + "type": "Identifier", + "name": "key", + "span": { + "type": "Span", + "start": 85, + "end": 88 + } + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "TextElement", + "value": "Value", + "span": { + "type": "Span", + "start": 104, + "end": 109 + } + } + ], + "span": { + "type": "Span", + "start": 100, + "end": 109 + } + }, + "default": true, + "span": { + "type": "Span", + "start": 74, + "end": 109 + } + } + ], + "span": { + "type": "Span", + "start": 62, + "end": 110 + } + }, + "span": { + "type": "Span", + "start": 60, + "end": 111 + } + } + ], + "span": { + "type": "Span", + "start": 60, + "end": 111 + } + }, + "attributes": [], + "comment": null, + "span": { + "type": "Span", + "start": 52, + "end": 111 + } + }, + { + "type": "Junk", + "annotations": [ + { + "type": "Annotation", + "code": "E0004", + "args": [ + "a-zA-Z" + ], + "message": "Expected a character from range: \"a-zA-Z\"", + "span": { + "type": "Span", + "start": 137, + "end": 137 + } + } + ], + "content": "err01 = { $sel ->\n *[\"key\"] Value\n}\n\n", + "span": { + "type": "Span", + "start": 113, + "end": 153 + } + }, + { + "type": "Junk", + "annotations": [ + { + "type": "Annotation", + "code": "E0004", + "args": [ + "0-9" + ], + "message": "Expected a character from range: \"0-9\"", + "span": { + "type": "Span", + "start": 178, + "end": 178 + } + } + ], + "content": "err02 = { $sel ->\n *[-key] Value\n}\n\n", + "span": { + "type": "Span", + "start": 153, + "end": 192 + } + }, + { + "type": "Junk", + "annotations": [ + { + "type": "Annotation", + "code": "E0004", + "args": [ + "0-9" + ], + "message": "Expected a character from range: \"0-9\"", + "span": { + "type": "Span", + "start": 217, + "end": 217 + } + } + ], + "content": "err03 = { $sel ->\n *[-key.attr] Value\n}\n\n", + "span": { + "type": "Span", + "start": 192, + "end": 236 + } + }, + { + "type": "Junk", + "annotations": [ + { + "type": "Annotation", + "code": "E0004", + "args": [ + "0-9" + ], + "message": "Expected a character from range: \"0-9\"", + "span": { + "type": "Span", + "start": 261, + "end": 261 + } + } + ], + "content": "err04 = { $sel ->\n *[-key()] Value\n}\n\n", + "span": { + "type": "Span", + "start": 236, + "end": 277 + } + }, + { + "type": "Junk", + "annotations": [ + { + "type": "Annotation", + "code": "E0004", + "args": [ + "0-9" + ], + "message": "Expected a character from range: \"0-9\"", + "span": { + "type": "Span", + "start": 302, + "end": 302 + } + } + ], + "content": "err05 = { $sel ->\n *[-key.attr()] Value\n}\n\n", + "span": { + "type": "Span", + "start": 277, + "end": 323 + } + }, + { + "type": "Junk", + "annotations": [ + { + "type": "Annotation", + "code": "E0003", + "args": [ + "]" + ], + "message": "Expected token: \"]\"", + "span": { + "type": "Span", + "start": 350, + "end": 350 + } + } + ], + "content": "err06 = { $sel ->\n *[key.attr] Value\n}\n\n", + "span": { + "type": "Span", + "start": 323, + "end": 366 + } + }, + { + "type": "Junk", + "annotations": [ + { + "type": "Annotation", + "code": "E0004", + "args": [ + "a-zA-Z" + ], + "message": "Expected a character from range: \"a-zA-Z\"", + "span": { + "type": "Span", + "start": 390, + "end": 390 + } + } + ], + "content": "err07 = { $sel ->\n *[$key] Value\n}\n\n", + "span": { + "type": "Span", + "start": 366, + "end": 405 + } + }, + { + "type": "Junk", + "annotations": [ + { + "type": "Annotation", + "code": "E0003", + "args": [ + "]" + ], + "message": "Expected token: \"]\"", + "span": { + "type": "Span", + "start": 433, + "end": 433 + } + } + ], + "content": "err08 = { $sel ->\n *[FUNC()] Value\n}\n\n", + "span": { + "type": "Span", + "start": 405, + "end": 446 + } + }, + { + "type": "Junk", + "annotations": [ + { + "type": "Annotation", + "code": "E0004", + "args": [ + "a-zA-Z" + ], + "message": "Expected a character from range: \"a-zA-Z\"", + "span": { + "type": "Span", + "start": 470, + "end": 470 + } + } + ], + "content": "err09 = { $sel ->\n *[{key}] Value\n}\n\n", + "span": { + "type": "Span", + "start": 446, + "end": 486 + } + }, + { + "type": "Junk", + "annotations": [ + { + "type": "Annotation", + "code": "E0004", + "args": [ + "a-zA-Z" + ], + "message": "Expected a character from range: \"a-zA-Z\"", + "span": { + "type": "Span", + "start": 510, + "end": 510 + } + } + ], + "content": "err10 = { $sel ->\n *[{\"key\"}] Value\n}\n\n", + "span": { + "type": "Span", + "start": 486, + "end": 528 + } + }, + { + "type": "Junk", + "annotations": [ + { + "type": "Annotation", + "code": "E0004", + "args": [ + "a-zA-Z" + ], + "message": "Expected a character from range: \"a-zA-Z\"", + "span": { + "type": "Span", + "start": 552, + "end": 552 + } + } + ], + "content": "err11 = { $sel ->\n *[{3.14}] Value\n}\n\n", + "span": { + "type": "Span", + "start": 528, + "end": 569 + } + }, + { + "type": "Junk", + "annotations": [ + { + "type": "Annotation", + "code": "E0004", + "args": [ + "a-zA-Z" + ], + "message": "Expected a character from range: \"a-zA-Z\"", + "span": { + "type": "Span", + "start": 593, + "end": 593 + } + } + ], + "content": "err12 = { $sel ->\n *[{$key}] Value\n}\n", + "span": { + "type": "Span", + "start": 569, + "end": 609 + } + } + ], + "span": { + "type": "Span", + "start": 0, + "end": 609 + } +} diff --git a/fluent/src/resolver.js b/fluent/src/resolver.js index 7eeb08cba..f32fe9060 100644 --- a/fluent/src/resolver.js +++ b/fluent/src/resolver.js @@ -17,23 +17,8 @@ * translation as possible. In rare situations where the resolver didn't know * how to recover from an error it will return an instance of `FluentNone`. * - * `MessageReference`, `VariantExpression`, `AttributeExpression` and - * `SelectExpression` resolve to raw Runtime Entries objects and the result of - * the resolution needs to be passed into `Type` to get their real value. - * This is useful for composing expressions. Consider: - * - * brand-name[nominative] - * - * which is a `VariantExpression` with properties `id: MessageReference` and - * `key: Keyword`. If `MessageReference` was resolved eagerly, it would - * instantly resolve to the value of the `brand-name` message. Instead, we - * want to get the message object and look for its `nominative` variant. - * - * All other expressions (except for `FunctionReference` which is only used as - * a callee in `FunctionExpression`) resolve to an instance of `FluentType`. - * The caller should use the `toString` method to convert the instance to a - * native value. - * + * All expressions resolve to an instance of `FluentType`. The caller should + * use the `toString` method to convert the instance to a native value. * * All functions in this file pass around a special object called `env`. * This object stores a set of elements used by all resolve functions: @@ -62,20 +47,7 @@ const FSI = "\u2068"; const PDI = "\u2069"; -/** - * Helper for matching a variant key to the given selector. - * - * Used in SelectExpressions and VariantExpressions. - * - * @param {FluentBundle} bundle - * Resolver environment object. - * @param {FluentType} key - * The key of the currently considered variant. - * @param {FluentType} selector - * The selector based om which the correct variant should be chosen. - * @returns {FluentType} - * @private - */ +// Helper: match a variant key to the given selector. function match(bundle, selector, key) { if (key === selector) { // Both are strings. @@ -100,23 +72,10 @@ function match(bundle, selector, key) { return false; } -/** - * Helper for choosing the default value from a set of members. - * - * Used in SelectExpressions and Type. - * - * @param {Object} env - * Resolver environment object. - * @param {Object} members - * Hash map of variants from which the default value is to be selected. - * @param {Number} star - * The index of the default variant. - * @returns {FluentType} - * @private - */ -function DefaultMember(env, members, star) { - if (members[star]) { - return members[star]; +// Helper: resolve the default variant from a list of variants. +function getDefault(env, variants, star) { + if (variants[star]) { + return Type(env, variants[star]); } const { errors } = env; @@ -124,176 +83,34 @@ function DefaultMember(env, members, star) { return new FluentNone(); } - -/** - * Resolve a reference to another message. - * - * @param {Object} env - * Resolver environment object. - * @param {Object} id - * The identifier of the message to be resolved. - * @param {String} id.name - * The name of the identifier. - * @returns {FluentType} - * @private - */ -function MessageReference(env, {name}) { - const { bundle, errors } = env; - const message = name[0] === "-" - ? bundle._terms.get(name) - : bundle._messages.get(name); - - if (!message) { - const err = name[0] === "-" - ? new ReferenceError(`Unknown term: ${name}`) - : new ReferenceError(`Unknown message: ${name}`); - errors.push(err); - return new FluentNone(name); - } - - return message; -} - -/** - * Resolve a variant expression to the variant object. - * - * @param {Object} env - * Resolver environment object. - * @param {Object} expr - * An expression to be resolved. - * @param {Object} expr.ref - * An Identifier of a message for which the variant is resolved. - * @param {Object} expr.id.name - * Name a message for which the variant is resolved. - * @param {Object} expr.key - * Variant key to be resolved. - * @returns {FluentType} - * @private - */ -function VariantExpression(env, {ref, selector}) { - const message = MessageReference(env, ref); - if (message instanceof FluentNone) { - return message; - } - - const { bundle, errors } = env; - const sel = Type(env, selector); - const value = message.value || message; - - function isVariantList(node) { - return Array.isArray(node) && - node[0].type === "select" && - node[0].selector === null; - } - - if (isVariantList(value)) { - // Match the specified key against keys of each variant, in order. - for (const variant of value[0].variants) { - const key = Type(env, variant.key); - if (match(env.bundle, sel, key)) { - return variant; +// Helper: resolve arguments to a call expression. +function getArguments(env, args) { + const positional = []; + const named = {}; + + if (args) { + for (const arg of args) { + if (arg.type === "narg") { + named[arg.name] = Type(env, arg.value); + } else { + positional.push(Type(env, arg)); } } } - errors.push( - new ReferenceError(`Unknown variant: ${sel.toString(bundle)}`)); - return Type(env, message); + return [positional, named]; } - -/** - * Resolve an attribute expression to the attribute object. - * - * @param {Object} env - * Resolver environment object. - * @param {Object} expr - * An expression to be resolved. - * @param {String} expr.ref - * An ID of a message for which the attribute is resolved. - * @param {String} expr.name - * Name of the attribute to be resolved. - * @returns {FluentType} - * @private - */ -function AttributeExpression(env, {ref, name}) { - const message = MessageReference(env, ref); - if (message instanceof FluentNone) { - return message; - } - - if (message.attrs) { - // Match the specified name against keys of each attribute. - for (const attrName in message.attrs) { - if (name === attrName) { - return message.attrs[name]; - } - } - } - - const { errors } = env; - errors.push(new ReferenceError(`Unknown attribute: ${name}`)); - return Type(env, message); -} - -/** - * Resolve a select expression to the member object. - * - * @param {Object} env - * Resolver environment object. - * @param {Object} expr - * An expression to be resolved. - * @param {String} expr.selector - * Selector expression - * @param {Array} expr.variants - * List of variants for the select expression. - * @param {Number} expr.star - * Index of the default variant. - * @returns {FluentType} - * @private - */ -function SelectExpression(env, {selector, variants, star}) { - if (selector === null) { - return DefaultMember(env, variants, star); - } - - let sel = Type(env, selector); - if (sel instanceof FluentNone) { - return DefaultMember(env, variants, star); - } - - // Match the selector against keys of each variant, in order. - for (const variant of variants) { - const key = Type(env, variant.key); - if (match(env.bundle, sel, key)) { - return variant; - } - } - - return DefaultMember(env, variants, star); -} - - -/** - * Resolve expression to a Fluent type. - * - * JavaScript strings are a special case. Since they natively have the - * `toString` method they can be used as if they were a Fluent type without - * paying the cost of creating a instance of one. - * - * @param {Object} env - * Resolver environment object. - * @param {Object} expr - * An expression object to be resolved into a Fluent type. - * @returns {FluentType} - * @private - */ +// Resolve an expression to a Fluent type. function Type(env, expr) { - // A fast-path for strings which are the most common case, and for - // `FluentNone` which doesn't require any additional logic. + // A fast-path for strings which are the most common case. Since they + // natively have the `toString` method they can be used as if they were + // a FluentType instance without incurring the cost of creating one. if (typeof expr === "string") { return env.bundle._transform(expr); } + + // A fast-path for `FluentNone` which doesn't require any additional logic. if (expr instanceof FluentNone) { return expr; } @@ -304,7 +121,6 @@ function Type(env, expr) { return Pattern(env, expr); } - switch (expr.type) { case "str": return expr.value; @@ -312,28 +128,14 @@ function Type(env, expr) { return new FluentNumber(expr.value); case "var": return VariableReference(env, expr); - case "call": - return expr.ref.name[0] === "-" - ? MacroExpression(env, expr) - : FunctionExpression(env, expr); - case "ref": { - const message = MessageReference(env, expr); - return expr.name[0] === "-" - ? Type({...env, args: {}}, message) - : Type(env, message); - } - case "getattr": { - const attr = AttributeExpression(env, expr); - return Type(env, attr); - } - case "getvar": { - const variant = VariantExpression(env, expr); - return Type(env, variant); - } - case "select": { - const member = SelectExpression(env, expr); - return Type(env, member); - } + case "term": + return TermReference({...env, args: {}}, expr); + case "ref": + return expr.args + ? FunctionReference(env, expr) + : MessageReference(env, expr); + case "select": + return SelectExpression(env, expr); case undefined: { // If it's a node with a value, resolve the value. if (expr.value !== null && expr.value !== undefined) { @@ -349,18 +151,7 @@ function Type(env, expr) { } } -/** - * Resolve a reference to a variable. - * - * @param {Object} env - * Resolver environment object. - * @param {Object} expr - * An expression to be resolved. - * @param {String} expr.name - * Name of an argument to be returned. - * @returns {FluentType} - * @private - */ +// Resolve a reference to a variable. function VariableReference(env, {name}) { const { args, errors } = env; @@ -394,22 +185,76 @@ function VariableReference(env, {name}) { } } -/** - * Resolve a reference to a function. - * - * @param {Object} env - * Resolver environment object. - * @param {Object} expr - * An expression to be resolved. - * @param {String} expr.name - * Name of the function to be returned. - * @returns {Function} - * @private - */ -function FunctionReference(env, {name}) { - // Some functions are built-in. Others may be provided by the runtime via +// Resolve a reference to another message. +function MessageReference(env, {name, attr}) { + const {bundle, errors} = env; + const message = bundle._messages.get(name); + if (!message) { + const err = new ReferenceError(`Unknown message: ${name}`); + errors.push(err); + return new FluentNone(name); + } + + if (attr) { + const attribute = message.attrs && message.attrs[attr]; + if (attribute) { + return Type(env, attribute); + } + errors.push(new ReferenceError(`Unknown attribute: ${attr}`)); + return Type(env, message); + } + + return Type(env, message); +} + +// Resolve a call to a Term with key-value arguments. +function TermReference(env, {name, attr, selector, args}) { + const {bundle, errors} = env; + + const id = `-${name}`; + const term = bundle._terms.get(id); + if (!term) { + const err = new ReferenceError(`Unknown term: ${id}`); + errors.push(err); + return new FluentNone(id); + } + + // Every TermReference has its own args. + const [, keyargs] = getArguments(env, args); + const local = {...env, args: keyargs}; + + if (attr) { + const attribute = term.attrs && term.attrs[attr]; + if (attribute) { + return Type(local, attribute); + } + errors.push(new ReferenceError(`Unknown attribute: ${attr}`)); + return Type(local, term); + } + + const variantList = getVariantList(term); + if (selector && variantList) { + return SelectExpression(local, {...variantList, selector}); + } + + return Type(local, term); +} + +// Helper: convert a value into a variant list, if possible. +function getVariantList(term) { + const value = term.value || term; + return Array.isArray(value) + && value[0].type === "select" + && value[0].selector === null + ? value[0] + : null; +} + +// Resolve a call to a Function with positional and key-value arguments. +function FunctionReference(env, {name, args}) { + // Some functions are built-in. Others may be provided by the runtime via // the `FluentBundle` constructor. - const { bundle: { _functions }, errors } = env; + const {bundle: {_functions}, errors} = env; const func = _functions[name] || builtins[name]; if (!func) { @@ -422,88 +267,39 @@ function FunctionReference(env, {name}) { return new FluentNone(`${name}()`); } - return func; -} - -/** - * Resolve a call to a Function with positional and key-value arguments. - * - * @param {Object} env - * Resolver environment object. - * @param {Object} expr - * An expression to be resolved. - * @param {Object} expr.ref - * FTL Function object. - * @param {Array} expr.args - * FTL Function argument list. - * @returns {FluentType} - * @private - */ -function FunctionExpression(env, {ref, args}) { - const func = FunctionReference(env, ref); - if (func instanceof FluentNone) { - return func; - } - - const posargs = []; - const keyargs = {}; - - for (const arg of args) { - if (arg.type === "narg") { - keyargs[arg.name] = Type(env, arg.value); - } else { - posargs.push(Type(env, arg)); - } - } - try { - return func(posargs, keyargs); + return func(...getArguments(env, args)); } catch (e) { // XXX Report errors. return new FluentNone(); } } -/** - * Resolve a call to a Term with key-value arguments. - * - * @param {Object} env - * Resolver environment object. - * @param {Object} expr - * An expression to be resolved. - * @param {Object} expr.ref - * FTL Function object. - * @param {Array} expr.args - * FTL Function argument list. - * @returns {FluentType} - * @private - */ -function MacroExpression(env, {ref, args}) { - const callee = MessageReference(env, ref); - if (callee instanceof FluentNone) { - return callee; +// Resolve a select expression to the member object. +function SelectExpression(env, {selector, variants, star}) { + if (selector === null) { + return getDefault(env, variants, star); + } + + let sel = Type(env, selector); + if (sel instanceof FluentNone) { + const variant = getDefault(env, variants, star); + return Type(env, variant); } - const keyargs = {}; - for (const arg of args) { - if (arg.type === "narg") { - keyargs[arg.name] = Type(env, arg.value); + // Match the selector against keys of each variant, in order. + for (const variant of variants) { + const key = Type(env, variant.key); + if (match(env.bundle, sel, key)) { + return Type(env, variant); } } - return Type({...env, args: keyargs}, callee); + const variant = getDefault(env, variants, star); + return Type(env, variant); } -/** - * Resolve a pattern (a complex string with placeables). - * - * @param {Object} env - * Resolver environment object. - * @param {Array} ptn - * Array of pattern elements. - * @returns {Array} - * @private - */ +// Resolve a pattern (a complex string with placeables). function Pattern(env, ptn) { const { bundle, dirty, errors } = env; diff --git a/fluent/src/resource.js b/fluent/src/resource.js index 7afa41a3e..cc9fe1d7b 100644 --- a/fluent/src/resource.js +++ b/fluent/src/resource.js @@ -2,17 +2,16 @@ import FluentError from "./error.js"; // This regex is used to iterate through the beginnings of messages and terms. // With the /m flag, the ^ matches at the beginning of every line. -const RE_MESSAGE_START = /^(-?[a-zA-Z][a-zA-Z0-9_-]*) *= */mg; +const RE_MESSAGE_START = /^(-?[a-zA-Z][\w-]*) *= */mg; // Both Attributes and Variants are parsed in while loops. These regexes are // used to break out of them. -const RE_ATTRIBUTE_START = /\.([a-zA-Z][a-zA-Z0-9_-]*) *= */y; -// [^] matches all characters, including newlines. -// XXX Use /s (dotall) when it's widely supported. -const RE_VARIANT_START = /\*?\[[^]*?] */y; +const RE_ATTRIBUTE_START = /\.([a-zA-Z][\w-]*) *= */y; +const RE_VARIANT_START = /\*?\[/y; -const RE_IDENTIFIER = /(-?[a-zA-Z][a-zA-Z0-9_-]*)/y; const RE_NUMBER_LITERAL = /(-?[0-9]+(\.[0-9]+)?)/y; +const RE_IDENTIFIER = /([a-zA-Z][\w-]*)/y; +const RE_REFERENCE = /([$-])?([a-zA-Z][\w-]*)(?:\.([a-zA-Z][\w-]*))?/y; // A "run" is a sequence of text or string literal characters which don't // require any special handling. For TextElements such special characters are: { @@ -39,8 +38,8 @@ const RE_INDENT = /( *)$/; const TOKEN_BRACE_OPEN = /{\s*/y; const TOKEN_BRACE_CLOSE = /\s*}/y; const TOKEN_BRACKET_OPEN = /\[\s*/y; -const TOKEN_BRACKET_CLOSE = /\s*]/y; -const TOKEN_PAREN_OPEN = /\(\s*/y; +const TOKEN_BRACKET_CLOSE = /\s*] */y; +const TOKEN_PAREN_OPEN = /\s*\(\s*/y; const TOKEN_ARROW = /\s*->\s*/y; const TOKEN_COLON = /\s*:\s*/y; // Note the optional comma. As a deviation from the Fluent EBNF, the parser @@ -134,7 +133,7 @@ export default class FluentResource extends Map { return false; } - // Execute a regex, advance the cursor, and return the capture group. + // Execute a regex, advance the cursor, and return all capture groups. function match(re) { re.lastIndex = cursor; let result = re.exec(source); @@ -142,7 +141,12 @@ export default class FluentResource extends Map { throw new FluentError(`Expected ${re.toString()}`); } cursor = re.lastIndex; - return result[1]; + return result; + } + + // Execute a regex, advance the cursor, and return the capture group. + function match1(re) { + return match(re)[1]; } function parseMessage() { @@ -163,7 +167,7 @@ export default class FluentResource extends Map { let attrs = {}; while (test(RE_ATTRIBUTE_START)) { - let name = match(RE_ATTRIBUTE_START); + let name = match1(RE_ATTRIBUTE_START); let value = parsePattern(); if (value === null) { throw new FluentError("Expected attribute value"); @@ -177,7 +181,7 @@ export default class FluentResource extends Map { function parsePattern() { // First try to parse any simple text on the same line as the id. if (test(RE_TEXT_RUN)) { - var first = match(RE_TEXT_RUN); + var first = match1(RE_TEXT_RUN); } // If there's a placeable on the first line, parse a complex pattern. @@ -216,7 +220,7 @@ export default class FluentResource extends Map { while (true) { if (test(RE_TEXT_RUN)) { - elements.push(match(RE_TEXT_RUN)); + elements.push(match1(RE_TEXT_RUN)); continue; } @@ -294,27 +298,20 @@ export default class FluentResource extends Map { return parsePlaceable(); } - if (consumeChar("$")) { - return {type: "var", name: match(RE_IDENTIFIER)}; - } - - if (test(RE_IDENTIFIER)) { - let ref = {type: "ref", name: match(RE_IDENTIFIER)}; - - if (consumeChar(".")) { - let name = match(RE_IDENTIFIER); - return {type: "getattr", ref, name}; - } + if (test(RE_REFERENCE)) { + let [, sigil, name, attr = null] = match(RE_REFERENCE); + let type = {"$": "var", "-": "term"}[sigil] || "ref"; if (source[cursor] === "[") { - return {type: "getvar", ref, selector: parseVariantKey()}; + // DEPRECATED VariantExpressions will be removed before 1.0. + return {type, name, selector: parseVariantKey()}; } if (consumeToken(TOKEN_PAREN_OPEN)) { - return {type: "call", ref, args: parseArguments()}; + return {type, name, attr, args: parseArguments()}; } - return ref; + return {type, name, attr, args: null}; } return parseLiteral(); @@ -363,7 +360,6 @@ export default class FluentResource extends Map { } let key = parseVariantKey(); - cursor = RE_VARIANT_START.lastIndex; let value = parsePattern(); if (value === null) { throw new FluentError("Expected variant value"); @@ -371,14 +367,22 @@ export default class FluentResource extends Map { variants[count++] = {key, value}; } - return count > 0 ? {variants, star} : null; + if (count === 0) { + return null; + } + + if (star === undefined) { + throw new FluentError("Expected default variant"); + } + + return {variants, star}; } function parseVariantKey() { consumeToken(TOKEN_BRACKET_OPEN, FluentError); let key = test(RE_NUMBER_LITERAL) ? parseNumberLiteral() - : match(RE_IDENTIFIER); + : match1(RE_IDENTIFIER); consumeToken(TOKEN_BRACKET_CLOSE, FluentError); return key; } @@ -396,14 +400,14 @@ export default class FluentResource extends Map { } function parseNumberLiteral() { - return {type: "num", value: match(RE_NUMBER_LITERAL)}; + return {type: "num", value: match1(RE_NUMBER_LITERAL)}; } function parseStringLiteral() { consumeChar("\"", FluentError); let value = ""; while (true) { - value += match(RE_STRING_RUN); + value += match1(RE_STRING_RUN); if (source[cursor] === "\\") { value += parseEscapeSequence(); @@ -422,7 +426,7 @@ export default class FluentResource extends Map { // Unescape known escape sequences. function parseEscapeSequence() { if (test(RE_UNICODE_ESCAPE)) { - let sequence = match(RE_UNICODE_ESCAPE); + let sequence = match1(RE_UNICODE_ESCAPE); let codepoint = parseInt(sequence, 16); return codepoint <= 0xD7FF || 0xE000 <= codepoint // It's a Unicode scalar value. @@ -433,7 +437,7 @@ export default class FluentResource extends Map { } if (test(RE_STRING_ESCAPE)) { - return match(RE_STRING_ESCAPE); + return match1(RE_STRING_ESCAPE); } throw new FluentError("Unknown escape sequence"); diff --git a/fluent/test/fixtures_reference/call_expressions.json b/fluent/test/fixtures_reference/call_expressions.json index 10682530e..9334ff18c 100644 --- a/fluent/test/fixtures_reference/call_expressions.json +++ b/fluent/test/fixtures_reference/call_expressions.json @@ -1,51 +1,9 @@ { - "function-callee": [ - { - "type": "call", - "ref": { - "type": "ref", - "name": "FUNCTION" - }, - "args": [] - } - ], - "term-callee": [ - { - "type": "call", - "ref": { - "type": "ref", - "name": "-term" - }, - "args": [] - } - ], - "mixed-case-callee": [ - { - "type": "call", - "ref": { - "type": "ref", - "name": "Function" - }, - "args": [] - } - ], - "message-callee": [ - { - "type": "call", - "ref": { - "type": "ref", - "name": "message" - }, - "args": [] - } - ], "positional-args": [ { - "type": "call", - "ref": { - "type": "ref", - "name": "FUN" - }, + "type": "ref", + "name": "FUN", + "attr": null, "args": [ { "type": "num", @@ -57,18 +15,18 @@ }, { "type": "ref", - "name": "msg" + "name": "msg", + "attr": null, + "args": null } ] } ], "named-args": [ { - "type": "call", - "ref": { - "type": "ref", - "name": "FUN" - }, + "type": "ref", + "name": "FUN", + "attr": null, "args": [ { "type": "narg", @@ -91,11 +49,9 @@ ], "dense-named-args": [ { - "type": "call", - "ref": { - "type": "ref", - "name": "FUN" - }, + "type": "ref", + "name": "FUN", + "attr": null, "args": [ { "type": "narg", @@ -118,11 +74,9 @@ ], "mixed-args": [ { - "type": "call", - "ref": { - "type": "ref", - "name": "FUN" - }, + "type": "ref", + "name": "FUN", + "attr": null, "args": [ { "type": "num", @@ -134,7 +88,9 @@ }, { "type": "ref", - "name": "msg" + "name": "msg", + "attr": null, + "args": null }, { "type": "narg", @@ -157,11 +113,9 @@ ], "shuffled-args": [ { - "type": "call", - "ref": { - "type": "ref", - "name": "FUN" - }, + "type": "ref", + "name": "FUN", + "attr": null, "args": [ { "type": "num", @@ -189,18 +143,18 @@ }, { "type": "ref", - "name": "msg" + "name": "msg", + "attr": null, + "args": null } ] } ], "duplicate-named-args": [ { - "type": "call", - "ref": { - "type": "ref", - "name": "FUN" - }, + "type": "ref", + "name": "FUN", + "attr": null, "args": [ { "type": "narg", @@ -223,11 +177,9 @@ ], "sparse-inline-call": [ { - "type": "call", - "ref": { - "type": "ref", - "name": "FUN" - }, + "type": "ref", + "name": "FUN", + "attr": null, "args": [ { "type": "str", @@ -235,7 +187,9 @@ }, { "type": "ref", - "name": "msg" + "name": "msg", + "attr": null, + "args": null }, { "type": "narg", @@ -250,21 +204,17 @@ ], "empty-inline-call": [ { - "type": "call", - "ref": { - "type": "ref", - "name": "FUN" - }, + "type": "ref", + "name": "FUN", + "attr": null, "args": [] } ], "multiline-call": [ { - "type": "call", - "ref": { - "type": "ref", - "name": "FUN" - }, + "type": "ref", + "name": "FUN", + "attr": null, "args": [ { "type": "str", @@ -272,7 +222,9 @@ }, { "type": "ref", - "name": "msg" + "name": "msg", + "attr": null, + "args": null }, { "type": "narg", @@ -287,11 +239,9 @@ ], "sparse-multiline-call": [ { - "type": "call", - "ref": { - "type": "ref", - "name": "FUN" - }, + "type": "ref", + "name": "FUN", + "attr": null, "args": [ { "type": "str", @@ -299,7 +249,9 @@ }, { "type": "ref", - "name": "msg" + "name": "msg", + "attr": null, + "args": null }, { "type": "narg", @@ -314,21 +266,17 @@ ], "empty-multiline-call": [ { - "type": "call", - "ref": { - "type": "ref", - "name": "FUN" - }, + "type": "ref", + "name": "FUN", + "attr": null, "args": [] } ], "unindented-arg-number": [ { - "type": "call", - "ref": { - "type": "ref", - "name": "FUN" - }, + "type": "ref", + "name": "FUN", + "attr": null, "args": [ { "type": "num", @@ -339,11 +287,9 @@ ], "unindented-arg-string": [ { - "type": "call", - "ref": { - "type": "ref", - "name": "FUN" - }, + "type": "ref", + "name": "FUN", + "attr": null, "args": [ { "type": "str", @@ -354,63 +300,59 @@ ], "unindented-arg-msg-ref": [ { - "type": "call", - "ref": { - "type": "ref", - "name": "FUN" - }, + "type": "ref", + "name": "FUN", + "attr": null, "args": [ { "type": "ref", - "name": "msg" + "name": "msg", + "attr": null, + "args": null } ] } ], "unindented-arg-term-ref": [ { - "type": "call", - "ref": { - "type": "ref", - "name": "FUN" - }, + "type": "ref", + "name": "FUN", + "attr": null, "args": [ { - "type": "ref", - "name": "-msg" + "type": "term", + "name": "msg", + "attr": null, + "args": null } ] } ], "unindented-arg-var-ref": [ { - "type": "call", - "ref": { - "type": "ref", - "name": "FUN" - }, + "type": "ref", + "name": "FUN", + "attr": null, "args": [ { "type": "var", - "name": "var" + "name": "var", + "attr": null, + "args": null } ] } ], "unindented-arg-call": [ { - "type": "call", - "ref": { - "type": "ref", - "name": "FUN" - }, + "type": "ref", + "name": "FUN", + "attr": null, "args": [ { - "type": "call", - "ref": { - "type": "ref", - "name": "OTHER" - }, + "type": "ref", + "name": "OTHER", + "attr": null, "args": [] } ] @@ -418,11 +360,9 @@ ], "unindented-named-arg": [ { - "type": "call", - "ref": { - "type": "ref", - "name": "FUN" - }, + "type": "ref", + "name": "FUN", + "attr": null, "args": [ { "type": "narg", @@ -437,26 +377,24 @@ ], "unindented-closing-paren": [ { - "type": "call", - "ref": { - "type": "ref", - "name": "FUN" - }, + "type": "ref", + "name": "FUN", + "attr": null, "args": [ { "type": "ref", - "name": "x" + "name": "x", + "attr": null, + "args": null } ] } ], "one-argument": [ { - "type": "call", - "ref": { - "type": "ref", - "name": "FUN" - }, + "type": "ref", + "name": "FUN", + "attr": null, "args": [ { "type": "num", @@ -467,11 +405,9 @@ ], "many-arguments": [ { - "type": "call", - "ref": { - "type": "ref", - "name": "FUN" - }, + "type": "ref", + "name": "FUN", + "attr": null, "args": [ { "type": "num", @@ -490,11 +426,9 @@ ], "inline-sparse-args": [ { - "type": "call", - "ref": { - "type": "ref", - "name": "FUN" - }, + "type": "ref", + "name": "FUN", + "attr": null, "args": [ { "type": "num", @@ -513,11 +447,9 @@ ], "mulitline-args": [ { - "type": "call", - "ref": { - "type": "ref", - "name": "FUN" - }, + "type": "ref", + "name": "FUN", + "attr": null, "args": [ { "type": "num", @@ -532,11 +464,9 @@ ], "mulitline-sparse-args": [ { - "type": "call", - "ref": { - "type": "ref", - "name": "FUN" - }, + "type": "ref", + "name": "FUN", + "attr": null, "args": [ { "type": "num", @@ -551,11 +481,9 @@ ], "sparse-named-arg": [ { - "type": "call", - "ref": { - "type": "ref", - "name": "FUN" - }, + "type": "ref", + "name": "FUN", + "attr": null, "args": [ { "type": "narg", @@ -586,11 +514,9 @@ ], "unindented-colon": [ { - "type": "call", - "ref": { - "type": "ref", - "name": "FUN" - }, + "type": "ref", + "name": "FUN", + "attr": null, "args": [ { "type": "narg", @@ -605,11 +531,9 @@ ], "unindented-value": [ { - "type": "call", - "ref": { - "type": "ref", - "name": "FUN" - }, + "type": "ref", + "name": "FUN", + "attr": null, "args": [ { "type": "narg", diff --git a/fluent/test/fixtures_reference/callee_expressions.json b/fluent/test/fixtures_reference/callee_expressions.json new file mode 100644 index 000000000..0db21c11a --- /dev/null +++ b/fluent/test/fixtures_reference/callee_expressions.json @@ -0,0 +1,184 @@ +{ + "function-callee-placeable": [ + { + "type": "ref", + "name": "FUNCTION", + "attr": null, + "args": [] + } + ], + "term-callee-placeable": [ + { + "type": "term", + "name": "term", + "attr": null, + "args": [] + } + ], + "message-callee-placeable": [ + { + "type": "ref", + "name": "message", + "attr": null, + "args": [] + } + ], + "mixed-case-callee-placeable": [ + { + "type": "ref", + "name": "Function", + "attr": null, + "args": [] + } + ], + "message-attr-callee-placeable": [ + { + "type": "ref", + "name": "message", + "attr": "attr", + "args": [] + } + ], + "term-attr-callee-placeable": [ + { + "type": "term", + "name": "term", + "attr": "attr", + "args": [] + } + ], + "variable-callee-placeable": [ + { + "type": "var", + "name": "variable", + "attr": null, + "args": [] + } + ], + "function-callee-selector": [ + { + "type": "select", + "selector": { + "type": "ref", + "name": "FUNCTION", + "attr": null, + "args": [] + }, + "variants": [ + { + "key": "key", + "value": "Value" + } + ], + "star": 0 + } + ], + "term-attr-callee-selector": [ + { + "type": "select", + "selector": { + "type": "term", + "name": "term", + "attr": "attr", + "args": [] + }, + "variants": [ + { + "key": "key", + "value": "Value" + } + ], + "star": 0 + } + ], + "message-callee-selector": [ + { + "type": "select", + "selector": { + "type": "ref", + "name": "message", + "attr": null, + "args": [] + }, + "variants": [ + { + "key": "key", + "value": "Value" + } + ], + "star": 0 + } + ], + "mixed-case-callee-selector": [ + { + "type": "select", + "selector": { + "type": "ref", + "name": "Function", + "attr": null, + "args": [] + }, + "variants": [ + { + "key": "key", + "value": "Value" + } + ], + "star": 0 + } + ], + "message-attr-callee-selector": [ + { + "type": "select", + "selector": { + "type": "ref", + "name": "message", + "attr": "attr", + "args": [] + }, + "variants": [ + { + "key": "key", + "value": "Value" + } + ], + "star": 0 + } + ], + "term-callee-selector": [ + { + "type": "select", + "selector": { + "type": "term", + "name": "term", + "attr": null, + "args": [] + }, + "variants": [ + { + "key": "key", + "value": "Value" + } + ], + "star": 0 + } + ], + "variable-callee-selector": [ + { + "type": "select", + "selector": { + "type": "var", + "name": "variable", + "attr": null, + "args": [] + }, + "variants": [ + { + "key": "key", + "value": "Value" + } + ], + "star": 0 + } + ] +} diff --git a/fluent/test/fixtures_reference/cr.json b/fluent/test/fixtures_reference/cr.json index 88e772c02..19382381e 100644 --- a/fluent/test/fixtures_reference/cr.json +++ b/fluent/test/fixtures_reference/cr.json @@ -17,7 +17,9 @@ "type": "select", "selector": { "type": "var", - "name": "sel" + "name": "sel", + "attr": null, + "args": null } } ] diff --git a/fluent/test/fixtures_reference/crlf.json b/fluent/test/fixtures_reference/crlf.json index aa9ccd77c..f3b058a2d 100644 --- a/fluent/test/fixtures_reference/crlf.json +++ b/fluent/test/fixtures_reference/crlf.json @@ -15,7 +15,9 @@ "type": "select", "selector": { "type": "var", - "name": "sel" + "name": "sel", + "attr": null, + "args": null } } ] diff --git a/fluent/test/fixtures_reference/escaped_characters.json b/fluent/test/fixtures_reference/escaped_characters.json index cc1619930..b5046fdb7 100644 --- a/fluent/test/fixtures_reference/escaped_characters.json +++ b/fluent/test/fixtures_reference/escaped_characters.json @@ -5,7 +5,9 @@ "Value with \\", { "type": "ref", - "name": "placeable" + "name": "placeable", + "attr": null, + "args": null } ], "text-backslash-u": "\\u0041", diff --git a/fluent/test/fixtures_reference/member_expressions.json b/fluent/test/fixtures_reference/member_expressions.json index 5d53602fd..b7d5eb7b9 100644 --- a/fluent/test/fixtures_reference/member_expressions.json +++ b/fluent/test/fixtures_reference/member_expressions.json @@ -1,22 +1,102 @@ { - "variant-expression": [ + "message-attribute-expression-placeable": [ { - "type": "getvar", - "ref": { + "type": "ref", + "name": "msg", + "attr": "attr", + "args": null + } + ], + "term-variant-expression-placeable": [ + { + "type": "term", + "name": "term", + "selector": "case" + } + ], + "message-variant-expression-placeable": [ + { + "type": "ref", + "name": "msg", + "selector": "case" + } + ], + "term-attribute-expression-placeable": [ + { + "type": "term", + "name": "term", + "attr": "attr", + "args": null + } + ], + "term-attribute-expression-selector": [ + { + "type": "select", + "selector": { + "type": "term", + "name": "term", + "attr": "attr", + "args": null + }, + "variants": [ + { + "key": "key", + "value": "Value" + } + ], + "star": 0 + } + ], + "message-attribute-expression-selector": [ + { + "type": "select", + "selector": { "type": "ref", - "name": "msg" + "name": "msg", + "attr": "attr", + "args": null }, - "selector": "case" + "variants": [ + { + "key": "key", + "value": "Value" + } + ], + "star": 0 + } + ], + "term-variant-expression-selector": [ + { + "type": "select", + "selector": { + "type": "term", + "name": "term", + "selector": "case" + }, + "variants": [ + { + "key": "key", + "value": "Value" + } + ], + "star": 0 } ], - "attribute-expression": [ + "message-variant-expression-selector": [ { - "type": "getattr", - "ref": { + "type": "select", + "selector": { "type": "ref", - "name": "-term" + "name": "msg", + "selector": "case" }, - "name": "attr" + "variants": [ + { + "key": "key", + "value": "Value" + } + ], + "star": 0 } ] } diff --git a/fluent/test/fixtures_reference/messages.json b/fluent/test/fixtures_reference/messages.json index 03f07b796..9e6a598a8 100644 --- a/fluent/test/fixtures_reference/messages.json +++ b/fluent/test/fixtures_reference/messages.json @@ -26,5 +26,6 @@ "attr1": "Attribute 1" } }, - "key06": [] + "key06": [], + "KEY09": "Value 09" } diff --git a/fluent/test/fixtures_reference/reference_expressions.json b/fluent/test/fixtures_reference/reference_expressions.json index c6734e51c..8d7aca81b 100644 --- a/fluent/test/fixtures_reference/reference_expressions.json +++ b/fluent/test/fixtures_reference/reference_expressions.json @@ -1,26 +1,106 @@ { - "message-reference": [ + "message-reference-placeable": [ { "type": "ref", - "name": "msg" + "name": "msg", + "attr": null, + "args": null } ], - "term-reference": [ + "term-reference-placeable": [ { - "type": "ref", - "name": "-term" + "type": "term", + "name": "term", + "attr": null, + "args": null } ], - "variable-reference": [ + "variable-reference-placeable": [ { "type": "var", - "name": "var" + "name": "var", + "attr": null, + "args": null } ], - "not-call-expression": [ + "function-reference-placeable": [ { "type": "ref", - "name": "FUN" + "name": "FUN", + "attr": null, + "args": null + } + ], + "variable-reference-selector": [ + { + "type": "select", + "selector": { + "type": "var", + "name": "var", + "attr": null, + "args": null + }, + "variants": [ + { + "key": "key", + "value": "Value" + } + ], + "star": 0 + } + ], + "message-reference-selector": [ + { + "type": "select", + "selector": { + "type": "ref", + "name": "msg", + "attr": null, + "args": null + }, + "variants": [ + { + "key": "key", + "value": "Value" + } + ], + "star": 0 + } + ], + "term-reference-selector": [ + { + "type": "select", + "selector": { + "type": "term", + "name": "term", + "attr": null, + "args": null + }, + "variants": [ + { + "key": "key", + "value": "Value" + } + ], + "star": 0 + } + ], + "function-expression-selector": [ + { + "type": "select", + "selector": { + "type": "ref", + "name": "FUN", + "attr": null, + "args": null + }, + "variants": [ + { + "key": "key", + "value": "Value" + } + ], + "star": 0 } ] } diff --git a/fluent/test/fixtures_reference/select_expressions.json b/fluent/test/fixtures_reference/select_expressions.json index 8b204d856..52ace6831 100644 --- a/fluent/test/fixtures_reference/select_expressions.json +++ b/fluent/test/fixtures_reference/select_expressions.json @@ -3,11 +3,9 @@ { "type": "select", "selector": { - "type": "call", - "ref": { - "type": "ref", - "name": "BUILTIN" - }, + "type": "ref", + "name": "BUILTIN", + "attr": null, "args": [] }, "variants": [ @@ -32,12 +30,10 @@ { "type": "select", "selector": { - "type": "getattr", - "ref": { - "type": "ref", - "name": "-term" - }, - "name": "case" + "type": "term", + "name": "term", + "attr": "case", + "args": null }, "variants": [ { @@ -52,8 +48,10 @@ { "type": "select", "selector": { - "type": "ref", - "name": "-term" + "type": "term", + "name": "term", + "attr": null, + "args": null }, "variants": [ { @@ -68,11 +66,8 @@ { "type": "select", "selector": { - "type": "getvar", - "ref": { - "type": "ref", - "name": "-term" - }, + "type": "term", + "name": "term", "selector": "case" }, "variants": [ @@ -88,11 +83,9 @@ { "type": "select", "selector": { - "type": "call", - "ref": { - "type": "ref", - "name": "-term" - }, + "type": "term", + "name": "term", + "attr": null, "args": [ { "type": "narg", diff --git a/fluent/test/fixtures_reference/select_indent.json b/fluent/test/fixtures_reference/select_indent.json index c8e21fea3..94e57c450 100644 --- a/fluent/test/fixtures_reference/select_indent.json +++ b/fluent/test/fixtures_reference/select_indent.json @@ -4,7 +4,9 @@ "type": "select", "selector": { "type": "var", - "name": "selector" + "name": "selector", + "attr": null, + "args": null }, "variants": [ { @@ -20,7 +22,9 @@ "type": "select", "selector": { "type": "var", - "name": "selector" + "name": "selector", + "attr": null, + "args": null }, "variants": [ { @@ -36,7 +40,9 @@ "type": "select", "selector": { "type": "var", - "name": "selector" + "name": "selector", + "attr": null, + "args": null }, "variants": [ { @@ -52,7 +58,9 @@ "type": "select", "selector": { "type": "var", - "name": "selector" + "name": "selector", + "attr": null, + "args": null }, "variants": [ { @@ -68,7 +76,9 @@ "type": "select", "selector": { "type": "var", - "name": "selector" + "name": "selector", + "attr": null, + "args": null }, "variants": [ { @@ -84,7 +94,9 @@ "type": "select", "selector": { "type": "var", - "name": "selector" + "name": "selector", + "attr": null, + "args": null }, "variants": [ { @@ -100,7 +112,9 @@ "type": "select", "selector": { "type": "var", - "name": "selector" + "name": "selector", + "attr": null, + "args": null }, "variants": [ { @@ -116,7 +130,9 @@ "type": "select", "selector": { "type": "var", - "name": "selector" + "name": "selector", + "attr": null, + "args": null }, "variants": [ { @@ -132,7 +148,9 @@ "type": "select", "selector": { "type": "var", - "name": "selector" + "name": "selector", + "attr": null, + "args": null }, "variants": [ { @@ -148,7 +166,9 @@ "type": "select", "selector": { "type": "var", - "name": "selector" + "name": "selector", + "attr": null, + "args": null }, "variants": [ { @@ -168,7 +188,9 @@ "type": "select", "selector": { "type": "var", - "name": "selector" + "name": "selector", + "attr": null, + "args": null }, "variants": [ { @@ -196,7 +218,9 @@ "type": "select", "selector": { "type": "var", - "name": "selector" + "name": "selector", + "attr": null, + "args": null }, "variants": [ { @@ -216,7 +240,9 @@ "type": "select", "selector": { "type": "var", - "name": "selector" + "name": "selector", + "attr": null, + "args": null }, "variants": [ { diff --git a/fluent/test/fixtures_reference/term_parameters.json b/fluent/test/fixtures_reference/term_parameters.json index 78764b97c..81d435d09 100644 --- a/fluent/test/fixtures_reference/term_parameters.json +++ b/fluent/test/fixtures_reference/term_parameters.json @@ -4,7 +4,9 @@ "type": "select", "selector": { "type": "var", - "name": "arg" + "name": "arg", + "attr": null, + "args": null }, "variants": [ { @@ -17,27 +19,25 @@ ], "key01": [ { - "type": "ref", - "name": "-term" + "type": "term", + "name": "term", + "attr": null, + "args": null } ], "key02": [ { - "type": "call", - "ref": { - "type": "ref", - "name": "-term" - }, + "type": "term", + "name": "term", + "attr": null, "args": [] } ], "key03": [ { - "type": "call", - "ref": { - "type": "ref", - "name": "-term" - }, + "type": "term", + "name": "term", + "attr": null, "args": [ { "type": "narg", @@ -52,11 +52,9 @@ ], "key04": [ { - "type": "call", - "ref": { - "type": "ref", - "name": "-term" - }, + "type": "term", + "name": "term", + "attr": null, "args": [ { "type": "str", diff --git a/fluent/test/fixtures_reference/variables.json b/fluent/test/fixtures_reference/variables.json index 6f0de9904..38038db6a 100644 --- a/fluent/test/fixtures_reference/variables.json +++ b/fluent/test/fixtures_reference/variables.json @@ -2,31 +2,33 @@ "key01": [ { "type": "var", - "name": "var" + "name": "var", + "attr": null, + "args": null } ], "key02": [ { "type": "var", - "name": "var" + "name": "var", + "attr": null, + "args": null } ], "key03": [ { "type": "var", - "name": "var" + "name": "var", + "attr": null, + "args": null } ], "key04": [ { "type": "var", - "name": "var" - } - ], - "err03": [ - { - "type": "var", - "name": "-var" + "name": "var", + "attr": null, + "args": null } ] } diff --git a/fluent/test/fixtures_structure/crlf.json b/fluent/test/fixtures_structure/crlf.json index aa9ccd77c..f3b058a2d 100644 --- a/fluent/test/fixtures_structure/crlf.json +++ b/fluent/test/fixtures_structure/crlf.json @@ -15,7 +15,9 @@ "type": "select", "selector": { "type": "var", - "name": "sel" + "name": "sel", + "attr": null, + "args": null } } ] diff --git a/fluent/test/fixtures_structure/escape_sequences.json b/fluent/test/fixtures_structure/escape_sequences.json index cc1619930..b5046fdb7 100644 --- a/fluent/test/fixtures_structure/escape_sequences.json +++ b/fluent/test/fixtures_structure/escape_sequences.json @@ -5,7 +5,9 @@ "Value with \\", { "type": "ref", - "name": "placeable" + "name": "placeable", + "attr": null, + "args": null } ], "text-backslash-u": "\\u0041", diff --git a/fluent/test/fixtures_structure/expressions_call_args.json b/fluent/test/fixtures_structure/expressions_call_args.json index a15662ec0..031801867 100644 --- a/fluent/test/fixtures_structure/expressions_call_args.json +++ b/fluent/test/fixtures_structure/expressions_call_args.json @@ -1,11 +1,9 @@ { "key": [ { - "type": "call", - "ref": { - "type": "ref", - "name": "FOO" - }, + "type": "ref", + "name": "FOO", + "attr": null, "args": [ { "type": "narg", diff --git a/fluent/test/fixtures_structure/placeable_at_eol.json b/fluent/test/fixtures_structure/placeable_at_eol.json index 6841b74db..9fe6f6c59 100644 --- a/fluent/test/fixtures_structure/placeable_at_eol.json +++ b/fluent/test/fixtures_structure/placeable_at_eol.json @@ -3,7 +3,9 @@ "A multiline message with a ", { "type": "ref", - "name": "placeable" + "name": "placeable", + "attr": null, + "args": null }, "\n", "at the end of line. The message should", @@ -14,14 +16,18 @@ "A multiline message with a ", { "type": "ref", - "name": "placeable" + "name": "placeable", + "attr": null, + "args": null } ], "key3": [ "A singleline message with a ", { "type": "ref", - "name": "placeable" + "name": "placeable", + "attr": null, + "args": null } ] } diff --git a/fluent/test/fixtures_structure/select_expressions.json b/fluent/test/fixtures_structure/select_expressions.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/fluent/test/fixtures_structure/select_expressions.json @@ -0,0 +1 @@ +{} diff --git a/fluent/test/fixtures_structure/sparse-messages.json b/fluent/test/fixtures_structure/sparse-messages.json index c88af5396..687ede0ae 100644 --- a/fluent/test/fixtures_structure/sparse-messages.json +++ b/fluent/test/fixtures_structure/sparse-messages.json @@ -28,7 +28,9 @@ "type": "select", "selector": { "type": "var", - "name": "sel" + "name": "sel", + "attr": null, + "args": null }, "variants": [ { diff --git a/fluent/test/fixtures_structure/term.json b/fluent/test/fixtures_structure/term.json index 37d6ac102..4e4aedd14 100644 --- a/fluent/test/fixtures_structure/term.json +++ b/fluent/test/fixtures_structure/term.json @@ -24,11 +24,8 @@ "update-command": [ "Zaktualizuj ", { - "type": "getvar", - "ref": { - "type": "ref", - "name": "-brand-name" - }, + "type": "term", + "name": "brand-name", "selector": "accusative" }, "." @@ -37,20 +34,20 @@ { "type": "select", "selector": { - "type": "getattr", - "ref": { - "type": "ref", - "name": "-brand-name" - }, - "name": "gender" + "type": "term", + "name": "brand-name", + "attr": "gender", + "args": null }, "variants": [ { "key": "masculine", "value": [ { - "type": "ref", - "name": "-brand-name" + "type": "term", + "name": "brand-name", + "attr": null, + "args": null }, " został pomyślnie zaktualizowany." ] @@ -59,8 +56,10 @@ "key": "feminine", "value": [ { - "type": "ref", - "name": "-brand-name" + "type": "term", + "name": "brand-name", + "attr": null, + "args": null }, " została pomyślnie zaktualizowana." ] @@ -70,8 +69,10 @@ "value": [ "Program ", { - "type": "ref", - "name": "-brand-name" + "type": "term", + "name": "brand-name", + "attr": null, + "args": null }, " został pomyślnie zaktualizowany." ] diff --git a/fluent/test/fixtures_structure/variant_keys.json b/fluent/test/fixtures_structure/variant_keys.json new file mode 100644 index 000000000..fbd2deb54 --- /dev/null +++ b/fluent/test/fixtures_structure/variant_keys.json @@ -0,0 +1,40 @@ +{ + "key01": [ + { + "type": "select", + "selector": { + "type": "var", + "name": "sel", + "attr": null, + "args": null + }, + "variants": [ + { + "key": "key", + "value": "Value" + } + ], + "star": 0 + } + ], + "key02": [ + { + "type": "select", + "selector": { + "type": "var", + "name": "sel", + "attr": null, + "args": null + }, + "variants": [ + { + "key": "key", + "value": [ + "Value" + ] + } + ], + "star": 0 + } + ] +} diff --git a/fluent/test/fixtures_structure/whitespace_trailing.json b/fluent/test/fixtures_structure/whitespace_trailing.json index 381195600..8e7512c7d 100644 --- a/fluent/test/fixtures_structure/whitespace_trailing.json +++ b/fluent/test/fixtures_structure/whitespace_trailing.json @@ -5,7 +5,9 @@ "Value ", { "type": "ref", - "name": "placeable" + "name": "placeable", + "attr": null, + "args": null }, "." ], diff --git a/fluent/test/macros_test.js b/fluent/test/macros_test.js index e1e9c775d..1f8d82f31 100644 --- a/fluent/test/macros_test.js +++ b/fluent/test/macros_test.js @@ -344,4 +344,96 @@ suite("Macros", function() { assert.equal(errs.length, 0); }); }); + + suite("Parameterized term attributes", function(){ + suiteSetup(function() { + bundle = new FluentBundle("en-US", { + useIsolating: false, + }); + bundle.addMessages(ftl` + -ship = Ship + .gender = {$style -> + *[traditional] neuter + [chicago] feminine + } + + ref-attr = {-ship.gender -> + *[masculine] He + [feminine] She + [neuter] It + } + call-attr-no-args = {-ship.gender() -> + *[masculine] He + [feminine] She + [neuter] It + } + call-attr-with-expected-arg = {-ship.gender(style: "chicago") -> + *[masculine] He + [feminine] She + [neuter] It + } + call-attr-with-other-arg = {-ship.gender(other: 3) -> + *[masculine] He + [feminine] She + [neuter] It + } + `); + }); + + test("Not parameterized, no externals", function() { + const msg = bundle.getMessage("ref-attr"); + const val = bundle.format(msg, {}, errs); + assert.equal(val, "It"); + assert.equal(errs.length, 1); + }); + + test("Not parameterized but with externals", function() { + const msg = bundle.getMessage("ref-attr"); + const val = bundle.format(msg, {style: "chicago"}, errs); + assert.equal(val, "It"); + assert.equal(errs.length, 1); + }); + + test("No arguments, no externals", function() { + const msg = bundle.getMessage("call-attr-no-args"); + const val = bundle.format(msg, {}, errs); + assert.equal(val, "It"); + assert.equal(errs.length, 1); + }); + + test("No arguments, but with externals", function() { + const msg = bundle.getMessage("call-attr-no-args"); + const val = bundle.format(msg, {style: "chicago"}, errs); + assert.equal(val, "It"); + assert.equal(errs.length, 1); + }); + + test("With expected args, no externals", function() { + const msg = bundle.getMessage("call-attr-with-expected-arg"); + const val = bundle.format(msg, {}, errs); + assert.equal(val, "She"); + assert.equal(errs.length, 0); + }); + + test("With expected args, and with externals", function() { + const msg = bundle.getMessage("call-attr-with-expected-arg"); + const val = bundle.format(msg, {style: "chicago"}, errs); + assert.equal(val, "She"); + assert.equal(errs.length, 0); + }); + + test("With other args, no externals", function() { + const msg = bundle.getMessage("call-attr-with-other-arg"); + const val = bundle.format(msg, {}, errs); + assert.equal(val, "It"); + assert.equal(errs.length, 1); + }); + + test("With other args, and with externals", function() { + const msg = bundle.getMessage("call-attr-with-other-arg"); + const val = bundle.format(msg, {style: "chicago"}, errs); + assert.equal(val, "It"); + assert.equal(errs.length, 1); + }); + }); }); diff --git a/fluent/test/values_ref_test.js b/fluent/test/values_ref_test.js index 4992d6827..10695eed9 100644 --- a/fluent/test/values_ref_test.js +++ b/fluent/test/values_ref_test.js @@ -12,12 +12,12 @@ suite('Referencing values', function(){ bundle = new FluentBundle('en-US', { useIsolating: false }); bundle.addMessages(ftl` key1 = Value 1 - key2 = { + -key2 = { [a] A2 *[b] B2 } key3 = Value { 3 } - key4 = { + -key4 = { [a] A{ 4 } *[b] B{ 4 } } @@ -26,16 +26,16 @@ suite('Referencing values', function(){ .b = B5 ref1 = { key1 } - ref2 = { key2 } + ref2 = { -key2 } ref3 = { key3 } - ref4 = { key4 } + ref4 = { -key4 } ref5 = { key5 } - ref6 = { key2[a] } - ref7 = { key2[b] } + ref6 = { -key2[a] } + ref7 = { -key2[b] } - ref8 = { key4[a] } - ref9 = { key4[b] } + ref8 = { -key4[a] } + ref9 = { -key4[b] } ref10 = { key5.a } ref11 = { key5.b }