From a7c58ea5c4716a4f4580173a81bfe8acc1f09fd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sta=C5=9B=20Ma=C5=82olepszy?= Date: Mon, 26 Nov 2018 14:16:43 +0100 Subject: [PATCH 1/3] (fluent) Print missing variables with the $ sigil --- fluent/src/resolver.js | 4 ++-- fluent/test/arguments_test.js | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/fluent/src/resolver.js b/fluent/src/resolver.js index b1a29a0d1..f69d61432 100644 --- a/fluent/src/resolver.js +++ b/fluent/src/resolver.js @@ -363,7 +363,7 @@ function VariableReference(env, {name}) { if (!args || !args.hasOwnProperty(name)) { errors.push(new ReferenceError(`Unknown variable: ${name}`)); - return new FluentNone(name); + return new FluentNone(`$${name}`); } const arg = args[name]; @@ -387,7 +387,7 @@ function VariableReference(env, {name}) { errors.push( new TypeError(`Unsupported variable type: ${name}, ${typeof arg}`) ); - return new FluentNone(name); + return new FluentNone(`$${name}`); } } diff --git a/fluent/test/arguments_test.js b/fluent/test/arguments_test.js index 4e7a452d0..ef51e6086 100644 --- a/fluent/test/arguments_test.js +++ b/fluent/test/arguments_test.js @@ -101,49 +101,49 @@ suite('Variables', function() { test('falls back to argument\'s name if it\'s missing', function() { const msg = bundle.getMessage('foo'); const val = bundle.format(msg, {}, errs); - assert.equal(val, 'arg'); + assert.equal(val, '$arg'); assert(errs[0] instanceof ReferenceError); // unknown variable }); test('cannot be arrays', function() { const msg = bundle.getMessage('foo'); const val = bundle.format(msg, { arg: [1, 2, 3] }, errs); - assert.equal(val, 'arg'); + assert.equal(val, '$arg'); assert(errs[0] instanceof TypeError); // unsupported variable type }); test('cannot be a dict-like object', function() { const msg = bundle.getMessage('foo'); const val = bundle.format(msg, { arg: { prop: 1 } }, errs); - assert.equal(val, 'arg'); + assert.equal(val, '$arg'); assert(errs[0] instanceof TypeError); // unsupported variable type }); test('cannot be a boolean', function() { const msg = bundle.getMessage('foo'); const val = bundle.format(msg, { arg: true }, errs); - assert.equal(val, 'arg'); + assert.equal(val, '$arg'); assert(errs[0] instanceof TypeError); // unsupported variable type }); test('cannot be undefined', function() { const msg = bundle.getMessage('foo'); const val = bundle.format(msg, { arg: undefined }, errs); - assert.equal(val, 'arg'); + assert.equal(val, '$arg'); assert(errs[0] instanceof TypeError); // unsupported variable type }); test('cannot be null', function() { const msg = bundle.getMessage('foo'); const val = bundle.format(msg, { arg: null }, errs); - assert.equal(val, 'arg'); + assert.equal(val, '$arg'); assert(errs[0] instanceof TypeError); // unsupported variable type }); test('cannot be a function', function() { const msg = bundle.getMessage('foo'); const val = bundle.format(msg, { arg: () => null }, errs); - assert.equal(val, 'arg'); + assert.equal(val, '$arg'); assert(errs[0] instanceof TypeError); // unsupported variable type }); }); From 8d5306d8a03ac57eb3d26bcdea32808ee98d5f73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sta=C5=9B=20Ma=C5=82olepszy?= Date: Mon, 26 Nov 2018 14:15:15 +0100 Subject: [PATCH 2/3] (fluent-syntax) Add parameterized terms --- fluent-syntax/src/errors.js | 2 +- fluent-syntax/src/parser.js | 67 +++--- fluent-syntax/src/serializer.js | 6 +- fluent-syntax/test/fixtures_behavior/term.ftl | 13 +- .../fixtures_reference/call_expressions.ftl | 3 +- .../fixtures_reference/call_expressions.json | 38 +++- .../fixtures_reference/select_expressions.ftl | 16 +- .../select_expressions.json | 22 +- .../fixtures_reference/term_parameters.ftl | 8 + .../fixtures_reference/term_parameters.json | 203 ++++++++++++++++++ fluent-syntax/test/serializer_test.js | 7 + 11 files changed, 328 insertions(+), 57 deletions(-) create mode 100644 fluent-syntax/test/fixtures_reference/term_parameters.ftl create mode 100644 fluent-syntax/test/fixtures_reference/term_parameters.json diff --git a/fluent-syntax/src/errors.js b/fluent-syntax/src/errors.js index b13c301e3..1cc8f1c57 100644 --- a/fluent-syntax/src/errors.js +++ b/fluent-syntax/src/errors.js @@ -51,7 +51,7 @@ function getErrorMessage(code, args) { case "E0016": return "Message references cannot be used as selectors"; case "E0017": - return "Variants cannot be used as selectors"; + return "Terms cannot be used as selectors"; case "E0018": return "Attributes of messages cannot be used as selectors"; case "E0019": diff --git a/fluent-syntax/src/parser.js b/fluent-syntax/src/parser.js index 89376f267..61a06d7c7 100644 --- a/fluent-syntax/src/parser.js +++ b/fluent-syntax/src/parser.js @@ -655,12 +655,18 @@ export default class FluentParser { throw new ParseError("E0016"); } - if (selector.type === "AttributeExpression" && - selector.ref.type === "MessageReference") { + if (selector.type === "AttributeExpression" + && selector.ref.type === "MessageReference") { throw new ParseError("E0018"); } - if (selector.type === "VariantExpression") { + if (selector.type === "TermReference" + || selector.type === "VariantExpression") { + throw new ParseError("E0017"); + } + + if (selector.type === "CallExpression" + && selector.callee.type === "TermReference") { throw new ParseError("E0017"); } @@ -672,8 +678,8 @@ export default class FluentParser { const variants = this.getVariants(ps, {allowVariantList: false}); return new AST.SelectExpression(selector, variants); - } else if (selector.type === "AttributeExpression" && - selector.ref.type === "TermReference") { + } else if (selector.type === "AttributeExpression" + && selector.ref.type === "TermReference") { throw new ParseError("E0019"); } @@ -685,60 +691,59 @@ export default class FluentParser { return this.getPlaceable(ps); } - const literal = this.getLiteral(ps); - - if (literal.type !== "MessageReference" - && literal.type !== "TermReference") { - return literal; + const selector = this.getLiteral(ps); + switch (selector.type) { + case "StringLiteral": + case "NumberLiteral": + case "VariableReference": + return selector; } - const ch = ps.currentChar; - - if (ch === ".") { + if (ps.currentChar === ".") { ps.next(); - const attr = this.getIdentifier(ps); - return new AST.AttributeExpression(literal, attr); + return new AST.AttributeExpression(selector, attr); } - if (ch === "[") { + if (ps.currentChar === "[") { ps.next(); - if (literal.type === "MessageReference") { + if (selector.type === "MessageReference") { throw new ParseError("E0024"); } const key = this.getVariantKey(ps); - ps.expectChar("]"); - - return new AST.VariantExpression(literal, key); + return new AST.VariantExpression(selector, key); } - if (ch === "(") { + if (ps.currentChar === "(") { ps.next(); - if (!/^[A-Z][A-Z_?-]*$/.test(literal.id.name)) { - throw new ParseError("E0008"); + if (selector.type === "MessageReference") { + if (/^[A-Z][A-Z_?-]*$/.test(selector.id.name)) { + // The callee is a Function. + var func = new AST.FunctionReference(selector.id); + if (this.withSpans) { + func.addSpan(selector.span.start, selector.span.end); + } + } else { + // Messages can't be callees. + throw new ParseError("E0008"); + } } const args = this.getCallArgs(ps); - ps.expectChar(")"); - const func = new AST.FunctionReference(literal.id); - if (this.withSpans) { - func.addSpan(literal.span.start, literal.span.end); - } - return new AST.CallExpression( - func, + func || selector, args.positional, args.named, ); } - return literal; + return selector; } getCallArg(ps) { diff --git a/fluent-syntax/src/serializer.js b/fluent-syntax/src/serializer.js index 48ef8d745..3c2c10a57 100644 --- a/fluent-syntax/src/serializer.js +++ b/fluent-syntax/src/serializer.js @@ -265,13 +265,13 @@ function serializeVariantExpression(expr) { function serializeCallExpression(expr) { - const fun = serializeExpression(expr.callee); + const callee = serializeExpression(expr.callee); const positional = expr.positional.map(serializeExpression).join(", "); const named = expr.named.map(serializeNamedArgument).join(", "); if (expr.positional.length > 0 && expr.named.length > 0) { - return `${fun}(${positional}, ${named})`; + return `${callee}(${positional}, ${named})`; } - return `${fun}(${positional || named})`; + return `${callee}(${positional || named})`; } diff --git a/fluent-syntax/test/fixtures_behavior/term.ftl b/fluent-syntax/test/fixtures_behavior/term.ftl index 58426d638..303225fde 100644 --- a/fluent-syntax/test/fixtures_behavior/term.ftl +++ b/fluent-syntax/test/fixtures_behavior/term.ftl @@ -11,22 +11,21 @@ key2 = key3 = Test { -brand-short-name[accusative] } +key4 = { -brand() } + +# ~ERROR E0004, pos 306, args "0-9" err1 = { $foo -> [one] Foo *[-other] Foo 2 } -# ~ERROR E0004, pos 285, args "0-9" +# ~ERROR E0004, pos 336, args "a-zA-Z" err2 = { $-foo } -# ~ERROR E0004, pos 315, args "a-zA-Z" - -err4 = { -brand() } -# ~ERROR E0008, pos 339 --err5 = # ~ERROR E0006, pos 351, args "err5" +-err5 = +# ~ERROR E0006, pos 360, args "err6" -err6 = .attr = Attribute -# ~ERROR E0006, pos 360, args "err6" diff --git a/fluent-syntax/test/fixtures_reference/call_expressions.ftl b/fluent-syntax/test/fixtures_reference/call_expressions.ftl index 9ed69bd1b..a4f61da40 100644 --- a/fluent-syntax/test/fixtures_reference/call_expressions.ftl +++ b/fluent-syntax/test/fixtures_reference/call_expressions.ftl @@ -1,14 +1,13 @@ ## 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 TermReference is not a valid callee. -term-callee = {-term()} # ERROR VariableReference is not a valid callee. variable-callee = {$variable()} diff --git a/fluent-syntax/test/fixtures_reference/call_expressions.json b/fluent-syntax/test/fixtures_reference/call_expressions.json index 0a3ae0697..4cc36d404 100644 --- a/fluent-syntax/test/fixtures_reference/call_expressions.json +++ b/fluent-syntax/test/fixtures_reference/call_expressions.json @@ -34,6 +34,35 @@ "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." @@ -52,15 +81,6 @@ "annotations": [], "content": "message-callee = {message()}\n" }, - { - "type": "Comment", - "content": "ERROR TermReference is not a valid callee." - }, - { - "type": "Junk", - "annotations": [], - "content": "term-callee = {-term()}\n" - }, { "type": "Comment", "content": "ERROR VariableReference is not a valid callee." diff --git a/fluent-syntax/test/fixtures_reference/select_expressions.ftl b/fluent-syntax/test/fixtures_reference/select_expressions.ftl index 859c01a04..ac888262b 100644 --- a/fluent-syntax/test/fixtures_reference/select_expressions.ftl +++ b/fluent-syntax/test/fixtures_reference/select_expressions.ftl @@ -4,17 +4,29 @@ new-messages = *[other] {""}Other } -valid-selector = +valid-selector-term-attribute = { -term.case -> *[key] value } # ERROR -invalid-selector = +invalid-selector-term-value = + { -term -> + *[key] value + } + +# ERROR +invalid-selector-term-variant = { -term[case] -> *[key] value } +# ERROR +invalid-selector-term-call = + { -term(case: "nominative") -> + *[key] value + } + empty-variant = { 1 -> *[one] {""} diff --git a/fluent-syntax/test/fixtures_reference/select_expressions.json b/fluent-syntax/test/fixtures_reference/select_expressions.json index 61e18fa5c..7cce4070d 100644 --- a/fluent-syntax/test/fixtures_reference/select_expressions.json +++ b/fluent-syntax/test/fixtures_reference/select_expressions.json @@ -81,7 +81,7 @@ "type": "Message", "id": { "type": "Identifier", - "name": "valid-selector" + "name": "valid-selector-term-attribute" }, "value": { "type": "Pattern", @@ -137,7 +137,25 @@ { "type": "Junk", "annotations": [], - "content": "invalid-selector =\n { -term[case] ->\n *[key] value\n }\n" + "content": "invalid-selector-term-value =\n { -term ->\n *[key] value\n }\n" + }, + { + "type": "Comment", + "content": "ERROR" + }, + { + "type": "Junk", + "annotations": [], + "content": "invalid-selector-term-variant =\n { -term[case] ->\n *[key] value\n }\n" + }, + { + "type": "Comment", + "content": "ERROR" + }, + { + "type": "Junk", + "annotations": [], + "content": "invalid-selector-term-call =\n { -term(case: \"nominative\") ->\n *[key] value\n }\n" }, { "type": "Message", diff --git a/fluent-syntax/test/fixtures_reference/term_parameters.ftl b/fluent-syntax/test/fixtures_reference/term_parameters.ftl new file mode 100644 index 000000000..614423611 --- /dev/null +++ b/fluent-syntax/test/fixtures_reference/term_parameters.ftl @@ -0,0 +1,8 @@ +-term = { $arg -> + *[key] Value +} + +key01 = { -term } +key02 = { -term() } +key03 = { -term(arg: 1) } +key04 = { -term("positional", narg1: 1, narg2: 2) } diff --git a/fluent-syntax/test/fixtures_reference/term_parameters.json b/fluent-syntax/test/fixtures_reference/term_parameters.json new file mode 100644 index 000000000..f9f09613e --- /dev/null +++ b/fluent-syntax/test/fixtures_reference/term_parameters.json @@ -0,0 +1,203 @@ +{ + "type": "Resource", + "body": [ + { + "type": "Term", + "id": { + "type": "Identifier", + "name": "term" + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "Placeable", + "expression": { + "type": "SelectExpression", + "selector": { + "type": "VariableReference", + "id": { + "type": "Identifier", + "name": "arg" + } + }, + "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": "key01" + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "Placeable", + "expression": { + "type": "TermReference", + "id": { + "type": "Identifier", + "name": "term" + } + } + } + ] + }, + "attributes": [], + "comment": null + }, + { + "type": "Message", + "id": { + "type": "Identifier", + "name": "key02" + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "Placeable", + "expression": { + "type": "CallExpression", + "callee": { + "type": "TermReference", + "id": { + "type": "Identifier", + "name": "term" + } + }, + "positional": [], + "named": [] + } + } + ] + }, + "attributes": [], + "comment": null + }, + { + "type": "Message", + "id": { + "type": "Identifier", + "name": "key03" + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "Placeable", + "expression": { + "type": "CallExpression", + "callee": { + "type": "TermReference", + "id": { + "type": "Identifier", + "name": "term" + } + }, + "positional": [], + "named": [ + { + "type": "NamedArgument", + "name": { + "type": "Identifier", + "name": "arg" + }, + "value": { + "type": "NumberLiteral", + "value": "1" + } + } + ] + } + } + ] + }, + "attributes": [], + "comment": null + }, + { + "type": "Message", + "id": { + "type": "Identifier", + "name": "key04" + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "Placeable", + "expression": { + "type": "CallExpression", + "callee": { + "type": "TermReference", + "id": { + "type": "Identifier", + "name": "term" + } + }, + "positional": [ + { + "type": "StringLiteral", + "raw": "positional", + "value": "positional" + } + ], + "named": [ + { + "type": "NamedArgument", + "name": { + "type": "Identifier", + "name": "narg1" + }, + "value": { + "type": "NumberLiteral", + "value": "1" + } + }, + { + "type": "NamedArgument", + "name": { + "type": "Identifier", + "name": "narg2" + }, + "value": { + "type": "NumberLiteral", + "value": "2" + } + } + ] + } + } + ] + }, + "attributes": [], + "comment": null + } + ] +} diff --git a/fluent-syntax/test/serializer_test.js b/fluent-syntax/test/serializer_test.js index cb32b9caa..54c537b32 100644 --- a/fluent-syntax/test/serializer_test.js +++ b/fluent-syntax/test/serializer_test.js @@ -450,6 +450,13 @@ suite("Serialize resource", function() { assert.equal(pretty(input), input); }); + test("macro call", function() { + const input = ftl` + foo = { -term() } + `; + assert.equal(pretty(input), input); + }); + test("nested placeables", function() { const input = ftl` foo = {{ FOO() }} From ecf32071f3f81e49f5c32e549fc77cf53c504257 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sta=C5=9B=20Ma=C5=82olepszy?= Date: Mon, 26 Nov 2018 14:16:58 +0100 Subject: [PATCH 3/3] (fluent) Add parameterized terms --- fluent/src/resolver.js | 58 ++- fluent/src/resource.js | 3 +- fluent/test/fixtures_behavior/term.json | 3 + .../fixtures_reference/call_expressions.json | 140 +++---- .../select_expressions.json | 53 ++- .../fixtures_reference/term_parameters.json | 84 +++++ .../expressions_call_args.json | 4 +- fluent/test/macros_test.js | 347 ++++++++++++++++++ 8 files changed, 601 insertions(+), 91 deletions(-) create mode 100644 fluent/test/fixtures_reference/term_parameters.json create mode 100644 fluent/test/macros_test.js diff --git a/fluent/src/resolver.js b/fluent/src/resolver.js index f69d61432..7eeb08cba 100644 --- a/fluent/src/resolver.js +++ b/fluent/src/resolver.js @@ -29,9 +29,10 @@ * 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 in - * `CallExpression`) resolve to an instance of `FluentType`. The caller should - * use the `toString` method to convert the instance to a native value. + * 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 functions in this file pass around a special object called `env`. @@ -138,12 +139,12 @@ function DefaultMember(env, members, star) { */ function MessageReference(env, {name}) { const { bundle, errors } = env; - const message = name.startsWith("-") + const message = name[0] === "-" ? bundle._terms.get(name) : bundle._messages.get(name); if (!message) { - const err = name.startsWith("-") + const err = name[0] === "-" ? new ReferenceError(`Unknown term: ${name}`) : new ReferenceError(`Unknown message: ${name}`); errors.push(err); @@ -311,13 +312,15 @@ function Type(env, expr) { return new FluentNumber(expr.value); case "var": return VariableReference(env, expr); - case "func": - return FunctionReference(env, expr); case "call": - return CallExpression(env, expr); + return expr.ref.name[0] === "-" + ? MacroExpression(env, expr) + : FunctionExpression(env, expr); case "ref": { const message = MessageReference(env, expr); - return Type(env, message); + return expr.name[0] === "-" + ? Type({...env, args: {}}, message) + : Type(env, message); } case "getattr": { const attr = AttributeExpression(env, expr); @@ -429,16 +432,15 @@ function FunctionReference(env, {name}) { * Resolver environment object. * @param {Object} expr * An expression to be resolved. - * @param {Object} expr.callee + * @param {Object} expr.ref * FTL Function object. * @param {Array} expr.args * FTL Function argument list. * @returns {FluentType} * @private */ -function CallExpression(env, {callee, args}) { - const func = FunctionReference(env, callee); - +function FunctionExpression(env, {ref, args}) { + const func = FunctionReference(env, ref); if (func instanceof FluentNone) { return func; } @@ -462,6 +464,36 @@ function CallExpression(env, {callee, args}) { } } +/** + * 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; + } + + const keyargs = {}; + for (const arg of args) { + if (arg.type === "narg") { + keyargs[arg.name] = Type(env, arg.value); + } + } + + return Type({...env, args: keyargs}, callee); +} + /** * Resolve a pattern (a complex string with placeables). * diff --git a/fluent/src/resource.js b/fluent/src/resource.js index d1b6421c4..7afa41a3e 100644 --- a/fluent/src/resource.js +++ b/fluent/src/resource.js @@ -311,8 +311,7 @@ export default class FluentResource extends Map { } if (consumeToken(TOKEN_PAREN_OPEN)) { - let callee = {...ref, type: "func"}; - return {type: "call", callee, args: parseArguments()}; + return {type: "call", ref, args: parseArguments()}; } return ref; diff --git a/fluent/test/fixtures_behavior/term.json b/fluent/test/fixtures_behavior/term.json index e8825fb2b..f90a9c246 100644 --- a/fluent/test/fixtures_behavior/term.json +++ b/fluent/test/fixtures_behavior/term.json @@ -13,5 +13,8 @@ }, "key3": { "value": true + }, + "key4": { + "value": true } } diff --git a/fluent/test/fixtures_reference/call_expressions.json b/fluent/test/fixtures_reference/call_expressions.json index 5f7c8ad63..10682530e 100644 --- a/fluent/test/fixtures_reference/call_expressions.json +++ b/fluent/test/fixtures_reference/call_expressions.json @@ -2,39 +2,39 @@ "function-callee": [ { "type": "call", - "callee": { - "type": "func", + "ref": { + "type": "ref", "name": "FUNCTION" }, "args": [] } ], - "mixed-case-callee": [ + "term-callee": [ { "type": "call", - "callee": { - "type": "func", - "name": "Function" + "ref": { + "type": "ref", + "name": "-term" }, "args": [] } ], - "message-callee": [ + "mixed-case-callee": [ { "type": "call", - "callee": { - "type": "func", - "name": "message" + "ref": { + "type": "ref", + "name": "Function" }, "args": [] } ], - "term-callee": [ + "message-callee": [ { "type": "call", - "callee": { - "type": "func", - "name": "-term" + "ref": { + "type": "ref", + "name": "message" }, "args": [] } @@ -42,8 +42,8 @@ "positional-args": [ { "type": "call", - "callee": { - "type": "func", + "ref": { + "type": "ref", "name": "FUN" }, "args": [ @@ -65,8 +65,8 @@ "named-args": [ { "type": "call", - "callee": { - "type": "func", + "ref": { + "type": "ref", "name": "FUN" }, "args": [ @@ -92,8 +92,8 @@ "dense-named-args": [ { "type": "call", - "callee": { - "type": "func", + "ref": { + "type": "ref", "name": "FUN" }, "args": [ @@ -119,8 +119,8 @@ "mixed-args": [ { "type": "call", - "callee": { - "type": "func", + "ref": { + "type": "ref", "name": "FUN" }, "args": [ @@ -158,8 +158,8 @@ "shuffled-args": [ { "type": "call", - "callee": { - "type": "func", + "ref": { + "type": "ref", "name": "FUN" }, "args": [ @@ -197,8 +197,8 @@ "duplicate-named-args": [ { "type": "call", - "callee": { - "type": "func", + "ref": { + "type": "ref", "name": "FUN" }, "args": [ @@ -224,8 +224,8 @@ "sparse-inline-call": [ { "type": "call", - "callee": { - "type": "func", + "ref": { + "type": "ref", "name": "FUN" }, "args": [ @@ -251,8 +251,8 @@ "empty-inline-call": [ { "type": "call", - "callee": { - "type": "func", + "ref": { + "type": "ref", "name": "FUN" }, "args": [] @@ -261,8 +261,8 @@ "multiline-call": [ { "type": "call", - "callee": { - "type": "func", + "ref": { + "type": "ref", "name": "FUN" }, "args": [ @@ -288,8 +288,8 @@ "sparse-multiline-call": [ { "type": "call", - "callee": { - "type": "func", + "ref": { + "type": "ref", "name": "FUN" }, "args": [ @@ -315,8 +315,8 @@ "empty-multiline-call": [ { "type": "call", - "callee": { - "type": "func", + "ref": { + "type": "ref", "name": "FUN" }, "args": [] @@ -325,8 +325,8 @@ "unindented-arg-number": [ { "type": "call", - "callee": { - "type": "func", + "ref": { + "type": "ref", "name": "FUN" }, "args": [ @@ -340,8 +340,8 @@ "unindented-arg-string": [ { "type": "call", - "callee": { - "type": "func", + "ref": { + "type": "ref", "name": "FUN" }, "args": [ @@ -355,8 +355,8 @@ "unindented-arg-msg-ref": [ { "type": "call", - "callee": { - "type": "func", + "ref": { + "type": "ref", "name": "FUN" }, "args": [ @@ -370,8 +370,8 @@ "unindented-arg-term-ref": [ { "type": "call", - "callee": { - "type": "func", + "ref": { + "type": "ref", "name": "FUN" }, "args": [ @@ -385,8 +385,8 @@ "unindented-arg-var-ref": [ { "type": "call", - "callee": { - "type": "func", + "ref": { + "type": "ref", "name": "FUN" }, "args": [ @@ -400,15 +400,15 @@ "unindented-arg-call": [ { "type": "call", - "callee": { - "type": "func", + "ref": { + "type": "ref", "name": "FUN" }, "args": [ { "type": "call", - "callee": { - "type": "func", + "ref": { + "type": "ref", "name": "OTHER" }, "args": [] @@ -419,8 +419,8 @@ "unindented-named-arg": [ { "type": "call", - "callee": { - "type": "func", + "ref": { + "type": "ref", "name": "FUN" }, "args": [ @@ -438,8 +438,8 @@ "unindented-closing-paren": [ { "type": "call", - "callee": { - "type": "func", + "ref": { + "type": "ref", "name": "FUN" }, "args": [ @@ -453,8 +453,8 @@ "one-argument": [ { "type": "call", - "callee": { - "type": "func", + "ref": { + "type": "ref", "name": "FUN" }, "args": [ @@ -468,8 +468,8 @@ "many-arguments": [ { "type": "call", - "callee": { - "type": "func", + "ref": { + "type": "ref", "name": "FUN" }, "args": [ @@ -491,8 +491,8 @@ "inline-sparse-args": [ { "type": "call", - "callee": { - "type": "func", + "ref": { + "type": "ref", "name": "FUN" }, "args": [ @@ -514,8 +514,8 @@ "mulitline-args": [ { "type": "call", - "callee": { - "type": "func", + "ref": { + "type": "ref", "name": "FUN" }, "args": [ @@ -533,8 +533,8 @@ "mulitline-sparse-args": [ { "type": "call", - "callee": { - "type": "func", + "ref": { + "type": "ref", "name": "FUN" }, "args": [ @@ -552,8 +552,8 @@ "sparse-named-arg": [ { "type": "call", - "callee": { - "type": "func", + "ref": { + "type": "ref", "name": "FUN" }, "args": [ @@ -587,8 +587,8 @@ "unindented-colon": [ { "type": "call", - "callee": { - "type": "func", + "ref": { + "type": "ref", "name": "FUN" }, "args": [ @@ -606,8 +606,8 @@ "unindented-value": [ { "type": "call", - "callee": { - "type": "func", + "ref": { + "type": "ref", "name": "FUN" }, "args": [ diff --git a/fluent/test/fixtures_reference/select_expressions.json b/fluent/test/fixtures_reference/select_expressions.json index 65ed7c426..8b204d856 100644 --- a/fluent/test/fixtures_reference/select_expressions.json +++ b/fluent/test/fixtures_reference/select_expressions.json @@ -4,8 +4,8 @@ "type": "select", "selector": { "type": "call", - "callee": { - "type": "func", + "ref": { + "type": "ref", "name": "BUILTIN" }, "args": [] @@ -28,7 +28,7 @@ "star": 1 } ], - "valid-selector": [ + "valid-selector-term-attribute": [ { "type": "select", "selector": { @@ -48,7 +48,23 @@ "star": 0 } ], - "invalid-selector": [ + "invalid-selector-term-value": [ + { + "type": "select", + "selector": { + "type": "ref", + "name": "-term" + }, + "variants": [ + { + "key": "key", + "value": "value" + } + ], + "star": 0 + } + ], + "invalid-selector-term-variant": [ { "type": "select", "selector": { @@ -68,6 +84,35 @@ "star": 0 } ], + "invalid-selector-term-call": [ + { + "type": "select", + "selector": { + "type": "call", + "ref": { + "type": "ref", + "name": "-term" + }, + "args": [ + { + "type": "narg", + "name": "case", + "value": { + "type": "str", + "value": "nominative" + } + } + ] + }, + "variants": [ + { + "key": "key", + "value": "value" + } + ], + "star": 0 + } + ], "empty-variant": [ { "type": "select", diff --git a/fluent/test/fixtures_reference/term_parameters.json b/fluent/test/fixtures_reference/term_parameters.json new file mode 100644 index 000000000..78764b97c --- /dev/null +++ b/fluent/test/fixtures_reference/term_parameters.json @@ -0,0 +1,84 @@ +{ + "-term": [ + { + "type": "select", + "selector": { + "type": "var", + "name": "arg" + }, + "variants": [ + { + "key": "key", + "value": "Value" + } + ], + "star": 0 + } + ], + "key01": [ + { + "type": "ref", + "name": "-term" + } + ], + "key02": [ + { + "type": "call", + "ref": { + "type": "ref", + "name": "-term" + }, + "args": [] + } + ], + "key03": [ + { + "type": "call", + "ref": { + "type": "ref", + "name": "-term" + }, + "args": [ + { + "type": "narg", + "name": "arg", + "value": { + "type": "num", + "value": "1" + } + } + ] + } + ], + "key04": [ + { + "type": "call", + "ref": { + "type": "ref", + "name": "-term" + }, + "args": [ + { + "type": "str", + "value": "positional" + }, + { + "type": "narg", + "name": "narg1", + "value": { + "type": "num", + "value": "1" + } + }, + { + "type": "narg", + "name": "narg2", + "value": { + "type": "num", + "value": "2" + } + } + ] + } + ] +} diff --git a/fluent/test/fixtures_structure/expressions_call_args.json b/fluent/test/fixtures_structure/expressions_call_args.json index 0739b1d84..a15662ec0 100644 --- a/fluent/test/fixtures_structure/expressions_call_args.json +++ b/fluent/test/fixtures_structure/expressions_call_args.json @@ -2,8 +2,8 @@ "key": [ { "type": "call", - "callee": { - "type": "func", + "ref": { + "type": "ref", "name": "FOO" }, "args": [ diff --git a/fluent/test/macros_test.js b/fluent/test/macros_test.js new file mode 100644 index 000000000..e1e9c775d --- /dev/null +++ b/fluent/test/macros_test.js @@ -0,0 +1,347 @@ +"use strict"; + +import assert from "assert"; + +import FluentBundle from "../src/bundle"; +import { ftl } from "../src/util"; + +suite("Macros", function() { + let bundle, errs; + + setup(function() { + errs = []; + }); + + suite("References and calls", function(){ + suiteSetup(function() { + bundle = new FluentBundle("en-US", { + useIsolating: false, + }); + bundle.addMessages(ftl` + foo = Foo + message-call = {foo()} + + -bar = Bar + term-ref = {-bar} + term-call = {-bar()} + `); + }); + + test("messages cannot be parameterized", function() { + const msg = bundle.getMessage("message-call"); + const val = bundle.format(msg, {}, errs); + assert.equal(val, "foo()"); + assert.equal(errs.length, 1); + }); + + test("terms can be referenced without parens", function() { + const msg = bundle.getMessage("term-ref"); + const val = bundle.format(msg, {}, errs); + assert.equal(val, "Bar"); + assert.equal(errs.length, 0); + }); + + test("terms can be parameterized", function() { + const msg = bundle.getMessage("term-call"); + const val = bundle.format(msg, {}, errs); + assert.equal(val, "Bar"); + assert.equal(errs.length, 0); + }); + }); + + suite("Passing arguments", function(){ + suiteSetup(function() { + bundle = new FluentBundle("en-US", { + useIsolating: false, + }); + bundle.addMessages(ftl` + -foo = Foo {$arg} + + ref-foo = {-foo} + call-foo-no-args = {-foo()} + call-foo-with-expected-arg = {-foo(arg: 1)} + call-foo-with-other-arg = {-foo(other: 3)} + `); + }); + + test("Not parameterized, no externals", function() { + const msg = bundle.getMessage("ref-foo"); + const val = bundle.format(msg, {}, errs); + assert.equal(val, "Foo $arg"); + assert.equal(errs.length, 1); + }); + + test("Not parameterized but with externals", function() { + const msg = bundle.getMessage("ref-foo"); + const val = bundle.format(msg, {arg: 1}, errs); + assert.equal(val, "Foo $arg"); + assert.equal(errs.length, 1); + }); + + test("No arguments, no externals", function() { + const msg = bundle.getMessage("call-foo-no-args"); + const val = bundle.format(msg, {}, errs); + assert.equal(val, "Foo $arg"); + assert.equal(errs.length, 1); + }); + + test("No arguments, but with externals", function() { + const msg = bundle.getMessage("call-foo-no-args"); + const val = bundle.format(msg, {arg: 1}, errs); + assert.equal(val, "Foo $arg"); + assert.equal(errs.length, 1); + }); + + test("With expected args, no externals", function() { + const msg = bundle.getMessage("call-foo-with-expected-arg"); + const val = bundle.format(msg, {}, errs); + assert.equal(val, "Foo 1"); + assert.equal(errs.length, 0); + }); + + test("With expected args, and with externals", function() { + const msg = bundle.getMessage("call-foo-with-expected-arg"); + const val = bundle.format(msg, {arg: 5}, errs); + assert.equal(val, "Foo 1"); + assert.equal(errs.length, 0); + }); + + test("With other args, no externals", function() { + const msg = bundle.getMessage("call-foo-with-other-arg"); + const val = bundle.format(msg, {}, errs); + assert.equal(val, "Foo $arg"); + assert.equal(errs.length, 1); + }); + + test("With other args, and with externals", function() { + const msg = bundle.getMessage("call-foo-with-other-arg"); + const val = bundle.format(msg, {arg: 5}, errs); + assert.equal(val, "Foo $arg"); + assert.equal(errs.length, 1); + }); + }); + + suite("Nesting message references", function(){ + suiteSetup(function() { + bundle = new FluentBundle("en-US", { + useIsolating: false, + }); + bundle.addMessages(ftl` + foo = Foo {$arg} + -bar = {foo} + ref-bar = {-bar} + call-bar = {-bar()} + call-bar-with-arg = {-bar(arg: 1)} + `); + }); + + test("No parameterization, no externals", function() { + const msg = bundle.getMessage("ref-bar"); + const val = bundle.format(msg, {}, errs); + assert.equal(val, "Foo $arg"); + assert.equal(errs.length, 1); + }); + + test("No parameterization, but with externals", function() { + const msg = bundle.getMessage("ref-bar"); + const val = bundle.format(msg, {arg: 5}, errs); + assert.equal(val, "Foo $arg"); + assert.equal(errs.length, 1); + }); + + test("No arguments, no externals", function() { + const msg = bundle.getMessage("call-bar"); + const val = bundle.format(msg, {}, errs); + assert.equal(val, "Foo $arg"); + assert.equal(errs.length, 1); + }); + + test("No arguments, but with externals", function() { + const msg = bundle.getMessage("call-bar"); + const val = bundle.format(msg, {arg: 5}, errs); + assert.equal(val, "Foo $arg"); + assert.equal(errs.length, 1); + }); + + test("With arguments, no externals", function() { + const msg = bundle.getMessage("call-bar-with-arg"); + const val = bundle.format(msg, {}, errs); + assert.equal(val, "Foo 1"); + assert.equal(errs.length, 0); + + }); + test("With arguments and with externals", function() { + const msg = bundle.getMessage("call-bar-with-arg"); + const val = bundle.format(msg, {arg: 5}, errs); + assert.equal(val, "Foo 1"); + assert.equal(errs.length, 0); + }); + }); + + suite("Nesting term references", function(){ + suiteSetup(function() { + bundle = new FluentBundle("en-US", { + useIsolating: false, + }); + bundle.addMessages(ftl` + -foo = Foo {$arg} + -bar = {-foo} + -baz = {-foo()} + -qux = {-foo(arg: 1)} + + ref-bar = {-bar} + ref-baz = {-baz} + ref-qux = {-qux} + + call-bar-no-args = {-bar()} + call-baz-no-args = {-baz()} + call-qux-no-args = {-qux()} + + call-bar-with-arg = {-bar(arg: 2)} + call-baz-with-arg = {-baz(arg: 2)} + call-qux-with-arg = {-qux(arg: 2)} + call-qux-with-other = {-qux(other: 3)} + `); + }); + + test("No parameterization, no parameterization, no externals", function() { + const msg = bundle.getMessage("ref-bar"); + const val = bundle.format(msg, {}, errs); + assert.equal(val, "Foo $arg"); + assert.equal(errs.length, 1); + }); + + test("No parameterization, no parameterization, with externals", function() { + const msg = bundle.getMessage("ref-bar"); + const val = bundle.format(msg, {arg: 5}, errs); + assert.equal(val, "Foo $arg"); + assert.equal(errs.length, 1); + }); + + test("No parameterization, no arguments, no externals", function() { + const msg = bundle.getMessage("ref-baz"); + const val = bundle.format(msg, {}, errs); + assert.equal(val, "Foo $arg"); + assert.equal(errs.length, 1); + }); + + test("No parameterization, no arguments, with externals", function() { + const msg = bundle.getMessage("ref-baz"); + const val = bundle.format(msg, {arg: 5}, errs); + assert.equal(val, "Foo $arg"); + assert.equal(errs.length, 1); + }); + + test("No parameterization, with arguments, no externals", function() { + const msg = bundle.getMessage("ref-qux"); + const val = bundle.format(msg, {}, errs); + assert.equal(val, "Foo 1"); + assert.equal(errs.length, 0); + }); + + test("No parameterization, with arguments, with externals", function() { + const msg = bundle.getMessage("ref-qux"); + const val = bundle.format(msg, {arg: 5}, errs); + assert.equal(val, "Foo 1"); + assert.equal(errs.length, 0); + }); + + test("No arguments, no parameterization, no externals", function() { + const msg = bundle.getMessage("call-bar-no-args"); + const val = bundle.format(msg, {}, errs); + assert.equal(val, "Foo $arg"); + assert.equal(errs.length, 1); + }); + + test("No arguments, no parameterization, with externals", function() { + const msg = bundle.getMessage("call-bar-no-args"); + const val = bundle.format(msg, {arg: 5}, errs); + assert.equal(val, "Foo $arg"); + assert.equal(errs.length, 1); + }); + + test("No arguments, no arguments, no externals", function() { + const msg = bundle.getMessage("call-baz-no-args"); + const val = bundle.format(msg, {}, errs); + assert.equal(val, "Foo $arg"); + assert.equal(errs.length, 1); + }); + + test("No arguments, no arguments, with externals", function() { + const msg = bundle.getMessage("call-baz-no-args"); + const val = bundle.format(msg, {arg: 5}, errs); + assert.equal(val, "Foo $arg"); + assert.equal(errs.length, 1); + }); + + test("No arguments, with arguments, no externals", function() { + const msg = bundle.getMessage("call-qux-no-args"); + const val = bundle.format(msg, {}, errs); + assert.equal(val, "Foo 1"); + assert.equal(errs.length, 0); + }); + + test("No arguments, with arguments, with externals", function() { + const msg = bundle.getMessage("call-qux-no-args"); + const val = bundle.format(msg, {arg: 5}, errs); + assert.equal(val, "Foo 1"); + assert.equal(errs.length, 0); + }); + + test("With arguments, no parameterization, no externals", function() { + const msg = bundle.getMessage("call-bar-with-arg"); + const val = bundle.format(msg, {}, errs); + assert.equal(val, "Foo $arg"); + assert.equal(errs.length, 1); + }); + + test("With arguments, no parameterization, with externals", function() { + const msg = bundle.getMessage("call-bar-with-arg"); + const val = bundle.format(msg, {arg: 5}, errs); + assert.equal(val, "Foo $arg"); + assert.equal(errs.length, 1); + }); + + test("With arguments, no arguments, no externals", function() { + const msg = bundle.getMessage("call-baz-with-arg"); + const val = bundle.format(msg, {}, errs); + assert.equal(val, "Foo $arg"); + assert.equal(errs.length, 1); + }); + + test("With arguments, no arguments, with externals", function() { + const msg = bundle.getMessage("call-baz-with-arg"); + const val = bundle.format(msg, {arg: 5}, errs); + assert.equal(val, "Foo $arg"); + assert.equal(errs.length, 1); + }); + + test("With arguments, with arguments, no externals", function() { + const msg = bundle.getMessage("call-qux-with-arg"); + const val = bundle.format(msg, {}, errs); + assert.equal(val, "Foo 1"); + assert.equal(errs.length, 0); + }); + + test("With arguments, with arguments, with externals", function() { + const msg = bundle.getMessage("call-qux-with-arg"); + const val = bundle.format(msg, {arg: 5}, errs); + assert.equal(val, "Foo 1"); + assert.equal(errs.length, 0); + }); + + test("With unexpected arguments, with arguments, no externals", function() { + const msg = bundle.getMessage("call-qux-with-other"); + const val = bundle.format(msg, {}, errs); + assert.equal(val, "Foo 1"); + assert.equal(errs.length, 0); + }); + + test("With unexpected arguments, with arguments, with externals", function() { + const msg = bundle.getMessage("call-qux-with-other"); + const val = bundle.format(msg, {arg: 5}, errs); + assert.equal(val, "Foo 1"); + assert.equal(errs.length, 0); + }); + }); +});