[feaLib] Implement markClass
statement
This commit is contained in:
parent
66386ab488
commit
dfa1551ece
@ -39,6 +39,7 @@ class Block(Statement):
|
|||||||
class FeatureFile(Block):
|
class FeatureFile(Block):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
Block.__init__(self, location=None)
|
Block.__init__(self, location=None)
|
||||||
|
self.markClasses = {} # name --> ast.MarkClassDefinition
|
||||||
|
|
||||||
|
|
||||||
class FeatureBlock(Block):
|
class FeatureBlock(Block):
|
||||||
@ -72,6 +73,13 @@ class GlyphClassDefinition(Statement):
|
|||||||
self.glyphs = glyphs
|
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):
|
class AlternateSubstitution(Statement):
|
||||||
def __init__(self, location, glyph, from_class):
|
def __init__(self, location, glyph, from_class):
|
||||||
Statement.__init__(self, location)
|
Statement.__init__(self, location)
|
||||||
|
@ -26,11 +26,12 @@ class Builder(object):
|
|||||||
self.cur_feature_name_ = None
|
self.cur_feature_name_ = None
|
||||||
self.lookups_ = []
|
self.lookups_ = []
|
||||||
self.features_ = {} # ('latn', 'DEU ', 'smcp') --> [LookupBuilder*]
|
self.features_ = {} # ('latn', 'DEU ', 'smcp') --> [LookupBuilder*]
|
||||||
|
self.parseTree = None
|
||||||
self.required_features_ = {} # ('latn', 'DEU ') --> 'scmp'
|
self.required_features_ = {} # ('latn', 'DEU ') --> 'scmp'
|
||||||
|
|
||||||
def build(self):
|
def build(self):
|
||||||
parsetree = Parser(self.featurefile_path).parse()
|
self.parseTree = Parser(self.featurefile_path).parse()
|
||||||
parsetree.build(self)
|
self.parseTree.build(self)
|
||||||
for tag in ('GPOS', 'GSUB'):
|
for tag in ('GPOS', 'GSUB'):
|
||||||
table = self.makeTable(tag)
|
table = self.makeTable(tag)
|
||||||
if (table.ScriptList.ScriptCount > 0 or
|
if (table.ScriptList.ScriptCount > 0 or
|
||||||
@ -40,6 +41,11 @@ class Builder(object):
|
|||||||
fontTable.table = table
|
fontTable.table = table
|
||||||
elif tag in self.font:
|
elif tag in self.font:
|
||||||
del self.font[tag]
|
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):
|
def get_lookup_(self, location, builder_class):
|
||||||
if (self.cur_lookup_ and
|
if (self.cur_lookup_ and
|
||||||
@ -64,6 +70,32 @@ class Builder(object):
|
|||||||
self.features_.setdefault(key, []).append(self.cur_lookup_)
|
self.features_.setdefault(key, []).append(self.cur_lookup_)
|
||||||
return 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):
|
def makeTable(self, tag):
|
||||||
table = getattr(otTables, tag, None)()
|
table = getattr(otTables, tag, None)()
|
||||||
table.Version = 1.0
|
table.Version = 1.0
|
||||||
|
@ -75,7 +75,7 @@ class BuilderTest(unittest.TestCase):
|
|||||||
|
|
||||||
def expect_ttx(self, font, expected_ttx):
|
def expect_ttx(self, font, expected_ttx):
|
||||||
path = self.temp_path(suffix=".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)
|
actual = self.read_ttx(path)
|
||||||
expected = self.read_ttx(expected_ttx)
|
expected = self.read_ttx(expected_ttx)
|
||||||
if actual != expected:
|
if actual != expected:
|
||||||
@ -218,6 +218,19 @@ class BuilderTest(unittest.TestCase):
|
|||||||
"it must be the first of the languagesystem statements",
|
"it must be the first of the languagesystem statements",
|
||||||
self.build, "languagesystem latn TRK; languagesystem DFLT dflt;")
|
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):
|
def test_script(self):
|
||||||
builder = Builder(None, makeTTFont())
|
builder = Builder(None, makeTTFont())
|
||||||
builder.start_feature(location=None, name='test')
|
builder.start_feature(location=None, name='test')
|
||||||
|
@ -35,14 +35,17 @@ class Parser(object):
|
|||||||
statements.append(self.parse_languagesystem_())
|
statements.append(self.parse_languagesystem_())
|
||||||
elif self.is_cur_keyword_("lookup"):
|
elif self.is_cur_keyword_("lookup"):
|
||||||
statements.append(self.parse_lookup_(vertical=False))
|
statements.append(self.parse_lookup_(vertical=False))
|
||||||
|
elif self.is_cur_keyword_("markClass"):
|
||||||
|
self.parse_markClass_()
|
||||||
elif self.is_cur_keyword_("feature"):
|
elif self.is_cur_keyword_("feature"):
|
||||||
statements.append(self.parse_feature_block_())
|
statements.append(self.parse_feature_block_())
|
||||||
elif self.is_cur_keyword_("valueRecordDef"):
|
elif self.is_cur_keyword_("valueRecordDef"):
|
||||||
statements.append(
|
statements.append(
|
||||||
self.parse_valuerecord_definition_(vertical=False))
|
self.parse_valuerecord_definition_(vertical=False))
|
||||||
else:
|
else:
|
||||||
raise FeatureLibError("Expected feature, languagesystem, "
|
raise FeatureLibError(
|
||||||
"lookup, or glyph class definition",
|
"Expected feature, languagesystem, lookup, markClass, "
|
||||||
|
"or glyph class definition",
|
||||||
self.cur_token_location_)
|
self.cur_token_location_)
|
||||||
return self.doc_
|
return self.doc_
|
||||||
|
|
||||||
@ -244,6 +247,21 @@ class Parser(object):
|
|||||||
self.lookups_.define(name, block)
|
self.lookups_.define(name, block)
|
||||||
return 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):
|
def is_next_glyphclass_(self):
|
||||||
return (self.next_token_ == "[" or
|
return (self.next_token_ == "[" or
|
||||||
self.next_token_type_ in (Lexer.GLYPHCLASS, Lexer.NAME))
|
self.next_token_type_ in (Lexer.GLYPHCLASS, Lexer.NAME))
|
||||||
@ -531,6 +549,8 @@ class Parser(object):
|
|||||||
statements.append(self.parse_language_())
|
statements.append(self.parse_language_())
|
||||||
elif self.is_cur_keyword_("lookup"):
|
elif self.is_cur_keyword_("lookup"):
|
||||||
statements.append(self.parse_lookup_(vertical))
|
statements.append(self.parse_lookup_(vertical))
|
||||||
|
elif self.is_cur_keyword_("markClass"):
|
||||||
|
self.parse_markClass_()
|
||||||
elif self.is_cur_keyword_({"pos", "position"}):
|
elif self.is_cur_keyword_({"pos", "position"}):
|
||||||
statements.append(
|
statements.append(
|
||||||
self.parse_position_(enumerated=False, vertical=vertical))
|
self.parse_position_(enumerated=False, vertical=vertical))
|
||||||
@ -566,6 +586,12 @@ class Parser(object):
|
|||||||
return self.cur_token_ in k
|
return self.cur_token_ in k
|
||||||
return False
|
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):
|
def expect_tag_(self):
|
||||||
self.advance_lexer_()
|
self.advance_lexer_()
|
||||||
if self.cur_token_type_ is not Lexer.NAME:
|
if self.cur_token_type_ is not Lexer.NAME:
|
||||||
|
@ -442,6 +442,21 @@ class ParserTest(unittest.TestCase):
|
|||||||
" enumerate position cursive A <anchor 12 -2> <anchor 2 3>;"
|
" enumerate position cursive A <anchor 12 -2> <anchor 2 3>;"
|
||||||
"} kern;")
|
"} 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):
|
def test_rsub_format_a(self):
|
||||||
doc = self.parse("feature test {rsub a [b B] c' d [e E] by C;} test;")
|
doc = self.parse("feature test {rsub a [b B] c' d [e E] by C;} test;")
|
||||||
rsub = doc.statements[0].statements[0]
|
rsub = doc.statements[0].statements[0]
|
||||||
|
12
Lib/fontTools/feaLib/testdata/markClass.fea
vendored
Normal file
12
Lib/fontTools/feaLib/testdata/markClass.fea
vendored
Normal 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;
|
15
Lib/fontTools/feaLib/testdata/markClass.ttx
vendored
Normal file
15
Lib/fontTools/feaLib/testdata/markClass.ttx
vendored
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user