[feaLib] Implement markClass statement

This commit is contained in:
Sascha Brawer 2015-12-08 17:04:21 +01:00
parent 66386ab488
commit dfa1551ece
7 changed files with 127 additions and 6 deletions

View File

@ -39,6 +39,7 @@ class Block(Statement):
class FeatureFile(Block):
def __init__(self):
Block.__init__(self, location=None)
self.markClasses = {} # name --> ast.MarkClassDefinition
class FeatureBlock(Block):
@ -72,6 +73,13 @@ class GlyphClassDefinition(Statement):
self.glyphs = glyphs
class MarkClassDefinition(object):
def __init__(self, location, name):
self.location, self.name = location, name
self.anchors = {} # glyph --> ast.Anchor
self.glyphLocations = {} # glyph --> (filepath, line, column)
class AlternateSubstitution(Statement):
def __init__(self, location, glyph, from_class):
Statement.__init__(self, location)

View File

@ -26,11 +26,12 @@ class Builder(object):
self.cur_feature_name_ = None
self.lookups_ = []
self.features_ = {} # ('latn', 'DEU ', 'smcp') --> [LookupBuilder*]
self.parseTree = None
self.required_features_ = {} # ('latn', 'DEU ') --> 'scmp'
def build(self):
parsetree = Parser(self.featurefile_path).parse()
parsetree.build(self)
self.parseTree = Parser(self.featurefile_path).parse()
self.parseTree.build(self)
for tag in ('GPOS', 'GSUB'):
table = self.makeTable(tag)
if (table.ScriptList.ScriptCount > 0 or
@ -40,6 +41,11 @@ class Builder(object):
fontTable.table = table
elif tag in self.font:
del self.font[tag]
gdef = self.makeGDEF()
if gdef:
self.font["GDEF"] = gdef
elif "GDEF" in self.font:
del self.font["GDEF"]
def get_lookup_(self, location, builder_class):
if (self.cur_lookup_ and
@ -64,6 +70,32 @@ class Builder(object):
self.features_.setdefault(key, []).append(self.cur_lookup_)
return self.cur_lookup_
def makeGDEF(self):
gdef = otTables.GDEF()
gdef.Version = 1.0
gdef.GlyphClassDef = otTables.GlyphClassDef()
gdef.GlyphClassDef.classDefs = {}
glyphMarkClass = {} # glyph --> markClass
for markClass in self.parseTree.markClasses.values():
for glyph in markClass.anchors.keys():
if glyph in glyphMarkClass:
other = glyphMarkClass[glyph]
name1, name2 = sorted([markClass.name, other.name])
raise FeatureLibError(
'glyph %s cannot be both in markClass @%s and @%s' %
(glyph, name1, name2), markClass.location)
glyphMarkClass[glyph] = markClass
gdef.GlyphClassDef.classDefs[glyph] = 3
gdef.AttachList = None
gdef.LigCaretList = None
gdef.MarkAttachClassDef = None
if len(gdef.GlyphClassDef.classDefs) == 0:
return None
result = getTableClass("GDEF")()
result.table = gdef
return result
def makeTable(self, tag):
table = getattr(otTables, tag, None)()
table.Version = 1.0

View File

@ -75,7 +75,7 @@ class BuilderTest(unittest.TestCase):
def expect_ttx(self, font, expected_ttx):
path = self.temp_path(suffix=".ttx")
font.saveXML(path, quiet=True, tables=['GSUB', 'GPOS'])
font.saveXML(path, quiet=True, tables=['GDEF', 'GSUB', 'GPOS'])
actual = self.read_ttx(path)
expected = self.read_ttx(expected_ttx)
if actual != expected:
@ -218,6 +218,19 @@ class BuilderTest(unittest.TestCase):
"it must be the first of the languagesystem statements",
self.build, "languagesystem latn TRK; languagesystem DFLT dflt;")
def test_markClass(self):
font = makeTTFont()
addOpenTypeFeatures(self.getpath("markClass.fea"), font)
self.expect_ttx(font, self.getpath("markClass.ttx"))
def test_markClass_redefine(self):
self.assertRaisesRegex(
FeatureLibError,
"glyph C cannot be both in markClass @MARK1 and @MARK2",
self.build,
"markClass [A B C] <anchor 100 50> @MARK1;"
"markClass [C D E] <anchor 200 80> @MARK2;")
def test_script(self):
builder = Builder(None, makeTTFont())
builder.start_feature(location=None, name='test')

View File

@ -35,14 +35,17 @@ class Parser(object):
statements.append(self.parse_languagesystem_())
elif self.is_cur_keyword_("lookup"):
statements.append(self.parse_lookup_(vertical=False))
elif self.is_cur_keyword_("markClass"):
self.parse_markClass_()
elif self.is_cur_keyword_("feature"):
statements.append(self.parse_feature_block_())
elif self.is_cur_keyword_("valueRecordDef"):
statements.append(
self.parse_valuerecord_definition_(vertical=False))
else:
raise FeatureLibError("Expected feature, languagesystem, "
"lookup, or glyph class definition",
raise FeatureLibError(
"Expected feature, languagesystem, lookup, markClass, "
"or glyph class definition",
self.cur_token_location_)
return self.doc_
@ -244,6 +247,21 @@ class Parser(object):
self.lookups_.define(name, block)
return block
def parse_markClass_(self):
assert self.is_cur_keyword_("markClass")
location = self.cur_token_location_
glyphs = self.parse_glyphclass_(accept_glyphname=True)
anchor = self.parse_anchor_()
name = self.expect_glyphclass_()
self.expect_symbol_(";")
markClass = self.doc_.markClasses.get(name)
if markClass is None:
markClass = ast.MarkClassDefinition(location, name)
self.doc_.markClasses[name] = markClass
for glyph in glyphs:
markClass.anchors[glyph] = anchor
markClass.glyphLocations[glyph] = location
def is_next_glyphclass_(self):
return (self.next_token_ == "[" or
self.next_token_type_ in (Lexer.GLYPHCLASS, Lexer.NAME))
@ -531,6 +549,8 @@ class Parser(object):
statements.append(self.parse_language_())
elif self.is_cur_keyword_("lookup"):
statements.append(self.parse_lookup_(vertical))
elif self.is_cur_keyword_("markClass"):
self.parse_markClass_()
elif self.is_cur_keyword_({"pos", "position"}):
statements.append(
self.parse_position_(enumerated=False, vertical=vertical))
@ -566,6 +586,12 @@ class Parser(object):
return self.cur_token_ in k
return False
def expect_glyphclass_(self):
self.advance_lexer_()
if self.cur_token_type_ is not Lexer.GLYPHCLASS:
raise FeatureLibError("Expected @NAME", self.cur_token_location_)
return self.cur_token_
def expect_tag_(self):
self.advance_lexer_()
if self.cur_token_type_ is not Lexer.NAME:

View File

@ -442,6 +442,21 @@ class ParserTest(unittest.TestCase):
" enumerate position cursive A <anchor 12 -2> <anchor 2 3>;"
"} kern;")
def test_markClass(self):
doc = self.parse("markClass [acute grave] <anchor 350 3> @MARKS;"
"feature test {"
" markClass cedilla <anchor 400 -4> @MARKS;"
"} test;")
markClass = doc.markClasses["MARKS"]
self.assertEqual(set(markClass.anchors.keys()),
{"acute", "cedilla", "grave"})
acuteAnchor = markClass.anchors["acute"]
cedillaAnchor = markClass.anchors["cedilla"]
graveAnchor = markClass.anchors["grave"]
self.assertEqual((acuteAnchor.x, acuteAnchor.y), (350, 3))
self.assertEqual((cedillaAnchor.x, cedillaAnchor.y), (400, -4))
self.assertEqual((graveAnchor.x, graveAnchor.y), (350, 3))
def test_rsub_format_a(self):
doc = self.parse("feature test {rsub a [b B] c' d [e E] by C;} test;")
rsub = doc.statements[0].statements[0]

View File

@ -0,0 +1,12 @@
languagesystem DFLT dflt;
markClass [acute] <anchor 350 0> @TOP_MARKS;
feature foo {
markClass [acute grave] <anchor 350 0> @TOP_MARKS;
markClass cedilla <anchor 300 0> @BOTTOM_MARKS;
} foo;
feature bar {
markClass [dieresis breve] <anchor 400 0> @TOP_MARKS;
} bar;

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<ttFont>
<GDEF>
<Version value="1.0"/>
<GlyphClassDef>
<ClassDef glyph="acute" class="3"/>
<ClassDef glyph="breve" class="3"/>
<ClassDef glyph="cedilla" class="3"/>
<ClassDef glyph="dieresis" class="3"/>
<ClassDef glyph="grave" class="3"/>
</GlyphClassDef>
</GDEF>
</ttFont>