Skip to content

Commit b9b4a65

Browse files
authored
Add 0.5 comments support (#32)
* Add 0.5 comments support * Support 0.5 comments in fluent.migrate
1 parent 44b1203 commit b9b4a65

23 files changed

+292
-232
lines changed

fluent/migrate/merge.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,7 @@ def merge_body(body):
2626

2727
def merge_entry(entry):
2828
# All standalone comments will be merged.
29-
if isinstance(entry, FTL.Comment):
30-
return entry
31-
32-
# All section headers will be merged.
33-
if isinstance(entry, FTL.Section):
29+
if isinstance(entry, FTL.BaseComment):
3430
return entry
3531

3632
# Ignore Junk
@@ -56,4 +52,4 @@ def merge_entry(entry):
5652
return evaluate(ctx, transform)
5753

5854
body = merge_body(reference.body)
59-
return FTL.Resource(body, reference.comment)
55+
return FTL.Resource(body)

fluent/syntax/ast.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -151,10 +151,9 @@ def add_span(self, start, end):
151151

152152

153153
class Resource(SyntaxNode):
154-
def __init__(self, body=None, comment=None, **kwargs):
154+
def __init__(self, body=None, **kwargs):
155155
super(Resource, self).__init__(**kwargs)
156156
self.body = body or []
157-
self.comment = comment
158157

159158

160159
class Entry(SyntaxNode):
@@ -266,16 +265,30 @@ class Symbol(Identifier):
266265
def __init__(self, name, **kwargs):
267266
super(Symbol, self).__init__(name, **kwargs)
268267

269-
class Comment(Entry):
268+
269+
class BaseComment(Entry):
270270
def __init__(self, content=None, **kwargs):
271-
super(Comment, self).__init__(**kwargs)
271+
super(BaseComment, self).__init__(**kwargs)
272272
self.content = content
273273

274-
class Section(Entry):
275-
def __init__(self, name, comment=None, **kwargs):
276-
super(Section, self).__init__(**kwargs)
277-
self.name = name
278-
self.comment = comment
274+
275+
class Comment(BaseComment):
276+
def __init__(self, content=None, **kwargs):
277+
zero_four_style = kwargs.pop("zero_four_style", False)
278+
super(Comment, self).__init__(content, **kwargs)
279+
if zero_four_style:
280+
self.zero_four_style = True
281+
282+
283+
class GroupComment(BaseComment):
284+
def __init__(self, content=None, **kwargs):
285+
super(GroupComment, self).__init__(content, **kwargs)
286+
287+
288+
class ResourceComment(BaseComment):
289+
def __init__(self, content=None, **kwargs):
290+
super(ResourceComment, self).__init__(content, **kwargs)
291+
279292

280293
class Function(Identifier):
281294
def __init__(self, name, **kwargs):

fluent/syntax/ftlstream.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,12 @@ def is_number_start(self):
8888

8989
return (cc >= 48 and cc <= 57) or cc == 45
9090

91-
def is_peek_next_line_comment(self):
91+
def is_peek_next_line_zero_four_style_comment(self):
9292
if not self.current_peek_is('\n'):
9393
return False
9494

9595
self.peek()
96+
9697
if self.current_peek_is('/'):
9798
self.peek()
9899
if self.current_peek_is('/'):
@@ -102,6 +103,34 @@ def is_peek_next_line_comment(self):
102103
self.reset_peek()
103104
return False
104105

106+
# -1 - any
107+
# 0 - comment
108+
# 1 - group comment
109+
# 2 - resource comment
110+
def is_peek_next_line_comment(self, level=-1):
111+
if not self.current_peek_is('\n'):
112+
return False
113+
114+
i = 0
115+
116+
while (i <= level or (level == -1 and i < 3)):
117+
self.peek()
118+
if not self.current_peek_is('#'):
119+
if i != level and level != -1:
120+
self.reset_peek()
121+
return False
122+
break
123+
i += 1
124+
125+
self.peek()
126+
127+
if self.current_peek() in [' ', '\n']:
128+
self.reset_peek()
129+
return True
130+
131+
self.reset_peek()
132+
return False
133+
105134
def is_peek_next_line_variant_start(self):
106135
if not self.current_peek_is('\n'):
107136
return False

fluent/syntax/parser.py

Lines changed: 68 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ def decorated(self, ps, *args):
1818
if node.span is not None:
1919
return node
2020

21-
# Spans of Messages and Sections should include the attached Comment.
22-
if isinstance(node, ast.Message) or isinstance(node, ast.Section):
21+
# Spans of Messages should include the attached Comment.
22+
if isinstance(node, ast.Message):
2323
if node.comment is not None:
2424
start = node.comment.span.start
2525

@@ -35,8 +35,6 @@ def __init__(self, with_spans=True):
3535
self.with_spans = with_spans
3636

3737
def parse(self, source):
38-
comment = None
39-
4038
ps = FTLParserStream(source)
4139
ps.skip_blank_lines()
4240

@@ -45,15 +43,21 @@ def parse(self, source):
4543
while ps.current():
4644
entry = self.get_entry_or_junk(ps)
4745

48-
if isinstance(entry, ast.Comment) and len(entries) == 0:
49-
comment = entry
46+
if entry is None:
47+
continue
48+
49+
if isinstance(entry, ast.Comment) \
50+
and hasattr(entry, "zero_four_style") and len(entries) == 0:
51+
comment = ast.ResourceComment(entry.content)
52+
comment.span = entry.span
53+
entries.append(comment)
5054
else:
5155
entries.append(entry)
5256

5357
ps.skip_inline_ws()
5458
ps.skip_blank_lines()
5559

56-
res = ast.Resource(entries, comment)
60+
res = ast.Resource(entries)
5761

5862
if self.with_spans:
5963
res.add_span(0, ps.get_index())
@@ -88,17 +92,21 @@ def get_entry_or_junk(self, ps):
8892
def get_entry(self, ps):
8993
comment = None
9094

91-
if ps.current_is('/'):
95+
if ps.current_is('/') or ps.current_is('#'):
9296
comment = self.get_comment(ps)
9397

9498
# The Comment content doesn't include the trailing newline. Consume
9599
# this newline to be ready for the next entry. None stands for EOF.
96100
ps.expect_char('\n' if ps.current() else None)
97101

98102
if ps.current_is('['):
99-
return self.get_section(ps, comment)
103+
self.skip_section(ps)
104+
if comment:
105+
return ast.GroupComment(comment.content)
106+
return None
100107

101-
if ps.is_id_start():
108+
if ps.is_id_start() \
109+
and (comment is None or isinstance(comment, ast.Comment)):
102110
return self.get_message(ps, comment)
103111

104112
if comment:
@@ -107,23 +115,20 @@ def get_entry(self, ps):
107115
raise ParseError('E0002')
108116

109117
@with_span
110-
def get_comment(self, ps):
118+
def get_zero_four_style_comment(self, ps):
111119
ps.expect_char('/')
112120
ps.expect_char('/')
113121
ps.take_char_if(' ')
114122

115123
content = ''
116124

117-
def until_eol(x):
118-
return x != '\n'
119-
120125
while True:
121-
ch = ps.take_char(until_eol)
126+
ch = ps.take_char(lambda x: x != '\n')
122127
while ch:
123128
content += ch
124-
ch = ps.take_char(until_eol)
129+
ch = ps.take_char(lambda x: x != '\n')
125130

126-
if ps.is_peek_next_line_comment():
131+
if ps.is_peek_next_line_zero_four_style_comment():
127132
content += '\n'
128133
ps.next()
129134
ps.expect_char('/')
@@ -132,23 +137,65 @@ def until_eol(x):
132137
else:
133138
break
134139

135-
return ast.Comment(content)
140+
comment = ast.Comment(content)
141+
comment.zero_four_style = True
142+
return comment
136143

137144
@with_span
138-
def get_section(self, ps, comment):
145+
def get_comment(self, ps):
146+
if ps.current_is('/'):
147+
return self.get_zero_four_style_comment(ps)
148+
149+
# 0 - comment
150+
# 1 - group comment
151+
# 2 - resource comment
152+
level = -1
153+
content = ''
154+
155+
while True:
156+
i = -1
157+
while ps.current_is('#') and (i < (2 if level == -1 else level)):
158+
ps.next()
159+
i += 1
160+
161+
if level == -1:
162+
level = i
163+
164+
if not ps.current_is('\n'):
165+
ps.expect_char(' ')
166+
ch = ps.take_char(lambda x: x != '\n')
167+
while ch:
168+
content += ch
169+
ch = ps.take_char(lambda x: x != '\n')
170+
171+
if ps.is_peek_next_line_comment(level):
172+
content += '\n'
173+
ps.next()
174+
else:
175+
break
176+
177+
if level == 0:
178+
return ast.Comment(content)
179+
elif level == 1:
180+
return ast.GroupComment(content)
181+
elif level == 2:
182+
return ast.ResourceComment(content)
183+
184+
def skip_section(self, ps):
139185
ps.expect_char('[')
140186
ps.expect_char('[')
141187

142188
ps.skip_inline_ws()
143189

144-
symb = self.get_symbol(ps)
190+
self.get_symbol(ps)
145191

146192
ps.skip_inline_ws()
147193

148194
ps.expect_char(']')
149195
ps.expect_char(']')
150196

151-
return ast.Section(symb, comment)
197+
ps.skip_inline_ws()
198+
ps.next()
152199

153200
@with_span
154201
def get_message(self, ps, comment):

fluent/syntax/serializer.py

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,39 +16,61 @@ def contain_new_line(elems):
1616

1717

1818
class FluentSerializer(object):
19+
HAS_ENTRIES = 1
20+
1921
def __init__(self, with_junk=False):
2022
self.with_junk = with_junk
2123

2224
def serialize(self, resource):
25+
state = 0
26+
2327
parts = []
24-
if resource.comment:
25-
parts.append(
26-
"{}\n\n".format(
27-
serialize_comment(resource.comment)
28-
)
29-
)
3028
for entry in resource.body:
3129
if not isinstance(entry, ast.Junk) or self.with_junk:
32-
parts.append(self.serialize_entry(entry))
30+
parts.append(self.serialize_entry(entry, state))
31+
if not state & self.HAS_ENTRIES:
32+
state |= self.HAS_ENTRIES
3333

3434
return "".join(parts)
3535

36-
def serialize_entry(self, entry):
36+
def serialize_entry(self, entry, state=0):
3737
if isinstance(entry, ast.Message):
3838
return serialize_message(entry)
39-
if isinstance(entry, ast.Section):
40-
return serialize_section(entry)
4139
if isinstance(entry, ast.Comment):
42-
return "\n{}\n\n".format(serialize_comment(entry))
40+
if state & self.HAS_ENTRIES:
41+
return "\n{}\n\n".format(serialize_comment(entry))
42+
return "{}\n\n".format(serialize_comment(entry))
43+
if isinstance(entry, ast.GroupComment):
44+
if state & self.HAS_ENTRIES:
45+
return "\n{}\n\n".format(serialize_group_comment(entry))
46+
return "{}\n\n".format(serialize_group_comment(entry))
47+
if isinstance(entry, ast.ResourceComment):
48+
if state & self.HAS_ENTRIES:
49+
return "\n{}\n\n".format(serialize_resource_comment(entry))
50+
return "{}\n\n".format(serialize_resource_comment(entry))
4351
if isinstance(entry, ast.Junk):
4452
return serialize_junk(entry)
4553
raise Exception('Unknown entry type: {}'.format(entry.type))
4654

4755

4856
def serialize_comment(comment):
49-
return "".join([
50-
"{}{}".format("// ", line)
51-
for line in comment.content.splitlines(True)
57+
return "\n".join([
58+
"#" if len(line) == 0 else "# {}".format(line)
59+
for line in comment.content.splitlines(False)
60+
])
61+
62+
63+
def serialize_group_comment(comment):
64+
return "\n".join([
65+
"##" if len(line) == 0 else "## {}".format(line)
66+
for line in comment.content.splitlines(False)
67+
])
68+
69+
70+
def serialize_resource_comment(comment):
71+
return "\n".join([
72+
"###" if len(line) == 0 else "### {}".format(line)
73+
for line in comment.content.splitlines(False)
5274
])
5375

5476

tests/migrate/fixtures/en-US/aboutDialog.ftl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
// This Source Code Form is subject to the terms of the Mozilla Public
2-
// License, v. 2.0. If a copy of the MPL was not distributed with this
3-
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
1+
# This Source Code Form is subject to the terms of the Mozilla Public
2+
# License, v. 2.0. If a copy of the MPL was not distributed with this
3+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
44

55
update-failed = Update failed. <a>Download manually</a>!
66
channel-desc = You are on the { $channelname } channel.

tests/migrate/fixtures/en-US/aboutDownloads.ftl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
// This Source Code Form is subject to the terms of the Mozilla Public
2-
// License, v. 2.0. If a copy of the MPL was not distributed with this
3-
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
1+
# This Source Code Form is subject to the terms of the Mozilla Public
2+
# License, v. 2.0. If a copy of the MPL was not distributed with this
3+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
44

55
title = Downloads
66
header = Your Downloads

tests/migrate/fixtures/en-US/privacy.ftl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
// This Source Code Form is subject to the terms of the Mozilla Public
2-
// License, v. 2.0. If a copy of the MPL was not distributed with this
3-
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
1+
# This Source Code Form is subject to the terms of the Mozilla Public
2+
# License, v. 2.0. If a copy of the MPL was not distributed with this
3+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
44

55
dnt-description =
66
Send websites a “Do Not Track” signal

0 commit comments

Comments
 (0)