diff --git a/Lib/fontTools/voltLib/ast.py b/Lib/fontTools/voltLib/ast.py index 7c799e9e2..4df869065 100644 --- a/Lib/fontTools/voltLib/ast.py +++ b/Lib/fontTools/voltLib/ast.py @@ -10,6 +10,18 @@ class Pos(NamedTuple): 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): @@ -18,6 +30,9 @@ class Element(object): def build(self, builder): pass + def __str__(self): + raise NotImplementedError + class Statement(Element): pass @@ -36,6 +51,9 @@ class VoltFile(Statement): 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): @@ -46,6 +64,21 @@ class GlyphDefinition(Statement): 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): @@ -67,6 +100,10 @@ class GroupDefinition(Statement): 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.""" @@ -77,6 +114,9 @@ class GlyphName(Expression): def glyphSet(self): return (self.glyph,) + def __str__(self): + return f' GLYPH "{self.glyph}"' + class Enum(Expression): """An enum""" @@ -97,6 +137,10 @@ class Enum(Expression): 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""" @@ -115,6 +159,9 @@ class GroupName(Expression): 'Group "%s" is used but undefined.' % (self.group), self.location) + def __str__(self): + return f' GROUP "{self.group}"' + class Range(Expression): """A glyph range""" @@ -127,6 +174,9 @@ class Range(Expression): 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): @@ -135,6 +185,16 @@ class ScriptDefinition(Statement): 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): @@ -143,6 +203,16 @@ class LangSysDefinition(Statement): 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): @@ -151,6 +221,12 @@ class FeatureDefinition(Statement): 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, @@ -168,12 +244,51 @@ class LookupDefinition(Statement): 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 is not None: + 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 @@ -197,6 +312,15 @@ class PositionAttachDefinition(Statement): 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): @@ -204,6 +328,17 @@ class PositionAttachCursiveDefinition(Statement): 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): @@ -212,12 +347,36 @@ class PositionAdjustPairDefinition(Statement): 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): @@ -226,6 +385,17 @@ class ContextDefinition(Statement): 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, @@ -238,9 +408,26 @@ class AnchorDefinition(Statement): 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}' diff --git a/Tests/voltLib/parser_test.py b/Tests/voltLib/parser_test.py index e407ddd6a..61bea69e4 100644 --- a/Tests/voltLib/parser_test.py +++ b/Tests/voltLib/parser_test.py @@ -37,7 +37,7 @@ class ParserTest(unittest.TestCase): ("space", 3, [0x0020], "BASE", None)) def test_def_glyph_base_with_unicodevalues(self): - [def_glyph] = self.parse( + [def_glyph] = self.parse_( 'DEF_GLYPH "CR" ID 2 UNICODEVALUES "U+0009" ' 'TYPE BASE END_GLYPH' ).statements @@ -55,7 +55,7 @@ class ParserTest(unittest.TestCase): ("CR", 2, [0x0009, 0x000D], "BASE", None)) def test_def_glyph_base_with_empty_unicodevalues(self): - [def_glyph] = self.parse( + [def_glyph] = self.parse_( 'DEF_GLYPH "i.locl" ID 269 UNICODEVALUES "" ' 'TYPE BASE END_GLYPH' ).statements @@ -106,7 +106,7 @@ class ParserTest(unittest.TestCase): def test_def_glyph_case_sensitive(self): def_glyphs = self.parse( 'DEF_GLYPH "A" ID 3 UNICODE 65 TYPE BASE END_GLYPH\n' - 'DEF_GLYPH "a" ID 4 UNICODE 97 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "a" ID 4 UNICODE 97 TYPE BASE END_GLYPH' ).statements self.assertEqual((def_glyphs[0].name, def_glyphs[0].id, def_glyphs[0].unicode, def_glyphs[0].type, @@ -120,10 +120,10 @@ class ParserTest(unittest.TestCase): def test_def_group_glyphs(self): [def_group] = self.parse( 'DEF_GROUP "aaccented"\n' - 'ENUM GLYPH "aacute" GLYPH "abreve" GLYPH "acircumflex" ' + ' ENUM GLYPH "aacute" GLYPH "abreve" GLYPH "acircumflex" ' 'GLYPH "adieresis" GLYPH "ae" GLYPH "agrave" GLYPH "amacron" ' 'GLYPH "aogonek" GLYPH "aring" GLYPH "atilde" END_ENUM\n' - 'END_GROUP\n' + 'END_GROUP' ).statements self.assertEqual((def_group.name, def_group.enum.glyphSet()), ("aaccented", @@ -134,14 +134,14 @@ class ParserTest(unittest.TestCase): def test_def_group_groups(self): [group1, group2, test_group] = self.parse( 'DEF_GROUP "Group1"\n' - 'ENUM GLYPH "a" GLYPH "b" GLYPH "c" GLYPH "d" END_ENUM\n' + ' ENUM GLYPH "a" GLYPH "b" GLYPH "c" GLYPH "d" END_ENUM\n' 'END_GROUP\n' 'DEF_GROUP "Group2"\n' - 'ENUM GLYPH "e" GLYPH "f" GLYPH "g" GLYPH "h" END_ENUM\n' + ' ENUM GLYPH "e" GLYPH "f" GLYPH "g" GLYPH "h" END_ENUM\n' 'END_GROUP\n' 'DEF_GROUP "TestGroup"\n' - 'ENUM GROUP "Group1" GROUP "Group2" END_ENUM\n' - 'END_GROUP\n' + ' ENUM GROUP "Group1" GROUP "Group2" END_ENUM\n' + 'END_GROUP' ).statements groups = [g.group for g in test_group.enum.enum] self.assertEqual((test_group.name, groups), @@ -151,20 +151,20 @@ class ParserTest(unittest.TestCase): [group1, test_group1, test_group2, test_group3, group2] = \ self.parse( 'DEF_GROUP "Group1"\n' - 'ENUM GLYPH "a" GLYPH "b" GLYPH "c" GLYPH "d" END_ENUM\n' + ' ENUM GLYPH "a" GLYPH "b" GLYPH "c" GLYPH "d" END_ENUM\n' 'END_GROUP\n' 'DEF_GROUP "TestGroup1"\n' - 'ENUM GROUP "Group1" GROUP "Group2" END_ENUM\n' + ' ENUM GROUP "Group1" GROUP "Group2" END_ENUM\n' 'END_GROUP\n' 'DEF_GROUP "TestGroup2"\n' - 'ENUM GROUP "Group2" END_ENUM\n' + ' ENUM GROUP "Group2" END_ENUM\n' 'END_GROUP\n' 'DEF_GROUP "TestGroup3"\n' - 'ENUM GROUP "Group2" GROUP "Group1" END_ENUM\n' + ' ENUM GROUP "Group2" GROUP "Group1" END_ENUM\n' 'END_GROUP\n' 'DEF_GROUP "Group2"\n' - 'ENUM GLYPH "e" GLYPH "f" GLYPH "g" GLYPH "h" END_ENUM\n' - 'END_GROUP\n' + ' ENUM GLYPH "e" GLYPH "f" GLYPH "g" GLYPH "h" END_ENUM\n' + 'END_GROUP' ).statements groups = [g.group for g in test_group1.enum.enum] self.assertEqual( @@ -195,12 +195,12 @@ class ParserTest(unittest.TestCase): def test_def_group_glyphs_and_group(self): [def_group1, def_group2] = self.parse( 'DEF_GROUP "aaccented"\n' - 'ENUM GLYPH "aacute" GLYPH "abreve" GLYPH "acircumflex" ' + ' ENUM GLYPH "aacute" GLYPH "abreve" GLYPH "acircumflex" ' 'GLYPH "adieresis" GLYPH "ae" GLYPH "agrave" GLYPH "amacron" ' 'GLYPH "aogonek" GLYPH "aring" GLYPH "atilde" END_ENUM\n' 'END_GROUP\n' 'DEF_GROUP "KERN_lc_a_2ND"\n' - 'ENUM GLYPH "a" GROUP "aaccented" END_ENUM\n' + ' ENUM GLYPH "a" GROUP "aaccented" END_ENUM\n' 'END_GROUP' ).statements items = def_group2.enum.enum @@ -219,7 +219,7 @@ class ParserTest(unittest.TestCase): 'DEF_GLYPH "ccedilla" ID 210 UNICODE 231 TYPE BASE END_GLYPH\n' 'DEF_GLYPH "cdotaccent" ID 210 UNICODE 267 TYPE BASE END_GLYPH\n' 'DEF_GROUP "KERN_lc_a_2ND"\n' - 'ENUM RANGE "a" TO "atilde" GLYPH "b" RANGE "c" TO "cdotaccent" ' + ' ENUM RANGE "a" TO "atilde" GLYPH "b" RANGE "c" TO "cdotaccent" ' 'END_ENUM\n' 'END_GROUP' ).statements[-1] @@ -238,7 +238,7 @@ class ParserTest(unittest.TestCase): 'END_GROUP\n' 'DEF_GROUP "dupe"\n' 'ENUM GLYPH "x" END_ENUM\n' - 'END_GROUP\n' + 'END_GROUP' ) def test_group_duplicate_case_insensitive(self): @@ -251,12 +251,12 @@ class ParserTest(unittest.TestCase): 'END_GROUP\n' 'DEF_GROUP "Dupe"\n' 'ENUM GLYPH "x" END_ENUM\n' - 'END_GROUP\n' + 'END_GROUP' ) def test_script_without_langsys(self): [script] = self.parse( - 'DEF_SCRIPT NAME "Latin" TAG "latn"\n' + 'DEF_SCRIPT NAME "Latin" TAG "latn"\n\n' 'END_SCRIPT' ).statements self.assertEqual((script.name, script.tag, script.langs), @@ -264,10 +264,10 @@ class ParserTest(unittest.TestCase): def test_langsys_normal(self): [def_script] = self.parse( - 'DEF_SCRIPT NAME "Latin" TAG "latn"\n' - 'DEF_LANGSYS NAME "Romanian" TAG "ROM "\n' + 'DEF_SCRIPT NAME "Latin" TAG "latn"\n\n' + 'DEF_LANGSYS NAME "Romanian" TAG "ROM "\n\n' 'END_LANGSYS\n' - 'DEF_LANGSYS NAME "Moldavian" TAG "MOL "\n' + 'DEF_LANGSYS NAME "Moldavian" TAG "MOL "\n\n' 'END_LANGSYS\n' 'END_SCRIPT' ).statements @@ -285,8 +285,8 @@ class ParserTest(unittest.TestCase): def test_langsys_no_script_name(self): [langsys] = self.parse( - 'DEF_SCRIPT TAG "latn"\n' - 'DEF_LANGSYS NAME "Default" TAG "dflt"\n' + 'DEF_SCRIPT TAG "latn"\n\n' + 'DEF_LANGSYS NAME "Default" TAG "dflt"\n\n' 'END_LANGSYS\n' 'END_SCRIPT' ).statements @@ -303,8 +303,8 @@ class ParserTest(unittest.TestCase): VoltLibError, r'.*Expected "TAG"'): [langsys] = self.parse( - 'DEF_SCRIPT NAME "Latin"\n' - 'DEF_LANGSYS NAME "Default" TAG "dflt"\n' + 'DEF_SCRIPT NAME "Latin"\n\n' + 'DEF_LANGSYS NAME "Default" TAG "dflt"\n\n' 'END_LANGSYS\n' 'END_SCRIPT' ).statements @@ -315,12 +315,12 @@ class ParserTest(unittest.TestCase): 'Script "DFLT" already defined, ' 'script tags are case insensitive'): [langsys1, langsys2] = self.parse( - 'DEF_SCRIPT NAME "Default" TAG "DFLT"\n' - 'DEF_LANGSYS NAME "Default" TAG "dflt"\n' + 'DEF_SCRIPT NAME "Default" TAG "DFLT"\n\n' + 'DEF_LANGSYS NAME "Default" TAG "dflt"\n\n' 'END_LANGSYS\n' 'END_SCRIPT\n' - 'DEF_SCRIPT TAG "DFLT"\n' - 'DEF_LANGSYS NAME "Default" TAG "dflt"\n' + 'DEF_SCRIPT TAG "DFLT"\n\n' + 'DEF_LANGSYS NAME "Default" TAG "dflt"\n\n' 'END_LANGSYS\n' 'END_SCRIPT' ).statements @@ -336,21 +336,21 @@ class ParserTest(unittest.TestCase): 'END_LANGSYS\n' 'DEF_LANGSYS NAME "Default" TAG "dflt"\n' 'END_LANGSYS\n' - 'END_SCRIPT\n' + 'END_SCRIPT' ).statements def test_langsys_lang_in_separate_scripts(self): [langsys1, langsys2] = self.parse( - 'DEF_SCRIPT NAME "Default" TAG "DFLT"\n' - 'DEF_LANGSYS NAME "Default" TAG "dflt"\n' + 'DEF_SCRIPT NAME "Default" TAG "DFLT"\n\n' + 'DEF_LANGSYS NAME "Default" TAG "dflt"\n\n' 'END_LANGSYS\n' - 'DEF_LANGSYS NAME "Default" TAG "ROM "\n' + 'DEF_LANGSYS NAME "Default" TAG "ROM "\n\n' 'END_LANGSYS\n' 'END_SCRIPT\n' - 'DEF_SCRIPT NAME "Latin" TAG "latn"\n' - 'DEF_LANGSYS NAME "Default" TAG "dflt"\n' + 'DEF_SCRIPT NAME "Latin" TAG "latn"\n\n' + 'DEF_LANGSYS NAME "Default" TAG "dflt"\n\n' 'END_LANGSYS\n' - 'DEF_LANGSYS NAME "Default" TAG "ROM "\n' + 'DEF_LANGSYS NAME "Default" TAG "ROM "\n\n' 'END_LANGSYS\n' 'END_SCRIPT' ).statements @@ -361,8 +361,8 @@ class ParserTest(unittest.TestCase): def test_langsys_no_lang_name(self): [langsys] = self.parse( - 'DEF_SCRIPT NAME "Latin" TAG "latn"\n' - 'DEF_LANGSYS TAG "dflt"\n' + 'DEF_SCRIPT NAME "Latin" TAG "latn"\n\n' + 'DEF_LANGSYS TAG "dflt"\n\n' 'END_LANGSYS\n' 'END_SCRIPT' ).statements @@ -379,18 +379,18 @@ class ParserTest(unittest.TestCase): VoltLibError, r'.*Expected "TAG"'): [langsys] = self.parse( - 'DEF_SCRIPT NAME "Latin" TAG "latn"\n' - 'DEF_LANGSYS NAME "Default"\n' + 'DEF_SCRIPT NAME "Latin" TAG "latn"\n\n' + 'DEF_LANGSYS NAME "Default"\n\n' 'END_LANGSYS\n' 'END_SCRIPT' ).statements def test_feature(self): [def_script] = self.parse( - 'DEF_SCRIPT NAME "Latin" TAG "latn"\n' - 'DEF_LANGSYS NAME "Romanian" TAG "ROM "\n' + 'DEF_SCRIPT NAME "Latin" TAG "latn"\n\n' + 'DEF_LANGSYS NAME "Romanian" TAG "ROM "\n\n' 'DEF_FEATURE NAME "Fractions" TAG "frac"\n' - 'LOOKUP "fraclookup"\n' + ' LOOKUP "fraclookup"\n' 'END_FEATURE\n' 'END_LANGSYS\n' 'END_SCRIPT' @@ -402,10 +402,10 @@ class ParserTest(unittest.TestCase): "frac", ["fraclookup"])) [def_script] = self.parse( - 'DEF_SCRIPT NAME "Latin" TAG "latn"\n' - 'DEF_LANGSYS NAME "Romanian" TAG "ROM "\n' + 'DEF_SCRIPT NAME "Latin" TAG "latn"\n\n' + 'DEF_LANGSYS NAME "Romanian" TAG "ROM "\n\n' 'DEF_FEATURE NAME "Kerning" TAG "kern"\n' - 'LOOKUP "kern1" LOOKUP "kern2"\n' + ' LOOKUP "kern1" LOOKUP "kern2"\n' 'END_FEATURE\n' 'END_LANGSYS\n' 'END_SCRIPT' @@ -572,12 +572,13 @@ class ParserTest(unittest.TestCase): def test_substitution_single_in_context(self): [group, lookup] = self.parse( - 'DEF_GROUP "Denominators" ENUM GLYPH "one.dnom" GLYPH "two.dnom" ' - 'END_ENUM END_GROUP\n' + 'DEF_GROUP "Denominators"\n' + ' ENUM GLYPH "one.dnom" GLYPH "two.dnom" END_ENUM\n' + 'END_GROUP\n' 'DEF_LOOKUP "fracdnom" PROCESS_BASE PROCESS_MARKS ALL ' 'DIRECTION LTR\n' - 'IN_CONTEXT LEFT ENUM GROUP "Denominators" GLYPH "fraction" ' - 'END_ENUM\n' + 'IN_CONTEXT\n' + ' LEFT ENUM GROUP "Denominators" GLYPH "fraction" END_ENUM\n' 'END_CONTEXT\n' 'AS_SUBSTITUTION\n' 'SUB GLYPH "one"\n' @@ -603,17 +604,18 @@ class ParserTest(unittest.TestCase): def test_substitution_single_in_contexts(self): [group, lookup] = self.parse( - 'DEF_GROUP "Hebrew" ENUM GLYPH "uni05D0" GLYPH "uni05D1" ' - 'END_ENUM END_GROUP\n' + 'DEF_GROUP "Hebrew"\n' + ' ENUM GLYPH "uni05D0" GLYPH "uni05D1" END_ENUM\n' + 'END_GROUP\n' 'DEF_LOOKUP "HebrewCurrency" PROCESS_BASE PROCESS_MARKS ALL ' 'DIRECTION LTR\n' 'IN_CONTEXT\n' - 'RIGHT GROUP "Hebrew"\n' - 'RIGHT GLYPH "one.Hebr"\n' + ' RIGHT GROUP "Hebrew"\n' + ' RIGHT GLYPH "one.Hebr"\n' 'END_CONTEXT\n' 'IN_CONTEXT\n' - 'LEFT GROUP "Hebrew"\n' - 'LEFT GLYPH "one.Hebr"\n' + ' LEFT GROUP "Hebrew"\n' + ' LEFT GLYPH "one.Hebr"\n' 'END_CONTEXT\n' 'AS_SUBSTITUTION\n' 'SUB GLYPH "dollar"\n' @@ -644,8 +646,9 @@ class ParserTest(unittest.TestCase): def test_substitution_skip_base(self): [group, lookup] = self.parse( - 'DEF_GROUP "SomeMarks" ENUM GLYPH "marka" GLYPH "markb" ' - 'END_ENUM END_GROUP\n' + 'DEF_GROUP "SomeMarks"\n' + ' ENUM GLYPH "marka" GLYPH "markb" END_ENUM\n' + 'END_GROUP\n' 'DEF_LOOKUP "SomeSub" SKIP_BASE PROCESS_MARKS ALL ' 'DIRECTION LTR\n' 'IN_CONTEXT\n' @@ -662,8 +665,9 @@ class ParserTest(unittest.TestCase): def test_substitution_process_base(self): [group, lookup] = self.parse( - 'DEF_GROUP "SomeMarks" ENUM GLYPH "marka" GLYPH "markb" ' - 'END_ENUM END_GROUP\n' + 'DEF_GROUP "SomeMarks"\n' + ' ENUM GLYPH "marka" GLYPH "markb" END_ENUM\n' + 'END_GROUP\n' 'DEF_LOOKUP "SomeSub" PROCESS_BASE PROCESS_MARKS ALL ' 'DIRECTION LTR\n' 'IN_CONTEXT\n' @@ -680,11 +684,15 @@ class ParserTest(unittest.TestCase): def test_substitution_process_marks(self): [group, lookup] = self.parse( - 'DEF_GROUP "SomeMarks" ENUM GLYPH "marka" GLYPH "markb" ' - 'END_ENUM END_GROUP\n' - 'DEF_LOOKUP "SomeSub" PROCESS_BASE PROCESS_MARKS "SomeMarks" ' + 'DEF_GROUP "SomeMarks"\n' + ' ENUM GLYPH "marka" GLYPH "markb" END_ENUM\n' + 'END_GROUP\n' + 'DEF_LOOKUP "SomeSub" PROCESS_BASE PROCESS_MARKS "SomeMarks"\n' + 'IN_CONTEXT\n' + 'END_CONTEXT\n' 'AS_SUBSTITUTION\n' - 'SUB GLYPH "A" WITH GLYPH "A.c2sc"\n' + 'SUB GLYPH "A"\n' + 'WITH GLYPH "A.c2sc"\n' 'END_SUB\n' 'END_SUBSTITUTION' ).statements @@ -694,9 +702,12 @@ class ParserTest(unittest.TestCase): def test_substitution_process_marks_all(self): [lookup] = self.parse( - 'DEF_LOOKUP "SomeSub" PROCESS_BASE PROCESS_MARKS "ALL" ' + 'DEF_LOOKUP "SomeSub" PROCESS_BASE PROCESS_MARKS ALL\n' + 'IN_CONTEXT\n' + 'END_CONTEXT\n' 'AS_SUBSTITUTION\n' - 'SUB GLYPH "A" WITH GLYPH "A.c2sc"\n' + 'SUB GLYPH "A"\n' + 'WITH GLYPH "A.c2sc"\n' 'END_SUB\n' 'END_SUBSTITUTION' ).statements @@ -705,10 +716,13 @@ class ParserTest(unittest.TestCase): ("SomeSub", True)) def test_substitution_process_marks_none(self): - [lookup] = self.parse( - 'DEF_LOOKUP "SomeSub" PROCESS_BASE PROCESS_MARKS "NONE" ' + [lookup] = self.parse_( + 'DEF_LOOKUP "SomeSub" PROCESS_BASE PROCESS_MARKS "NONE"\n' + 'IN_CONTEXT\n' + 'END_CONTEXT\n' 'AS_SUBSTITUTION\n' - 'SUB GLYPH "A" WITH GLYPH "A.c2sc"\n' + 'SUB GLYPH "A"\n' + 'WITH GLYPH "A.c2sc"\n' 'END_SUB\n' 'END_SUBSTITUTION' ).statements @@ -732,10 +746,10 @@ class ParserTest(unittest.TestCase): def test_substitution_skip_marks(self): [group, lookup] = self.parse( - 'DEF_GROUP "SomeMarks" ENUM GLYPH "marka" GLYPH "markb" ' - 'END_ENUM END_GROUP\n' - 'DEF_LOOKUP "SomeSub" PROCESS_BASE SKIP_MARKS ' - 'DIRECTION LTR\n' + 'DEF_GROUP "SomeMarks"\n' + ' ENUM GLYPH "marka" GLYPH "markb" END_ENUM\n' + 'END_GROUP\n' + 'DEF_LOOKUP "SomeSub" PROCESS_BASE SKIP_MARKS DIRECTION LTR\n' 'IN_CONTEXT\n' 'END_CONTEXT\n' 'AS_SUBSTITUTION\n' @@ -750,11 +764,13 @@ class ParserTest(unittest.TestCase): def test_substitution_mark_attachment(self): [group, lookup] = self.parse( - 'DEF_GROUP "SomeMarks" ENUM GLYPH "acutecmb" GLYPH "gravecmb" ' - 'END_ENUM END_GROUP\n' + 'DEF_GROUP "SomeMarks"\n' + ' ENUM GLYPH "acutecmb" GLYPH "gravecmb" END_ENUM\n' + 'END_GROUP\n' 'DEF_LOOKUP "SomeSub" PROCESS_BASE ' - 'PROCESS_MARKS "SomeMarks" \n' - 'DIRECTION RTL\n' + 'PROCESS_MARKS "SomeMarks" DIRECTION RTL\n' + 'IN_CONTEXT\n' + 'END_CONTEXT\n' 'AS_SUBSTITUTION\n' 'SUB GLYPH "A"\n' 'WITH GLYPH "A.c2sc"\n' @@ -767,11 +783,13 @@ class ParserTest(unittest.TestCase): def test_substitution_mark_glyph_set(self): [group, lookup] = self.parse( - 'DEF_GROUP "SomeMarks" ENUM GLYPH "acutecmb" GLYPH "gravecmb" ' - 'END_ENUM END_GROUP\n' + 'DEF_GROUP "SomeMarks"\n' + ' ENUM GLYPH "acutecmb" GLYPH "gravecmb" END_ENUM\n' + 'END_GROUP\n' 'DEF_LOOKUP "SomeSub" PROCESS_BASE ' - 'PROCESS_MARKS MARK_GLYPH_SET "SomeMarks" \n' - 'DIRECTION RTL\n' + 'PROCESS_MARKS MARK_GLYPH_SET "SomeMarks" DIRECTION RTL\n' + 'IN_CONTEXT\n' + 'END_CONTEXT\n' 'AS_SUBSTITUTION\n' 'SUB GLYPH "A"\n' 'WITH GLYPH "A.c2sc"\n' @@ -784,11 +802,13 @@ class ParserTest(unittest.TestCase): def test_substitution_process_all_marks(self): [group, lookup] = self.parse( - 'DEF_GROUP "SomeMarks" ENUM GLYPH "acutecmb" GLYPH "gravecmb" ' - 'END_ENUM END_GROUP\n' - 'DEF_LOOKUP "SomeSub" PROCESS_BASE ' - 'PROCESS_MARKS ALL \n' + 'DEF_GROUP "SomeMarks"\n' + ' ENUM GLYPH "acutecmb" GLYPH "gravecmb" END_ENUM\n' + 'END_GROUP\n' + 'DEF_LOOKUP "SomeSub" PROCESS_BASE PROCESS_MARKS ALL ' 'DIRECTION RTL\n' + 'IN_CONTEXT\n' + 'END_CONTEXT\n' 'AS_SUBSTITUTION\n' 'SUB GLYPH "A"\n' 'WITH GLYPH "A.c2sc"\n' @@ -805,7 +825,7 @@ class ParserTest(unittest.TestCase): 'DEF_LOOKUP "Lookup" PROCESS_BASE PROCESS_MARKS ALL ' 'DIRECTION LTR\n' 'IN_CONTEXT\n' - 'RIGHT ENUM GLYPH "a" GLYPH "b" END_ENUM\n' + ' RIGHT ENUM GLYPH "a" GLYPH "b" END_ENUM\n' 'END_CONTEXT\n' 'AS_SUBSTITUTION\n' 'SUB GLYPH "a"\n' @@ -821,15 +841,15 @@ class ParserTest(unittest.TestCase): def test_substitution_reversal(self): lookup = self.parse( 'DEF_GROUP "DFLT_Num_standardFigures"\n' - 'ENUM GLYPH "zero" GLYPH "one" GLYPH "two" END_ENUM\n' + ' ENUM GLYPH "zero" GLYPH "one" GLYPH "two" END_ENUM\n' 'END_GROUP\n' 'DEF_GROUP "DFLT_Num_numerators"\n' - 'ENUM GLYPH "zero.numr" GLYPH "one.numr" GLYPH "two.numr" END_ENUM\n' + ' ENUM GLYPH "zero.numr" GLYPH "one.numr" GLYPH "two.numr" END_ENUM\n' 'END_GROUP\n' 'DEF_LOOKUP "RevLookup" PROCESS_BASE PROCESS_MARKS ALL ' 'DIRECTION LTR REVERSAL\n' 'IN_CONTEXT\n' - 'RIGHT ENUM GLYPH "a" GLYPH "b" END_ENUM\n' + ' RIGHT ENUM GLYPH "a" GLYPH "b" END_ENUM\n' 'END_CONTEXT\n' 'AS_SUBSTITUTION\n' 'SUB GROUP "DFLT_Num_standardFigures"\n' @@ -885,7 +905,7 @@ class ParserTest(unittest.TestCase): 'DEF_LOOKUP "numr" PROCESS_BASE PROCESS_MARKS ALL ' 'DIRECTION LTR REVERSAL\n' 'IN_CONTEXT\n' - 'RIGHT ENUM ' + ' RIGHT ENUM ' 'GLYPH "fraction" ' 'RANGE "zero.numr" TO "nine.numr" ' 'END_ENUM\n' @@ -926,7 +946,7 @@ class ParserTest(unittest.TestCase): 'DEF_LOOKUP "empty_position" PROCESS_BASE PROCESS_MARKS ALL ' 'DIRECTION LTR\n' 'EXCEPT_CONTEXT\n' - 'LEFT GLYPH "glyph"\n' + ' LEFT GLYPH "glyph"\n' 'END_CONTEXT\n' 'AS_POSITION\n' 'END_POSITION' @@ -945,13 +965,13 @@ class ParserTest(unittest.TestCase): 'END_ATTACH\n' 'END_POSITION\n' 'DEF_ANCHOR "MARK_top" ON 120 GLYPH acutecomb COMPONENT 1 ' - 'AT POS DX 0 DY 450 END_POS END_ANCHOR\n' + 'AT POS DX 0 DY 450 END_POS END_ANCHOR\n' 'DEF_ANCHOR "MARK_top" ON 121 GLYPH gravecomb COMPONENT 1 ' - 'AT POS DX 0 DY 450 END_POS END_ANCHOR\n' + 'AT POS DX 0 DY 450 END_POS END_ANCHOR\n' 'DEF_ANCHOR "top" ON 31 GLYPH a COMPONENT 1 ' - 'AT POS DX 210 DY 450 END_POS END_ANCHOR\n' + 'AT POS DX 210 DY 450 END_POS END_ANCHOR\n' 'DEF_ANCHOR "top" ON 35 GLYPH e COMPONENT 1 ' - 'AT POS DX 215 DY 450 END_POS END_ANCHOR\n' + 'AT POS DX 215 DY 450 END_POS END_ANCHOR' ).statements pos = lookup.pos coverage = [g.glyph for g in pos.coverage] @@ -991,9 +1011,9 @@ class ParserTest(unittest.TestCase): 'IN_CONTEXT\n' 'END_CONTEXT\n' 'AS_POSITION\n' - 'ATTACH_CURSIVE EXIT GLYPH "a" GLYPH "b" ENTER GLYPH "c"\n' + 'ATTACH_CURSIVE\nEXIT GLYPH "a" GLYPH "b"\nENTER GLYPH "c"\n' 'END_ATTACH\n' - 'END_POSITION\n' + 'END_POSITION' ).statements exit = [[g.glyph for g in v] for v in lookup.pos.coverages_exit] enter = [[g.glyph for g in v] for v in lookup.pos.coverages_enter] @@ -1010,12 +1030,12 @@ class ParserTest(unittest.TestCase): 'END_CONTEXT\n' 'AS_POSITION\n' 'ADJUST_PAIR\n' - ' FIRST GLYPH "A"\n' - ' SECOND GLYPH "V"\n' + ' FIRST GLYPH "A"\n' + ' SECOND GLYPH "V"\n' ' 1 2 BY POS ADV -30 END_POS POS END_POS\n' - ' 2 1 BY POS ADV -30 END_POS POS END_POS\n' + ' 2 1 BY POS ADV -30 END_POS POS END_POS\n\n' 'END_ADJUST\n' - 'END_POSITION\n' + 'END_POSITION' ).statements coverages_1 = [[g.glyph for g in v] for v in lookup.pos.coverages_1] coverages_2 = [[g.glyph for g in v] for v in lookup.pos.coverages_2] @@ -1034,15 +1054,15 @@ class ParserTest(unittest.TestCase): 'DEF_LOOKUP "TestLookup" PROCESS_BASE PROCESS_MARKS ALL ' 'DIRECTION LTR\n' 'IN_CONTEXT\n' - # 'LEFT GLYPH "leftGlyph"\n' - # 'RIGHT GLYPH "rightGlyph"\n' + # ' LEFT GLYPH "leftGlyph"\n' + # ' RIGHT GLYPH "rightGlyph"\n' 'END_CONTEXT\n' 'AS_POSITION\n' 'ADJUST_SINGLE' - ' GLYPH "glyph1" BY POS ADV 0 DX 123 END_POS\n' + ' GLYPH "glyph1" BY POS ADV 0 DX 123 END_POS' ' GLYPH "glyph2" BY POS ADV 0 DX 456 END_POS\n' 'END_ADJUST\n' - 'END_POSITION\n' + 'END_POSITION' ).statements pos = lookup.pos adjust = [[[g.glyph for g in a], b] for (a, b) in pos.adjust_single] @@ -1056,11 +1076,11 @@ class ParserTest(unittest.TestCase): def test_def_anchor(self): [anchor1, anchor2, anchor3] = self.parse( 'DEF_ANCHOR "top" ON 120 GLYPH a ' - 'COMPONENT 1 AT POS DX 250 DY 450 END_POS END_ANCHOR\n' + 'COMPONENT 1 AT POS DX 250 DY 450 END_POS END_ANCHOR\n' 'DEF_ANCHOR "MARK_top" ON 120 GLYPH acutecomb ' - 'COMPONENT 1 AT POS DX 0 DY 450 END_POS END_ANCHOR\n' + 'COMPONENT 1 AT POS DX 0 DY 450 END_POS END_ANCHOR\n' 'DEF_ANCHOR "bottom" ON 120 GLYPH a ' - 'COMPONENT 1 AT POS DX 250 DY 0 END_POS END_ANCHOR\n' + 'COMPONENT 1 AT POS DX 250 DY 0 END_POS END_ANCHOR' ).statements self.assertEqual( (anchor1.name, anchor1.gid, anchor1.glyph_name, anchor1.component, @@ -1084,9 +1104,9 @@ class ParserTest(unittest.TestCase): def test_def_anchor_multi_component(self): [anchor1, anchor2] = self.parse( 'DEF_ANCHOR "top" ON 120 GLYPH a ' - 'COMPONENT 1 AT POS DX 250 DY 450 END_POS END_ANCHOR\n' + 'COMPONENT 1 AT POS DX 250 DY 450 END_POS END_ANCHOR\n' 'DEF_ANCHOR "top" ON 120 GLYPH a ' - 'COMPONENT 2 AT POS DX 250 DY 450 END_POS END_ANCHOR\n' + 'COMPONENT 2 AT POS DX 250 DY 450 END_POS END_ANCHOR' ).statements self.assertEqual( (anchor1.name, anchor1.gid, anchor1.glyph_name, anchor1.component), @@ -1104,15 +1124,15 @@ class ParserTest(unittest.TestCase): 'anchor names are case insensitive', self.parse, 'DEF_ANCHOR "dupe" ON 120 GLYPH a ' - 'COMPONENT 1 AT POS DX 250 DY 450 END_POS END_ANCHOR\n' + 'COMPONENT 1 AT POS DX 250 DY 450 END_POS END_ANCHOR\n' 'DEF_ANCHOR "dupe" ON 120 GLYPH a ' - 'COMPONENT 1 AT POS DX 250 DY 450 END_POS END_ANCHOR\n' + 'COMPONENT 1 AT POS DX 250 DY 450 END_POS END_ANCHOR' ) def test_def_anchor_locked(self): [anchor] = self.parse( 'DEF_ANCHOR "top" ON 120 GLYPH a ' - 'COMPONENT 1 LOCKED AT POS DX 250 DY 450 END_POS END_ANCHOR\n' + 'COMPONENT 1 LOCKED AT POS DX 250 DY 450 END_POS END_ANCHOR' ).statements self.assertEqual( (anchor.name, anchor.gid, anchor.glyph_name, anchor.component, @@ -1124,7 +1144,7 @@ class ParserTest(unittest.TestCase): def test_anchor_adjust_device(self): [anchor] = self.parse( 'DEF_ANCHOR "MARK_top" ON 123 GLYPH diacglyph ' - 'COMPONENT 1 AT POS DX 0 DY 456 ADJUST_BY 12 AT 34 ' + 'COMPONENT 1 AT POS DX 0 DY 456 ADJUST_BY 12 AT 34 ' 'ADJUST_BY 56 AT 78 END_POS END_ANCHOR' ).statements self.assertEqual( @@ -1136,7 +1156,7 @@ class ParserTest(unittest.TestCase): [grid_ppem, pres_ppem, ppos_ppem] = self.parse( 'GRID_PPEM 20\n' 'PRESENTATION_PPEM 72\n' - 'PPOSITIONING_PPEM 144\n' + 'PPOSITIONING_PPEM 144' ).statements self.assertEqual( ((grid_ppem.name, grid_ppem.value), @@ -1149,7 +1169,7 @@ class ParserTest(unittest.TestCase): def test_compiler_flags(self): [setting1, setting2] = self.parse( 'COMPILER_USEEXTENSIONLOOKUPS\n' - 'COMPILER_USEPAIRPOSFORMAT2\n' + 'COMPILER_USEPAIRPOSFORMAT2' ).statements self.assertEqual( ((setting1.name, setting1.value), @@ -1162,7 +1182,7 @@ class ParserTest(unittest.TestCase): [cmap_format1, cmap_format2, cmap_format3] = self.parse( 'CMAP_FORMAT 0 3 4\n' 'CMAP_FORMAT 1 0 6\n' - 'CMAP_FORMAT 3 1 4\n' + 'CMAP_FORMAT 3 1 4' ).statements self.assertEqual( ((cmap_format1.name, cmap_format1.value), @@ -1174,14 +1194,22 @@ class ParserTest(unittest.TestCase): ) def test_stop_at_end(self): - [def_glyph] = self.parse( + doc = self.parse_( 'DEF_GLYPH ".notdef" ID 0 TYPE BASE END_GLYPH END\0\0\0\0' - ).statements + ) + [def_glyph] = doc.statements self.assertEqual((def_glyph.name, def_glyph.id, def_glyph.unicode, def_glyph.type, def_glyph.components), (".notdef", 0, None, "BASE", None)) + self.assertEqual(str(doc), + '\nDEF_GLYPH ".notdef" ID 0 TYPE BASE END_GLYPH END\n') + + def parse_(self, text): + return Parser(UnicodeIO(text)).parse() def parse(self, text): + doc = self.parse_(text) + self.assertEqual('\n'.join(str(s) for s in doc.statements), text) return Parser(UnicodeIO(text)).parse() if __name__ == "__main__":