@@ -79,104 +79,105 @@ def eval_node(subnode):
7979 return node .traverse (eval_node )
8080
8181
82+ def get_text (element ):
83+ '''Get text content of a PatternElement.'''
84+ if isinstance (element , FTL .TextElement ):
85+ return element .value
86+ if isinstance (element , FTL .Placeable ):
87+ if isinstance (element .expression , FTL .StringExpression ):
88+ return element .expression .value
89+ else :
90+ return None
91+ raise RuntimeError ('Expected PatternElement' )
92+
93+
94+ def chain_elements (elements ):
95+ '''Flatten a list of FTL nodes into an iterator over PatternElements.'''
96+ for element in elements :
97+ if isinstance (element , FTL .Pattern ):
98+ # PY3 yield from element.elements
99+ for child in element .elements :
100+ yield child
101+ elif isinstance (element , FTL .PatternElement ):
102+ yield element
103+ elif isinstance (element , FTL .Expression ):
104+ yield FTL .Placeable (element )
105+ else :
106+ raise RuntimeError (
107+ 'Expected Pattern, PatternElement or Expression' )
108+
109+
110+ re_leading_ws = re .compile (r'^(?P<whitespace>\s+)(?P<text>.*?)$' )
111+ re_trailing_ws = re .compile (r'^(?P<text>.*?)(?P<whitespace>\s+)$' )
112+
113+
114+ def extract_whitespace (regex , element ):
115+ '''Extract leading or trailing whitespace from a TextElement.
116+
117+ Return a tuple of (Placeable, TextElement) in which the Placeable
118+ encodes the extracted whitespace as a StringExpression and the
119+ TextElement has the same amount of whitespace removed. The
120+ Placeable with the extracted whitespace is always returned first.
121+ '''
122+ match = re .search (regex , element .value )
123+ if match :
124+ whitespace = match .group ('whitespace' )
125+ placeable = FTL .Placeable (FTL .StringExpression (whitespace ))
126+ if whitespace == element .value :
127+ return placeable , None
128+ else :
129+ return placeable , FTL .TextElement (match .group ('text' ))
130+ else :
131+ return None , element
132+
133+
82134class Transform (FTL .BaseNode ):
83135 def __call__ (self , ctx ):
84136 raise NotImplementedError
85137
86138 @staticmethod
87- def flatten_elements (elements ):
88- '''Flatten a list of FTL nodes into an iterator over PatternElements.'''
89- for element in elements :
90- if isinstance (element , FTL .Pattern ):
91- # PY3 yield from element.elements
92- for child in element .elements :
93- yield child
94- elif isinstance (element , FTL .PatternElement ):
95- yield element
96- elif isinstance (element , FTL .Expression ):
97- yield FTL .Placeable (element )
98- else :
99- raise RuntimeError (
100- 'Expected Pattern, PatternElement or Expression' )
139+ def pattern_of (* elements ):
140+ normalized = []
101141
102- @staticmethod
103- def normalize_text_content (elements ):
104- '''Normalize PatternElements with text content.
105-
106- Convert TextElements and StringExpressions into TextElements and join
107- adjacent ones.
108- '''
109-
110- def get_text (element ):
111- if isinstance (element , FTL .TextElement ):
112- return element .value
113- elif isinstance (element , FTL .Placeable ):
114- if isinstance (element .expression , FTL .StringExpression ):
115- return element .expression .value
116-
117- joined = []
118- for current in elements :
142+ # Normalize text content: convert all text to TextElements, join
143+ # adjacent text and prune empty.
144+ for current in chain_elements (elements ):
119145 current_text = get_text (current )
120146 if current_text is None :
121- joined .append (current )
147+ normalized .append (current )
122148 continue
123149
124- previous = joined [- 1 ] if len (joined ) else None
150+ previous = normalized [- 1 ] if len (normalized ) else None
125151 if isinstance (previous , FTL .TextElement ):
152+ # Join adjacent TextElements
126153 previous .value += current_text
127154 elif len (current_text ) > 0 :
128- # Normalize to a TextElement
129- joined .append (FTL .TextElement (current_text ))
130- return joined
155+ # Normalize non-empty text to a TextElement
156+ normalized .append (FTL .TextElement (current_text ))
157+ else :
158+ # Prune empty text
159+ pass
131160
132- @staticmethod
133- def preserve_whitespace (elements ):
134161 # Handle empty values
135- if len (elements ) == 0 :
136- return [FTL .Placeable (FTL .StringExpression ('' ))]
137-
138- re_leading = re .compile (r'^(?P<whitespace>\s+)(?P<text>.*?)$' )
139- re_trailing = re .compile (r'^(?P<text>.*?)(?P<whitespace>\s+)$' )
140-
141- def extract_whitespace (regex , element ):
142- '''Extract leading or trailing whitespace from a TextElement.
143-
144- Return a tuple of (Placeable, TextElement) in which the Placeable
145- encodes the extracted whitespace as a StringExpression and the
146- TextElement has the same amount of whitespace removed. The
147- Placeable with the extracted whitespace is always returned first.
148- '''
149- match = re .search (regex , element .value )
150- if match :
151- whitespace = match .group ('whitespace' )
152- empty_expr = FTL .Placeable (FTL .StringExpression (whitespace ))
153- if whitespace == element .value :
154- return empty_expr , None
155- else :
156- return empty_expr , FTL .TextElement (match .group ('text' ))
157- else :
158- return None , element
162+ if len (normalized ) == 0 :
163+ empty = FTL .Placeable (FTL .StringExpression ('' ))
164+ return FTL .Pattern ([empty ])
159165
160- if isinstance (elements [0 ], FTL .TextElement ):
161- ws , text = extract_whitespace (re_leading , elements [0 ])
162- elements [:1 ] = [ws , text ]
166+ # Handle explicit leading whitespace
167+ if isinstance (normalized [0 ], FTL .TextElement ):
168+ ws , text = extract_whitespace (re_leading_ws , normalized [0 ])
169+ normalized [:1 ] = [ws , text ]
163170
164- if isinstance (elements [- 1 ], FTL .TextElement ):
165- ws , text = extract_whitespace (re_trailing , elements [- 1 ])
166- elements [- 1 :] = [text , ws ]
171+ # Handle explicit trailing whitespace
172+ if isinstance (normalized [- 1 ], FTL .TextElement ):
173+ ws , text = extract_whitespace (re_trailing_ws , normalized [- 1 ])
174+ normalized [- 1 :] = [text , ws ]
167175
168- return [
176+ return FTL . Pattern ( [
169177 element
170- for element in elements
178+ for element in normalized
171179 if element is not None
172- ]
173-
174- @staticmethod
175- def pattern_of (* elements ):
176- elements = Transform .flatten_elements (elements )
177- elements = Transform .normalize_text_content (elements )
178- elements = Transform .preserve_whitespace (elements )
179- return FTL .Pattern (elements )
180+ ])
180181
181182
182183class Source (Transform ):
@@ -315,8 +316,8 @@ def __call__(self, ctx):
315316 key for key in reversed (self .DEFAULT_ORDER ) if key in keys
316317 ][0 ]
317318
318- # Match keys to legacy forms in order they are defined in
319- # Gecko's PluralForm.jsm. Filter out empty forms.
319+ # Match keys to legacy forms in the order they are defined in Gecko's
320+ # PluralForm.jsm. Filter out empty forms.
320321 pairs = [
321322 (key , var )
322323 for key , var in zip (keys , forms )
@@ -332,11 +333,12 @@ def __call__(self, ctx):
332333 # variant. We don't need to insert a SelectExpression for them.
333334 if len (pairs ) == 1 :
334335 _ , only_form = pairs [0 ]
335- return evaluate (ctx , self .foreach (only_form ))
336+ only_variant = evaluate (ctx , self .foreach (only_form ))
337+ return Transform .pattern_of (only_variant )
336338
337339 # Make sure the default key is defined. If it's missing, use the last
338340 # form (in CLDR order) found in the legacy translation.
339- pairs .sort (key = lambda ( k , v ) : self .DEFAULT_ORDER .index (k ))
341+ pairs .sort (key = lambda pair : self .DEFAULT_ORDER .index (pair [ 0 ] ))
340342 last_key , last_form = pairs [- 1 ]
341343 if last_key != default_key :
342344 pairs .append ((default_key , last_form ))
0 commit comments