From bc8279bab1b0a1d4b4c7ec9d5dcbc0aace1eb0e9 Mon Sep 17 00:00:00 2001 From: Sascha Brawer Date: Wed, 5 Aug 2015 10:41:04 +0200 Subject: [PATCH] [feaLib] Parse `ignore sub` and `ignore substitute` statements --- Lib/fontTools/feaLib/ast.py | 7 +++- Lib/fontTools/feaLib/parser.py | 53 +++++++++++++++++++++-------- Lib/fontTools/feaLib/parser_test.py | 25 ++++++++++++++ 3 files changed, 70 insertions(+), 15 deletions(-) diff --git a/Lib/fontTools/feaLib/ast.py b/Lib/fontTools/feaLib/ast.py index 2fccd9864..7dd204bbc 100644 --- a/Lib/fontTools/feaLib/ast.py +++ b/Lib/fontTools/feaLib/ast.py @@ -50,10 +50,15 @@ class LanguageSystemStatement(object): (self.script.strip(), self.language.strip(), linesep)) +class IgnoreSubstitutionRule(object): + def __init__(self, location, prefix, glyphs, suffix): + self.location = location + self.prefix, self.glyphs, self.suffix = (prefix, glyphs, suffix) + + class SubstitutionRule(object): def __init__(self, location, old, new): self.location, self.old, self.new = (location, old, new) - self.ignored = False self.old_prefix = [] self.old_suffix = [] diff --git a/Lib/fontTools/feaLib/parser.py b/Lib/fontTools/feaLib/parser.py index 99a4440da..20f234bf8 100644 --- a/Lib/fontTools/feaLib/parser.py +++ b/Lib/fontTools/feaLib/parser.py @@ -105,25 +105,40 @@ class Parser(object): self.expect_symbol_("]") return result - def parse_substitute_(self): - assert self.is_cur_keyword_("substitute") or self.is_cur_keyword("sub") - location = self.cur_token_location_ - old_prefix, old, old_suffix = ([], [], []) - while self.next_token_ != "by": + def parse_glyph_pattern_(self): + prefix, glyphs, suffix = ([], [], []) + while self.next_token_ not in {"by", ";"}: gc = self.parse_glyphclass_(accept_glyphname=True) marked = False if self.next_token_ == "'": self.expect_symbol_("'") + marked = True if marked: - old.append(gc) - elif old: - old_suffix.append(gc) + glyphs.append(gc) + elif glyphs: + suffix.append(gc) else: - old_prefix.append(gc) - if not old and not old_suffix: # eg., "sub f f i by" - old = old_prefix - old_prefix = [] - assert self.expect_name_() == "by" + prefix.append(gc) + if not glyphs and not suffix: # eg., "sub f f i by" + return ([], prefix, []) + else: + return (prefix, glyphs, 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, suffix = self.parse_glyph_pattern_() + self.expect_symbol_(";") + return ast.IgnoreSubstitutionRule(location, prefix, glyphs, suffix) + raise ParserError("Expected \"substitute\"", self.next_token_location_) + + def parse_substitute_(self): + assert self.cur_token_ in {"substitute", "sub"} + location = self.cur_token_location_ + old_prefix, old, old_suffix = self.parse_glyph_pattern_() + self.expect_keyword_("by") new = self.parse_glyphclass_(accept_glyphname=True) self.expect_symbol_(";") rule = ast.SubstitutionRule(location, old, [new]) @@ -193,6 +208,8 @@ class Parser(object): self.advance_lexer_() if self.cur_token_type_ is Lexer.GLYPHCLASS: statements.append(self.parse_glyphclass_definition_()) + elif self.is_cur_keyword_("ignore"): + statements.append(self.parse_ignore_()) elif (self.is_cur_keyword_("substitute") or self.is_cur_keyword_("sub")): statements.append(self.parse_substitute_()) @@ -200,7 +217,8 @@ class Parser(object): statements.append(self.parse_valuerecord_definition_(vertical)) else: raise ParserError( - "Expected glyph class definition or valueRecordDef", + "Expected glyph class definition, substitute, " + "or valueRecordDef", self.cur_token_location_) self.expect_symbol_("}") @@ -231,6 +249,13 @@ class Parser(object): return symbol raise ParserError("Expected '%s'" % symbol, self.cur_token_location_) + def expect_keyword_(self, keyword): + self.advance_lexer_() + if self.cur_token_type_ is Lexer.NAME and self.cur_token_ == keyword: + return self.cur_token_ + raise ParserError("Expected \"%s\"" % keyword, + self.cur_token_location_) + def expect_name_(self): self.advance_lexer_() if self.cur_token_type_ is Lexer.NAME: diff --git a/Lib/fontTools/feaLib/parser_test.py b/Lib/fontTools/feaLib/parser_test.py index a9e7ca85b..819f77faf 100644 --- a/Lib/fontTools/feaLib/parser_test.py +++ b/Lib/fontTools/feaLib/parser_test.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals from fontTools.feaLib.lexer import LexerError from fontTools.feaLib.parser import Parser, ParserError, SymbolTable from fontTools.misc.py23 import * +import fontTools.feaLib.ast as ast import codecs import os import shutil @@ -109,6 +110,25 @@ class ParserTest(unittest.TestCase): self.assertEqual(liga.statements[0].glyphs, {"a", "b", "l"}) self.assertEqual(smcp.statements[0].glyphs, {"a", "b", "s"}) + def test_ignore_sub(self): + doc = self.parse("feature test {ignore sub e t' c;} test;") + s = doc.statements[0].statements[0] + self.assertEqual(type(s), ast.IgnoreSubstitutionRule) + self.assertEqual(s.prefix, [{"e"}]) + self.assertEqual(s.glyphs, [{"t"}]) + self.assertEqual(s.suffix, [{"c"}]) + + def test_ignore_substitute(self): + doc = self.parse( + "feature test {" + " ignore substitute f [a e] d' [a u]' [e y];" + "} test;") + s = doc.statements[0].statements[0] + self.assertEqual(type(s), ast.IgnoreSubstitutionRule) + self.assertEqual(s.prefix, [{"f"}, {"a", "e"}]) + self.assertEqual(s.glyphs, [{"d"}, {"a", "u"}]) + self.assertEqual(s.suffix, [{"e", "y"}]) + def test_substitute_single_format_a(self): # GSUB LookupType 1 doc = self.parse("feature smcp {substitute a by a.sc;} smcp;") sub = doc.statements[0].statements[0] @@ -147,6 +167,11 @@ class ParserTest(unittest.TestCase): self.assertEqual(sub.old_suffix, []) self.assertEqual(sub.new, [{"f_f_i"}]) + def test_substitute_missing_by(self): + self.assertRaisesRegex( + ParserError, "Expected \"by\"", + self.parse, "feature liga {substitute f f i;} liga;") + def test_valuerecord_format_a_horizontal(self): doc = self.parse("feature liga {valueRecordDef 123 foo;} liga;") value = doc.statements[0].statements[0].value