[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:
Sascha Brawer 2016-01-25 12:13:40 +01:00
parent 2afa2b07cf
commit 82a444d659
4 changed files with 96 additions and 46 deletions

View File

@ -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):

View File

@ -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))

View File

@ -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_

View File

@ -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]