summaryrefslogtreecommitdiffstats
path: root/third_party/python/fluent.syntax/fluent/syntax/serializer.py
blob: 68ea89b3d300a28647bf44abc9464744341fd353 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
from typing import List, Union
from . import ast


def indent_except_first_line(content: str) -> str:
    return "    ".join(
        content.splitlines(True)
    )


def includes_new_line(elem: Union[ast.TextElement, ast.Placeable]) -> bool:
    return isinstance(elem, ast.TextElement) and "\n" in elem.value


def is_select_expr(elem: Union[ast.TextElement, ast.Placeable]) -> bool:
    return (
        isinstance(elem, ast.Placeable) and
        isinstance(elem.expression, ast.SelectExpression))


def should_start_on_new_line(pattern: ast.Pattern) -> bool:
    is_multiline = any(is_select_expr(elem) for elem in pattern.elements) \
        or any(includes_new_line(elem) for elem in pattern.elements)

    if is_multiline:
        first_element = pattern.elements[0]
        if isinstance(first_element, ast.TextElement):
            first_char = first_element.value[0]
            if first_char in ("[", ".", "*"):
                return False
        return True
    return False


class FluentSerializer:
    """FluentSerializer converts :class:`.ast.SyntaxNode` objects to unicode strings.

    `with_junk` controls if parse errors are written back or not.
    """
    HAS_ENTRIES = 1

    def __init__(self, with_junk: bool = False):
        self.with_junk = with_junk

    def serialize(self, resource: ast.Resource) -> str:
        "Serialize a :class:`.ast.Resource` to a string."
        if not isinstance(resource, ast.Resource):
            raise Exception('Unknown resource type: {}'.format(type(resource)))

        state = 0

        parts: List[str] = []
        for entry in resource.body:
            if not isinstance(entry, ast.Junk) or self.with_junk:
                parts.append(self.serialize_entry(entry, state))
                if not state & self.HAS_ENTRIES:
                    state |= self.HAS_ENTRIES

        return "".join(parts)

    def serialize_entry(self, entry: ast.EntryType, state: int = 0) -> str:
        "Serialize an :class:`.ast.Entry` to a string."
        if isinstance(entry, ast.Message):
            return serialize_message(entry)
        if isinstance(entry, ast.Term):
            return serialize_term(entry)
        if isinstance(entry, ast.Comment):
            if state & self.HAS_ENTRIES:
                return "\n{}\n".format(serialize_comment(entry, "#"))
            return "{}\n".format(serialize_comment(entry, "#"))
        if isinstance(entry, ast.GroupComment):
            if state & self.HAS_ENTRIES:
                return "\n{}\n".format(serialize_comment(entry, "##"))
            return "{}\n".format(serialize_comment(entry, "##"))
        if isinstance(entry, ast.ResourceComment):
            if state & self.HAS_ENTRIES:
                return "\n{}\n".format(serialize_comment(entry, "###"))
            return "{}\n".format(serialize_comment(entry, "###"))
        if isinstance(entry, ast.Junk):
            return serialize_junk(entry)
        raise Exception('Unknown entry type: {}'.format(type(entry)))


def serialize_comment(comment: Union[ast.Comment, ast.GroupComment, ast.ResourceComment], prefix: str = "#") -> str:
    if not comment.content:
        return f'{prefix}\n'

    prefixed = "\n".join([
        prefix if len(line) == 0 else f"{prefix} {line}"
        for line in comment.content.split("\n")
    ])
    # Add the trailing line break.
    return f'{prefixed}\n'


def serialize_junk(junk: ast.Junk) -> str:
    return junk.content or ''


def serialize_message(message: ast.Message) -> str:
    parts: List[str] = []

    if message.comment:
        parts.append(serialize_comment(message.comment))

    parts.append(f"{message.id.name} =")

    if message.value:
        parts.append(serialize_pattern(message.value))

    if message.attributes:
        for attribute in message.attributes:
            parts.append(serialize_attribute(attribute))

    parts.append("\n")
    return ''.join(parts)


def serialize_term(term: ast.Term) -> str:
    parts: List[str] = []

    if term.comment:
        parts.append(serialize_comment(term.comment))

    parts.append(f"-{term.id.name} =")
    parts.append(serialize_pattern(term.value))

    if term.attributes:
        for attribute in term.attributes:
            parts.append(serialize_attribute(attribute))

    parts.append("\n")
    return ''.join(parts)


def serialize_attribute(attribute: ast.Attribute) -> str:
    return "\n    .{} ={}".format(
        attribute.id.name,
        indent_except_first_line(serialize_pattern(attribute.value))
    )


def serialize_pattern(pattern: ast.Pattern) -> str:
    content = "".join(serialize_element(elem) for elem in pattern.elements)
    content = indent_except_first_line(content)

    if should_start_on_new_line(pattern):
        return f'\n    {content}'

    return f' {content}'


def serialize_element(element: ast.PatternElement) -> str:
    if isinstance(element, ast.TextElement):
        return element.value
    if isinstance(element, ast.Placeable):
        return serialize_placeable(element)
    raise Exception('Unknown element type: {}'.format(type(element)))


def serialize_placeable(placeable: ast.Placeable) -> str:
    expr = placeable.expression
    if isinstance(expr, ast.Placeable):
        return "{{{}}}".format(serialize_placeable(expr))
    if isinstance(expr, ast.SelectExpression):
        # Special-case select expressions to control the withespace around the
        # opening and the closing brace.
        return "{{ {}}}".format(serialize_expression(expr))
    if isinstance(expr, ast.Expression):
        return "{{ {} }}".format(serialize_expression(expr))
    raise Exception('Unknown expression type: {}'.format(type(expr)))


def serialize_expression(expression: Union[ast.Expression, ast.Placeable]) -> str:
    if isinstance(expression, ast.StringLiteral):
        return f'"{expression.value}"'
    if isinstance(expression, ast.NumberLiteral):
        return expression.value
    if isinstance(expression, ast.VariableReference):
        return f"${expression.id.name}"
    if isinstance(expression, ast.TermReference):
        out = f"-{expression.id.name}"
        if expression.attribute is not None:
            out += f".{expression.attribute.name}"
        if expression.arguments is not None:
            out += serialize_call_arguments(expression.arguments)
        return out
    if isinstance(expression, ast.MessageReference):
        out = expression.id.name
        if expression.attribute is not None:
            out += f".{expression.attribute.name}"
        return out
    if isinstance(expression, ast.FunctionReference):
        args = serialize_call_arguments(expression.arguments)
        return f"{expression.id.name}{args}"
    if isinstance(expression, ast.SelectExpression):
        out = "{} ->".format(
            serialize_expression(expression.selector))
        for variant in expression.variants:
            out += serialize_variant(variant)
        return f"{out}\n"
    if isinstance(expression, ast.Placeable):
        return serialize_placeable(expression)
    raise Exception('Unknown expression type: {}'.format(type(expression)))


def serialize_variant(variant: ast.Variant) -> str:
    return "\n{}[{}]{}".format(
        "   *" if variant.default else "    ",
        serialize_variant_key(variant.key),
        indent_except_first_line(serialize_pattern(variant.value))
    )


def serialize_call_arguments(expr: ast.CallArguments) -> str:
    positional = ", ".join(
        serialize_expression(arg) for arg in expr.positional)
    named = ", ".join(
        serialize_named_argument(arg) for arg in expr.named)
    if len(expr.positional) > 0 and len(expr.named) > 0:
        return f'({positional}, {named})'
    return '({})'.format(positional or named)


def serialize_named_argument(arg: ast.NamedArgument) -> str:
    return "{}: {}".format(
        arg.name.name,
        serialize_expression(arg.value)
    )


def serialize_variant_key(key: Union[ast.Identifier, ast.NumberLiteral]) -> str:
    if isinstance(key, ast.Identifier):
        return key.name
    if isinstance(key, ast.NumberLiteral):
        return key.value
    raise Exception('Unknown variant key type: {}'.format(type(key)))