Skip to content

Commit ed746d7

Browse files
authored
Implement Fluent 0.5 Private Messages (#35)
1 parent b9b4a65 commit ed746d7

25 files changed

+712
-91
lines changed

fluent/migrate/transforms.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ def createVariant(zipped_enum):
245245
# variant. Then evaluate it to a migrated FTL node.
246246
value = evaluate(ctx, self.foreach(variant))
247247
return FTL.Variant(
248-
key=FTL.Symbol(key),
248+
key=FTL.VariantName(key),
249249
value=value,
250250
default=index == last_index
251251
)

fluent/syntax/ast.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,9 +261,9 @@ def __init__(self, name, **kwargs):
261261
super(Identifier, self).__init__(**kwargs)
262262
self.name = name
263263

264-
class Symbol(Identifier):
264+
class VariantName(Identifier):
265265
def __init__(self, name, **kwargs):
266-
super(Symbol, self).__init__(name, **kwargs)
266+
super(VariantName, self).__init__(name, **kwargs)
267267

268268

269269
class BaseComment(Entry):

fluent/syntax/errors.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ def get_error_message(code, args):
2525
if code == 'E0007':
2626
return 'Keyword cannot end with a whitespace'
2727
if code == 'E0008':
28-
return 'Callee has to be a simple identifier'
28+
return 'The callee has to be a simple, upper-case identifier'
2929
if code == 'E0009':
30-
return 'Key has to be a simple identifier'
30+
return 'The key has to be a simple identifier'
3131
if code == 'E0010':
3232
return 'Expected one of the variants to be marked as default (*)'
3333
if code == 'E0011':
@@ -38,4 +38,12 @@ def get_error_message(code, args):
3838
return 'Expected literal'
3939
if code == 'E0015':
4040
return 'Only one variant can be marked as default (*)'
41+
if code == 'E0016':
42+
return 'Message references cannot be used as selectors'
43+
if code == 'E0017':
44+
return 'Variants cannot be used as selectors'
45+
if code == 'E0018':
46+
return 'Attributes of public messages cannot be used as selectors'
47+
if code == 'E0019':
48+
return 'Attributes of private messages cannot be used as placeables'
4149
return code

fluent/syntax/ftlstream.py

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -73,20 +73,31 @@ def take_char(self, f):
7373
return ch
7474
return None
7575

76-
def is_id_start(self):
77-
if self.ch is None:
76+
def is_char_id_start(self, ch=None):
77+
if ch is None:
7878
return False
7979

80-
cc = ord(self.ch)
81-
80+
cc = ord(ch)
8281
return (cc >= 97 and cc <= 122) or \
83-
(cc >= 65 and cc <= 90) or \
84-
cc == 95
82+
(cc >= 65 and cc <= 90)
83+
84+
def is_message_id_start(self):
85+
if self.current_is('-'):
86+
self.peek()
87+
88+
ch = self.current_peek()
89+
is_id = self.is_char_id_start(ch)
90+
self.reset_peek()
91+
return is_id
8592

8693
def is_number_start(self):
87-
cc = ord(self.ch)
94+
if self.current_is('-'):
95+
self.peek()
8896

89-
return (cc >= 48 and cc <= 57) or cc == 45
97+
cc = ord(self.current_peek())
98+
is_digit = cc >= 48 and cc <= 57
99+
self.reset_peek()
100+
return is_digit
90101

91102
def is_peek_next_line_zero_four_style_comment(self):
92103
if not self.current_peek_is('\n'):
@@ -211,19 +222,24 @@ def skip_to_next_entry_start(self):
211222
if self.current_is('\n') and not self.peek_char_is('\n'):
212223
self.next()
213224

214-
if self.ch is None or self.is_id_start() or \
225+
if self.ch is None or self.is_message_id_start() or \
215226
(self.current_is('/') and self.peek_char_is('/')) or \
216227
(self.current_is('[') and self.peek_char_is('[')):
217228
break
218229
self.next()
219230

220-
def take_id_start(self):
221-
if self.is_id_start():
231+
def take_id_start(self, allow_private):
232+
if allow_private and self.current_is('-'):
233+
self.next()
234+
return '-'
235+
236+
if self.is_char_id_start(self.ch):
222237
ret = self.ch
223238
self.next()
224239
return ret
225240

226-
raise ParseError('E0004', 'a-zA-Z_')
241+
allowed_range = 'a-zA-Z-' if allow_private else 'a-zA-Z'
242+
raise ParseError('E0004', allowed_range)
227243

228244
def take_id_char(self):
229245
def closure(ch):
@@ -234,7 +250,7 @@ def closure(ch):
234250
cc == 95 or cc == 45)
235251
return self.take_char(closure)
236252

237-
def take_symb_char(self):
253+
def take_variant_name_char(self):
238254
def closure(ch):
239255
if ch is None:
240256
return False

fluent/syntax/parser.py

Lines changed: 55 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ def get_entry(self, ps):
105105
return ast.GroupComment(comment.content)
106106
return None
107107

108-
if ps.is_id_start() \
108+
if ps.is_message_id_start() \
109109
and (comment is None or isinstance(comment, ast.Comment)):
110110
return self.get_message(ps, comment)
111111

@@ -187,7 +187,7 @@ def skip_section(self, ps):
187187

188188
ps.skip_inline_ws()
189189

190-
self.get_symbol(ps)
190+
self.get_variant_name(ps)
191191

192192
ps.skip_inline_ws()
193193

@@ -199,7 +199,7 @@ def skip_section(self, ps):
199199

200200
@with_span
201201
def get_message(self, ps, comment):
202-
id = self.get_identifier(ps)
202+
id = self.get_private_identifier(ps)
203203

204204
ps.skip_inline_ws()
205205

@@ -225,7 +225,7 @@ def get_message(self, ps, comment):
225225
def get_attribute(self, ps):
226226
ps.expect_char('.')
227227

228-
key = self.get_identifier(ps)
228+
key = self.get_public_identifier(ps)
229229

230230
ps.skip_inline_ws()
231231
ps.expect_char('=')
@@ -252,10 +252,17 @@ def get_attributes(self, ps):
252252
return attrs
253253

254254
@with_span
255-
def get_identifier(self, ps):
255+
def get_private_identifier(self, ps):
256+
return self.get_identifier(ps, True)
257+
258+
@with_span
259+
def get_public_identifier(self, ps):
260+
return self.get_identifier(ps, False)
261+
262+
def get_identifier(self, ps, allow_private):
256263
name = ''
257264

258-
name += ps.take_id_start()
265+
name += ps.take_id_start(allow_private)
259266

260267
ch = ps.take_id_char()
261268
while ch:
@@ -270,10 +277,11 @@ def get_variant_key(self, ps):
270277
if ch is None:
271278
raise ParseError('E0013')
272279

273-
if ps.is_number_start():
280+
cc = ord(ch)
281+
if ((cc >= 48 and cc <= 57) or cc == 45): # 0-9, -
274282
return self.get_number(ps)
275283

276-
return self.get_symbol(ps)
284+
return self.get_variant_name(ps)
277285

278286
@with_span
279287
def get_variant(self, ps, has_default):
@@ -323,19 +331,19 @@ def get_variants(self, ps):
323331
return variants
324332

325333
@with_span
326-
def get_symbol(self, ps):
334+
def get_variant_name(self, ps):
327335
name = ''
328336

329-
name += ps.take_id_start()
337+
name += ps.take_id_start(False)
330338

331339
while True:
332-
ch = ps.take_symb_char()
340+
ch = ps.take_variant_name_char()
333341
if ch:
334342
name += ch
335343
else:
336344
break
337345

338-
return ast.Symbol(name.rstrip())
346+
return ast.VariantName(name.rstrip())
339347

340348
def get_digits(self, ps):
341349
num = ''
@@ -455,22 +463,37 @@ def get_expression(self, ps):
455463

456464
if ps.current_is('-'):
457465
ps.peek()
466+
458467
if not ps.current_peek_is('>'):
459468
ps.reset_peek()
460-
else:
461-
ps.next()
462-
ps.next()
469+
return selector
463470

464-
ps.skip_inline_ws()
471+
if isinstance(selector, ast.MessageReference):
472+
raise ParseError('E0016')
465473

466-
variants = self.get_variants(ps)
474+
if isinstance(selector, ast.AttributeExpression) and \
475+
not selector.id.name.startswith('-'):
476+
raise ParseError('E0018')
467477

468-
if len(variants) == 0:
469-
raise ParseError('E0011')
478+
if isinstance(selector, ast.VariantExpression):
479+
raise ParseError('E0017')
470480

471-
ps.expect_indent()
481+
ps.next()
482+
ps.next()
472483

473-
return ast.SelectExpression(selector, variants)
484+
ps.skip_inline_ws()
485+
486+
variants = self.get_variants(ps)
487+
488+
if len(variants) == 0:
489+
raise ParseError('E0011')
490+
491+
ps.expect_indent()
492+
493+
return ast.SelectExpression(selector, variants)
494+
elif isinstance(selector, ast.AttributeExpression) and \
495+
selector.id.name.startswith('-'):
496+
raise ParseError('E0019')
474497

475498
return selector
476499

@@ -485,7 +508,7 @@ def get_selector_expression(self, ps):
485508

486509
if (ch == '.'):
487510
ps.next()
488-
attr = self.get_identifier(ps)
511+
attr = self.get_public_identifier(ps)
489512
return ast.AttributeExpression(literal.id, attr)
490513

491514
if (ch == '['):
@@ -501,7 +524,7 @@ def get_selector_expression(self, ps):
501524

502525
ps.expect_char(')')
503526

504-
if not re.match('^[A-Z_-]+$', literal.id.name):
527+
if not re.match('^[A-Z][A-Z_?-]*$', literal.id.name):
505528
raise ParseError('E0008')
506529

507530
return ast.CallExpression(
@@ -582,14 +605,16 @@ def get_literal(self, ps):
582605
if ch is None:
583606
raise ParseError('E0014')
584607

585-
if ps.is_number_start():
608+
if ch == '$':
609+
ps.next()
610+
name = self.get_public_identifier(ps)
611+
return ast.ExternalArgument(name)
612+
elif ps.is_message_id_start():
613+
name = self.get_private_identifier(ps)
614+
return ast.MessageReference(name)
615+
elif ps.is_number_start():
586616
return self.get_number(ps)
587617
elif ch == '"':
588618
return self.get_string(ps)
589-
elif ch == '$':
590-
ps.next()
591-
name = self.get_identifier(ps)
592-
return ast.ExternalArgument(name)
593619

594-
name = self.get_identifier(ps)
595-
return ast.MessageReference(name)
620+
raise ParseError('E0014')

fluent/syntax/serializer.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,11 @@ def serialize_section(section):
7878
if section.comment:
7979
return "\n\n{}\n[[ {} ]]\n\n".format(
8080
serialize_comment(section.comment),
81-
serialize_symbol(section.name)
81+
serialize_variant_name(section.name)
8282
)
8383
else:
8484
return "\n\n[[ {} ]]\n\n".format(
85-
serialize_symbol(section.name)
85+
serialize_variant_name(section.name)
8686
)
8787

8888

@@ -269,13 +269,13 @@ def serialize_identifier(identifier):
269269
return identifier.name
270270

271271

272-
def serialize_symbol(symbol):
272+
def serialize_variant_name(symbol):
273273
return symbol.name
274274

275275

276276
def serialize_variant_key(key):
277-
if isinstance(key, ast.Symbol):
278-
return serialize_symbol(key)
277+
if isinstance(key, ast.VariantName):
278+
return serialize_variant_name(key)
279279
if isinstance(key, ast.NumberExpression):
280280
return serialize_number_expression(key)
281281
raise Exception('Unknown variant key type: {}'.format(key.type))
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
key = { foo.23 }
22
3-
//~ ERROR E0004, pos 12, args "a-zA-Z_"
3+
//~ ERROR E0004, pos 12, args "a-zA-Z"
44

55
key = { foo. }
66
7-
//~ ERROR E0004, pos 31, args "a-zA-Z_"
7+
//~ ERROR E0004, pos 31, args "a-zA-Z"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
err1 = { -brand.gender }
2+
//~ ERROR E0019, pos 23
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
err1 =
2+
{ foo.bar ->
3+
[1] One
4+
*[2] Two
5+
}
6+
//~ ERROR E0018, pos 21

tests/syntax/fixtures_behavior/broken_number.ftl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ key = { -2.4. }
55
//~ ERROR E0003, pos 30, args "}"
66

77
key = { -.4 }
8-
//~ ERROR E0004, pos 44, args "0-9"
8+
//~ ERROR E0014, pos 43
99

1010
key = { -2..4 }
1111
//~ ERROR E0004, pos 61, args "0-9"

tests/syntax/fixtures_behavior/indent.ftl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
key2 = {
55
a }
6-
//~ ERROR E0004, pos 20, args "a-zA-Z_"
6+
//~ ERROR E0014, pos 20
77
//~ ERROR E0005, pos 23, args "a"
88

99
key3 = { a
@@ -12,4 +12,4 @@ key3 = { a
1212

1313
key4 = {
1414
{ a }}
15-
//~ ERROR E0004, pos 48, args "a-zA-Z_"
15+
//~ ERROR E0014, pos 48
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
err1 =
2+
{ foo ->
3+
*[1] One
4+
}
5+
//~ ERROR E0016, pos 17
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
key = Value
22
.2 = Foo
3-
//~ ERROR E0004, pos 17, args "a-zA-Z_"
3+
//~ ERROR E0004, pos 17, args "a-zA-Z"

0 commit comments

Comments
 (0)