From 873d69314b3fa2b4ca2e68f397fce6c897eaa9bd Mon Sep 17 00:00:00 2001 From: Zibi Braniecki Date: Mon, 2 Oct 2017 13:40:22 +0200 Subject: [PATCH 1/6] Handle sparse messages in fluent-syntax --- fluent-syntax/src/ftlstream.js | 25 +- fluent-syntax/src/parser.js | 13 +- fluent-syntax/src/stream.js | 13 +- .../fixtures_structure/sparse-messages.ftl | 46 ++ .../fixtures_structure/sparse-messages.json | 426 ++++++++++++++++++ 5 files changed, 516 insertions(+), 7 deletions(-) create mode 100644 fluent-syntax/test/fixtures_structure/sparse-messages.ftl create mode 100644 fluent-syntax/test/fixtures_structure/sparse-messages.json diff --git a/fluent-syntax/src/ftlstream.js b/fluent-syntax/src/ftlstream.js index d76515231..60fcc31ba 100644 --- a/fluent-syntax/src/ftlstream.js +++ b/fluent-syntax/src/ftlstream.js @@ -31,6 +31,21 @@ export class FTLParserStream extends ParserStream { } } + peekSkipBlankLines() { + while (true) { + let lineStart = this.getPeekIndex(); + + this.peekInlineWS(); + + if (this.currentPeekIs('\n')) { + this.peek(); + } else { + this.resetPeek(lineStart); + break; + } + } + } + skipInlineWS() { while (this.ch) { if (!includes(INLINE_WS, this.ch)) { @@ -94,6 +109,8 @@ export class FTLParserStream extends ParserStream { this.peek(); + this.peekSkipBlankLines(); + const ptr = this.getPeekIndex(); this.peekInlineWS(); @@ -122,6 +139,8 @@ export class FTLParserStream extends ParserStream { this.peek(); + this.peekSkipBlankLines(); + const ptr = this.getPeekIndex(); this.peekInlineWS(); @@ -140,13 +159,15 @@ export class FTLParserStream extends ParserStream { return false; } - isPeekNextLinePattern() { + isPeekNextNonBlankLinePattern() { if (!this.currentPeekIs('\n')) { return false; } this.peek(); + this.peekSkipBlankLines(); + const ptr = this.getPeekIndex(); this.peekInlineWS(); @@ -176,6 +197,8 @@ export class FTLParserStream extends ParserStream { this.peek(); + this.peekSkipBlankLines(); + const ptr = this.getPeekIndex(); this.peekInlineWS(); diff --git a/fluent-syntax/src/parser.js b/fluent-syntax/src/parser.js index 00780bef2..b92e5bc91 100644 --- a/fluent-syntax/src/parser.js +++ b/fluent-syntax/src/parser.js @@ -197,6 +197,7 @@ export default class FluentParser { if (ps.currentIs('=')) { ps.next(); ps.skipInlineWS(); + ps.skipBlankLines(); pattern = this.getPattern(ps); } @@ -242,6 +243,7 @@ export default class FluentParser { while (true) { ps.expectChar('\n'); + ps.skipBlankLines(); ps.skipInlineWS(); const attr = this.getAttribute(ps); @@ -265,6 +267,7 @@ export default class FluentParser { while (true) { ps.expectChar('\n'); + ps.skipBlankLines(); ps.skipInlineWS(); const tag = this.getTag(ps); @@ -341,6 +344,7 @@ export default class FluentParser { while (true) { ps.expectChar('\n'); + ps.skipBlankLines(); ps.skipInlineWS(); const variant = this.getVariant(ps, hasDefault); @@ -419,7 +423,7 @@ export default class FluentParser { ps.skipInlineWS(); // Special-case: trim leading whitespace and newlines. - if (ps.isPeekNextLinePattern()) { + if (ps.isPeekNextNonBlankLinePattern()) { ps.skipBlankLines(); ps.skipInlineWS(); } @@ -429,7 +433,7 @@ export default class FluentParser { // The end condition for getPattern's while loop is a newline // which is not followed by a valid pattern continuation. - if (ch === '\n' && !ps.isPeekNextLinePattern()) { + if (ch === '\n' && !ps.isPeekNextNonBlankLinePattern()) { break; } @@ -456,7 +460,7 @@ export default class FluentParser { } if (ch === '\n') { - if (!ps.isPeekNextLinePattern()) { + if (!ps.isPeekNextNonBlankLinePattern()) { return new AST.TextElement(buffer); } @@ -499,6 +503,7 @@ export default class FluentParser { const variants = this.getVariants(ps); ps.expectChar('\n'); + ps.skipBlankLines(); ps.expectChar(' '); ps.skipInlineWS(); @@ -523,11 +528,13 @@ export default class FluentParser { const variants = this.getVariants(ps); + if (variants.length === 0) { throw new ParseError('E0011'); } ps.expectChar('\n'); + ps.skipBlankLines(); ps.expectChar(' '); ps.skipInlineWS(); diff --git a/fluent-syntax/src/stream.js b/fluent-syntax/src/stream.js index 29a6f7a01..ef485c503 100644 --- a/fluent-syntax/src/stream.js +++ b/fluent-syntax/src/stream.js @@ -102,9 +102,16 @@ export class ParserStream { return ret === ch; } - resetPeek() { - this.peekIndex = this.index; - this.peekEnd = this.iterEnd; + resetPeek(pos = false) { + if (pos === false) { + this.peekIndex = this.index; + this.peekEnd = this.iterEnd; + } else { + if (pos < this.peekIndex) { + this.peekEnd = false; + } + this.peekIndex = pos; + } } skipToPeek() { diff --git a/fluent-syntax/test/fixtures_structure/sparse-messages.ftl b/fluent-syntax/test/fixtures_structure/sparse-messages.ftl new file mode 100644 index 000000000..8c7418a29 --- /dev/null +++ b/fluent-syntax/test/fixtures_structure/sparse-messages.ftl @@ -0,0 +1,46 @@ +key = + + + Value + +key2 + + + .attr = Attribute + + +key3 = + Value + Value2 + + + Value 4 + Value3 + + + + .attr2 = Attr 2 + + +key4 = Value + + + #tag1 + + + #tag2 +key5 = Value 5 + +key6 = { + + + [one] One + + + + + *[two] Two + + + + } diff --git a/fluent-syntax/test/fixtures_structure/sparse-messages.json b/fluent-syntax/test/fixtures_structure/sparse-messages.json new file mode 100644 index 000000000..506cdff1d --- /dev/null +++ b/fluent-syntax/test/fixtures_structure/sparse-messages.json @@ -0,0 +1,426 @@ +{ + "type": "Resource", + "body": [ + { + "type": "Message", + "annotations": [], + "id": { + "type": "Identifier", + "name": "key", + "span": { + "type": "Span", + "start": 0, + "end": 3 + } + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "TextElement", + "value": "Value", + "span": { + "type": "Span", + "start": 12, + "end": 17 + } + } + ], + "span": { + "type": "Span", + "start": 8, + "end": 17 + } + }, + "attributes": [], + "tags": [], + "comment": null, + "span": { + "type": "Span", + "start": 0, + "end": 17 + } + }, + { + "type": "Message", + "annotations": [], + "id": { + "type": "Identifier", + "name": "key2", + "span": { + "type": "Span", + "start": 19, + "end": 23 + } + }, + "value": null, + "attributes": [ + { + "type": "Attribute", + "id": { + "type": "Identifier", + "name": "attr", + "span": { + "type": "Span", + "start": 31, + "end": 35 + } + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "TextElement", + "value": "Attribute", + "span": { + "type": "Span", + "start": 38, + "end": 47 + } + } + ], + "span": { + "type": "Span", + "start": 38, + "end": 47 + } + }, + "span": { + "type": "Span", + "start": 30, + "end": 47 + } + } + ], + "tags": [], + "comment": null, + "span": { + "type": "Span", + "start": 19, + "end": 47 + } + }, + { + "type": "Message", + "annotations": [], + "id": { + "type": "Identifier", + "name": "key3", + "span": { + "type": "Span", + "start": 50, + "end": 54 + } + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "TextElement", + "value": "Value\nValue2\n\n\nValue 4\nValue3", + "span": { + "type": "Span", + "start": 61, + "end": 102 + } + } + ], + "span": { + "type": "Span", + "start": 57, + "end": 102 + } + }, + "attributes": [ + { + "type": "Attribute", + "id": { + "type": "Identifier", + "name": "attr2", + "span": { + "type": "Span", + "start": 111, + "end": 116 + } + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "TextElement", + "value": "Attr 2", + "span": { + "type": "Span", + "start": 119, + "end": 125 + } + } + ], + "span": { + "type": "Span", + "start": 119, + "end": 125 + } + }, + "span": { + "type": "Span", + "start": 110, + "end": 125 + } + } + ], + "tags": [], + "comment": null, + "span": { + "type": "Span", + "start": 50, + "end": 125 + } + }, + { + "type": "Message", + "annotations": [], + "id": { + "type": "Identifier", + "name": "key4", + "span": { + "type": "Span", + "start": 128, + "end": 132 + } + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "TextElement", + "value": "Value", + "span": { + "type": "Span", + "start": 135, + "end": 140 + } + } + ], + "span": { + "type": "Span", + "start": 135, + "end": 140 + } + }, + "attributes": [], + "tags": [ + { + "type": "Tag", + "name": { + "type": "Symbol", + "name": "tag1", + "span": { + "type": "Span", + "start": 148, + "end": 152 + } + }, + "span": { + "type": "Span", + "start": 147, + "end": 152 + } + }, + { + "type": "Tag", + "name": { + "type": "Symbol", + "name": "tag2", + "span": { + "type": "Span", + "start": 160, + "end": 164 + } + }, + "span": { + "type": "Span", + "start": 159, + "end": 164 + } + } + ], + "comment": null, + "span": { + "type": "Span", + "start": 128, + "end": 164 + } + }, + { + "type": "Message", + "annotations": [], + "id": { + "type": "Identifier", + "name": "key5", + "span": { + "type": "Span", + "start": 165, + "end": 169 + } + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "TextElement", + "value": "Value 5", + "span": { + "type": "Span", + "start": 172, + "end": 179 + } + } + ], + "span": { + "type": "Span", + "start": 172, + "end": 179 + } + }, + "attributes": [], + "tags": [], + "comment": null, + "span": { + "type": "Span", + "start": 165, + "end": 179 + } + }, + { + "type": "Message", + "annotations": [], + "id": { + "type": "Identifier", + "name": "key6", + "span": { + "type": "Span", + "start": 181, + "end": 185 + } + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "Placeable", + "expression": { + "type": "SelectExpression", + "expression": null, + "variants": [ + { + "type": "Variant", + "key": { + "type": "Symbol", + "name": "one", + "span": { + "type": "Span", + "start": 202, + "end": 205 + } + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "TextElement", + "value": "One", + "span": { + "type": "Span", + "start": 207, + "end": 210 + } + } + ], + "span": { + "type": "Span", + "start": 207, + "end": 210 + } + }, + "default": false, + "span": { + "type": "Span", + "start": 201, + "end": 210 + } + }, + { + "type": "Variant", + "key": { + "type": "Symbol", + "name": "two", + "span": { + "type": "Span", + "start": 225, + "end": 228 + } + }, + "value": { + "type": "Pattern", + "elements": [ + { + "type": "TextElement", + "value": "Two", + "span": { + "type": "Span", + "start": 230, + "end": 233 + } + } + ], + "span": { + "type": "Span", + "start": 230, + "end": 233 + } + }, + "default": true, + "span": { + "type": "Span", + "start": 223, + "end": 233 + } + } + ], + "span": { + "type": "Span", + "start": 189, + "end": 241 + } + }, + "span": { + "type": "Span", + "start": 188, + "end": 242 + } + } + ], + "span": { + "type": "Span", + "start": 188, + "end": 242 + } + }, + "attributes": [], + "tags": [], + "comment": null, + "span": { + "type": "Span", + "start": 181, + "end": 242 + } + } + ], + "comment": null, + "span": { + "type": "Span", + "start": 0, + "end": 243 + } +} From 8bf4cff72c81534d3a571038b10b974859867b39 Mon Sep 17 00:00:00 2001 From: Zibi Braniecki Date: Mon, 2 Oct 2017 13:41:21 +0200 Subject: [PATCH 2/6] Fix tests affected by the sparse message parsing change --- .../test/fixtures_structure/message_with_empty_pattern.json | 6 +++--- fluent-syntax/test/fixtures_structure/placeable_at_eol.json | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) 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 cb64fc3aa..ed3397206 100644 --- a/fluent-syntax/test/fixtures_structure/message_with_empty_pattern.json +++ b/fluent-syntax/test/fixtures_structure/message_with_empty_pattern.json @@ -18,8 +18,8 @@ "elements": [], "span": { "type": "Span", - "start": 6, - "end": 6 + "start": 7, + "end": 7 } }, "attributes": [], @@ -28,7 +28,7 @@ "span": { "type": "Span", "start": 0, - "end": 6 + "end": 7 } } ], diff --git a/fluent-syntax/test/fixtures_structure/placeable_at_eol.json b/fluent-syntax/test/fixtures_structure/placeable_at_eol.json index 8310b559e..6b68da343 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": 6, + "start": 7, "end": 131 } }, @@ -127,7 +127,7 @@ ], "span": { "type": "Span", - "start": 139, + "start": 140, "end": 184 } }, From 188d7170e38e5976e2127eb05acc0b3be3667f09 Mon Sep 17 00:00:00 2001 From: Zibi Braniecki Date: Mon, 2 Oct 2017 13:42:32 +0200 Subject: [PATCH 3/6] Handle sparse messages in fluent runtime --- fluent/src/parser.js | 35 ++++++++++++++ .../fixtures_structure/sparse-messages.json | 48 +++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 fluent/test/fixtures_structure/sparse-messages.json diff --git a/fluent/src/parser.js b/fluent/src/parser.js index 4fda09035..6784ada33 100644 --- a/fluent/src/parser.js +++ b/fluent/src/parser.js @@ -130,6 +130,8 @@ class RuntimeParser { if (ch === '=') { this._index++; + this.skipInlineWS(); + this.skipBlankLines(); this.skipInlineWS(); val = this.getPattern(); @@ -141,6 +143,7 @@ class RuntimeParser { if (ch === '\n') { this._index++; + this.skipBlankLines(); this.skipInlineWS(); ch = this._source[this._index]; } @@ -200,6 +203,26 @@ class RuntimeParser { } } + /** + * Skip blank lines. + * + * @private + */ + skipBlankLines() { + while (true) { + let ptr = this._index; + + this.skipInlineWS(); + + if (this._source[ptr] === '\n') { + this._index += 1; + } else { + this._index = ptr; + break; + } + } + } + /** * Get Message identifier. * @@ -344,6 +367,16 @@ class RuntimeParser { // by new line and `|` character at the beginning of the next one. if (ch === '\n') { this._index++; + + // We want to capture the start and end pointers + // around blank lines and add them to the buffer + // but only if the blank lines are in the middle + // of the string. + let ptr = this._index; + this.skipBlankLines(); + let ptr2 = this._index; + + if (this._source[this._index] !== ' ') { break; } @@ -357,6 +390,8 @@ class RuntimeParser { break; } + buffer += this._source.substring(ptr, ptr2); + if (buffer.length || content.length) { buffer += '\n'; } diff --git a/fluent/test/fixtures_structure/sparse-messages.json b/fluent/test/fixtures_structure/sparse-messages.json new file mode 100644 index 000000000..3fa16969a --- /dev/null +++ b/fluent/test/fixtures_structure/sparse-messages.json @@ -0,0 +1,48 @@ +{ + "key": "Value", + "key2": { + "attrs": { + "attr": "Attribute" + }, + "val": null + }, + "key3": { + "val": "Value\nValue2\n\n\nValue 4\nValue3", + "attrs": { + "attr2": "Attr 2" + } + }, + "key4": { + "val": "Value", + "tags": [ + "tag1", + "tag2" + ] + }, + "key5": "Value 5", + "key6": { + "val": [ + { + "type": "sel", + "exp": null, + "vars": [ + { + "key": { + "type": "sym", + "name": "one" + }, + "val": "One" + }, + { + "key": { + "type": "sym", + "name": "two" + }, + "val": "Two" + } + ], + "def": 1 + } + ] + } +} From 292361da102cfe5759afd7872d5f095c287b7591 Mon Sep 17 00:00:00 2001 From: Zibi Braniecki Date: Mon, 2 Oct 2017 17:38:05 +0200 Subject: [PATCH 4/6] Fix linting errors --- fluent-syntax/src/ftlstream.js | 2 +- fluent/src/parser.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/fluent-syntax/src/ftlstream.js b/fluent-syntax/src/ftlstream.js index 60fcc31ba..ce35909e4 100644 --- a/fluent-syntax/src/ftlstream.js +++ b/fluent-syntax/src/ftlstream.js @@ -33,7 +33,7 @@ export class FTLParserStream extends ParserStream { peekSkipBlankLines() { while (true) { - let lineStart = this.getPeekIndex(); + const lineStart = this.getPeekIndex(); this.peekInlineWS(); diff --git a/fluent/src/parser.js b/fluent/src/parser.js index 6784ada33..a881f99bd 100644 --- a/fluent/src/parser.js +++ b/fluent/src/parser.js @@ -210,7 +210,7 @@ class RuntimeParser { */ skipBlankLines() { while (true) { - let ptr = this._index; + const ptr = this._index; this.skipInlineWS(); @@ -372,9 +372,9 @@ class RuntimeParser { // around blank lines and add them to the buffer // but only if the blank lines are in the middle // of the string. - let ptr = this._index; + const blankLinesStart = this._index; this.skipBlankLines(); - let ptr2 = this._index; + const blankLinesEnd = this._index; if (this._source[this._index] !== ' ') { @@ -390,7 +390,7 @@ class RuntimeParser { break; } - buffer += this._source.substring(ptr, ptr2); + buffer += this._source.substring(blankLinesStart, blankLinesEnd); if (buffer.length || content.length) { buffer += '\n'; From 0388600c98219d506e501c09853aeabfb2800b25 Mon Sep 17 00:00:00 2001 From: Zibi Braniecki Date: Wed, 4 Oct 2017 14:48:26 +0200 Subject: [PATCH 5/6] Apply reviewers feedback --- fluent-syntax/src/ftlstream.js | 17 ++++++++++++----- fluent-syntax/src/parser.js | 18 ++++-------------- fluent-syntax/src/stream.js | 10 +++++----- .../selector_expression_ends_abruptly.ftl | 2 +- fluent/src/parser.js | 2 +- 5 files changed, 23 insertions(+), 26 deletions(-) diff --git a/fluent-syntax/src/ftlstream.js b/fluent-syntax/src/ftlstream.js index ce35909e4..4d0e90b36 100644 --- a/fluent-syntax/src/ftlstream.js +++ b/fluent-syntax/src/ftlstream.js @@ -31,7 +31,7 @@ export class FTLParserStream extends ParserStream { } } - peekSkipBlankLines() { + peekBlankLines() { while (true) { const lineStart = this.getPeekIndex(); @@ -69,6 +69,13 @@ export class FTLParserStream extends ParserStream { throw new ParseError('E0003', ch); } + expectIndent() { + this.expectChar('\n'); + this.skipBlankLines(); + this.expectChar(' '); + this.skipInlineWS(); + } + takeCharIf(ch) { if (this.ch === ch) { this.next(); @@ -109,7 +116,7 @@ export class FTLParserStream extends ParserStream { this.peek(); - this.peekSkipBlankLines(); + this.peekBlankLines(); const ptr = this.getPeekIndex(); @@ -139,7 +146,7 @@ export class FTLParserStream extends ParserStream { this.peek(); - this.peekSkipBlankLines(); + this.peekBlankLines(); const ptr = this.getPeekIndex(); @@ -166,7 +173,7 @@ export class FTLParserStream extends ParserStream { this.peek(); - this.peekSkipBlankLines(); + this.peekBlankLines(); const ptr = this.getPeekIndex(); @@ -197,7 +204,7 @@ export class FTLParserStream extends ParserStream { this.peek(); - this.peekSkipBlankLines(); + this.peekBlankLines(); const ptr = this.getPeekIndex(); diff --git a/fluent-syntax/src/parser.js b/fluent-syntax/src/parser.js index b92e5bc91..402286b6a 100644 --- a/fluent-syntax/src/parser.js +++ b/fluent-syntax/src/parser.js @@ -266,9 +266,7 @@ export default class FluentParser { const tags = []; while (true) { - ps.expectChar('\n'); - ps.skipBlankLines(); - ps.skipInlineWS(); + ps.expectIndent(); const tag = this.getTag(ps); tags.push(tag); @@ -343,9 +341,7 @@ export default class FluentParser { let hasDefault = false; while (true) { - ps.expectChar('\n'); - ps.skipBlankLines(); - ps.skipInlineWS(); + ps.expectIndent(); const variant = this.getVariant(ps, hasDefault); @@ -502,10 +498,7 @@ export default class FluentParser { if (ps.isPeekNextLineVariantStart()) { const variants = this.getVariants(ps); - ps.expectChar('\n'); - ps.skipBlankLines(); - ps.expectChar(' '); - ps.skipInlineWS(); + ps.expectIndent(); return new AST.SelectExpression(null, variants); } @@ -533,10 +526,7 @@ export default class FluentParser { throw new ParseError('E0011'); } - ps.expectChar('\n'); - ps.skipBlankLines(); - ps.expectChar(' '); - ps.skipInlineWS(); + ps.expectIndent(); return new AST.SelectExpression(selector, variants); } diff --git a/fluent-syntax/src/stream.js b/fluent-syntax/src/stream.js index ef485c503..0eb281c5f 100644 --- a/fluent-syntax/src/stream.js +++ b/fluent-syntax/src/stream.js @@ -102,15 +102,15 @@ export class ParserStream { return ret === ch; } - resetPeek(pos = false) { - if (pos === false) { - this.peekIndex = this.index; - this.peekEnd = this.iterEnd; - } else { + resetPeek(pos) { + if (pos) { if (pos < this.peekIndex) { this.peekEnd = false; } this.peekIndex = pos; + } else { + this.peekIndex = this.index; + this.peekEnd = this.iterEnd; } } diff --git a/fluent-syntax/test/fixtures_behavior/selector_expression_ends_abruptly.ftl b/fluent-syntax/test/fixtures_behavior/selector_expression_ends_abruptly.ftl index a2ad26bec..e0f7d4bf9 100644 --- a/fluent-syntax/test/fixtures_behavior/selector_expression_ends_abruptly.ftl +++ b/fluent-syntax/test/fixtures_behavior/selector_expression_ends_abruptly.ftl @@ -1,2 +1,2 @@ key = { $foo -> -//~ ERROR E0003, pos 16, args "[" +//~ ERROR E0003, pos 16, args " " diff --git a/fluent/src/parser.js b/fluent/src/parser.js index a881f99bd..946682a72 100644 --- a/fluent/src/parser.js +++ b/fluent/src/parser.js @@ -364,7 +364,7 @@ class RuntimeParser { while (this._index < this._length) { // This block handles multi-line strings combining strings separated - // by new line and `|` character at the beginning of the next one. + // by new line. if (ch === '\n') { this._index++; From c0f282fd870985b2b0dead405366978986b7cfe2 Mon Sep 17 00:00:00 2001 From: Zibi Braniecki Date: Wed, 4 Oct 2017 15:39:23 +0200 Subject: [PATCH 6/6] One more indent --- fluent-syntax/src/parser.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/fluent-syntax/src/parser.js b/fluent-syntax/src/parser.js index 402286b6a..fd146a9fe 100644 --- a/fluent-syntax/src/parser.js +++ b/fluent-syntax/src/parser.js @@ -242,9 +242,7 @@ export default class FluentParser { const attrs = []; while (true) { - ps.expectChar('\n'); - ps.skipBlankLines(); - ps.skipInlineWS(); + ps.expectIndent(); const attr = this.getAttribute(ps); attrs.push(attr);