Skip to content

Commit 6e46458

Browse files
authored
Merge pull request #235 from stasm/comment-junk
Parse Comments as standalone if they're attached to Junk
2 parents dacc294 + 11b96ed commit 6e46458

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
@@ -56,12 +56,35 @@ export default class FluentParser {
5656
ps.skipBlankLines();
5757

5858
const entries = [];
59+
let lastComment = null;
5960

6061
while (ps.current()) {
6162
const entry = this.getEntryOrJunk(ps);
63+
const blankLines = ps.skipBlankLines();
64+
65+
// Regular Comments require special logic. Comments may be attached to
66+
// Messages or Terms if they are followed immediately by them. However
67+
// they should parse as standalone when they're followed by Junk.
68+
// Consequently, we only attach Comments once we know that the Message
69+
// or the Term parsed successfully.
70+
if (entry.type === "Comment" && blankLines === 0 && ps.current()) {
71+
// Stash the comment and decide what to do with it in the next pass.
72+
lastComment = entry;
73+
continue;
74+
}
6275

76+
if (lastComment) {
77+
if (entry.type === "Message" || entry.type === "Term") {
78+
entry.comment = lastComment;
79+
} else {
80+
entries.push(lastComment);
81+
}
82+
// In either case, the stashed comment has been dealt with; clear it.
83+
lastComment = null;
84+
}
85+
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)