python/moz/l10n/formats/mf2/serialize.py (96 lines of code) (raw):

# Copyright Mozilla Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations from collections.abc import Iterator from re import compile from typing import Literal from ...model import ( CatchallKey, Expression, Markup, Message, Pattern, PatternMessage, SelectMessage, VariableRef, ) from .validate import name_re, number_re complex_start_re = compile(r"[\t\n\r \u3000]*\.") literal_esc_re = compile(r"[\\|]") text_esc_re = compile(r"[\\{}]") def mf2_serialize_message(message: Message) -> str: """ Serialize a message using MessageFormat 2 syntax. Does not validate the message before serialization; for that, use `mf2_validate_message()`. """ if ( isinstance(message, PatternMessage) and not message.declarations and ( not message.pattern or not isinstance(part0 := message.pattern[0], str) or not complex_start_re.match(part0) ) ): # simple message return "".join(mf2_serialize_pattern(message.pattern)) res = "" for name, expr in message.declarations.items(): # TODO: Fix order by dependencies if isinstance(expr.arg, VariableRef) and expr.arg.name == name: res += ".input " else: res += f".local ${name} = " for s in _expression(expr): res += s res += "\n" if isinstance(message, PatternMessage): for s in _quoted_pattern(message.pattern): res += s else: assert isinstance(message, SelectMessage) res += ".match" for sel in message.selectors: res += f" ${sel.name}" for keys, pattern in message.variants.items(): res += "\n" for key in keys: res += "* " if isinstance(key, CatchallKey) else f"{_literal(key)} " for s in _quoted_pattern(pattern): res += s return res def mf2_serialize_pattern(pattern: Pattern) -> Iterator[str]: if not pattern: yield "" for part in pattern: if isinstance(part, Expression): yield from _expression(part) elif isinstance(part, Markup): yield from _markup(part) else: assert isinstance(part, str) yield text_esc_re.sub(r"\\\g<0>", part) def _quoted_pattern(pattern: Pattern) -> Iterator[str]: yield "{{" yield from mf2_serialize_pattern(pattern) yield "}}" def _expression(expr: Expression) -> Iterator[str]: yield "{" if expr.arg: yield _value(expr.arg) if expr.function: yield f" :{expr.function}" if expr.arg else f":{expr.function}" yield from _options(expr.options) yield from _attributes(expr.attributes) yield "}" def _markup(markup: Markup) -> Iterator[str]: yield "{/" if markup.kind == "close" else "{#" yield markup.name yield from _options(markup.options) yield from _attributes(markup.attributes) yield "/}" if markup.kind == "standalone" else "}" def _options(options: dict[str, str | VariableRef]) -> Iterator[str]: for name, value in options.items(): yield f" {name}={_value(value)}" def _attributes(attributes: dict[str, str | Literal[True]]) -> Iterator[str]: for name, value in attributes.items(): yield f" @{name}" if value is True else f" @{name}={_literal(value)}" def _value(value: str | VariableRef) -> str: return _literal(value) if isinstance(value, str) else f"${value.name}" def _literal(literal: str) -> str: if name_re.fullmatch(literal) or number_re.fullmatch(literal): return literal esc_literal = literal_esc_re.sub(r"\\\g<0>", literal) return f"|{esc_literal}|"