diff --git a/fluent-react/examples/text-input/src/l10n.js b/fluent-react/examples/text-input/src/l10n.js index 32a224c83..2f8c045ac 100644 --- a/fluent-react/examples/text-input/src/l10n.js +++ b/fluent-react/examples/text-input/src/l10n.js @@ -6,13 +6,13 @@ const MESSAGES_ALL = { 'pl': ` hello = Cześć { $username }! hello-no-name = Witaj nieznajomy! -type-name +type-name = .placeholder = Twoje imię `, 'en-US': ` hello = Hello, { $username }! hello-no-name = Hello, stranger! -type-name +type-name = .placeholder = Your name `, }; diff --git a/fluent-syntax/src/ftlstream.js b/fluent-syntax/src/ftlstream.js index 84eeac937..f97c7394d 100644 --- a/fluent-syntax/src/ftlstream.js +++ b/fluent-syntax/src/ftlstream.js @@ -5,8 +5,18 @@ import { ParseError } from './errors'; import { includes } from './util'; const INLINE_WS = [' ', '\t']; +const SPECIAL_LINE_START_CHARS = ['}', '.', '[', '*']; export class FTLParserStream extends ParserStream { + skipInlineWS() { + while (this.ch) { + if (!includes(INLINE_WS, this.ch)) { + break; + } + this.next(); + } + } + peekInlineWS() { let ch = this.currentPeek(); while (ch) { @@ -46,13 +56,9 @@ export class FTLParserStream extends ParserStream { } } - skipInlineWS() { - while (this.ch) { - if (!includes(INLINE_WS, this.ch)) { - break; - } - this.next(); - } + skipIndent() { + this.skipBlankLines(); + this.skipInlineWS(); } expectChar(ch) { @@ -125,6 +131,24 @@ export class FTLParserStream extends ParserStream { return isDigit; } + isCharPatternStart(ch) { + return !includes(SPECIAL_LINE_START_CHARS, ch); + } + + isPeekPatternStart() { + this.peekInlineWS(); + + const ch = this.currentPeek(); + + if (ch === '\n') { + return this.isPeekNextLinePatternStart(); + } + + const isPattern = this.isCharPatternStart(this.currentPeek()); + this.resetPeek(); + return isPattern; + } + isPeekNextLineZeroFourStyleComment() { if (!this.currentPeekIs('\n')) { return false; @@ -234,7 +258,7 @@ export class FTLParserStream extends ParserStream { return false; } - isPeekNextNonBlankLinePattern() { + isPeekNextLinePatternStart() { if (!this.currentPeekIs('\n')) { return false; } @@ -252,10 +276,7 @@ export class FTLParserStream extends ParserStream { return false; } - if (this.currentPeekIs('}') || - this.currentPeekIs('.') || - this.currentPeekIs('[') || - this.currentPeekIs('*')) { + if (!this.isCharPatternStart(this.currentPeek())) { this.resetPeek(); return false; } diff --git a/fluent-syntax/src/parser.js b/fluent-syntax/src/parser.js index da16bd0e4..70e93a60d 100644 --- a/fluent-syntax/src/parser.js +++ b/fluent-syntax/src/parser.js @@ -74,7 +74,6 @@ export default class FluentParser { entries.push(entry); } - ps.skipInlineWS(); ps.skipBlankLines(); } @@ -258,12 +257,15 @@ export default class FluentParser { let pattern; let attrs; + // XXX Syntax 0.4 compatibility. + // XXX Replace with ps.expectChar('='). if (ps.currentIs('=')) { ps.next(); - ps.skipInlineWS(); - ps.skipBlankLines(); - pattern = this.getPattern(ps); + if (ps.isPeekPatternStart()) { + ps.skipIndent(); + pattern = this.getPattern(ps); + } } if (ps.isPeekNextLineAttributeStart()) { @@ -278,29 +280,27 @@ export default class FluentParser { } getAttribute(ps) { + ps.expectIndent(); ps.expectChar('.'); const key = this.getPublicIdentifier(ps); ps.skipInlineWS(); ps.expectChar('='); - ps.skipInlineWS(); - - const value = this.getPattern(ps); - if (value === undefined) { - throw new ParseError('E0006', 'value'); + if (ps.isPeekPatternStart()) { + ps.skipIndent(); + const value = this.getPattern(ps); + return new AST.Attribute(key, value); } - return new AST.Attribute(key, value); + throw new ParseError('E0006', 'value'); } getAttributes(ps) { const attrs = []; while (true) { - ps.expectIndent(); - const attr = this.getAttribute(ps); attrs.push(attr); @@ -349,6 +349,8 @@ export default class FluentParser { } getVariant(ps, hasDefault) { + ps.expectIndent(); + let defaultIndex = false; if (ps.currentIs('*')) { @@ -366,15 +368,13 @@ export default class FluentParser { ps.expectChar(']'); - ps.skipInlineWS(); - - const value = this.getPattern(ps); - - if (!value) { - throw new ParseError('E0006', 'value'); + if (ps.isPeekPatternStart()) { + ps.skipIndent(); + const value = this.getPattern(ps); + return new AST.Variant(key, value, defaultIndex); } - return new AST.Variant(key, value, defaultIndex); + throw new ParseError('E0006', 'value'); } getVariants(ps) { @@ -382,8 +382,6 @@ export default class FluentParser { let hasDefault = false; while (true) { - ps.expectIndent(); - const variant = this.getVariant(ps, hasDefault); if (variant.default) { @@ -459,18 +457,12 @@ export default class FluentParser { const elements = []; ps.skipInlineWS(); - // Special-case: trim leading whitespace and newlines. - if (ps.isPeekNextNonBlankLinePattern()) { - ps.skipBlankLines(); - ps.skipInlineWS(); - } - let ch; while ((ch = ps.current())) { // The end condition for getPattern's while loop is a newline // which is not followed by a valid pattern continuation. - if (ch === '\n' && !ps.isPeekNextNonBlankLinePattern()) { + if (ch === '\n' && !ps.isPeekNextLinePatternStart()) { break; } @@ -491,13 +483,12 @@ export default class FluentParser { let ch; while ((ch = ps.current())) { - if (ch === '{') { return new AST.TextElement(buffer); } if (ch === '\n') { - if (!ps.isPeekNextNonBlankLinePattern()) { + if (!ps.isPeekNextLinePatternStart()) { return new AST.TextElement(buffer); } diff --git a/fluent-syntax/src/serializer.js b/fluent-syntax/src/serializer.js index 7cd20de30..c3d5373a3 100644 --- a/fluent-syntax/src/serializer.js +++ b/fluent-syntax/src/serializer.js @@ -113,9 +113,9 @@ function serializeMessage(message) { } parts.push(serializeIdentifier(message.id)); + parts.push(' ='); if (message.value) { - parts.push(' ='); parts.push(serializeValue(message.value)); } diff --git a/fluent-syntax/test/fixtures_behavior/attribute_with_empty_pattern.ftl b/fluent-syntax/test/fixtures_behavior/attribute_with_empty_pattern.ftl index 66e4df3e2..c3bd163a1 100644 --- a/fluent-syntax/test/fixtures_behavior/attribute_with_empty_pattern.ftl +++ b/fluent-syntax/test/fixtures_behavior/attribute_with_empty_pattern.ftl @@ -1,2 +1,3 @@ key = Value .label = +//~ ERROR E0006, pos 24, args "value" diff --git a/fluent-syntax/test/fixtures_behavior/variant_with_empty_pattern.ftl b/fluent-syntax/test/fixtures_behavior/variant_with_empty_pattern.ftl index 8dfb5bc14..7db7def1f 100644 --- a/fluent-syntax/test/fixtures_behavior/variant_with_empty_pattern.ftl +++ b/fluent-syntax/test/fixtures_behavior/variant_with_empty_pattern.ftl @@ -1,3 +1,8 @@ -key = { +key1 = { + *[one] {""} + } + +err1 = { *[one] } +//~ ERROR E0006, pos 51, args "value" diff --git a/fluent-syntax/test/fixtures_structure/attribute_with_empty_pattern.ftl b/fluent-syntax/test/fixtures_structure/attribute_with_empty_pattern.ftl new file mode 100644 index 000000000..aba7c4267 --- /dev/null +++ b/fluent-syntax/test/fixtures_structure/attribute_with_empty_pattern.ftl @@ -0,0 +1,17 @@ +key1 = Value 1 + .attr = + +key2 = + .attr = + +key3 = + .attr1 = Attr 1 + .attr2 = + +key4 = + .attr1 = + .attr2 = Attr 2 + +key5 = + .attr1 = + .attr2 = diff --git a/fluent-syntax/test/fixtures_structure/attribute_with_empty_pattern.json b/fluent-syntax/test/fixtures_structure/attribute_with_empty_pattern.json new file mode 100644 index 000000000..7e1fde353 --- /dev/null +++ b/fluent-syntax/test/fixtures_structure/attribute_with_empty_pattern.json @@ -0,0 +1,130 @@ +{ + "type": "Resource", + "body": [ + { + "type": "Junk", + "annotations": [ + { + "type": "Annotation", + "code": "E0006", + "args": [ + "value" + ], + "message": "Expected field: \"value\"", + "span": { + "type": "Span", + "start": 26, + "end": 26 + } + } + ], + "content": "key1 = Value 1\n .attr =\n\n", + "span": { + "type": "Span", + "start": 0, + "end": 28 + } + }, + { + "type": "Junk", + "annotations": [ + { + "type": "Annotation", + "code": "E0006", + "args": [ + "value" + ], + "message": "Expected field: \"value\"", + "span": { + "type": "Span", + "start": 46, + "end": 46 + } + } + ], + "content": "key2 =\n .attr =\n\n", + "span": { + "type": "Span", + "start": 28, + "end": 48 + } + }, + { + "type": "Junk", + "annotations": [ + { + "type": "Annotation", + "code": "E0006", + "args": [ + "value" + ], + "message": "Expected field: \"value\"", + "span": { + "type": "Span", + "start": 87, + "end": 87 + } + } + ], + "content": "key3 =\n .attr1 = Attr 1\n .attr2 =\n\n", + "span": { + "type": "Span", + "start": 48, + "end": 89 + } + }, + { + "type": "Junk", + "annotations": [ + { + "type": "Annotation", + "code": "E0006", + "args": [ + "value" + ], + "message": "Expected field: \"value\"", + "span": { + "type": "Span", + "start": 108, + "end": 108 + } + } + ], + "content": "key4 =\n .attr1 =\n .attr2 = Attr 2\n\n", + "span": { + "type": "Span", + "start": 89, + "end": 130 + } + }, + { + "type": "Junk", + "annotations": [ + { + "type": "Annotation", + "code": "E0006", + "args": [ + "value" + ], + "message": "Expected field: \"value\"", + "span": { + "type": "Span", + "start": 149, + "end": 149 + } + } + ], + "content": "key5 =\n .attr1 =\n .attr2 =\n", + "span": { + "type": "Span", + "start": 130, + "end": 163 + } + } + ], + "span": { + "type": "Span", + "start": 0, + "end": 163 + } +} diff --git a/fluent-syntax/test/fixtures_structure/elements_indent.json b/fluent-syntax/test/fixtures_structure/elements_indent.json index 131459a34..584cccf3f 100644 --- a/fluent-syntax/test/fixtures_structure/elements_indent.json +++ b/fluent-syntax/test/fixtures_structure/elements_indent.json @@ -126,7 +126,7 @@ }, "span": { "type": "Span", - "start": 42, + "start": 37, "end": 61 } } diff --git a/fluent-syntax/test/fixtures_structure/message_with_empty_pattern.json b/fluent-syntax/test/fixtures_structure/message_with_empty_pattern.json index 072d6168d..65b2e1f6a 100644 --- a/fluent-syntax/test/fixtures_structure/message_with_empty_pattern.json +++ b/fluent-syntax/test/fixtures_structure/message_with_empty_pattern.json @@ -2,28 +2,23 @@ "type": "Resource", "body": [ { - "type": "Message", - "annotations": [], - "id": { - "type": "Identifier", - "name": "foo", - "span": { - "type": "Span", - "start": 0, - "end": 3 + "type": "Junk", + "annotations": [ + { + "type": "Annotation", + "code": "E0005", + "args": [ + "foo" + ], + "message": "Expected entry \"foo\" to have a value or attributes", + "span": { + "type": "Span", + "start": 5, + "end": 5 + } } - }, - "value": { - "type": "Pattern", - "elements": [], - "span": { - "type": "Span", - "start": 7, - "end": 7 - } - }, - "attributes": [], - "comment": null, + ], + "content": "foo = \n", "span": { "type": "Span", "start": 0, diff --git a/fluent-syntax/test/fixtures_structure/placeable_at_eol.json b/fluent-syntax/test/fixtures_structure/placeable_at_eol.json index 6b441ea43..15c820f6a 100644 --- a/fluent-syntax/test/fixtures_structure/placeable_at_eol.json +++ b/fluent-syntax/test/fixtures_structure/placeable_at_eol.json @@ -62,7 +62,7 @@ ], "span": { "type": "Span", - "start": 7, + "start": 11, "end": 131 } }, @@ -126,7 +126,7 @@ ], "span": { "type": "Span", - "start": 140, + "start": 144, "end": 184 } }, diff --git a/fluent-syntax/test/fixtures_structure/private_message.json b/fluent-syntax/test/fixtures_structure/private_message.json index dfac5c8ba..8c72dba75 100644 --- a/fluent-syntax/test/fixtures_structure/private_message.json +++ b/fluent-syntax/test/fixtures_structure/private_message.json @@ -55,7 +55,7 @@ "default": true, "span": { "type": "Span", - "start": 27, + "start": 19, "end": 48 } }, @@ -92,7 +92,7 @@ "default": false, "span": { "type": "Span", - "start": 57, + "start": 48, "end": 78 } } @@ -112,7 +112,7 @@ ], "span": { "type": "Span", - "start": 14, + "start": 18, "end": 84 } }, @@ -149,7 +149,7 @@ }, "span": { "type": "Span", - "start": 89, + "start": 84, "end": 108 } } @@ -231,7 +231,7 @@ ], "span": { "type": "Span", - "start": 127, + "start": 131, "end": 171 } }, @@ -347,7 +347,7 @@ "default": false, "span": { "type": "Span", - "start": 229, + "start": 220, "end": 289 } }, @@ -409,7 +409,7 @@ "default": false, "span": { "type": "Span", - "start": 298, + "start": 289, "end": 358 } }, @@ -480,7 +480,7 @@ "default": true, "span": { "type": "Span", - "start": 366, + "start": 358, "end": 431 } } @@ -500,7 +500,7 @@ ], "span": { "type": "Span", - "start": 193, + "start": 197, "end": 437 } }, diff --git a/fluent-syntax/test/fixtures_structure/sparse-messages.json b/fluent-syntax/test/fixtures_structure/sparse-messages.json index fe42d6c1a..eda441189 100644 --- a/fluent-syntax/test/fixtures_structure/sparse-messages.json +++ b/fluent-syntax/test/fixtures_structure/sparse-messages.json @@ -28,7 +28,7 @@ ], "span": { "type": "Span", - "start": 8, + "start": 12, "end": 17 } }, @@ -86,7 +86,7 @@ }, "span": { "type": "Span", - "start": 30, + "start": 23, "end": 47 } } @@ -125,7 +125,7 @@ ], "span": { "type": "Span", - "start": 57, + "start": 61, "end": 102 } }, @@ -162,7 +162,7 @@ }, "span": { "type": "Span", - "start": 110, + "start": 102, "end": 125 } } @@ -267,7 +267,7 @@ "default": false, "span": { "type": "Span", - "start": 164, + "start": 152, "end": 173 } }, @@ -304,7 +304,7 @@ "default": true, "span": { "type": "Span", - "start": 186, + "start": 173, "end": 196 } } diff --git a/fluent-syntax/test/fixtures_structure/syntax_zero_four.ftl b/fluent-syntax/test/fixtures_structure/syntax_zero_four.ftl new file mode 100644 index 000000000..2bf51ae77 --- /dev/null +++ b/fluent-syntax/test/fixtures_structure/syntax_zero_four.ftl @@ -0,0 +1,6 @@ +key1 + .attr1 = Attr 1 + +key2 + .attr1 = Attr 1 + .attr2 = Attr 2 diff --git a/fluent-syntax/test/fixtures_structure/syntax_zero_four.json b/fluent-syntax/test/fixtures_structure/syntax_zero_four.json new file mode 100644 index 000000000..9fef17aea --- /dev/null +++ b/fluent-syntax/test/fixtures_structure/syntax_zero_four.json @@ -0,0 +1,162 @@ +{ + "type": "Resource", + "body": [ + { + "type": "Message", + "annotations": [], + "id": { + "type": "Identifier", + "name": "key1", + "span": { + "type": "Span", + "start": 0, + "end": 4 + } + }, + "value": null, + "attributes": [ + { + "type": "Attribute", + "id": { + "type": "Identifier", + "name": "attr1", + "span": { + "type": "Span", + "start": 10, + "end": 15 + } + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "TextElement", + "value": "Attr 1", + "span": { + "type": "Span", + "start": 18, + "end": 24 + } + } + ], + "span": { + "type": "Span", + "start": 18, + "end": 24 + } + }, + "span": { + "type": "Span", + "start": 4, + "end": 24 + } + } + ], + "comment": null, + "span": { + "type": "Span", + "start": 0, + "end": 24 + } + }, + { + "type": "Message", + "annotations": [], + "id": { + "type": "Identifier", + "name": "key2", + "span": { + "type": "Span", + "start": 26, + "end": 30 + } + }, + "value": null, + "attributes": [ + { + "type": "Attribute", + "id": { + "type": "Identifier", + "name": "attr1", + "span": { + "type": "Span", + "start": 36, + "end": 41 + } + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "TextElement", + "value": "Attr 1", + "span": { + "type": "Span", + "start": 44, + "end": 50 + } + } + ], + "span": { + "type": "Span", + "start": 44, + "end": 50 + } + }, + "span": { + "type": "Span", + "start": 30, + "end": 50 + } + }, + { + "type": "Attribute", + "id": { + "type": "Identifier", + "name": "attr2", + "span": { + "type": "Span", + "start": 56, + "end": 61 + } + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "TextElement", + "value": "Attr 2", + "span": { + "type": "Span", + "start": 64, + "end": 70 + } + } + ], + "span": { + "type": "Span", + "start": 64, + "end": 70 + } + }, + "span": { + "type": "Span", + "start": 50, + "end": 70 + } + } + ], + "comment": null, + "span": { + "type": "Span", + "start": 26, + "end": 70 + } + } + ], + "span": { + "type": "Span", + "start": 0, + "end": 71 + } +} diff --git a/fluent-syntax/test/fixtures_structure/variant_with_empty_pattern.ftl b/fluent-syntax/test/fixtures_structure/variant_with_empty_pattern.ftl new file mode 100644 index 000000000..75a54b200 --- /dev/null +++ b/fluent-syntax/test/fixtures_structure/variant_with_empty_pattern.ftl @@ -0,0 +1,4 @@ +key1 = + { 1 -> + *[one] {""} + } diff --git a/fluent-syntax/test/fixtures_structure/variant_with_empty_pattern.json b/fluent-syntax/test/fixtures_structure/variant_with_empty_pattern.json new file mode 100644 index 000000000..799cf0efa --- /dev/null +++ b/fluent-syntax/test/fixtures_structure/variant_with_empty_pattern.json @@ -0,0 +1,112 @@ +{ + "type": "Resource", + "body": [ + { + "type": "Message", + "annotations": [], + "id": { + "type": "Identifier", + "name": "key1", + "span": { + "type": "Span", + "start": 0, + "end": 4 + } + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "Placeable", + "expression": { + "type": "SelectExpression", + "expression": { + "type": "NumberExpression", + "value": "1", + "span": { + "type": "Span", + "start": 13, + "end": 14 + } + }, + "variants": [ + { + "type": "Variant", + "key": { + "type": "VariantName", + "name": "one", + "span": { + "type": "Span", + "start": 27, + "end": 30 + } + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "Placeable", + "expression": { + "type": "StringExpression", + "value": "", + "span": { + "type": "Span", + "start": 33, + "end": 35 + } + }, + "span": { + "type": "Span", + "start": 32, + "end": 36 + } + } + ], + "span": { + "type": "Span", + "start": 32, + "end": 36 + } + }, + "default": true, + "span": { + "type": "Span", + "start": 17, + "end": 36 + } + } + ], + "span": { + "type": "Span", + "start": 12, + "end": 41 + } + }, + "span": { + "type": "Span", + "start": 11, + "end": 42 + } + } + ], + "span": { + "type": "Span", + "start": 11, + "end": 42 + } + }, + "attributes": [], + "comment": null, + "span": { + "type": "Span", + "start": 0, + "end": 42 + } + } + ], + "span": { + "type": "Span", + "start": 0, + "end": 43 + } +} diff --git a/fluent-syntax/test/serializer_test.js b/fluent-syntax/test/serializer_test.js index 4f65442cf..454bcec3d 100644 --- a/fluent-syntax/test/serializer_test.js +++ b/fluent-syntax/test/serializer_test.js @@ -131,17 +131,29 @@ suite('Serializer', function() { assert.equal(pretty(input), input); }); - test('attribute', function() { + test('attribute (Syntax 0.4)', function() { const input = ftl` foo .attr = Foo Attr `; + const output = ftl` + foo = + .attr = Foo Attr + `; + assert.equal(pretty(input), output); + }); + + test('attribute', function() { + const input = ftl` + foo = + .attr = Foo Attr + `; assert.equal(pretty(input), input); }); test('multiline attribute', function() { const input = ftl` - foo + foo = .attr = Foo Attr Continued @@ -149,12 +161,26 @@ suite('Serializer', function() { assert.equal(pretty(input), input); }); - test('two attribute', function() { + test('two attributes (Syntax 0.4)', function() { const input = ftl` foo .attr-a = Foo Attr A .attr-b = Foo Attr B `; + const output = ftl` + foo = + .attr-a = Foo Attr A + .attr-b = Foo Attr B + `; + assert.equal(pretty(input), output); + }); + + test('two attributes', function() { + const input = ftl` + foo = + .attr-a = Foo Attr A + .attr-b = Foo Attr B + `; assert.equal(pretty(input), input); }); diff --git a/fluent-web/examples/localization/en-US/fluent-widget.ftl b/fluent-web/examples/localization/en-US/fluent-widget.ftl index c809e8ab2..b467addf1 100644 --- a/fluent-web/examples/localization/en-US/fluent-widget.ftl +++ b/fluent-web/examples/localization/en-US/fluent-widget.ftl @@ -1,4 +1,4 @@ -proceed-button - .label = Do you want to proceed? - .action-ok = Yes +proceed-button = + .label = Do you want to proceed? + .action-ok = Yes .action-cancel = No diff --git a/fluent-web/examples/localization/en-US/main.ftl b/fluent-web/examples/localization/en-US/main.ftl index a209b2ee5..e5a12c70c 100644 --- a/fluent-web/examples/localization/en-US/main.ftl +++ b/fluent-web/examples/localization/en-US/main.ftl @@ -1,5 +1,5 @@ hello-world = Hello, world! sub-title = Fluent is awesome. -click-me +click-me = .value = Click me! alert-msg = This is a message that goes straight to JS. diff --git a/fluent-web/examples/localization/pl/fluent-widget.ftl b/fluent-web/examples/localization/pl/fluent-widget.ftl index 8b57af3c4..08962c447 100644 --- a/fluent-web/examples/localization/pl/fluent-widget.ftl +++ b/fluent-web/examples/localization/pl/fluent-widget.ftl @@ -1,4 +1,4 @@ -proceed-button - .label = Czy chcesz przejść dalej? - .action-ok = Tak +proceed-button = + .label = Czy chcesz przejść dalej? + .action-ok = Tak .action-cancel = Nie diff --git a/fluent-web/examples/localization/pl/main.ftl b/fluent-web/examples/localization/pl/main.ftl index 94cb103cb..414be63ff 100644 --- a/fluent-web/examples/localization/pl/main.ftl +++ b/fluent-web/examples/localization/pl/main.ftl @@ -1,4 +1,4 @@ hello-world = Witaj Świecie! -click-me +click-me = .value = Naciśnij mnie! alert-msg = Ta wiadomość idzie prosto do JS. diff --git a/fluent/src/parser.js b/fluent/src/parser.js index e6387ce93..f7c9107ea 100644 --- a/fluent/src/parser.js +++ b/fluent/src/parser.js @@ -121,47 +121,25 @@ class RuntimeParser { */ getMessage() { const id = this.getPrivateIdentifier(); - let attrs = null; this.skipInlineWS(); - let ch = this._source[this._index]; - - let val; - - if (ch === '=') { + if (this._source[this._index] === '=') { this._index++; + } - this.skipInlineWS(); + this.skipInlineWS(); - if (this._source[this._index] === '\n') { - this.skipBlankLines(); - if (this._source[this._index] === ' ') { - this.skipInlineWS(); - val = this.getPattern(); - } - } else { - // This is a fast-path for the most common - // case of `key = Value` where the value - // is in the same line as the key. - val = this.getPattern(); - } - } else { - this.skipInlineWS(); - this.skipBlankLines(); - } + const val = this.getPattern(); - ch = this._source[this._index]; + let attrs = null; - if (ch === ' ') { + if (this._source[this._index] === ' ') { const lineStart = this._index; this.skipInlineWS(); - ch = this._source[this._index]; - - this._index = lineStart; - - if (ch === '.') { + if (this._source[this._index] === '.') { + this._index = lineStart; attrs = this.getAttributes(); } } @@ -169,13 +147,17 @@ class RuntimeParser { if (attrs === null && typeof val === 'string') { this.entries[id] = val; } else { - if (val === undefined && attrs === null) { - throw this.error(`Expected a value (like: " = value") or - an attribute (like: ".key = value")`); + if (val === null && attrs === null) { + throw this.error('Expected a value or an attribute'); } - this.entries[id] = { val }; - if (attrs) { + this.entries[id] = {}; + + if (val !== null) { + this.entries[id].val = val; + } + + if (attrs !== null) { this.entries[id].attrs = attrs; } } @@ -349,10 +331,10 @@ class RuntimeParser { eol = this._length; } - const line = start !== eol ? - this._source.slice(start, eol) : undefined; + const firstLineContent = start !== eol ? + this._source.slice(start, eol) : null; - if (line !== undefined && line.includes('{')) { + if (firstLineContent && firstLineContent.includes('{')) { return this.getComplexPattern(); } @@ -360,12 +342,29 @@ class RuntimeParser { this.skipBlankLines(); - if (this._source[this._index] === ' ') { + if (this._source[this._index] !== ' ') { + // No indentation means we're done with this message. + return firstLineContent; + } + + const lineStart = this._index; + + this.skipInlineWS(); + + if (this._source[this._index] === '.') { + // The pattern is followed by an attribute. Rewind _index to the first + // column of the current line as expected by getAttributes. + this._index = lineStart; + return firstLineContent; + } + + if (firstLineContent) { + // It's a multiline pattern which started on the same line as the + // identifier. Reparse the whole pattern to make sure we get all of it. this._index = start; - return this.getComplexPattern(); } - return line; + return this.getComplexPattern(); } /** @@ -453,7 +452,7 @@ class RuntimeParser { } if (content.length === 0) { - return buffer.length ? buffer : undefined; + return buffer.length ? buffer : null; } if (buffer.length) { diff --git a/fluent/src/resolver.js b/fluent/src/resolver.js index d3dfa59e7..f60a12952 100644 --- a/fluent/src/resolver.js +++ b/fluent/src/resolver.js @@ -294,7 +294,7 @@ function Type(env, expr) { } case undefined: { // If it's a node with a value, resolve the value. - if (expr.val !== undefined) { + if (expr.val !== null && expr.val !== undefined) { return Type(env, expr.val); } diff --git a/fluent/test/fixtures_structure/attribute_with_empty_pattern.json b/fluent/test/fixtures_structure/attribute_with_empty_pattern.json new file mode 100644 index 000000000..d65c7551f --- /dev/null +++ b/fluent/test/fixtures_structure/attribute_with_empty_pattern.json @@ -0,0 +1,43 @@ +{ + "key1": { + "val": "Value 1", + "attrs": { + "attr": { + "val": null + } + } + }, + "key2": { + "attrs": { + "attr": { + "val": null + } + } + }, + "key3": { + "attrs": { + "attr1": "Attr 1", + "attr2": { + "val": null + } + } + }, + "key4": { + "attrs": { + "attr1": { + "val": null + }, + "attr2": "Attr 2" + } + }, + "key5": { + "attrs": { + "attr1": { + "val": null + }, + "attr2": { + "val": null + } + } + } +} diff --git a/fluent/test/fixtures_structure/sparse-messages.json b/fluent/test/fixtures_structure/sparse-messages.json index 122faf2c8..eacb0c06a 100644 --- a/fluent/test/fixtures_structure/sparse-messages.json +++ b/fluent/test/fixtures_structure/sparse-messages.json @@ -3,8 +3,7 @@ "key2": { "attrs": { "attr": "Attribute" - }, - "val": null + } }, "key3": { "val": "Value\nValue2\n\n\nValue 4\nValue3", diff --git a/fluent/test/fixtures_structure/syntax_zero_four.json b/fluent/test/fixtures_structure/syntax_zero_four.json new file mode 100644 index 000000000..0542c8b73 --- /dev/null +++ b/fluent/test/fixtures_structure/syntax_zero_four.json @@ -0,0 +1,13 @@ +{ + "key1": { + "attrs": { + "attr1": "Attr 1" + } + }, + "key2": { + "attrs": { + "attr1": "Attr 1", + "attr2": "Attr 2" + } + } +} diff --git a/fluent/test/fixtures_structure/variant_with_empty_pattern.json b/fluent/test/fixtures_structure/variant_with_empty_pattern.json new file mode 100644 index 000000000..3694a9045 --- /dev/null +++ b/fluent/test/fixtures_structure/variant_with_empty_pattern.json @@ -0,0 +1,25 @@ +{ + "key1": { + "val": [ + { + "type": "sel", + "exp": { + "type": "num", + "val": "1" + }, + "vars": [ + { + "key": { + "type": "varname", + "name": "one" + }, + "val": [ + "" + ] + } + ], + "def": 0 + } + ] + } +} diff --git a/tools/perf/workload-low.ftl b/tools/perf/workload-low.ftl index 938890ee3..c715da3ba 100644 --- a/tools/perf/workload-low.ftl +++ b/tools/perf/workload-low.ftl @@ -182,23 +182,23 @@ full-zoom-enlarge-menuitem full-zoom-enlarge-key1 .key = + full-zoom-enlarge-key2 - .key = + .key = {""} full-zoom-enlarge-key3 - .key = "" + .key = {""} full-zoom-reduce-menuitem .label = Zoom Out .accesskey = O full-zoom-reduce-key1 .key = - full-zoom-reduce-key2 - .key = "" + .key = {""} full-zoom-reset-menuitem .label = Reset .accesskey = R full-zoom-reset-key1 .key = 0 full-zoom-reset-key2 - .key = "" + .key = {""} full-zoom-toggle-menuitem .label = Zoom Text Only .accesskey = T