Skip to content

Commit 5469213

Browse files
committed
Parse Comments as standalone if they're attached to Junk
Syntax 0.6 changes the parsing logic of Comments. Comments attached to what ends up being Junk are not part of Junk anymore. Instead, they end up as standalone Comments in the final Resource. This PR also changes the behavior of FluentParser.parseEntry() to ignore all valid comments and only start parsing when a Message or a Term is encountered. Comments with syntax errors are not skipped, however.
1 parent 6707825 commit 5469213

File tree

4 files changed

+235
-76
lines changed

4 files changed

+235
-76
lines changed

fluent-syntax/src/ftlstream.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,21 @@ export class FTLParserStream extends ParserStream {
2828
}
2929

3030
skipBlankLines() {
31+
// Many Parser methods leave the cursor at the line break
32+
// without going into the next line. We want to count fully blank lines in
33+
// this case. Starting the count at -1 will give the right number.
34+
let lineCount = this.currentIs("\n") ? -1 : 0;
35+
3136
while (true) {
3237
this.peekInlineWS();
3338

3439
if (this.currentPeekIs("\n")) {
3540
this.skipToPeek();
3641
this.next();
42+
lineCount++;
3743
} else {
3844
this.resetPeek();
39-
break;
45+
return lineCount;
4046
}
4147
}
4248
}

fluent-syntax/src/parser.js

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,32 @@ export default class FluentParser {
5959

6060
while (ps.current()) {
6161
const entry = this.getEntryOrJunk(ps);
62+
const blankLines = ps.skipBlankLines();
63+
64+
// Regular Comments require special logic. Comments may be attached to
65+
// Messages or Terms if they are followed immediately by them. However
66+
// they should parse as standalone when they're followed by Junk.
67+
// Consequently, we only attach them once we know that the Message or the
68+
// Term parsed successfully.
69+
if (entry.type === "Comment" && blankLines === 0 && ps.current()) {
70+
const next = this.getEntryOrJunk(ps);
71+
ps.skipBlankLines();
72+
73+
if (next.type === "Message" || next.type === "Term") {
74+
// Attach it.
75+
next.comment = entry;
76+
entries.push(next);
77+
} else {
78+
// entry is a standalone Comment and next is Junk, GroupComment or
79+
// ResourceComment. Push both independently of each other.
80+
entries.push(entry, next);
81+
}
82+
83+
continue;
84+
}
6285

86+
// No special logic for other types of entries.
6387
entries.push(entry);
64-
ps.skipBlankLines();
6588
}
6689

6790
const res = new AST.Resource(entries);
@@ -73,9 +96,28 @@ export default class FluentParser {
7396
return res;
7497
}
7598

99+
/*
100+
* Parse the first Message or Term in `source`.
101+
*
102+
* Skip all encountered comments and start parsing at the first Message or
103+
* Term start. Return Junk if the parsing is not successful.
104+
*
105+
* Preceding comments are ignored unless they contain syntax errors
106+
* themselves, in which case Junk for the invalid comment is returned.
107+
*/
76108
parseEntry(source) {
77109
const ps = new FTLParserStream(source);
78110
ps.skipBlankLines();
111+
112+
while (ps.currentIs("#")) {
113+
const skipped = this.getEntryOrJunk(ps);
114+
if (skipped.type === "Junk") {
115+
// Don't skip Junk comments.
116+
return skipped;
117+
}
118+
ps.skipBlankLines();
119+
}
120+
79121
return this.getEntryOrJunk(ps);
80122
}
81123

@@ -107,34 +149,16 @@ export default class FluentParser {
107149
}
108150

109151
getEntry(ps) {
110-
let comment;
111-
112152
if (ps.currentIs("#")) {
113-
comment = this.getComment(ps);
114-
115-
// The Comment content doesn't include the trailing newline. Consume
116-
// this newline here to be ready for the next entry. undefined stands
117-
// for EOF.
118-
ps.expectChar(ps.current() ? "\n" : undefined);
119-
120-
if (comment.type === "GroupComment"
121-
|| comment.type === "ResourceComment") {
122-
// Group and Resource comments are always standalone.
123-
return comment;
124-
}
153+
return this.getComment(ps);
125154
}
126155

127156
if (ps.currentIs("-")) {
128-
return this.getTerm(ps, comment);
157+
return this.getTerm(ps);
129158
}
130159

131160
if (ps.isIdentifierStart()) {
132-
return this.getMessage(ps, comment);
133-
}
134-
135-
if (comment) {
136-
// It's a standalone Comment.
137-
return comment;
161+
return this.getMessage(ps);
138162
}
139163

140164
throw new ParseError("E0002");
@@ -189,7 +213,7 @@ export default class FluentParser {
189213
return new Comment(content);
190214
}
191215

192-
getMessage(ps, comment) {
216+
getMessage(ps) {
193217
const id = this.getIdentifier(ps);
194218

195219
ps.skipInlineWS();
@@ -210,10 +234,10 @@ export default class FluentParser {
210234
throw new ParseError("E0005", id.name);
211235
}
212236

213-
return new AST.Message(id, pattern, attrs, comment);
237+
return new AST.Message(id, pattern, attrs);
214238
}
215239

216-
getTerm(ps, comment) {
240+
getTerm(ps) {
217241
const id = this.getTermIdentifier(ps);
218242

219243
ps.skipInlineWS();
@@ -230,7 +254,7 @@ export default class FluentParser {
230254
var attrs = this.getAttributes(ps);
231255
}
232256

233-
return new AST.Term(id, pattern, attrs, comment);
257+
return new AST.Term(id, pattern, attrs);
234258
}
235259

236260
getAttribute(ps) {

fluent-syntax/test/entry_test.js

Lines changed: 118 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { FluentParser, FluentSerializer } from '../src';
55

66
suite('Parse entry', function() {
77
setup(function() {
8-
this.parser = new FluentParser();
8+
this.parser = new FluentParser({withSpans: false});
99
});
1010

1111
test('simple message', function() {
@@ -14,47 +14,141 @@ suite('Parse entry', function() {
1414
`;
1515
const output = {
1616
"comment": null,
17-
"span": {
18-
"start": 0,
19-
"end": 9,
20-
"type": "Span"
17+
"value": {
18+
"elements": [
19+
{
20+
"type": "TextElement",
21+
"value": "Foo"
22+
}
23+
],
24+
"type": "Pattern"
2125
},
26+
"annotations": [],
27+
"attributes": [],
28+
"type": "Message",
29+
"id": {
30+
"type": "Identifier",
31+
"name": "foo"
32+
}
33+
};
34+
35+
const message = this.parser.parseEntry(input)
36+
assert.deepEqual(message, output)
37+
});
38+
39+
test('ignore attached comment', function() {
40+
const input = ftl`
41+
# Attached Comment
42+
foo = Foo
43+
`;
44+
const output = {
45+
"comment": null,
2246
"value": {
2347
"elements": [
2448
{
2549
"type": "TextElement",
26-
"value": "Foo",
27-
"span": {
28-
"start": 6,
29-
"end": 9,
30-
"type": "Span"
31-
}
50+
"value": "Foo"
3251
}
3352
],
34-
"type": "Pattern",
35-
"span": {
36-
"start": 6,
37-
"end": 9,
38-
"type": "Span"
39-
}
53+
"type": "Pattern"
4054
},
4155
"annotations": [],
4256
"attributes": [],
4357
"type": "Message",
4458
"id": {
4559
"type": "Identifier",
46-
"name": "foo",
47-
"span": {
48-
"start": 0,
49-
"end": 3,
50-
"type": "Span"
60+
"name": "foo"
61+
}
62+
};
63+
64+
const message = this.parser.parseEntry(input)
65+
assert.deepEqual(message, output)
66+
});
67+
68+
test('return junk', function() {
69+
const input = ftl`
70+
# Attached Comment
71+
junk
72+
`;
73+
const output = {
74+
"content": "junk\n",
75+
"annotations": [
76+
{
77+
"args": ["="],
78+
"code": "E0003",
79+
"message": "Expected token: \"=\"",
80+
"span": {
81+
"end": 23,
82+
"start": 23,
83+
"type": "Span"
84+
},
85+
"type": "Annotation"
5186
}
87+
],
88+
"type": "Junk"
89+
};
90+
91+
const message = this.parser.parseEntry(input)
92+
assert.deepEqual(message, output)
93+
});
94+
95+
test('ignore all valid comments', function() {
96+
const input = ftl`
97+
# Attached Comment
98+
## Group Comment
99+
### Resource Comment
100+
foo = Foo
101+
`;
102+
const output = {
103+
"comment": null,
104+
"value": {
105+
"elements": [
106+
{
107+
"type": "TextElement",
108+
"value": "Foo"
109+
}
110+
],
111+
"type": "Pattern"
112+
},
113+
"annotations": [],
114+
"attributes": [],
115+
"type": "Message",
116+
"id": {
117+
"type": "Identifier",
118+
"name": "foo"
52119
}
53120
};
54121

55122
const message = this.parser.parseEntry(input)
56123
assert.deepEqual(message, output)
57124
});
125+
126+
test('do not ignore invalid comments', function() {
127+
const input = ftl`
128+
# Attached Comment
129+
##Invalid Comment
130+
`;
131+
const output = {
132+
"content": "##Invalid Comment\n",
133+
"annotations": [
134+
{
135+
"args": [" "],
136+
"code": "E0003",
137+
"message": "Expected token: \" \"",
138+
"span": {
139+
"end": 21,
140+
"start": 21,
141+
"type": "Span"
142+
},
143+
"type": "Annotation"
144+
}
145+
],
146+
"type": "Junk"
147+
};
148+
149+
const message = this.parser.parseEntry(input)
150+
assert.deepEqual(message, output)
151+
});
58152
});
59153

60154

@@ -66,36 +160,21 @@ suite('Serialize entry', function() {
66160
test('simple message', function() {
67161
const input = {
68162
"comment": null,
69-
"span": {
70-
"start": 0,
71-
"end": 9,
72-
"type": "Span"
73-
},
74163
"value": {
75164
"elements": [
76165
{
77166
"type": "TextElement",
78167
"value": "Foo"
79168
}
80169
],
81-
"type": "Pattern",
82-
"span": {
83-
"start": 6,
84-
"end": 9,
85-
"type": "Span"
86-
}
170+
"type": "Pattern"
87171
},
88172
"annotations": [],
89173
"attributes": [],
90174
"type": "Message",
91175
"id": {
92176
"type": "Identifier",
93-
"name": "foo",
94-
"span": {
95-
"start": 0,
96-
"end": 3,
97-
"type": "Span"
98-
}
177+
"name": "foo"
99178
}
100179
};
101180
const output = ftl`

0 commit comments

Comments
 (0)