[feaLib] Parse glyph patterns with multiple values
For example, we now recognize `pos one' 1 two' 2 three' 3;` as a SinglePos statement, like makeotf does. https://github.com/behdad/fonttools/issues/485
This commit is contained in:
parent
2afa2b07cf
commit
82a444d659
@ -475,13 +475,15 @@ class ScriptStatement(Statement):
|
||||
|
||||
|
||||
class SinglePosStatement(Statement):
|
||||
def __init__(self, location, glyphs, valuerecord):
|
||||
def __init__(self, location, pos, prefix, suffix):
|
||||
Statement.__init__(self, location)
|
||||
self.glyphs, self.valuerecord = glyphs, valuerecord
|
||||
self.pos, self.prefix, self.suffix = pos, prefix, suffix
|
||||
|
||||
def build(self, builder):
|
||||
for glyph in self.glyphs.glyphSet():
|
||||
builder.add_single_pos(self.location, glyph, self.valuerecord)
|
||||
pos = {}
|
||||
for glyphs, value in self.pos:
|
||||
pos.update({glyph: value for glyph in glyphs.glyphSet()})
|
||||
builder.add_single_pos(self.location, self.prefix, self.suffix, pos)
|
||||
|
||||
|
||||
class SubtableStatement(Statement):
|
||||
|
@ -620,9 +620,14 @@ class Builder(object):
|
||||
lookup = self.get_lookup_(location, SpecificPairPosBuilder)
|
||||
lookup.add_pair(location, glyph1, value1, glyph2, value2)
|
||||
|
||||
def add_single_pos(self, location, glyph, valuerecord):
|
||||
def add_single_pos(self, location, prefix, suffix, mapping):
|
||||
if prefix or suffix:
|
||||
# TODO: https://github.com/behdad/fonttools/issues/485
|
||||
raise FeatureLibError("Contextual SinglePos not yet implemented",
|
||||
self.location)
|
||||
lookup = self.get_lookup_(location, SinglePosBuilder)
|
||||
lookup.add_pos(location, glyph, valuerecord)
|
||||
for glyph, value in mapping.items():
|
||||
lookup.add_pos(location, glyph, value)
|
||||
|
||||
def setGlyphClass_(self, location, glyph, glyphClass):
|
||||
oldClass, oldLocation = self.glyphClassDefs_.get(glyph, (None, None))
|
||||
|
@ -243,10 +243,9 @@ class Parser(object):
|
||||
else:
|
||||
return ast.GlyphClassName(self.cur_token_location_, gc)
|
||||
|
||||
def parse_glyph_pattern_(self):
|
||||
prefix, glyphs, lookups, suffix = ([], [], [], [])
|
||||
while (self.next_token_ not in {"by", "from", ";", "<"} and
|
||||
self.next_token_type_ != Lexer.NUMBER):
|
||||
def parse_glyph_pattern_(self, vertical):
|
||||
prefix, glyphs, lookups, values, suffix = ([], [], [], [], [])
|
||||
while self.next_token_ not in {"by", "from", ";"}:
|
||||
gc = self.parse_glyphclass_(accept_glyphname=True)
|
||||
marked = False
|
||||
if self.next_token_ == "'":
|
||||
@ -259,6 +258,11 @@ class Parser(object):
|
||||
else:
|
||||
prefix.append(gc)
|
||||
|
||||
if self.is_next_value_():
|
||||
values.append(self.parse_valuerecord_(vertical))
|
||||
else:
|
||||
values.append(None)
|
||||
|
||||
lookup = None
|
||||
if self.next_token_ == "lookup":
|
||||
self.expect_keyword_("lookup")
|
||||
@ -277,17 +281,22 @@ class Parser(object):
|
||||
|
||||
if not glyphs and not suffix: # eg., "sub f f i by"
|
||||
assert lookups == []
|
||||
return ([], prefix, [None] * len(prefix), [])
|
||||
return ([], prefix, [None] * len(prefix), values, [])
|
||||
else:
|
||||
return (prefix, glyphs, lookups, suffix)
|
||||
return (prefix, glyphs, lookups, values, suffix)
|
||||
|
||||
def parse_ignore_(self):
|
||||
assert self.is_cur_keyword_("ignore")
|
||||
location = self.cur_token_location_
|
||||
self.advance_lexer_()
|
||||
if self.cur_token_ in ["substitute", "sub"]:
|
||||
prefix, glyphs, lookups, suffix = self.parse_glyph_pattern_()
|
||||
prefix, glyphs, lookups, values, suffix = \
|
||||
self.parse_glyph_pattern_(vertical=False)
|
||||
self.expect_symbol_(";")
|
||||
if any(lookups):
|
||||
raise FeatureLibError(
|
||||
"No lookups can be specified for \"ignore sub\"",
|
||||
location)
|
||||
return ast.IgnoreSubstitutionRule(location, prefix, glyphs, suffix)
|
||||
raise FeatureLibError(
|
||||
"Expected \"substitute\"", self.next_token_location_)
|
||||
@ -415,35 +424,33 @@ class Parser(object):
|
||||
return self.parse_position_mark_(enumerated, vertical)
|
||||
|
||||
location = self.cur_token_location_
|
||||
prefix, glyphs, lookups, suffix = self.parse_glyph_pattern_()
|
||||
gc2, value2 = None, None
|
||||
if not prefix and len(glyphs) == 2 and not suffix and not any(lookups):
|
||||
# Pair positioning, format B: 'pos' glyphs gc2 value1
|
||||
gc2 = glyphs[1]
|
||||
glyphs = [glyphs[0]]
|
||||
prefix, glyphs, lookups, values, suffix = \
|
||||
self.parse_glyph_pattern_(vertical)
|
||||
self.expect_symbol_(";")
|
||||
|
||||
if prefix or len(glyphs) > 1 or suffix or any(lookups):
|
||||
# GPOS type 8: Chaining contextual positioning
|
||||
self.expect_symbol_(";")
|
||||
if any(lookups):
|
||||
# GPOS type 8: Chaining contextual positioning; explicit lookups
|
||||
if any(values):
|
||||
raise FeatureLibError(
|
||||
"If \"lookup\" is present, no values must be specified",
|
||||
location)
|
||||
return ast.ChainContextPosStatement(
|
||||
location, prefix, glyphs, suffix, lookups)
|
||||
|
||||
value1 = self.parse_valuerecord_(vertical)
|
||||
if self.next_token_ != ";" and gc2 is None:
|
||||
# Pair positioning, format A: 'pos' gc1 value1 gc2 value2
|
||||
gc2 = self.parse_glyphclass_(accept_glyphname=True)
|
||||
value2 = self.parse_valuerecord_(vertical)
|
||||
self.expect_symbol_(";")
|
||||
# Pair positioning, format A: "pos V 10 A -10;"
|
||||
# Pair positioning, format B: "pos V A -20;"
|
||||
if not prefix and not suffix and len(glyphs) == 2:
|
||||
if values[0] is None: # Format B: "pos V A -20;"
|
||||
values.reverse()
|
||||
return ast.PairPosStatement(
|
||||
location, enumerated,
|
||||
glyphs[0], values[0], glyphs[1], values[1])
|
||||
|
||||
if gc2 is None:
|
||||
if enumerated:
|
||||
raise FeatureLibError(
|
||||
'"enumerate" is only allowed with pair positionings',
|
||||
self.cur_token_location_)
|
||||
return ast.SinglePosStatement(location, glyphs[0], value1)
|
||||
else:
|
||||
return ast.PairPosStatement(location, enumerated,
|
||||
glyphs[0], value1, gc2, value2)
|
||||
if enumerated:
|
||||
raise FeatureLibError(
|
||||
'"enumerate" is only allowed with pair positionings', location)
|
||||
return ast.SinglePosStatement(location, zip(glyphs, values),
|
||||
prefix, suffix)
|
||||
|
||||
def parse_position_cursive_(self, enumerated, vertical):
|
||||
location = self.cur_token_location_
|
||||
@ -512,8 +519,11 @@ class Parser(object):
|
||||
assert self.cur_token_ in {"substitute", "sub", "reversesub", "rsub"}
|
||||
location = self.cur_token_location_
|
||||
reverse = self.cur_token_ in {"reversesub", "rsub"}
|
||||
old_prefix, old, lookups, old_suffix = self.parse_glyph_pattern_()
|
||||
|
||||
old_prefix, old, lookups, values, old_suffix = \
|
||||
self.parse_glyph_pattern_(vertical=False)
|
||||
if any(values):
|
||||
raise FeatureLibError(
|
||||
"Substitution statements cannot contain values", location)
|
||||
new = []
|
||||
if self.next_token_ == "by":
|
||||
keyword = self.expect_keyword_("by")
|
||||
@ -696,6 +706,9 @@ class Parser(object):
|
||||
self.expect_symbol_(">")
|
||||
return result
|
||||
|
||||
def is_next_value_(self):
|
||||
return self.next_token_type_ is Lexer.NUMBER or self.next_token_ == "<"
|
||||
|
||||
def parse_valuerecord_(self, vertical):
|
||||
if self.next_token_type_ is Lexer.NUMBER:
|
||||
number, location = self.expect_number_(), self.cur_token_location_
|
||||
|
@ -485,23 +485,37 @@ class ParserTest(unittest.TestCase):
|
||||
doc = self.parse("feature kern {pos one <1 2 3 4>;} kern;")
|
||||
pos = doc.statements[0].statements[0]
|
||||
self.assertIsInstance(pos, ast.SinglePosStatement)
|
||||
self.assertEqual(glyphstr([pos.glyphs]), "one")
|
||||
self.assertEqual(pos.valuerecord.makeString(vertical=False),
|
||||
"<1 2 3 4>")
|
||||
[(glyphs, value)] = pos.pos
|
||||
self.assertEqual(glyphstr([glyphs]), "one")
|
||||
self.assertEqual(value.makeString(vertical=False), "<1 2 3 4>")
|
||||
|
||||
def test_gpos_type_1_glyphclass_horizontal(self):
|
||||
doc = self.parse("feature kern {pos [one two] -300;} kern;")
|
||||
pos = doc.statements[0].statements[0]
|
||||
self.assertIsInstance(pos, ast.SinglePosStatement)
|
||||
self.assertEqual(glyphstr([pos.glyphs]), "[one two]")
|
||||
self.assertEqual(pos.valuerecord.makeString(vertical=False), "-300")
|
||||
[(glyphs, value)] = pos.pos
|
||||
self.assertEqual(glyphstr([glyphs]), "[one two]")
|
||||
self.assertEqual(value.makeString(vertical=False), "-300")
|
||||
|
||||
def test_gpos_type_1_glyphclass_vertical(self):
|
||||
doc = self.parse("feature vkrn {pos [one two] -300;} vkrn;")
|
||||
pos = doc.statements[0].statements[0]
|
||||
self.assertIsInstance(pos, ast.SinglePosStatement)
|
||||
self.assertEqual(glyphstr([pos.glyphs]), "[one two]")
|
||||
self.assertEqual(pos.valuerecord.makeString(vertical=True), "-300")
|
||||
[(glyphs, value)] = pos.pos
|
||||
self.assertEqual(glyphstr([glyphs]), "[one two]")
|
||||
self.assertEqual(value.makeString(vertical=True), "-300")
|
||||
|
||||
def test_gpos_type_1_multiple(self):
|
||||
doc = self.parse("feature f {pos one'1 two'2 [five six]'56;} f;")
|
||||
pos = doc.statements[0].statements[0]
|
||||
self.assertIsInstance(pos, ast.SinglePosStatement)
|
||||
[(glyphs1, val1), (glyphs2, val2), (glyphs3, val3)] = pos.pos
|
||||
self.assertEqual(glyphstr([glyphs1]), "one")
|
||||
self.assertEqual(val1.makeString(vertical=False), "1")
|
||||
self.assertEqual(glyphstr([glyphs2]), "two")
|
||||
self.assertEqual(val2.makeString(vertical=False), "2")
|
||||
self.assertEqual(glyphstr([glyphs3]), "[five six]")
|
||||
self.assertEqual(val3.makeString(vertical=False), "56")
|
||||
|
||||
def test_gpos_type_1_enumerated(self):
|
||||
self.assertRaisesRegex(
|
||||
@ -724,6 +738,16 @@ class ParserTest(unittest.TestCase):
|
||||
self.assertEqual(glyphstr(pos.suffix), "[Y y] [Z z]")
|
||||
self.assertEqual(pos.lookups, [lookup1, lookup2, None])
|
||||
|
||||
def test_gpos_type_8_lookup_with_values(self):
|
||||
self.assertRaisesRegex(
|
||||
FeatureLibError,
|
||||
'If "lookup" is present, no values must be specified',
|
||||
self.parse,
|
||||
"lookup L1 {pos one 100;} L1;"
|
||||
"feature test {"
|
||||
" pos A' lookup L1 B' 20;"
|
||||
"} test;")
|
||||
|
||||
def test_markClass(self):
|
||||
doc = self.parse("markClass [acute grave] <anchor 350 3> @MARKS;")
|
||||
mc = doc.statements[0]
|
||||
@ -902,6 +926,12 @@ class ParserTest(unittest.TestCase):
|
||||
'but found a glyph class with 26 elements',
|
||||
self.parse, "feature smcp {sub [a-d] by [A.sc-Z.sc];} smcp;")
|
||||
|
||||
def test_sub_with_values(self):
|
||||
self.assertRaisesRegex(
|
||||
FeatureLibError,
|
||||
"Substitution statements cannot contain values",
|
||||
self.parse, "feature smcp {sub A' 20 by A.sc;} smcp;")
|
||||
|
||||
def test_substitute_multiple(self): # GSUB LookupType 2
|
||||
doc = self.parse("lookup Look {substitute f_f_i by f f i;} Look;")
|
||||
sub = doc.statements[0].statements[0]
|
||||
|
Loading…
x
Reference in New Issue
Block a user