Merge pull request #2170 from simoncozens/delete-glyph
[feaLib] Allow deleting glyphs
This commit is contained in:
commit
45201f68a6
@ -188,6 +188,21 @@ class Comment(Element):
|
|||||||
return self.text
|
return self.text
|
||||||
|
|
||||||
|
|
||||||
|
class NullGlyph(Expression):
|
||||||
|
"""The NULL glyph, used in glyph deletion substitutions."""
|
||||||
|
|
||||||
|
def __init__(self, location=None):
|
||||||
|
Expression.__init__(self, location)
|
||||||
|
#: The name itself as a string
|
||||||
|
|
||||||
|
def glyphSet(self):
|
||||||
|
"""The glyphs in this class as a tuple of :class:`GlyphName` objects."""
|
||||||
|
return ()
|
||||||
|
|
||||||
|
def asFea(self, indent=""):
|
||||||
|
return "NULL"
|
||||||
|
|
||||||
|
|
||||||
class GlyphName(Expression):
|
class GlyphName(Expression):
|
||||||
"""A single glyph name, such as ``cedilla``."""
|
"""A single glyph name, such as ``cedilla``."""
|
||||||
|
|
||||||
@ -1246,8 +1261,9 @@ class MultipleSubstStatement(Statement):
|
|||||||
res += " " + " ".join(map(asFea, self.suffix))
|
res += " " + " ".join(map(asFea, self.suffix))
|
||||||
else:
|
else:
|
||||||
res += asFea(self.glyph)
|
res += asFea(self.glyph)
|
||||||
|
replacement = self.replacement or [ NullGlyph() ]
|
||||||
res += " by "
|
res += " by "
|
||||||
res += " ".join(map(asFea, self.replacement))
|
res += " ".join(map(asFea, replacement))
|
||||||
res += ";"
|
res += ";"
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
@ -314,10 +314,15 @@ class Parser(object):
|
|||||||
location,
|
location,
|
||||||
)
|
)
|
||||||
|
|
||||||
def parse_glyphclass_(self, accept_glyphname):
|
def parse_glyphclass_(self, accept_glyphname, accept_null=False):
|
||||||
# Parses a glyph class, either named or anonymous, or (if
|
# Parses a glyph class, either named or anonymous, or (if
|
||||||
# ``bool(accept_glyphname)``) a glyph name.
|
# ``bool(accept_glyphname)``) a glyph name. If ``bool(accept_null)`` then
|
||||||
|
# also accept the special NULL glyph.
|
||||||
if accept_glyphname and self.next_token_type_ in (Lexer.NAME, Lexer.CID):
|
if accept_glyphname and self.next_token_type_ in (Lexer.NAME, Lexer.CID):
|
||||||
|
if accept_null and self.next_token_ == "NULL":
|
||||||
|
# If you want a glyph called NULL, you should escape it.
|
||||||
|
self.advance_lexer_()
|
||||||
|
return self.ast.NullGlyph(location=self.cur_token_location_)
|
||||||
glyph = self.expect_glyph_()
|
glyph = self.expect_glyph_()
|
||||||
self.check_glyph_name_in_glyph_set(glyph)
|
self.check_glyph_name_in_glyph_set(glyph)
|
||||||
return self.ast.GlyphName(glyph, location=self.cur_token_location_)
|
return self.ast.GlyphName(glyph, location=self.cur_token_location_)
|
||||||
@ -375,7 +380,8 @@ class Parser(object):
|
|||||||
self.expect_symbol_("-")
|
self.expect_symbol_("-")
|
||||||
range_end = self.expect_cid_()
|
range_end = self.expect_cid_()
|
||||||
self.check_glyph_name_in_glyph_set(
|
self.check_glyph_name_in_glyph_set(
|
||||||
f"cid{range_start:05d}", f"cid{range_end:05d}",
|
f"cid{range_start:05d}",
|
||||||
|
f"cid{range_end:05d}",
|
||||||
)
|
)
|
||||||
glyphs.add_cid_range(
|
glyphs.add_cid_range(
|
||||||
range_start,
|
range_start,
|
||||||
@ -804,7 +810,7 @@ class Parser(object):
|
|||||||
if self.next_token_ == "by":
|
if self.next_token_ == "by":
|
||||||
keyword = self.expect_keyword_("by")
|
keyword = self.expect_keyword_("by")
|
||||||
while self.next_token_ != ";":
|
while self.next_token_ != ";":
|
||||||
gc = self.parse_glyphclass_(accept_glyphname=True)
|
gc = self.parse_glyphclass_(accept_glyphname=True, accept_null=True)
|
||||||
new.append(gc)
|
new.append(gc)
|
||||||
elif self.next_token_ == "from":
|
elif self.next_token_ == "from":
|
||||||
keyword = self.expect_keyword_("from")
|
keyword = self.expect_keyword_("from")
|
||||||
@ -837,6 +843,11 @@ class Parser(object):
|
|||||||
|
|
||||||
num_lookups = len([l for l in lookups if l is not None])
|
num_lookups = len([l for l in lookups if l is not None])
|
||||||
|
|
||||||
|
is_deletion = False
|
||||||
|
if len(new) == 1 and len(new[0].glyphSet()) == 0:
|
||||||
|
new = [] # Deletion
|
||||||
|
is_deletion = True
|
||||||
|
|
||||||
# GSUB lookup type 1: Single substitution.
|
# GSUB lookup type 1: Single substitution.
|
||||||
# Format A: "substitute a by a.sc;"
|
# Format A: "substitute a by a.sc;"
|
||||||
# Format B: "substitute [one.fitted one.oldstyle] by one;"
|
# Format B: "substitute [one.fitted one.oldstyle] by one;"
|
||||||
@ -863,8 +874,10 @@ class Parser(object):
|
|||||||
not reverse
|
not reverse
|
||||||
and len(old) == 1
|
and len(old) == 1
|
||||||
and len(old[0].glyphSet()) == 1
|
and len(old[0].glyphSet()) == 1
|
||||||
and len(new) > 1
|
and (
|
||||||
and max([len(n.glyphSet()) for n in new]) == 1
|
(len(new) > 1 and max([len(n.glyphSet()) for n in new]) == 1)
|
||||||
|
or len(new) == 0
|
||||||
|
)
|
||||||
and num_lookups == 0
|
and num_lookups == 0
|
||||||
):
|
):
|
||||||
return self.ast.MultipleSubstStatement(
|
return self.ast.MultipleSubstStatement(
|
||||||
@ -936,7 +949,7 @@ class Parser(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# If there are remaining glyphs to parse, this is an invalid GSUB statement
|
# If there are remaining glyphs to parse, this is an invalid GSUB statement
|
||||||
if len(new) != 0:
|
if len(new) != 0 or is_deletion:
|
||||||
raise FeatureLibError("Invalid substitution statement", location)
|
raise FeatureLibError("Invalid substitution statement", location)
|
||||||
|
|
||||||
# GSUB lookup type 6: Chaining contextual substitution.
|
# GSUB lookup type 6: Chaining contextual substitution.
|
||||||
|
@ -73,7 +73,7 @@ class BuilderTest(unittest.TestCase):
|
|||||||
LigatureSubtable AlternateSubtable MultipleSubstSubtable
|
LigatureSubtable AlternateSubtable MultipleSubstSubtable
|
||||||
SingleSubstSubtable aalt_chain_contextual_subst AlternateChained
|
SingleSubstSubtable aalt_chain_contextual_subst AlternateChained
|
||||||
MultipleLookupsPerGlyph MultipleLookupsPerGlyph2 GSUB_6_formats
|
MultipleLookupsPerGlyph MultipleLookupsPerGlyph2 GSUB_6_formats
|
||||||
GSUB_5_formats
|
GSUB_5_formats delete_glyph
|
||||||
""".split()
|
""".split()
|
||||||
|
|
||||||
def __init__(self, methodName):
|
def __init__(self, methodName):
|
||||||
|
3
Tests/feaLib/data/delete_glyph.fea
Normal file
3
Tests/feaLib/data/delete_glyph.fea
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
feature test {
|
||||||
|
sub a by NULL;
|
||||||
|
} test;
|
43
Tests/feaLib/data/delete_glyph.ttx
Normal file
43
Tests/feaLib/data/delete_glyph.ttx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ttFont>
|
||||||
|
|
||||||
|
<GSUB>
|
||||||
|
<Version value="0x00010000"/>
|
||||||
|
<ScriptList>
|
||||||
|
<!-- ScriptCount=1 -->
|
||||||
|
<ScriptRecord index="0">
|
||||||
|
<ScriptTag value="DFLT"/>
|
||||||
|
<Script>
|
||||||
|
<DefaultLangSys>
|
||||||
|
<ReqFeatureIndex value="65535"/>
|
||||||
|
<!-- FeatureCount=1 -->
|
||||||
|
<FeatureIndex index="0" value="0"/>
|
||||||
|
</DefaultLangSys>
|
||||||
|
<!-- LangSysCount=0 -->
|
||||||
|
</Script>
|
||||||
|
</ScriptRecord>
|
||||||
|
</ScriptList>
|
||||||
|
<FeatureList>
|
||||||
|
<!-- FeatureCount=1 -->
|
||||||
|
<FeatureRecord index="0">
|
||||||
|
<FeatureTag value="test"/>
|
||||||
|
<Feature>
|
||||||
|
<!-- LookupCount=1 -->
|
||||||
|
<LookupListIndex index="0" value="0"/>
|
||||||
|
</Feature>
|
||||||
|
</FeatureRecord>
|
||||||
|
</FeatureList>
|
||||||
|
<LookupList>
|
||||||
|
<!-- LookupCount=1 -->
|
||||||
|
<Lookup index="0">
|
||||||
|
<LookupType value="2"/>
|
||||||
|
<LookupFlag value="0"/>
|
||||||
|
<!-- SubTableCount=1 -->
|
||||||
|
<MultipleSubst index="0">
|
||||||
|
<Substitution in="a" out=""/>
|
||||||
|
</MultipleSubst>
|
||||||
|
</Lookup>
|
||||||
|
</LookupList>
|
||||||
|
</GSUB>
|
||||||
|
|
||||||
|
</ttFont>
|
Loading…
x
Reference in New Issue
Block a user