| from fontTools.voltLib.error import VoltLibError |
| from typing import NamedTuple |
| |
| |
| class Pos(NamedTuple): |
| adv: int |
| dx: int |
| dy: int |
| adv_adjust_by: dict |
| dx_adjust_by: dict |
| dy_adjust_by: dict |
| |
| def __str__(self): |
| res = ' POS' |
| for attr in ('adv', 'dx', 'dy'): |
| value = getattr(self, attr) |
| if value is not None: |
| res += f' {attr.upper()} {value}' |
| adjust_by = getattr(self, f'{attr}_adjust_by', {}) |
| for size, adjustment in adjust_by.items(): |
| res += f' ADJUST_BY {adjustment} AT {size}' |
| res += ' END_POS' |
| return res |
| |
| |
| class Element(object): |
| def __init__(self, location=None): |
| self.location = location |
| |
| def build(self, builder): |
| pass |
| |
| def __str__(self): |
| raise NotImplementedError |
| |
| |
| class Statement(Element): |
| pass |
| |
| |
| class Expression(Element): |
| pass |
| |
| |
| class VoltFile(Statement): |
| def __init__(self): |
| Statement.__init__(self, location=None) |
| self.statements = [] |
| |
| def build(self, builder): |
| for s in self.statements: |
| s.build(builder) |
| |
| def __str__(self): |
| return '\n' + '\n'.join(str(s) for s in self.statements) + ' END\n' |
| |
| |
| class GlyphDefinition(Statement): |
| def __init__(self, name, gid, gunicode, gtype, components, location=None): |
| Statement.__init__(self, location) |
| self.name = name |
| self.id = gid |
| self.unicode = gunicode |
| self.type = gtype |
| self.components = components |
| |
| def __str__(self): |
| res = f'DEF_GLYPH "{self.name}" ID {self.id}' |
| if self.unicode is not None: |
| if len(self.unicode) > 1: |
| unicodes = ','.join(f'U+{u:04X}' for u in self.unicode) |
| res += f' UNICODEVALUES "{unicodes}"' |
| else: |
| res += f' UNICODE {self.unicode[0]}' |
| if self.type is not None: |
| res += f' TYPE {self.type}' |
| if self.components is not None: |
| res += f' COMPONENTS {self.components}' |
| res += ' END_GLYPH' |
| return res |
| |
| |
| class GroupDefinition(Statement): |
| def __init__(self, name, enum, location=None): |
| Statement.__init__(self, location) |
| self.name = name |
| self.enum = enum |
| self.glyphs_ = None |
| |
| def glyphSet(self, groups=None): |
| if groups is not None and self.name in groups: |
| raise VoltLibError( |
| 'Group "%s" contains itself.' % (self.name), |
| self.location) |
| if self.glyphs_ is None: |
| if groups is None: |
| groups = set({self.name}) |
| else: |
| groups.add(self.name) |
| self.glyphs_ = self.enum.glyphSet(groups) |
| return self.glyphs_ |
| |
| def __str__(self): |
| enum = self.enum and str(self.enum) or '' |
| return f'DEF_GROUP "{self.name}"\n{enum}\nEND_GROUP' |
| |
| |
| class GlyphName(Expression): |
| """A single glyph name, such as cedilla.""" |
| def __init__(self, glyph, location=None): |
| Expression.__init__(self, location) |
| self.glyph = glyph |
| |
| def glyphSet(self): |
| return (self.glyph,) |
| |
| def __str__(self): |
| return f' GLYPH "{self.glyph}"' |
| |
| |
| class Enum(Expression): |
| """An enum""" |
| def __init__(self, enum, location=None): |
| Expression.__init__(self, location) |
| self.enum = enum |
| |
| def __iter__(self): |
| for e in self.glyphSet(): |
| yield e |
| |
| def glyphSet(self, groups=None): |
| glyphs = [] |
| for element in self.enum: |
| if isinstance(element, (GroupName, Enum)): |
| glyphs.extend(element.glyphSet(groups)) |
| else: |
| glyphs.extend(element.glyphSet()) |
| return tuple(glyphs) |
| |
| def __str__(self): |
| enum = ''.join(str(e) for e in self.enum) |
| return f' ENUM{enum} END_ENUM' |
| |
| |
| class GroupName(Expression): |
| """A glyph group""" |
| def __init__(self, group, parser, location=None): |
| Expression.__init__(self, location) |
| self.group = group |
| self.parser_ = parser |
| |
| def glyphSet(self, groups=None): |
| group = self.parser_.resolve_group(self.group) |
| if group is not None: |
| self.glyphs_ = group.glyphSet(groups) |
| return self.glyphs_ |
| else: |
| raise VoltLibError( |
| 'Group "%s" is used but undefined.' % (self.group), |
| self.location) |
| |
| def __str__(self): |
| return f' GROUP "{self.group}"' |
| |
| |
| class Range(Expression): |
| """A glyph range""" |
| def __init__(self, start, end, parser, location=None): |
| Expression.__init__(self, location) |
| self.start = start |
| self.end = end |
| self.parser = parser |
| |
| def glyphSet(self): |
| return tuple(self.parser.glyph_range(self.start, self.end)) |
| |
| def __str__(self): |
| return f' RANGE "{self.start}" TO "{self.end}"' |
| |
| |
| class ScriptDefinition(Statement): |
| def __init__(self, name, tag, langs, location=None): |
| Statement.__init__(self, location) |
| self.name = name |
| self.tag = tag |
| self.langs = langs |
| |
| def __str__(self): |
| res = 'DEF_SCRIPT' |
| if self.name is not None: |
| res += f' NAME "{self.name}"' |
| res += f' TAG "{self.tag}"\n\n' |
| for lang in self.langs: |
| res += f'{lang}' |
| res += 'END_SCRIPT' |
| return res |
| |
| |
| class LangSysDefinition(Statement): |
| def __init__(self, name, tag, features, location=None): |
| Statement.__init__(self, location) |
| self.name = name |
| self.tag = tag |
| self.features = features |
| |
| def __str__(self): |
| res = 'DEF_LANGSYS' |
| if self.name is not None: |
| res += f' NAME "{self.name}"' |
| res += f' TAG "{self.tag}"\n\n' |
| for feature in self.features: |
| res += f'{feature}' |
| res += 'END_LANGSYS\n' |
| return res |
| |
| |
| class FeatureDefinition(Statement): |
| def __init__(self, name, tag, lookups, location=None): |
| Statement.__init__(self, location) |
| self.name = name |
| self.tag = tag |
| self.lookups = lookups |
| |
| def __str__(self): |
| res = f'DEF_FEATURE NAME "{self.name}" TAG "{self.tag}"\n' |
| res += ' ' + ' '.join(f'LOOKUP "{l}"' for l in self.lookups) + '\n' |
| res += 'END_FEATURE\n' |
| return res |
| |
| |
| class LookupDefinition(Statement): |
| def __init__(self, name, process_base, process_marks, mark_glyph_set, |
| direction, reversal, comments, context, sub, pos, |
| location=None): |
| Statement.__init__(self, location) |
| self.name = name |
| self.process_base = process_base |
| self.process_marks = process_marks |
| self.mark_glyph_set = mark_glyph_set |
| self.direction = direction |
| self.reversal = reversal |
| self.comments = comments |
| self.context = context |
| self.sub = sub |
| self.pos = pos |
| |
| def __str__(self): |
| res = f'DEF_LOOKUP "{self.name}"' |
| res += f' {self.process_base and "PROCESS_BASE" or "SKIP_BASE"}' |
| if self.process_marks: |
| res += ' PROCESS_MARKS ' |
| if self.mark_glyph_set: |
| res += f'MARK_GLYPH_SET "{self.mark_glyph_set}"' |
| elif isinstance(self.process_marks, str): |
| res += f'"{self.process_marks}"' |
| else: |
| res += 'ALL' |
| else: |
| res += ' SKIP_MARKS' |
| if self.direction is not None: |
| res += f' DIRECTION {self.direction}' |
| if self.reversal: |
| res += ' REVERSAL' |
| if self.comments is not None: |
| comments = self.comments.replace('\n', r'\n') |
| res += f'\nCOMMENTS "{comments}"' |
| if self.context: |
| res += '\n' + '\n'.join(str(c) for c in self.context) |
| else: |
| res += '\nIN_CONTEXT\nEND_CONTEXT' |
| if self.sub: |
| res += f'\n{self.sub}' |
| if self.pos: |
| res += f'\n{self.pos}' |
| return res |
| |
| |
| class SubstitutionDefinition(Statement): |
| def __init__(self, mapping, location=None): |
| Statement.__init__(self, location) |
| self.mapping = mapping |
| |
| def __str__(self): |
| res = 'AS_SUBSTITUTION\n' |
| for src, dst in self.mapping.items(): |
| src = ''.join(str(s) for s in src) |
| dst = ''.join(str(d) for d in dst) |
| res += f'SUB{src}\nWITH{dst}\nEND_SUB\n' |
| res += 'END_SUBSTITUTION' |
| return res |
| |
| |
| class SubstitutionSingleDefinition(SubstitutionDefinition): |
| pass |
| |
| |
| class SubstitutionMultipleDefinition(SubstitutionDefinition): |
| pass |
| |
| |
| class SubstitutionLigatureDefinition(SubstitutionDefinition): |
| pass |
| |
| |
| class SubstitutionReverseChainingSingleDefinition(SubstitutionDefinition): |
| pass |
| |
| |
| class PositionAttachDefinition(Statement): |
| def __init__(self, coverage, coverage_to, location=None): |
| Statement.__init__(self, location) |
| self.coverage = coverage |
| self.coverage_to = coverage_to |
| |
| def __str__(self): |
| coverage = ''.join(str(c) for c in self.coverage) |
| res = f'AS_POSITION\nATTACH{coverage}\nTO' |
| for coverage, anchor in self.coverage_to: |
| coverage = ''.join(str(c) for c in coverage) |
| res += f'{coverage} AT ANCHOR "{anchor}"' |
| res += '\nEND_ATTACH\nEND_POSITION' |
| return res |
| |
| |
| class PositionAttachCursiveDefinition(Statement): |
| def __init__(self, coverages_exit, coverages_enter, location=None): |
| Statement.__init__(self, location) |
| self.coverages_exit = coverages_exit |
| self.coverages_enter = coverages_enter |
| |
| def __str__(self): |
| res = 'AS_POSITION\nATTACH_CURSIVE' |
| for coverage in self.coverages_exit: |
| coverage = ''.join(str(c) for c in coverage) |
| res += f'\nEXIT {coverage}' |
| for coverage in self.coverages_enter: |
| coverage = ''.join(str(c) for c in coverage) |
| res += f'\nENTER {coverage}' |
| res += '\nEND_ATTACH\nEND_POSITION' |
| return res |
| |
| |
| class PositionAdjustPairDefinition(Statement): |
| def __init__(self, coverages_1, coverages_2, adjust_pair, location=None): |
| Statement.__init__(self, location) |
| self.coverages_1 = coverages_1 |
| self.coverages_2 = coverages_2 |
| self.adjust_pair = adjust_pair |
| |
| def __str__(self): |
| res = 'AS_POSITION\nADJUST_PAIR\n' |
| for coverage in self.coverages_1: |
| coverage = ' '.join(str(c) for c in coverage) |
| res += f' FIRST {coverage}' |
| res += '\n' |
| for coverage in self.coverages_2: |
| coverage = ' '.join(str(c) for c in coverage) |
| res += f' SECOND {coverage}' |
| res += '\n' |
| for (id_1, id_2), (pos_1, pos_2) in self.adjust_pair.items(): |
| res += f' {id_1} {id_2} BY{pos_1}{pos_2}\n' |
| res += '\nEND_ADJUST\nEND_POSITION' |
| return res |
| |
| |
| class PositionAdjustSingleDefinition(Statement): |
| def __init__(self, adjust_single, location=None): |
| Statement.__init__(self, location) |
| self.adjust_single = adjust_single |
| |
| def __str__(self): |
| res = 'AS_POSITION\nADJUST_SINGLE' |
| for coverage, pos in self.adjust_single: |
| coverage = ''.join(str(c) for c in coverage) |
| res += f'{coverage} BY{pos}' |
| res += '\nEND_ADJUST\nEND_POSITION' |
| return res |
| |
| |
| |
| class ContextDefinition(Statement): |
| def __init__(self, ex_or_in, left=None, right=None, location=None): |
| Statement.__init__(self, location) |
| self.ex_or_in = ex_or_in |
| self.left = left if left is not None else [] |
| self.right = right if right is not None else [] |
| |
| def __str__(self): |
| res = self.ex_or_in + '\n' |
| for coverage in self.left: |
| coverage = ''.join(str(c) for c in coverage) |
| res += f' LEFT{coverage}\n' |
| for coverage in self.right: |
| coverage = ''.join(str(c) for c in coverage) |
| res += f' RIGHT{coverage}\n' |
| res += 'END_CONTEXT' |
| return res |
| |
| |
| class AnchorDefinition(Statement): |
| def __init__(self, name, gid, glyph_name, component, locked, |
| pos, location=None): |
| Statement.__init__(self, location) |
| self.name = name |
| self.gid = gid |
| self.glyph_name = glyph_name |
| self.component = component |
| self.locked = locked |
| self.pos = pos |
| |
| def __str__(self): |
| locked = self.locked and ' LOCKED' or '' |
| return (f'DEF_ANCHOR "{self.name}"' |
| f' ON {self.gid}' |
| f' GLYPH {self.glyph_name}' |
| f' COMPONENT {self.component}' |
| f'{locked}' |
| f' AT {self.pos} END_ANCHOR') |
| |
| |
| class SettingDefinition(Statement): |
| def __init__(self, name, value, location=None): |
| Statement.__init__(self, location) |
| self.name = name |
| self.value = value |
| |
| def __str__(self): |
| if self.value is True: |
| return f'{self.name}' |
| if isinstance(self.value, (tuple, list)): |
| value = " ".join(str(v) for v in self.value) |
| return f'{self.name} {value}' |
| return f'{self.name} {self.value}' |