[voltLib] Support writing back ast as VOLT data

Adds __str__() method to ast classes that writes back VOLT data. Tries
to replicate VOLT syntax idiosyncrasies as much as possible for better
round-trip conversion.
This commit is contained in:
Khaled Hosny 2020-06-04 00:46:37 +02:00
parent 3c4f5a75bf
commit a1df9175ee
2 changed files with 337 additions and 122 deletions

View File

@ -10,6 +10,18 @@ class Pos(NamedTuple):
dx_adjust_by: dict dx_adjust_by: dict
dy_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): class Element(object):
def __init__(self, location=None): def __init__(self, location=None):
@ -18,6 +30,9 @@ class Element(object):
def build(self, builder): def build(self, builder):
pass pass
def __str__(self):
raise NotImplementedError
class Statement(Element): class Statement(Element):
pass pass
@ -36,6 +51,9 @@ class VoltFile(Statement):
for s in self.statements: for s in self.statements:
s.build(builder) s.build(builder)
def __str__(self):
return '\n' + '\n'.join(str(s) for s in self.statements) + ' END\n'
class GlyphDefinition(Statement): class GlyphDefinition(Statement):
def __init__(self, name, gid, gunicode, gtype, components, location=None): def __init__(self, name, gid, gunicode, gtype, components, location=None):
@ -46,6 +64,21 @@ class GlyphDefinition(Statement):
self.type = gtype self.type = gtype
self.components = components 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): class GroupDefinition(Statement):
def __init__(self, name, enum, location=None): def __init__(self, name, enum, location=None):
@ -67,6 +100,10 @@ class GroupDefinition(Statement):
self.glyphs_ = self.enum.glyphSet(groups) self.glyphs_ = self.enum.glyphSet(groups)
return self.glyphs_ 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): class GlyphName(Expression):
"""A single glyph name, such as cedilla.""" """A single glyph name, such as cedilla."""
@ -77,6 +114,9 @@ class GlyphName(Expression):
def glyphSet(self): def glyphSet(self):
return (self.glyph,) return (self.glyph,)
def __str__(self):
return f' GLYPH "{self.glyph}"'
class Enum(Expression): class Enum(Expression):
"""An enum""" """An enum"""
@ -97,6 +137,10 @@ class Enum(Expression):
glyphs.extend(element.glyphSet()) glyphs.extend(element.glyphSet())
return tuple(glyphs) return tuple(glyphs)
def __str__(self):
enum = ''.join(str(e) for e in self.enum)
return f' ENUM{enum} END_ENUM'
class GroupName(Expression): class GroupName(Expression):
"""A glyph group""" """A glyph group"""
@ -115,6 +159,9 @@ class GroupName(Expression):
'Group "%s" is used but undefined.' % (self.group), 'Group "%s" is used but undefined.' % (self.group),
self.location) self.location)
def __str__(self):
return f' GROUP "{self.group}"'
class Range(Expression): class Range(Expression):
"""A glyph range""" """A glyph range"""
@ -127,6 +174,9 @@ class Range(Expression):
def glyphSet(self): def glyphSet(self):
return tuple(self.parser.glyph_range(self.start, self.end)) return tuple(self.parser.glyph_range(self.start, self.end))
def __str__(self):
return f' RANGE "{self.start}" TO "{self.end}"'
class ScriptDefinition(Statement): class ScriptDefinition(Statement):
def __init__(self, name, tag, langs, location=None): def __init__(self, name, tag, langs, location=None):
@ -135,6 +185,16 @@ class ScriptDefinition(Statement):
self.tag = tag self.tag = tag
self.langs = langs 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): class LangSysDefinition(Statement):
def __init__(self, name, tag, features, location=None): def __init__(self, name, tag, features, location=None):
@ -143,6 +203,16 @@ class LangSysDefinition(Statement):
self.tag = tag self.tag = tag
self.features = features 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): class FeatureDefinition(Statement):
def __init__(self, name, tag, lookups, location=None): def __init__(self, name, tag, lookups, location=None):
@ -151,6 +221,12 @@ class FeatureDefinition(Statement):
self.tag = tag self.tag = tag
self.lookups = lookups 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): class LookupDefinition(Statement):
def __init__(self, name, process_base, process_marks, mark_glyph_set, def __init__(self, name, process_base, process_marks, mark_glyph_set,
@ -168,12 +244,51 @@ class LookupDefinition(Statement):
self.sub = sub self.sub = sub
self.pos = pos 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): class SubstitutionDefinition(Statement):
def __init__(self, mapping, location=None): def __init__(self, mapping, location=None):
Statement.__init__(self, location) Statement.__init__(self, location)
self.mapping = mapping 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): class SubstitutionSingleDefinition(SubstitutionDefinition):
pass pass
@ -197,6 +312,15 @@ class PositionAttachDefinition(Statement):
self.coverage = coverage self.coverage = coverage
self.coverage_to = coverage_to 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): class PositionAttachCursiveDefinition(Statement):
def __init__(self, coverages_exit, coverages_enter, location=None): def __init__(self, coverages_exit, coverages_enter, location=None):
@ -204,6 +328,17 @@ class PositionAttachCursiveDefinition(Statement):
self.coverages_exit = coverages_exit self.coverages_exit = coverages_exit
self.coverages_enter = coverages_enter 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): class PositionAdjustPairDefinition(Statement):
def __init__(self, coverages_1, coverages_2, adjust_pair, location=None): def __init__(self, coverages_1, coverages_2, adjust_pair, location=None):
@ -212,12 +347,36 @@ class PositionAdjustPairDefinition(Statement):
self.coverages_2 = coverages_2 self.coverages_2 = coverages_2
self.adjust_pair = adjust_pair 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): class PositionAdjustSingleDefinition(Statement):
def __init__(self, adjust_single, location=None): def __init__(self, adjust_single, location=None):
Statement.__init__(self, location) Statement.__init__(self, location)
self.adjust_single = adjust_single 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): class ContextDefinition(Statement):
def __init__(self, ex_or_in, left=None, right=None, location=None): 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.left = left if left is not None else []
self.right = right if right 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): class AnchorDefinition(Statement):
def __init__(self, name, gid, glyph_name, component, locked, def __init__(self, name, gid, glyph_name, component, locked,
@ -238,9 +408,26 @@ class AnchorDefinition(Statement):
self.locked = locked self.locked = locked
self.pos = pos 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): class SettingDefinition(Statement):
def __init__(self, name, value, location=None): def __init__(self, name, value, location=None):
Statement.__init__(self, location) Statement.__init__(self, location)
self.name = name self.name = name
self.value = value 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}'

View File

@ -37,7 +37,7 @@ class ParserTest(unittest.TestCase):
("space", 3, [0x0020], "BASE", None)) ("space", 3, [0x0020], "BASE", None))
def test_def_glyph_base_with_unicodevalues(self): def test_def_glyph_base_with_unicodevalues(self):
[def_glyph] = self.parse( [def_glyph] = self.parse_(
'DEF_GLYPH "CR" ID 2 UNICODEVALUES "U+0009" ' 'DEF_GLYPH "CR" ID 2 UNICODEVALUES "U+0009" '
'TYPE BASE END_GLYPH' 'TYPE BASE END_GLYPH'
).statements ).statements
@ -55,7 +55,7 @@ class ParserTest(unittest.TestCase):
("CR", 2, [0x0009, 0x000D], "BASE", None)) ("CR", 2, [0x0009, 0x000D], "BASE", None))
def test_def_glyph_base_with_empty_unicodevalues(self): def test_def_glyph_base_with_empty_unicodevalues(self):
[def_glyph] = self.parse( [def_glyph] = self.parse_(
'DEF_GLYPH "i.locl" ID 269 UNICODEVALUES "" ' 'DEF_GLYPH "i.locl" ID 269 UNICODEVALUES "" '
'TYPE BASE END_GLYPH' 'TYPE BASE END_GLYPH'
).statements ).statements
@ -106,7 +106,7 @@ class ParserTest(unittest.TestCase):
def test_def_glyph_case_sensitive(self): def test_def_glyph_case_sensitive(self):
def_glyphs = self.parse( def_glyphs = self.parse(
'DEF_GLYPH "A" ID 3 UNICODE 65 TYPE BASE END_GLYPH\n' '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 ).statements
self.assertEqual((def_glyphs[0].name, def_glyphs[0].id, self.assertEqual((def_glyphs[0].name, def_glyphs[0].id,
def_glyphs[0].unicode, def_glyphs[0].type, def_glyphs[0].unicode, def_glyphs[0].type,
@ -120,10 +120,10 @@ class ParserTest(unittest.TestCase):
def test_def_group_glyphs(self): def test_def_group_glyphs(self):
[def_group] = self.parse( [def_group] = self.parse(
'DEF_GROUP "aaccented"\n' '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 "adieresis" GLYPH "ae" GLYPH "agrave" GLYPH "amacron" '
'GLYPH "aogonek" GLYPH "aring" GLYPH "atilde" END_ENUM\n' 'GLYPH "aogonek" GLYPH "aring" GLYPH "atilde" END_ENUM\n'
'END_GROUP\n' 'END_GROUP'
).statements ).statements
self.assertEqual((def_group.name, def_group.enum.glyphSet()), self.assertEqual((def_group.name, def_group.enum.glyphSet()),
("aaccented", ("aaccented",
@ -134,14 +134,14 @@ class ParserTest(unittest.TestCase):
def test_def_group_groups(self): def test_def_group_groups(self):
[group1, group2, test_group] = self.parse( [group1, group2, test_group] = self.parse(
'DEF_GROUP "Group1"\n' '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' 'END_GROUP\n'
'DEF_GROUP "Group2"\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' 'END_GROUP\n'
'DEF_GROUP "TestGroup"\n' 'DEF_GROUP "TestGroup"\n'
'ENUM GROUP "Group1" GROUP "Group2" END_ENUM\n' ' ENUM GROUP "Group1" GROUP "Group2" END_ENUM\n'
'END_GROUP\n' 'END_GROUP'
).statements ).statements
groups = [g.group for g in test_group.enum.enum] groups = [g.group for g in test_group.enum.enum]
self.assertEqual((test_group.name, groups), self.assertEqual((test_group.name, groups),
@ -151,20 +151,20 @@ class ParserTest(unittest.TestCase):
[group1, test_group1, test_group2, test_group3, group2] = \ [group1, test_group1, test_group2, test_group3, group2] = \
self.parse( self.parse(
'DEF_GROUP "Group1"\n' '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' 'END_GROUP\n'
'DEF_GROUP "TestGroup1"\n' 'DEF_GROUP "TestGroup1"\n'
'ENUM GROUP "Group1" GROUP "Group2" END_ENUM\n' ' ENUM GROUP "Group1" GROUP "Group2" END_ENUM\n'
'END_GROUP\n' 'END_GROUP\n'
'DEF_GROUP "TestGroup2"\n' 'DEF_GROUP "TestGroup2"\n'
'ENUM GROUP "Group2" END_ENUM\n' ' ENUM GROUP "Group2" END_ENUM\n'
'END_GROUP\n' 'END_GROUP\n'
'DEF_GROUP "TestGroup3"\n' 'DEF_GROUP "TestGroup3"\n'
'ENUM GROUP "Group2" GROUP "Group1" END_ENUM\n' ' ENUM GROUP "Group2" GROUP "Group1" END_ENUM\n'
'END_GROUP\n' 'END_GROUP\n'
'DEF_GROUP "Group2"\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' 'END_GROUP'
).statements ).statements
groups = [g.group for g in test_group1.enum.enum] groups = [g.group for g in test_group1.enum.enum]
self.assertEqual( self.assertEqual(
@ -195,12 +195,12 @@ class ParserTest(unittest.TestCase):
def test_def_group_glyphs_and_group(self): def test_def_group_glyphs_and_group(self):
[def_group1, def_group2] = self.parse( [def_group1, def_group2] = self.parse(
'DEF_GROUP "aaccented"\n' '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 "adieresis" GLYPH "ae" GLYPH "agrave" GLYPH "amacron" '
'GLYPH "aogonek" GLYPH "aring" GLYPH "atilde" END_ENUM\n' 'GLYPH "aogonek" GLYPH "aring" GLYPH "atilde" END_ENUM\n'
'END_GROUP\n' 'END_GROUP\n'
'DEF_GROUP "KERN_lc_a_2ND"\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' 'END_GROUP'
).statements ).statements
items = def_group2.enum.enum 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 "ccedilla" ID 210 UNICODE 231 TYPE BASE END_GLYPH\n'
'DEF_GLYPH "cdotaccent" ID 210 UNICODE 267 TYPE BASE END_GLYPH\n' 'DEF_GLYPH "cdotaccent" ID 210 UNICODE 267 TYPE BASE END_GLYPH\n'
'DEF_GROUP "KERN_lc_a_2ND"\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_ENUM\n'
'END_GROUP' 'END_GROUP'
).statements[-1] ).statements[-1]
@ -238,7 +238,7 @@ class ParserTest(unittest.TestCase):
'END_GROUP\n' 'END_GROUP\n'
'DEF_GROUP "dupe"\n' 'DEF_GROUP "dupe"\n'
'ENUM GLYPH "x" END_ENUM\n' 'ENUM GLYPH "x" END_ENUM\n'
'END_GROUP\n' 'END_GROUP'
) )
def test_group_duplicate_case_insensitive(self): def test_group_duplicate_case_insensitive(self):
@ -251,12 +251,12 @@ class ParserTest(unittest.TestCase):
'END_GROUP\n' 'END_GROUP\n'
'DEF_GROUP "Dupe"\n' 'DEF_GROUP "Dupe"\n'
'ENUM GLYPH "x" END_ENUM\n' 'ENUM GLYPH "x" END_ENUM\n'
'END_GROUP\n' 'END_GROUP'
) )
def test_script_without_langsys(self): def test_script_without_langsys(self):
[script] = self.parse( [script] = self.parse(
'DEF_SCRIPT NAME "Latin" TAG "latn"\n' 'DEF_SCRIPT NAME "Latin" TAG "latn"\n\n'
'END_SCRIPT' 'END_SCRIPT'
).statements ).statements
self.assertEqual((script.name, script.tag, script.langs), self.assertEqual((script.name, script.tag, script.langs),
@ -264,10 +264,10 @@ class ParserTest(unittest.TestCase):
def test_langsys_normal(self): def test_langsys_normal(self):
[def_script] = self.parse( [def_script] = self.parse(
'DEF_SCRIPT NAME "Latin" TAG "latn"\n' 'DEF_SCRIPT NAME "Latin" TAG "latn"\n\n'
'DEF_LANGSYS NAME "Romanian" TAG "ROM "\n' 'DEF_LANGSYS NAME "Romanian" TAG "ROM "\n\n'
'END_LANGSYS\n' 'END_LANGSYS\n'
'DEF_LANGSYS NAME "Moldavian" TAG "MOL "\n' 'DEF_LANGSYS NAME "Moldavian" TAG "MOL "\n\n'
'END_LANGSYS\n' 'END_LANGSYS\n'
'END_SCRIPT' 'END_SCRIPT'
).statements ).statements
@ -285,8 +285,8 @@ class ParserTest(unittest.TestCase):
def test_langsys_no_script_name(self): def test_langsys_no_script_name(self):
[langsys] = self.parse( [langsys] = self.parse(
'DEF_SCRIPT TAG "latn"\n' 'DEF_SCRIPT TAG "latn"\n\n'
'DEF_LANGSYS NAME "Default" TAG "dflt"\n' 'DEF_LANGSYS NAME "Default" TAG "dflt"\n\n'
'END_LANGSYS\n' 'END_LANGSYS\n'
'END_SCRIPT' 'END_SCRIPT'
).statements ).statements
@ -303,8 +303,8 @@ class ParserTest(unittest.TestCase):
VoltLibError, VoltLibError,
r'.*Expected "TAG"'): r'.*Expected "TAG"'):
[langsys] = self.parse( [langsys] = self.parse(
'DEF_SCRIPT NAME "Latin"\n' 'DEF_SCRIPT NAME "Latin"\n\n'
'DEF_LANGSYS NAME "Default" TAG "dflt"\n' 'DEF_LANGSYS NAME "Default" TAG "dflt"\n\n'
'END_LANGSYS\n' 'END_LANGSYS\n'
'END_SCRIPT' 'END_SCRIPT'
).statements ).statements
@ -315,12 +315,12 @@ class ParserTest(unittest.TestCase):
'Script "DFLT" already defined, ' 'Script "DFLT" already defined, '
'script tags are case insensitive'): 'script tags are case insensitive'):
[langsys1, langsys2] = self.parse( [langsys1, langsys2] = self.parse(
'DEF_SCRIPT NAME "Default" TAG "DFLT"\n' 'DEF_SCRIPT NAME "Default" TAG "DFLT"\n\n'
'DEF_LANGSYS NAME "Default" TAG "dflt"\n' 'DEF_LANGSYS NAME "Default" TAG "dflt"\n\n'
'END_LANGSYS\n' 'END_LANGSYS\n'
'END_SCRIPT\n' 'END_SCRIPT\n'
'DEF_SCRIPT TAG "DFLT"\n' 'DEF_SCRIPT TAG "DFLT"\n\n'
'DEF_LANGSYS NAME "Default" TAG "dflt"\n' 'DEF_LANGSYS NAME "Default" TAG "dflt"\n\n'
'END_LANGSYS\n' 'END_LANGSYS\n'
'END_SCRIPT' 'END_SCRIPT'
).statements ).statements
@ -336,21 +336,21 @@ class ParserTest(unittest.TestCase):
'END_LANGSYS\n' 'END_LANGSYS\n'
'DEF_LANGSYS NAME "Default" TAG "dflt"\n' 'DEF_LANGSYS NAME "Default" TAG "dflt"\n'
'END_LANGSYS\n' 'END_LANGSYS\n'
'END_SCRIPT\n' 'END_SCRIPT'
).statements ).statements
def test_langsys_lang_in_separate_scripts(self): def test_langsys_lang_in_separate_scripts(self):
[langsys1, langsys2] = self.parse( [langsys1, langsys2] = self.parse(
'DEF_SCRIPT NAME "Default" TAG "DFLT"\n' 'DEF_SCRIPT NAME "Default" TAG "DFLT"\n\n'
'DEF_LANGSYS NAME "Default" TAG "dflt"\n' 'DEF_LANGSYS NAME "Default" TAG "dflt"\n\n'
'END_LANGSYS\n' 'END_LANGSYS\n'
'DEF_LANGSYS NAME "Default" TAG "ROM "\n' 'DEF_LANGSYS NAME "Default" TAG "ROM "\n\n'
'END_LANGSYS\n' 'END_LANGSYS\n'
'END_SCRIPT\n' 'END_SCRIPT\n'
'DEF_SCRIPT NAME "Latin" TAG "latn"\n' 'DEF_SCRIPT NAME "Latin" TAG "latn"\n\n'
'DEF_LANGSYS NAME "Default" TAG "dflt"\n' 'DEF_LANGSYS NAME "Default" TAG "dflt"\n\n'
'END_LANGSYS\n' 'END_LANGSYS\n'
'DEF_LANGSYS NAME "Default" TAG "ROM "\n' 'DEF_LANGSYS NAME "Default" TAG "ROM "\n\n'
'END_LANGSYS\n' 'END_LANGSYS\n'
'END_SCRIPT' 'END_SCRIPT'
).statements ).statements
@ -361,8 +361,8 @@ class ParserTest(unittest.TestCase):
def test_langsys_no_lang_name(self): def test_langsys_no_lang_name(self):
[langsys] = self.parse( [langsys] = self.parse(
'DEF_SCRIPT NAME "Latin" TAG "latn"\n' 'DEF_SCRIPT NAME "Latin" TAG "latn"\n\n'
'DEF_LANGSYS TAG "dflt"\n' 'DEF_LANGSYS TAG "dflt"\n\n'
'END_LANGSYS\n' 'END_LANGSYS\n'
'END_SCRIPT' 'END_SCRIPT'
).statements ).statements
@ -379,18 +379,18 @@ class ParserTest(unittest.TestCase):
VoltLibError, VoltLibError,
r'.*Expected "TAG"'): r'.*Expected "TAG"'):
[langsys] = self.parse( [langsys] = self.parse(
'DEF_SCRIPT NAME "Latin" TAG "latn"\n' 'DEF_SCRIPT NAME "Latin" TAG "latn"\n\n'
'DEF_LANGSYS NAME "Default"\n' 'DEF_LANGSYS NAME "Default"\n\n'
'END_LANGSYS\n' 'END_LANGSYS\n'
'END_SCRIPT' 'END_SCRIPT'
).statements ).statements
def test_feature(self): def test_feature(self):
[def_script] = self.parse( [def_script] = self.parse(
'DEF_SCRIPT NAME "Latin" TAG "latn"\n' 'DEF_SCRIPT NAME "Latin" TAG "latn"\n\n'
'DEF_LANGSYS NAME "Romanian" TAG "ROM "\n' 'DEF_LANGSYS NAME "Romanian" TAG "ROM "\n\n'
'DEF_FEATURE NAME "Fractions" TAG "frac"\n' 'DEF_FEATURE NAME "Fractions" TAG "frac"\n'
'LOOKUP "fraclookup"\n' ' LOOKUP "fraclookup"\n'
'END_FEATURE\n' 'END_FEATURE\n'
'END_LANGSYS\n' 'END_LANGSYS\n'
'END_SCRIPT' 'END_SCRIPT'
@ -402,10 +402,10 @@ class ParserTest(unittest.TestCase):
"frac", "frac",
["fraclookup"])) ["fraclookup"]))
[def_script] = self.parse( [def_script] = self.parse(
'DEF_SCRIPT NAME "Latin" TAG "latn"\n' 'DEF_SCRIPT NAME "Latin" TAG "latn"\n\n'
'DEF_LANGSYS NAME "Romanian" TAG "ROM "\n' 'DEF_LANGSYS NAME "Romanian" TAG "ROM "\n\n'
'DEF_FEATURE NAME "Kerning" TAG "kern"\n' 'DEF_FEATURE NAME "Kerning" TAG "kern"\n'
'LOOKUP "kern1" LOOKUP "kern2"\n' ' LOOKUP "kern1" LOOKUP "kern2"\n'
'END_FEATURE\n' 'END_FEATURE\n'
'END_LANGSYS\n' 'END_LANGSYS\n'
'END_SCRIPT' 'END_SCRIPT'
@ -572,12 +572,13 @@ class ParserTest(unittest.TestCase):
def test_substitution_single_in_context(self): def test_substitution_single_in_context(self):
[group, lookup] = self.parse( [group, lookup] = self.parse(
'DEF_GROUP "Denominators" ENUM GLYPH "one.dnom" GLYPH "two.dnom" ' 'DEF_GROUP "Denominators"\n'
'END_ENUM END_GROUP\n' ' ENUM GLYPH "one.dnom" GLYPH "two.dnom" END_ENUM\n'
'END_GROUP\n'
'DEF_LOOKUP "fracdnom" PROCESS_BASE PROCESS_MARKS ALL ' 'DEF_LOOKUP "fracdnom" PROCESS_BASE PROCESS_MARKS ALL '
'DIRECTION LTR\n' 'DIRECTION LTR\n'
'IN_CONTEXT LEFT ENUM GROUP "Denominators" GLYPH "fraction" ' 'IN_CONTEXT\n'
'END_ENUM\n' ' LEFT ENUM GROUP "Denominators" GLYPH "fraction" END_ENUM\n'
'END_CONTEXT\n' 'END_CONTEXT\n'
'AS_SUBSTITUTION\n' 'AS_SUBSTITUTION\n'
'SUB GLYPH "one"\n' 'SUB GLYPH "one"\n'
@ -603,17 +604,18 @@ class ParserTest(unittest.TestCase):
def test_substitution_single_in_contexts(self): def test_substitution_single_in_contexts(self):
[group, lookup] = self.parse( [group, lookup] = self.parse(
'DEF_GROUP "Hebrew" ENUM GLYPH "uni05D0" GLYPH "uni05D1" ' 'DEF_GROUP "Hebrew"\n'
'END_ENUM END_GROUP\n' ' ENUM GLYPH "uni05D0" GLYPH "uni05D1" END_ENUM\n'
'END_GROUP\n'
'DEF_LOOKUP "HebrewCurrency" PROCESS_BASE PROCESS_MARKS ALL ' 'DEF_LOOKUP "HebrewCurrency" PROCESS_BASE PROCESS_MARKS ALL '
'DIRECTION LTR\n' 'DIRECTION LTR\n'
'IN_CONTEXT\n' 'IN_CONTEXT\n'
'RIGHT GROUP "Hebrew"\n' ' RIGHT GROUP "Hebrew"\n'
'RIGHT GLYPH "one.Hebr"\n' ' RIGHT GLYPH "one.Hebr"\n'
'END_CONTEXT\n' 'END_CONTEXT\n'
'IN_CONTEXT\n' 'IN_CONTEXT\n'
'LEFT GROUP "Hebrew"\n' ' LEFT GROUP "Hebrew"\n'
'LEFT GLYPH "one.Hebr"\n' ' LEFT GLYPH "one.Hebr"\n'
'END_CONTEXT\n' 'END_CONTEXT\n'
'AS_SUBSTITUTION\n' 'AS_SUBSTITUTION\n'
'SUB GLYPH "dollar"\n' 'SUB GLYPH "dollar"\n'
@ -644,8 +646,9 @@ class ParserTest(unittest.TestCase):
def test_substitution_skip_base(self): def test_substitution_skip_base(self):
[group, lookup] = self.parse( [group, lookup] = self.parse(
'DEF_GROUP "SomeMarks" ENUM GLYPH "marka" GLYPH "markb" ' 'DEF_GROUP "SomeMarks"\n'
'END_ENUM END_GROUP\n' ' ENUM GLYPH "marka" GLYPH "markb" END_ENUM\n'
'END_GROUP\n'
'DEF_LOOKUP "SomeSub" SKIP_BASE PROCESS_MARKS ALL ' 'DEF_LOOKUP "SomeSub" SKIP_BASE PROCESS_MARKS ALL '
'DIRECTION LTR\n' 'DIRECTION LTR\n'
'IN_CONTEXT\n' 'IN_CONTEXT\n'
@ -662,8 +665,9 @@ class ParserTest(unittest.TestCase):
def test_substitution_process_base(self): def test_substitution_process_base(self):
[group, lookup] = self.parse( [group, lookup] = self.parse(
'DEF_GROUP "SomeMarks" ENUM GLYPH "marka" GLYPH "markb" ' 'DEF_GROUP "SomeMarks"\n'
'END_ENUM END_GROUP\n' ' ENUM GLYPH "marka" GLYPH "markb" END_ENUM\n'
'END_GROUP\n'
'DEF_LOOKUP "SomeSub" PROCESS_BASE PROCESS_MARKS ALL ' 'DEF_LOOKUP "SomeSub" PROCESS_BASE PROCESS_MARKS ALL '
'DIRECTION LTR\n' 'DIRECTION LTR\n'
'IN_CONTEXT\n' 'IN_CONTEXT\n'
@ -680,11 +684,15 @@ class ParserTest(unittest.TestCase):
def test_substitution_process_marks(self): def test_substitution_process_marks(self):
[group, lookup] = self.parse( [group, lookup] = self.parse(
'DEF_GROUP "SomeMarks" ENUM GLYPH "marka" GLYPH "markb" ' 'DEF_GROUP "SomeMarks"\n'
'END_ENUM END_GROUP\n' ' ENUM GLYPH "marka" GLYPH "markb" END_ENUM\n'
'DEF_LOOKUP "SomeSub" PROCESS_BASE PROCESS_MARKS "SomeMarks" ' 'END_GROUP\n'
'DEF_LOOKUP "SomeSub" PROCESS_BASE PROCESS_MARKS "SomeMarks"\n'
'IN_CONTEXT\n'
'END_CONTEXT\n'
'AS_SUBSTITUTION\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_SUB\n'
'END_SUBSTITUTION' 'END_SUBSTITUTION'
).statements ).statements
@ -694,9 +702,12 @@ class ParserTest(unittest.TestCase):
def test_substitution_process_marks_all(self): def test_substitution_process_marks_all(self):
[lookup] = self.parse( [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' 'AS_SUBSTITUTION\n'
'SUB GLYPH "A" WITH GLYPH "A.c2sc"\n' 'SUB GLYPH "A"\n'
'WITH GLYPH "A.c2sc"\n'
'END_SUB\n' 'END_SUB\n'
'END_SUBSTITUTION' 'END_SUBSTITUTION'
).statements ).statements
@ -705,10 +716,13 @@ class ParserTest(unittest.TestCase):
("SomeSub", True)) ("SomeSub", True))
def test_substitution_process_marks_none(self): def test_substitution_process_marks_none(self):
[lookup] = self.parse( [lookup] = self.parse_(
'DEF_LOOKUP "SomeSub" PROCESS_BASE PROCESS_MARKS "NONE" ' 'DEF_LOOKUP "SomeSub" PROCESS_BASE PROCESS_MARKS "NONE"\n'
'IN_CONTEXT\n'
'END_CONTEXT\n'
'AS_SUBSTITUTION\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_SUB\n'
'END_SUBSTITUTION' 'END_SUBSTITUTION'
).statements ).statements
@ -732,10 +746,10 @@ class ParserTest(unittest.TestCase):
def test_substitution_skip_marks(self): def test_substitution_skip_marks(self):
[group, lookup] = self.parse( [group, lookup] = self.parse(
'DEF_GROUP "SomeMarks" ENUM GLYPH "marka" GLYPH "markb" ' 'DEF_GROUP "SomeMarks"\n'
'END_ENUM END_GROUP\n' ' ENUM GLYPH "marka" GLYPH "markb" END_ENUM\n'
'DEF_LOOKUP "SomeSub" PROCESS_BASE SKIP_MARKS ' 'END_GROUP\n'
'DIRECTION LTR\n' 'DEF_LOOKUP "SomeSub" PROCESS_BASE SKIP_MARKS DIRECTION LTR\n'
'IN_CONTEXT\n' 'IN_CONTEXT\n'
'END_CONTEXT\n' 'END_CONTEXT\n'
'AS_SUBSTITUTION\n' 'AS_SUBSTITUTION\n'
@ -750,11 +764,13 @@ class ParserTest(unittest.TestCase):
def test_substitution_mark_attachment(self): def test_substitution_mark_attachment(self):
[group, lookup] = self.parse( [group, lookup] = self.parse(
'DEF_GROUP "SomeMarks" ENUM GLYPH "acutecmb" GLYPH "gravecmb" ' 'DEF_GROUP "SomeMarks"\n'
'END_ENUM END_GROUP\n' ' ENUM GLYPH "acutecmb" GLYPH "gravecmb" END_ENUM\n'
'END_GROUP\n'
'DEF_LOOKUP "SomeSub" PROCESS_BASE ' 'DEF_LOOKUP "SomeSub" PROCESS_BASE '
'PROCESS_MARKS "SomeMarks" \n' 'PROCESS_MARKS "SomeMarks" DIRECTION RTL\n'
'DIRECTION RTL\n' 'IN_CONTEXT\n'
'END_CONTEXT\n'
'AS_SUBSTITUTION\n' 'AS_SUBSTITUTION\n'
'SUB GLYPH "A"\n' 'SUB GLYPH "A"\n'
'WITH GLYPH "A.c2sc"\n' 'WITH GLYPH "A.c2sc"\n'
@ -767,11 +783,13 @@ class ParserTest(unittest.TestCase):
def test_substitution_mark_glyph_set(self): def test_substitution_mark_glyph_set(self):
[group, lookup] = self.parse( [group, lookup] = self.parse(
'DEF_GROUP "SomeMarks" ENUM GLYPH "acutecmb" GLYPH "gravecmb" ' 'DEF_GROUP "SomeMarks"\n'
'END_ENUM END_GROUP\n' ' ENUM GLYPH "acutecmb" GLYPH "gravecmb" END_ENUM\n'
'END_GROUP\n'
'DEF_LOOKUP "SomeSub" PROCESS_BASE ' 'DEF_LOOKUP "SomeSub" PROCESS_BASE '
'PROCESS_MARKS MARK_GLYPH_SET "SomeMarks" \n' 'PROCESS_MARKS MARK_GLYPH_SET "SomeMarks" DIRECTION RTL\n'
'DIRECTION RTL\n' 'IN_CONTEXT\n'
'END_CONTEXT\n'
'AS_SUBSTITUTION\n' 'AS_SUBSTITUTION\n'
'SUB GLYPH "A"\n' 'SUB GLYPH "A"\n'
'WITH GLYPH "A.c2sc"\n' 'WITH GLYPH "A.c2sc"\n'
@ -784,11 +802,13 @@ class ParserTest(unittest.TestCase):
def test_substitution_process_all_marks(self): def test_substitution_process_all_marks(self):
[group, lookup] = self.parse( [group, lookup] = self.parse(
'DEF_GROUP "SomeMarks" ENUM GLYPH "acutecmb" GLYPH "gravecmb" ' 'DEF_GROUP "SomeMarks"\n'
'END_ENUM END_GROUP\n' ' ENUM GLYPH "acutecmb" GLYPH "gravecmb" END_ENUM\n'
'DEF_LOOKUP "SomeSub" PROCESS_BASE ' 'END_GROUP\n'
'PROCESS_MARKS ALL \n' 'DEF_LOOKUP "SomeSub" PROCESS_BASE PROCESS_MARKS ALL '
'DIRECTION RTL\n' 'DIRECTION RTL\n'
'IN_CONTEXT\n'
'END_CONTEXT\n'
'AS_SUBSTITUTION\n' 'AS_SUBSTITUTION\n'
'SUB GLYPH "A"\n' 'SUB GLYPH "A"\n'
'WITH GLYPH "A.c2sc"\n' 'WITH GLYPH "A.c2sc"\n'
@ -805,7 +825,7 @@ class ParserTest(unittest.TestCase):
'DEF_LOOKUP "Lookup" PROCESS_BASE PROCESS_MARKS ALL ' 'DEF_LOOKUP "Lookup" PROCESS_BASE PROCESS_MARKS ALL '
'DIRECTION LTR\n' 'DIRECTION LTR\n'
'IN_CONTEXT\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' 'END_CONTEXT\n'
'AS_SUBSTITUTION\n' 'AS_SUBSTITUTION\n'
'SUB GLYPH "a"\n' 'SUB GLYPH "a"\n'
@ -821,15 +841,15 @@ class ParserTest(unittest.TestCase):
def test_substitution_reversal(self): def test_substitution_reversal(self):
lookup = self.parse( lookup = self.parse(
'DEF_GROUP "DFLT_Num_standardFigures"\n' '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' 'END_GROUP\n'
'DEF_GROUP "DFLT_Num_numerators"\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' 'END_GROUP\n'
'DEF_LOOKUP "RevLookup" PROCESS_BASE PROCESS_MARKS ALL ' 'DEF_LOOKUP "RevLookup" PROCESS_BASE PROCESS_MARKS ALL '
'DIRECTION LTR REVERSAL\n' 'DIRECTION LTR REVERSAL\n'
'IN_CONTEXT\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' 'END_CONTEXT\n'
'AS_SUBSTITUTION\n' 'AS_SUBSTITUTION\n'
'SUB GROUP "DFLT_Num_standardFigures"\n' 'SUB GROUP "DFLT_Num_standardFigures"\n'
@ -885,7 +905,7 @@ class ParserTest(unittest.TestCase):
'DEF_LOOKUP "numr" PROCESS_BASE PROCESS_MARKS ALL ' 'DEF_LOOKUP "numr" PROCESS_BASE PROCESS_MARKS ALL '
'DIRECTION LTR REVERSAL\n' 'DIRECTION LTR REVERSAL\n'
'IN_CONTEXT\n' 'IN_CONTEXT\n'
'RIGHT ENUM ' ' RIGHT ENUM '
'GLYPH "fraction" ' 'GLYPH "fraction" '
'RANGE "zero.numr" TO "nine.numr" ' 'RANGE "zero.numr" TO "nine.numr" '
'END_ENUM\n' 'END_ENUM\n'
@ -926,7 +946,7 @@ class ParserTest(unittest.TestCase):
'DEF_LOOKUP "empty_position" PROCESS_BASE PROCESS_MARKS ALL ' 'DEF_LOOKUP "empty_position" PROCESS_BASE PROCESS_MARKS ALL '
'DIRECTION LTR\n' 'DIRECTION LTR\n'
'EXCEPT_CONTEXT\n' 'EXCEPT_CONTEXT\n'
'LEFT GLYPH "glyph"\n' ' LEFT GLYPH "glyph"\n'
'END_CONTEXT\n' 'END_CONTEXT\n'
'AS_POSITION\n' 'AS_POSITION\n'
'END_POSITION' 'END_POSITION'
@ -945,13 +965,13 @@ class ParserTest(unittest.TestCase):
'END_ATTACH\n' 'END_ATTACH\n'
'END_POSITION\n' 'END_POSITION\n'
'DEF_ANCHOR "MARK_top" ON 120 GLYPH acutecomb COMPONENT 1 ' '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 ' '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 ' '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 ' '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 ).statements
pos = lookup.pos pos = lookup.pos
coverage = [g.glyph for g in pos.coverage] coverage = [g.glyph for g in pos.coverage]
@ -991,9 +1011,9 @@ class ParserTest(unittest.TestCase):
'IN_CONTEXT\n' 'IN_CONTEXT\n'
'END_CONTEXT\n' 'END_CONTEXT\n'
'AS_POSITION\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_ATTACH\n'
'END_POSITION\n' 'END_POSITION'
).statements ).statements
exit = [[g.glyph for g in v] for v in lookup.pos.coverages_exit] 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] 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' 'END_CONTEXT\n'
'AS_POSITION\n' 'AS_POSITION\n'
'ADJUST_PAIR\n' 'ADJUST_PAIR\n'
' FIRST GLYPH "A"\n' ' FIRST GLYPH "A"\n'
' SECOND GLYPH "V"\n' ' SECOND GLYPH "V"\n'
' 1 2 BY POS ADV -30 END_POS POS END_POS\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_ADJUST\n'
'END_POSITION\n' 'END_POSITION'
).statements ).statements
coverages_1 = [[g.glyph for g in v] for v in lookup.pos.coverages_1] 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] 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 ' 'DEF_LOOKUP "TestLookup" PROCESS_BASE PROCESS_MARKS ALL '
'DIRECTION LTR\n' 'DIRECTION LTR\n'
'IN_CONTEXT\n' 'IN_CONTEXT\n'
# 'LEFT GLYPH "leftGlyph"\n' # ' LEFT GLYPH "leftGlyph"\n'
# 'RIGHT GLYPH "rightGlyph"\n' # ' RIGHT GLYPH "rightGlyph"\n'
'END_CONTEXT\n' 'END_CONTEXT\n'
'AS_POSITION\n' 'AS_POSITION\n'
'ADJUST_SINGLE' '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' ' GLYPH "glyph2" BY POS ADV 0 DX 456 END_POS\n'
'END_ADJUST\n' 'END_ADJUST\n'
'END_POSITION\n' 'END_POSITION'
).statements ).statements
pos = lookup.pos pos = lookup.pos
adjust = [[[g.glyph for g in a], b] for (a, b) in pos.adjust_single] 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): def test_def_anchor(self):
[anchor1, anchor2, anchor3] = self.parse( [anchor1, anchor2, anchor3] = self.parse(
'DEF_ANCHOR "top" ON 120 GLYPH a ' '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 ' '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 ' '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 ).statements
self.assertEqual( self.assertEqual(
(anchor1.name, anchor1.gid, anchor1.glyph_name, anchor1.component, (anchor1.name, anchor1.gid, anchor1.glyph_name, anchor1.component,
@ -1084,9 +1104,9 @@ class ParserTest(unittest.TestCase):
def test_def_anchor_multi_component(self): def test_def_anchor_multi_component(self):
[anchor1, anchor2] = self.parse( [anchor1, anchor2] = self.parse(
'DEF_ANCHOR "top" ON 120 GLYPH a ' '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 ' '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 ).statements
self.assertEqual( self.assertEqual(
(anchor1.name, anchor1.gid, anchor1.glyph_name, anchor1.component), (anchor1.name, anchor1.gid, anchor1.glyph_name, anchor1.component),
@ -1104,15 +1124,15 @@ class ParserTest(unittest.TestCase):
'anchor names are case insensitive', 'anchor names are case insensitive',
self.parse, self.parse,
'DEF_ANCHOR "dupe" ON 120 GLYPH a ' '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 ' '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): def test_def_anchor_locked(self):
[anchor] = self.parse( [anchor] = self.parse(
'DEF_ANCHOR "top" ON 120 GLYPH a ' '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 ).statements
self.assertEqual( self.assertEqual(
(anchor.name, anchor.gid, anchor.glyph_name, anchor.component, (anchor.name, anchor.gid, anchor.glyph_name, anchor.component,
@ -1124,7 +1144,7 @@ class ParserTest(unittest.TestCase):
def test_anchor_adjust_device(self): def test_anchor_adjust_device(self):
[anchor] = self.parse( [anchor] = self.parse(
'DEF_ANCHOR "MARK_top" ON 123 GLYPH diacglyph ' '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' 'ADJUST_BY 56 AT 78 END_POS END_ANCHOR'
).statements ).statements
self.assertEqual( self.assertEqual(
@ -1136,7 +1156,7 @@ class ParserTest(unittest.TestCase):
[grid_ppem, pres_ppem, ppos_ppem] = self.parse( [grid_ppem, pres_ppem, ppos_ppem] = self.parse(
'GRID_PPEM 20\n' 'GRID_PPEM 20\n'
'PRESENTATION_PPEM 72\n' 'PRESENTATION_PPEM 72\n'
'PPOSITIONING_PPEM 144\n' 'PPOSITIONING_PPEM 144'
).statements ).statements
self.assertEqual( self.assertEqual(
((grid_ppem.name, grid_ppem.value), ((grid_ppem.name, grid_ppem.value),
@ -1149,7 +1169,7 @@ class ParserTest(unittest.TestCase):
def test_compiler_flags(self): def test_compiler_flags(self):
[setting1, setting2] = self.parse( [setting1, setting2] = self.parse(
'COMPILER_USEEXTENSIONLOOKUPS\n' 'COMPILER_USEEXTENSIONLOOKUPS\n'
'COMPILER_USEPAIRPOSFORMAT2\n' 'COMPILER_USEPAIRPOSFORMAT2'
).statements ).statements
self.assertEqual( self.assertEqual(
((setting1.name, setting1.value), ((setting1.name, setting1.value),
@ -1162,7 +1182,7 @@ class ParserTest(unittest.TestCase):
[cmap_format1, cmap_format2, cmap_format3] = self.parse( [cmap_format1, cmap_format2, cmap_format3] = self.parse(
'CMAP_FORMAT 0 3 4\n' 'CMAP_FORMAT 0 3 4\n'
'CMAP_FORMAT 1 0 6\n' 'CMAP_FORMAT 1 0 6\n'
'CMAP_FORMAT 3 1 4\n' 'CMAP_FORMAT 3 1 4'
).statements ).statements
self.assertEqual( self.assertEqual(
((cmap_format1.name, cmap_format1.value), ((cmap_format1.name, cmap_format1.value),
@ -1174,14 +1194,22 @@ class ParserTest(unittest.TestCase):
) )
def test_stop_at_end(self): 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' '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, self.assertEqual((def_glyph.name, def_glyph.id, def_glyph.unicode,
def_glyph.type, def_glyph.components), def_glyph.type, def_glyph.components),
(".notdef", 0, None, "BASE", None)) (".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): 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() return Parser(UnicodeIO(text)).parse()
if __name__ == "__main__": if __name__ == "__main__":