Skip to content

Commit 3d791a7

Browse files
authored
Merge pull request #41 from stasm/replace-pattern
Bug 1434998 - Allow for REPLACE values to be Patterns or PatternElements
2 parents cb318cb + e731e8d commit 3d791a7

File tree

4 files changed

+117
-40
lines changed

4 files changed

+117
-40
lines changed

fluent/migrate/transforms.py

Lines changed: 42 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
"""
6565

6666
from __future__ import unicode_literals
67+
import itertools
6768

6869
import fluent.syntax.ast as FTL
6970
from .errors import NotSupportedError
@@ -128,11 +129,11 @@ def __call__(self, ctx):
128129

129130

130131
class REPLACE_IN_TEXT(Transform):
131-
"""Replace various placeables in the translation with FTL placeables.
132+
"""Replace various placeables in the translation with FTL.
132133
133134
The original placeables are defined as keys on the `replacements` dict.
134-
For each key the value is defined as a list of FTL Expressions to be
135-
interpolated.
135+
For each key the value is defined as a FTL Pattern, Placeable,
136+
TextElement or Expressions to be interpolated.
136137
"""
137138

138139
def __init__(self, value, replacements):
@@ -141,7 +142,7 @@ def __init__(self, value, replacements):
141142

142143
def __call__(self, ctx):
143144

144-
# Only replace placeable which are present in the translation.
145+
# Only replace placeables which are present in the translation.
145146
replacements = {
146147
key: evaluate(ctx, repl)
147148
for key, repl in self.replacements.iteritems()
@@ -154,40 +155,43 @@ def __call__(self, ctx):
154155
lambda x, y: self.value.find(x) - self.value.find(y)
155156
)
156157

157-
# Used to reduce the `keys_in_order` list.
158-
def replace(acc, cur):
159-
"""Convert original placeables and text into FTL Nodes.
160-
161-
For each original placeable the translation will be partitioned
162-
around it and the text before it will be converted into an
163-
`FTL.TextElement` and the placeable will be replaced with its
164-
replacement. The text following the placebale will be fed again to
165-
the `replace` function.
166-
"""
167-
168-
parts, rest = acc
169-
before, key, after = rest.value.partition(cur)
170-
171-
placeable = FTL.Placeable(replacements[key])
172-
173-
# Return the elements found and converted so far, and the remaining
174-
# text which hasn't been scanned for placeables yet.
175-
return (
176-
parts + [FTL.TextElement(before), placeable],
177-
FTL.TextElement(after)
178-
)
179-
180-
def is_non_empty(elem):
181-
"""Used for filtering empty `FTL.TextElement` nodes out."""
182-
return not isinstance(elem, FTL.TextElement) or len(elem.value)
183-
184-
# Start with an empty list of elements and the original translation.
185-
init = ([], FTL.TextElement(self.value))
186-
parts, tail = reduce(replace, keys_in_order, init)
187-
188-
# Explicitly concat the trailing part to get the full list of elements
189-
# and filter out the empty ones.
190-
elements = filter(is_non_empty, parts + [tail])
158+
# A list of PatternElements built from the legacy translation and the
159+
# FTL replacements. It may contain empty or adjacent TextElements.
160+
parts = []
161+
tail = self.value
162+
163+
# Convert original placeables and text into FTL Nodes. For each
164+
# original placeable the translation will be partitioned around it and
165+
# the text before it will be converted into an `FTL.TextElement` and
166+
# the placeable will be replaced with its replacement.
167+
for key in keys_in_order:
168+
before, key, tail = tail.partition(key)
169+
170+
# The replacement value can be of different types.
171+
replacement = replacements[key]
172+
if isinstance(replacement, FTL.Pattern):
173+
repl_elements = replacement.elements
174+
elif isinstance(replacement, FTL.PatternElement):
175+
repl_elements = [replacement]
176+
elif isinstance(replacement, FTL.Expression):
177+
repl_elements = [FTL.Placeable(replacement)]
178+
179+
parts.append(FTL.TextElement(before))
180+
parts.extend(repl_elements)
181+
182+
# Dont' forget about the tail after the loop ends.
183+
parts.append(FTL.TextElement(tail))
184+
185+
# Join adjacent TextElements.
186+
elements = []
187+
for elem_type, elems in itertools.groupby(parts, key=type):
188+
if elem_type is FTL.TextElement:
189+
text = FTL.TextElement(''.join(elem.value for elem in elems))
190+
# And remove empty ones.
191+
if len(text.value) > 0:
192+
elements.append(text)
193+
else:
194+
elements.extend(elems)
191195

192196
return FTL.Pattern(elements)
193197

fluent/syntax/ast.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,12 +190,15 @@ def __init__(self, elements, **kwargs):
190190
super(Pattern, self).__init__(**kwargs)
191191
self.elements = elements
192192

193-
class TextElement(SyntaxNode):
193+
class PatternElement(SyntaxNode):
194+
pass
195+
196+
class TextElement(PatternElement):
194197
def __init__(self, value, **kwargs):
195198
super(TextElement, self).__init__(**kwargs)
196199
self.value = value
197200

198-
class Placeable(SyntaxNode):
201+
class Placeable(PatternElement):
199202
def __init__(self, expression, **kwargs):
200203
super(Placeable, self).__init__(**kwargs)
201204
self.expression = expression

tests/migrate/test_concat.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616

1717
class MockContext(unittest.TestCase):
18+
maxDiff = None
19+
1820
def get_source(self, path, key):
1921
# Ignore path (test.properties) and get translations from self.strings
2022
# defined in setUp.

tests/migrate/test_replace.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616

1717
class MockContext(unittest.TestCase):
18+
maxDiff = None
19+
1820
def get_source(self, path, key):
1921
# Ignore path (test.properties) and get translations from self.strings
2022
# defined in setUp.
@@ -148,6 +150,72 @@ def test_replace_last(self):
148150
''')
149151
)
150152

153+
def test_replace_with_placeable(self):
154+
msg = FTL.Message(
155+
FTL.Identifier(u'hello'),
156+
value=REPLACE(
157+
'test.properties',
158+
'hello',
159+
{
160+
'#1': FTL.Placeable(
161+
EXTERNAL_ARGUMENT('user')
162+
)
163+
}
164+
)
165+
)
166+
167+
self.assertEqual(
168+
evaluate(self, msg).to_json(),
169+
ftl_message_to_json('''
170+
hello = Hello, { $user }!
171+
''')
172+
)
173+
174+
def test_replace_with_text_element(self):
175+
msg = FTL.Message(
176+
FTL.Identifier(u'hello'),
177+
value=REPLACE(
178+
'test.properties',
179+
'hello',
180+
{
181+
'#1': FTL.TextElement('you')
182+
}
183+
)
184+
)
185+
186+
self.assertEqual(
187+
evaluate(self, msg).to_json(),
188+
ftl_message_to_json('''
189+
hello = Hello, you!
190+
''')
191+
)
192+
193+
def test_replace_with_pattern(self):
194+
msg = FTL.Message(
195+
FTL.Identifier(u'hello'),
196+
value=REPLACE(
197+
'test.properties',
198+
'hello',
199+
{
200+
'#1': FTL.Pattern(
201+
elements=[
202+
FTL.TextElement('<img> '),
203+
FTL.Placeable(
204+
EXTERNAL_ARGUMENT('user')
205+
)
206+
]
207+
)
208+
}
209+
)
210+
)
211+
212+
self.assertEqual(
213+
evaluate(self, msg).to_json(),
214+
ftl_message_to_json('''
215+
hello = Hello, <img> { $user }!
216+
''')
217+
)
218+
151219

152220
if __name__ == '__main__':
153221
unittest.main()

0 commit comments

Comments
 (0)