1from fontTools.voltLib.error import VoltLibError
2from typing import NamedTuple
3
4
5class Pos(NamedTuple):
6    adv: int
7    dx: int
8    dy: int
9    adv_adjust_by: dict
10    dx_adjust_by: dict
11    dy_adjust_by: dict
12
13    def __str__(self):
14        res = ' POS'
15        for attr in ('adv', 'dx', 'dy'):
16            value = getattr(self, attr)
17            if value is not None:
18                res += f' {attr.upper()} {value}'
19                adjust_by = getattr(self, f'{attr}_adjust_by', {})
20                for size, adjustment in adjust_by.items():
21                    res += f' ADJUST_BY {adjustment} AT {size}'
22        res += ' END_POS'
23        return res
24
25
26class Element(object):
27    def __init__(self, location=None):
28        self.location = location
29
30    def build(self, builder):
31        pass
32
33    def __str__(self):
34        raise NotImplementedError
35
36
37class Statement(Element):
38    pass
39
40
41class Expression(Element):
42    pass
43
44
45class VoltFile(Statement):
46    def __init__(self):
47        Statement.__init__(self, location=None)
48        self.statements = []
49
50    def build(self, builder):
51        for s in self.statements:
52            s.build(builder)
53
54    def __str__(self):
55        return '\n' + '\n'.join(str(s) for s in self.statements) + ' END\n'
56
57
58class GlyphDefinition(Statement):
59    def __init__(self, name, gid, gunicode, gtype, components, location=None):
60        Statement.__init__(self, location)
61        self.name = name
62        self.id = gid
63        self.unicode = gunicode
64        self.type = gtype
65        self.components = components
66
67    def __str__(self):
68        res = f'DEF_GLYPH "{self.name}" ID {self.id}'
69        if self.unicode is not None:
70            if len(self.unicode) > 1:
71                unicodes = ','.join(f'U+{u:04X}' for u in self.unicode)
72                res += f' UNICODEVALUES "{unicodes}"'
73            else:
74                res += f' UNICODE {self.unicode[0]}'
75        if self.type is not None:
76            res += f' TYPE {self.type}'
77        if self.components is not None:
78            res += f' COMPONENTS {self.components}'
79        res += ' END_GLYPH'
80        return res
81
82
83class GroupDefinition(Statement):
84    def __init__(self, name, enum, location=None):
85        Statement.__init__(self, location)
86        self.name = name
87        self.enum = enum
88        self.glyphs_ = None
89
90    def glyphSet(self, groups=None):
91        if groups is not None and self.name in groups:
92            raise VoltLibError(
93                'Group "%s" contains itself.' % (self.name),
94                self.location)
95        if self.glyphs_ is None:
96            if groups is None:
97                groups = set({self.name})
98            else:
99                groups.add(self.name)
100            self.glyphs_ = self.enum.glyphSet(groups)
101        return self.glyphs_
102
103    def __str__(self):
104        enum = self.enum and str(self.enum) or ''
105        return f'DEF_GROUP "{self.name}"\n{enum}\nEND_GROUP'
106
107
108class GlyphName(Expression):
109    """A single glyph name, such as cedilla."""
110    def __init__(self, glyph, location=None):
111        Expression.__init__(self, location)
112        self.glyph = glyph
113
114    def glyphSet(self):
115        return (self.glyph,)
116
117    def __str__(self):
118        return f' GLYPH "{self.glyph}"'
119
120
121class Enum(Expression):
122    """An enum"""
123    def __init__(self, enum, location=None):
124        Expression.__init__(self, location)
125        self.enum = enum
126
127    def __iter__(self):
128        for e in self.glyphSet():
129            yield e
130
131    def glyphSet(self, groups=None):
132        glyphs = []
133        for element in self.enum:
134            if isinstance(element, (GroupName, Enum)):
135                glyphs.extend(element.glyphSet(groups))
136            else:
137                glyphs.extend(element.glyphSet())
138        return tuple(glyphs)
139
140    def __str__(self):
141        enum = ''.join(str(e) for e in self.enum)
142        return f' ENUM{enum} END_ENUM'
143
144
145class GroupName(Expression):
146    """A glyph group"""
147    def __init__(self, group, parser, location=None):
148        Expression.__init__(self, location)
149        self.group = group
150        self.parser_ = parser
151
152    def glyphSet(self, groups=None):
153        group = self.parser_.resolve_group(self.group)
154        if group is not None:
155            self.glyphs_ = group.glyphSet(groups)
156            return self.glyphs_
157        else:
158            raise VoltLibError(
159                'Group "%s" is used but undefined.' % (self.group),
160                self.location)
161
162    def __str__(self):
163        return f' GROUP "{self.group}"'
164
165
166class Range(Expression):
167    """A glyph range"""
168    def __init__(self, start, end, parser, location=None):
169        Expression.__init__(self, location)
170        self.start = start
171        self.end = end
172        self.parser = parser
173
174    def glyphSet(self):
175        return tuple(self.parser.glyph_range(self.start, self.end))
176
177    def __str__(self):
178        return f' RANGE "{self.start}" TO "{self.end}"'
179
180
181class ScriptDefinition(Statement):
182    def __init__(self, name, tag, langs, location=None):
183        Statement.__init__(self, location)
184        self.name = name
185        self.tag = tag
186        self.langs = langs
187
188    def __str__(self):
189        res = 'DEF_SCRIPT'
190        if self.name is not None:
191            res += f' NAME "{self.name}"'
192        res += f' TAG "{self.tag}"\n\n'
193        for lang in self.langs:
194            res += f'{lang}'
195        res += 'END_SCRIPT'
196        return res
197
198
199class LangSysDefinition(Statement):
200    def __init__(self, name, tag, features, location=None):
201        Statement.__init__(self, location)
202        self.name = name
203        self.tag = tag
204        self.features = features
205
206    def __str__(self):
207        res = 'DEF_LANGSYS'
208        if self.name is not None:
209            res += f' NAME "{self.name}"'
210        res += f' TAG "{self.tag}"\n\n'
211        for feature in self.features:
212            res += f'{feature}'
213        res += 'END_LANGSYS\n'
214        return res
215
216
217class FeatureDefinition(Statement):
218    def __init__(self, name, tag, lookups, location=None):
219        Statement.__init__(self, location)
220        self.name = name
221        self.tag = tag
222        self.lookups = lookups
223
224    def __str__(self):
225        res = f'DEF_FEATURE NAME "{self.name}" TAG "{self.tag}"\n'
226        res += ' ' + ' '.join(f'LOOKUP "{l}"' for l in self.lookups) + '\n'
227        res += 'END_FEATURE\n'
228        return res
229
230
231class LookupDefinition(Statement):
232    def __init__(self, name, process_base, process_marks, mark_glyph_set,
233                 direction, reversal, comments, context, sub, pos,
234                 location=None):
235        Statement.__init__(self, location)
236        self.name = name
237        self.process_base = process_base
238        self.process_marks = process_marks
239        self.mark_glyph_set = mark_glyph_set
240        self.direction = direction
241        self.reversal = reversal
242        self.comments = comments
243        self.context = context
244        self.sub = sub
245        self.pos = pos
246
247    def __str__(self):
248        res = f'DEF_LOOKUP "{self.name}"'
249        res += f' {self.process_base and "PROCESS_BASE" or "SKIP_BASE"}'
250        if self.process_marks:
251            res += ' PROCESS_MARKS '
252            if self.mark_glyph_set:
253                res += f'MARK_GLYPH_SET "{self.mark_glyph_set}"'
254            elif isinstance(self.process_marks, str):
255                res += f'"{self.process_marks}"'
256            else:
257                res += 'ALL'
258        else:
259            res += ' SKIP_MARKS'
260        if self.direction is not None:
261            res += f' DIRECTION {self.direction}'
262        if self.reversal:
263            res += ' REVERSAL'
264        if self.comments is not None:
265            comments = self.comments.replace('\n', r'\n')
266            res += f'\nCOMMENTS "{comments}"'
267        if self.context:
268            res += '\n' + '\n'.join(str(c) for c in self.context)
269        else:
270            res += '\nIN_CONTEXT\nEND_CONTEXT'
271        if self.sub:
272            res += f'\n{self.sub}'
273        if self.pos:
274            res += f'\n{self.pos}'
275        return res
276
277
278class SubstitutionDefinition(Statement):
279    def __init__(self, mapping, location=None):
280        Statement.__init__(self, location)
281        self.mapping = mapping
282
283    def __str__(self):
284        res = 'AS_SUBSTITUTION\n'
285        for src, dst in self.mapping.items():
286            src = ''.join(str(s) for s in src)
287            dst = ''.join(str(d) for d in dst)
288            res += f'SUB{src}\nWITH{dst}\nEND_SUB\n'
289        res += 'END_SUBSTITUTION'
290        return res
291
292
293class SubstitutionSingleDefinition(SubstitutionDefinition):
294    pass
295
296
297class SubstitutionMultipleDefinition(SubstitutionDefinition):
298    pass
299
300
301class SubstitutionLigatureDefinition(SubstitutionDefinition):
302    pass
303
304
305class SubstitutionReverseChainingSingleDefinition(SubstitutionDefinition):
306    pass
307
308
309class PositionAttachDefinition(Statement):
310    def __init__(self, coverage, coverage_to, location=None):
311        Statement.__init__(self, location)
312        self.coverage = coverage
313        self.coverage_to = coverage_to
314
315    def __str__(self):
316        coverage = ''.join(str(c) for c in self.coverage)
317        res = f'AS_POSITION\nATTACH{coverage}\nTO'
318        for coverage, anchor in self.coverage_to:
319            coverage = ''.join(str(c) for c in coverage)
320            res += f'{coverage} AT ANCHOR "{anchor}"'
321        res += '\nEND_ATTACH\nEND_POSITION'
322        return res
323
324
325class PositionAttachCursiveDefinition(Statement):
326    def __init__(self, coverages_exit, coverages_enter, location=None):
327        Statement.__init__(self, location)
328        self.coverages_exit = coverages_exit
329        self.coverages_enter = coverages_enter
330
331    def __str__(self):
332        res = 'AS_POSITION\nATTACH_CURSIVE'
333        for coverage in self.coverages_exit:
334            coverage = ''.join(str(c) for c in coverage)
335            res += f'\nEXIT {coverage}'
336        for coverage in self.coverages_enter:
337            coverage = ''.join(str(c) for c in coverage)
338            res += f'\nENTER {coverage}'
339        res += '\nEND_ATTACH\nEND_POSITION'
340        return res
341
342
343class PositionAdjustPairDefinition(Statement):
344    def __init__(self, coverages_1, coverages_2, adjust_pair, location=None):
345        Statement.__init__(self, location)
346        self.coverages_1 = coverages_1
347        self.coverages_2 = coverages_2
348        self.adjust_pair = adjust_pair
349
350    def __str__(self):
351        res = 'AS_POSITION\nADJUST_PAIR\n'
352        for coverage in self.coverages_1:
353            coverage = ' '.join(str(c) for c in coverage)
354            res += f' FIRST {coverage}'
355        res += '\n'
356        for coverage in self.coverages_2:
357            coverage = ' '.join(str(c) for c in coverage)
358            res += f' SECOND {coverage}'
359        res += '\n'
360        for (id_1, id_2), (pos_1, pos_2) in self.adjust_pair.items():
361            res += f' {id_1} {id_2} BY{pos_1}{pos_2}\n'
362        res += '\nEND_ADJUST\nEND_POSITION'
363        return res
364
365
366class PositionAdjustSingleDefinition(Statement):
367    def __init__(self, adjust_single, location=None):
368        Statement.__init__(self, location)
369        self.adjust_single = adjust_single
370
371    def __str__(self):
372        res = 'AS_POSITION\nADJUST_SINGLE'
373        for coverage, pos in self.adjust_single:
374            coverage = ''.join(str(c) for c in coverage)
375            res += f'{coverage} BY{pos}'
376        res += '\nEND_ADJUST\nEND_POSITION'
377        return res
378
379
380
381class ContextDefinition(Statement):
382    def __init__(self, ex_or_in, left=None, right=None, location=None):
383        Statement.__init__(self, location)
384        self.ex_or_in = ex_or_in
385        self.left = left if left is not None else []
386        self.right = right if right is not None else []
387
388    def __str__(self):
389        res = self.ex_or_in + '\n'
390        for coverage in self.left:
391            coverage = ''.join(str(c) for c in coverage)
392            res += f' LEFT{coverage}\n'
393        for coverage in self.right:
394            coverage = ''.join(str(c) for c in coverage)
395            res += f' RIGHT{coverage}\n'
396        res += 'END_CONTEXT'
397        return res
398
399
400class AnchorDefinition(Statement):
401    def __init__(self, name, gid, glyph_name, component, locked,
402                 pos, location=None):
403        Statement.__init__(self, location)
404        self.name = name
405        self.gid = gid
406        self.glyph_name = glyph_name
407        self.component = component
408        self.locked = locked
409        self.pos = pos
410
411    def __str__(self):
412        locked = self.locked and ' LOCKED' or ''
413        return (f'DEF_ANCHOR "{self.name}"'
414                f' ON {self.gid}'
415                f' GLYPH {self.glyph_name}'
416                f' COMPONENT {self.component}'
417                f'{locked}'
418                f' AT {self.pos} END_ANCHOR')
419
420
421class SettingDefinition(Statement):
422    def __init__(self, name, value, location=None):
423        Statement.__init__(self, location)
424        self.name = name
425        self.value = value
426
427    def __str__(self):
428        if self.value is True:
429            return f'{self.name}'
430        if isinstance(self.value, (tuple, list)):
431            value = " ".join(str(v) for v in self.value)
432            return f'{self.name} {value}'
433        return f'{self.name} {self.value}'
434