Merge pull request #350 from brawer/feaLib

[feaLib] Implement GSUB lookup types 1, 3 and 4
This commit is contained in:
Sascha Brawer 2015-09-09 15:14:01 +02:00
commit 91197a58a7
19 changed files with 1637 additions and 186 deletions

View File

@ -1,98 +1,179 @@
from __future__ import print_function, division, absolute_import
from __future__ import unicode_literals
import itertools
class FeatureFile(object):
class Statement(object):
def __init__(self, location):
self.location = location
def build(self, builder):
pass
class Block(Statement):
def __init__(self, location):
Statement.__init__(self, location)
self.statements = []
def build(self, builder):
for s in self.statements:
s.build(builder)
class FeatureFile(Block):
def __init__(self):
self.statements = []
Block.__init__(self, location=None)
class FeatureBlock(object):
class FeatureBlock(Block):
def __init__(self, location, name, use_extension):
self.location = location
Block.__init__(self, location)
self.name, self.use_extension = name, use_extension
self.statements = []
def build(self, builder):
# TODO(sascha): Handle use_extension.
builder.start_feature(self.location, self.name)
Block.build(self, builder)
builder.end_feature()
class LookupBlock(object):
class LookupBlock(Block):
def __init__(self, location, name, use_extension):
self.location = location
Block.__init__(self, location)
self.name, self.use_extension = name, use_extension
self.statements = []
def build(self, builder):
# TODO(sascha): Handle use_extension.
builder.start_lookup_block(self.location, self.name)
Block.build(self, builder)
builder.end_lookup_block()
class GlyphClassDefinition(object):
class GlyphClassDefinition(Statement):
def __init__(self, location, name, glyphs):
self.location = location
Statement.__init__(self, location)
self.name = name
self.glyphs = glyphs
class AlternateSubstitution(object):
class AlternateSubstitution(Statement):
def __init__(self, location, glyph, from_class):
self.location = location
Statement.__init__(self, location)
self.glyph, self.from_class = (glyph, from_class)
def build(self, builder):
builder.add_alternate_substitution(self.location, self.glyph,
self.from_class)
class AnchorDefinition(object):
class AnchorDefinition(Statement):
def __init__(self, location, name, x, y, contourpoint):
self.location = location
Statement.__init__(self, location)
self.name, self.x, self.y, self.contourpoint = name, x, y, contourpoint
class LanguageStatement(object):
class LanguageStatement(Statement):
def __init__(self, location, language, include_default, required):
self.location = location
Statement.__init__(self, location)
assert(len(language) == 4)
self.language = language
self.include_default = include_default
self.required = required
def build(self, builder):
builder.set_language(location=self.location, language=self.language,
include_default=self.include_default,
required=self.required)
class LanguageSystemStatement(object):
class LanguageSystemStatement(Statement):
def __init__(self, location, script, language):
self.location = location
Statement.__init__(self, location)
self.script, self.language = (script, language)
def build(self, builder):
builder.add_language_system(self.location, self.script, self.language)
class IgnoreSubstitutionRule(object):
class IgnoreSubstitutionRule(Statement):
def __init__(self, location, prefix, glyphs, suffix):
self.location = location
Statement.__init__(self, location)
self.prefix, self.glyphs, self.suffix = (prefix, glyphs, suffix)
class LookupReferenceStatement(object):
class LigatureSubstitution(Statement):
def __init__(self, location, glyphs, replacement):
Statement.__init__(self, location)
self.glyphs, self.replacement = (glyphs, replacement)
def build(self, builder):
# OpenType feature file syntax, section 5.d, "Ligature substitution":
# "Since the OpenType specification does not allow ligature
# substitutions to be specified on target sequences that contain
# glyph classes, the implementation software will enumerate
# all specific glyph sequences if glyph classes are detected"
for glyphs in sorted(itertools.product(*self.glyphs)):
builder.add_ligature_substitution(
self.location, glyphs, self.replacement)
class LookupReferenceStatement(Statement):
def __init__(self, location, lookup):
Statement.__init__(self, location)
self.location, self.lookup = (location, lookup)
class ScriptStatement(object):
class MultipleSubstitution(Statement):
def __init__(self, location, glyph, replacement):
Statement.__init__(self, location)
self.glyph, self.replacement = glyph, replacement
def build(self, builder):
builder.add_multiple_substitution(self.location, glyph, replacement)
class SingleSubstitution(Statement):
def __init__(self, location, mapping):
Statement.__init__(self, location)
self.mapping = mapping
def build(self, builder):
builder.add_single_substitution(self.location, self.mapping)
class ScriptStatement(Statement):
def __init__(self, location, script):
self.location = location
Statement.__init__(self, location)
self.script = script
def build(self, builder):
builder.set_script(self.location, self.script)
class SubtableStatement(object):
class SubtableStatement(Statement):
def __init__(self, location):
self.location = location
Statement.__init__(self, location)
class SubstitutionRule(object):
class SubstitutionRule(Statement):
def __init__(self, location, old, new):
self.location, self.old, self.new = (location, old, new)
Statement.__init__(self, location)
self.old, self.new = (old, new)
self.old_prefix = []
self.old_suffix = []
self.lookups = [None] * len(old)
class ValueRecord(object):
class ValueRecord(Statement):
def __init__(self, location, xPlacement, yPlacement, xAdvance, yAdvance):
self.location = location
Statement.__init__(self, location)
self.xPlacement, self.yPlacement = (xPlacement, yPlacement)
self.xAdvance, self.yAdvance = (xAdvance, yAdvance)
class ValueRecordDefinition(object):
class ValueRecordDefinition(Statement):
def __init__(self, location, name, value):
self.location = location
Statement.__init__(self, location)
self.name = name
self.value = value

View File

@ -0,0 +1,353 @@
from __future__ import print_function, division, absolute_import
from __future__ import unicode_literals
from fontTools.feaLib.error import FeatureLibError
from fontTools.feaLib.parser import Parser
from fontTools.ttLib.tables import otTables
import warnings
def addOpenTypeFeatures(featurefile_path, font):
builder = Builder(featurefile_path, font)
builder.build()
class Builder(object):
def __init__(self, featurefile_path, font):
self.featurefile_path = featurefile_path
self.font = font
self.default_language_systems_ = set()
self.script_ = None
self.lookup_flag_ = 0
self.language_systems = set()
self.named_lookups_ = {}
self.cur_lookup_ = None
self.cur_lookup_name_ = None
self.cur_feature_name_ = None
self.lookups_ = []
self.features_ = {} # ('latn', 'DEU ', 'smcp') --> [LookupBuilder*]
self.required_features_ = {} # ('latn', 'DEU ') --> 'scmp'
def build(self):
parsetree = Parser(self.featurefile_path).parse()
parsetree.build(self)
self.gpos = self.font['GPOS'] = self.makeTable('GPOS')
self.gsub = self.font['GSUB'] = self.makeTable('GSUB')
def get_lookup_(self, location, builder_class):
if (self.cur_lookup_ and
type(self.cur_lookup_) == builder_class and
self.cur_lookup_.lookup_flag == self.lookup_flag_):
return self.cur_lookup_
if self.cur_lookup_name_ and self.cur_lookup_:
raise FeatureLibError(
"Within a named lookup block, all rules must be of "
"the same lookup type and flag", location)
self.cur_lookup_ = builder_class(location, self.lookup_flag_)
self.lookups_.append(self.cur_lookup_)
if self.cur_lookup_name_:
# We are starting a lookup rule inside a named lookup block.
self.named_lookups_[self.cur_lookup_name_] = self.cur_lookup_
else:
# We are starting a lookup rule inside a feature.
for script, lang in self.language_systems:
key = (script, lang, self.cur_feature_name_)
self.features_.setdefault(key, []).append(self.cur_lookup_)
return self.cur_lookup_
def makeTable(self, tag):
table = getattr(otTables, tag, None)()
table.Version = 1.0
table.ScriptList = otTables.ScriptList()
table.ScriptList.ScriptRecord = []
table.FeatureList = otTables.FeatureList()
table.FeatureList.FeatureRecord = []
table.LookupList = otTables.LookupList()
table.LookupList.Lookup = []
for lookup in self.lookups_:
lookup.lookup_index = None
for i, lookup_builder in enumerate(self.lookups_):
if lookup_builder.table != tag:
continue
# If multiple lookup builders would build equivalent lookups,
# emit them only once. This is quadratic in the number of lookups,
# but the checks are cheap. If performance ever becomes an issue,
# we could hash the lookup content and only compare those with
# the same hash value.
equivalent_builder = None
for other_builder in self.lookups_[:i]:
if lookup_builder.equals(other_builder):
equivalent_builder = other_builder
if equivalent_builder is not None:
lookup_builder.lookup_index = equivalent_builder.lookup_index
continue
lookup_builder.lookup_index = len(table.LookupList.Lookup)
table.LookupList.Lookup.append(lookup_builder.build())
# Build a table for mapping (tag, lookup_indices) to feature_index.
# For example, ('liga', (2,3,7)) --> 23.
feature_indices = {}
required_feature_indices = {} # ('latn', 'DEU') --> 23
scripts = {} # 'latn' --> {'DEU': [23, 24]} for feature #23,24
for key, lookups in sorted(self.features_.items()):
script, lang, feature_tag = key
# l.lookup_index will be None when a lookup is not needed
# for the table under construction. For example, substitution
# rules will have no lookup_index while building GPOS tables.
lookup_indices = tuple([l.lookup_index for l in lookups
if l.lookup_index is not None])
if len(lookup_indices) == 0:
continue
feature_key = (feature_tag, lookup_indices)
feature_index = feature_indices.get(feature_key)
if feature_index is None:
feature_index = len(table.FeatureList.FeatureRecord)
frec = otTables.FeatureRecord()
frec.FeatureTag = feature_tag
frec.Feature = otTables.Feature()
frec.Feature.FeatureParams = None
frec.Feature.LookupListIndex = lookup_indices
frec.Feature.LookupCount = len(lookup_indices)
table.FeatureList.FeatureRecord.append(frec)
feature_indices[feature_key] = feature_index
scripts.setdefault(script, {}).setdefault(lang, []).append(
feature_index)
if self.required_features_.get((script, lang)) == feature_tag:
required_feature_indices[(script, lang)] = feature_index
# Build ScriptList.
for script, lang_features in sorted(scripts.items()):
srec = otTables.ScriptRecord()
srec.ScriptTag = script
srec.Script = otTables.Script()
srec.Script.DefaultLangSys = None
srec.Script.LangSysRecord = []
for lang, feature_indices in sorted(lang_features.items()):
langrec = otTables.LangSysRecord()
langrec.LangSys = otTables.LangSys()
langrec.LangSys.LookupOrder = None
req_feature_index = \
required_feature_indices.get((script, lang))
if req_feature_index is None:
langrec.LangSys.ReqFeatureIndex = 0xFFFF
else:
langrec.LangSys.ReqFeatureIndex = req_feature_index
langrec.LangSys.FeatureIndex = [i for i in feature_indices
if i != req_feature_index]
langrec.LangSys.FeatureCount = \
len(langrec.LangSys.FeatureIndex)
if lang == "dflt":
srec.Script.DefaultLangSys = langrec.LangSys
else:
langrec.LangSysTag = lang
srec.Script.LangSysRecord.append(langrec)
srec.Script.LangSysCount = len(srec.Script.LangSysRecord)
table.ScriptList.ScriptRecord.append(srec)
table.ScriptList.ScriptCount = len(table.ScriptList.ScriptRecord)
table.FeatureList.FeatureCount = len(table.FeatureList.FeatureRecord)
table.LookupList.LookupCount = len(table.LookupList.Lookup)
return table
def add_language_system(self, location, script, language):
# OpenType Feature File Specification, section 4.b.i
if (script == "DFLT" and language == "dflt" and
self.default_language_systems_):
raise FeatureLibError(
'If "languagesystem DFLT dflt" is present, it must be '
'the first of the languagesystem statements', location)
if (script, language) in self.default_language_systems_:
raise FeatureLibError(
'"languagesystem %s %s" has already been specified' %
(script.strip(), language.strip()), location)
self.default_language_systems_.add((script, language))
def get_default_language_systems_(self):
# OpenType Feature File specification, 4.b.i. languagesystem:
# If no "languagesystem" statement is present, then the
# implementation must behave exactly as though the following
# statement were present at the beginning of the feature file:
# languagesystem DFLT dflt;
if self.default_language_systems_:
return frozenset(self.default_language_systems_)
else:
return frozenset({('DFLT', 'dflt')})
def start_feature(self, location, name):
self.language_systems = self.get_default_language_systems_()
self.cur_lookup_ = None
self.cur_feature_name_ = name
def end_feature(self):
assert self.cur_feature_name_ is not None
self.cur_feature_name_ = None
self.language_systems = None
self.cur_lookup_ = None
def start_lookup_block(self, location, name):
if name in self.named_lookups_:
raise FeatureLibError(
'Lookup "%s" has already been defined' % name, location)
self.cur_lookup_name_ = name
self.named_lookups_[name] = None
self.cur_lookup_ = None
def end_lookup_block(self):
assert self.cur_lookup_name_ is not None
self.cur_lookup_name_ = None
self.cur_lookup_ = None
def set_language(self, location, language, include_default, required):
assert(len(language) == 4)
if self.cur_lookup_name_:
raise FeatureLibError(
"Within a named lookup block, it is not allowed "
"to change the language", location)
if self.cur_feature_name_ in ('aalt', 'size'):
raise FeatureLibError(
"Language statements are not allowed "
"within \"feature %s\"" % self.cur_feature_name_, location)
self.cur_lookup_ = None
if include_default:
langsys = set(self.get_default_language_systems_())
else:
langsys = set()
langsys.add((self.script_, language))
self.language_systems = frozenset(langsys)
if required:
key = (self.script_, language)
if key in self.required_features_:
raise FeatureLibError(
"Language %s (script %s) has already "
"specified feature %s as its required feature" % (
language.strip(), self.script_.strip(),
self.required_features_[key].strip()),
location)
self.required_features_[key] = self.cur_feature_name_
def set_script(self, location, script):
if self.cur_lookup_name_:
raise FeatureLibError(
"Within a named lookup block, it is not allowed "
"to change the script", location)
if self.cur_feature_name_ in ('aalt', 'size'):
raise FeatureLibError(
"Script statements are not allowed "
"within \"feature %s\"" % self.cur_feature_name_, location)
self.cur_lookup_ = None
self.script_ = script
self.lookup_flag_ = 0
self.set_language(location, "dflt",
include_default=True, required=False)
def add_alternate_substitution(self, location, glyph, from_class):
lookup = self.get_lookup_(location, AlternateSubstBuilder)
if glyph in lookup.alternates:
raise FeatureLibError(
'Already defined alternates for glyph "%s"' % glyph,
location)
lookup.alternates[glyph] = from_class
def add_ligature_substitution(self, location, glyphs, replacement):
lookup = self.get_lookup_(location, LigatureSubstBuilder)
lookup.ligatures[glyphs] = replacement
def add_multiple_substitution(self, location, glyph, replacements):
# TODO(sascha): Implement this, possibly via a new class
# otTables.MultipleSubst modeled after otTables.SingleSubst.
warnings.warn('Multiple substitution (GPOS LookupType 2) '
'is not yet implemented')
def add_single_substitution(self, location, mapping):
lookup = self.get_lookup_(location, SingleSubstBuilder)
for (from_glyph, to_glyph) in mapping.items():
if from_glyph in lookup.mapping:
raise FeatureLibError(
'Already defined rule for replacing glyph "%s" by "%s"' %
(from_glyph, lookup.mapping[from_glyph]),
location)
lookup.mapping[from_glyph] = to_glyph
class LookupBuilder(object):
def __init__(self, location, table, lookup_type, lookup_flag):
self.location = location
self.table, self.lookup_type = table, lookup_type
self.lookup_flag = lookup_flag
self.lookup_index = None # assigned when making final tables
assert table in ('GPOS', 'GSUB')
def equals(self, other):
return (isinstance(other, self.__class__) and
self.table == other.table and
self.lookup_flag == other.lookup_flag)
class AlternateSubstBuilder(LookupBuilder):
def __init__(self, location, lookup_flag):
LookupBuilder.__init__(self, location, 'GSUB', 3, lookup_flag)
self.alternates = {}
def equals(self, other):
return (LookupBuilder.equals(self, other) and
self.alternates == other.alternates)
def build(self):
lookup = otTables.AlternateSubst()
lookup.Format = 1
lookup.alternates = self.alternates
return lookup
class LigatureSubstBuilder(LookupBuilder):
def __init__(self, location, lookup_flag):
LookupBuilder.__init__(self, location, 'GSUB', 4, lookup_flag)
self.ligatures = {} # {('f','f','i'): 'f_f_i'}
def equals(self, other):
return (LookupBuilder.equals(self, other) and
self.ligatures == other.ligatures)
@staticmethod
def make_key(components):
"""Computes a key for ordering ligatures in a GSUB Type-4 lookup.
When building the OpenType lookup, we need to make sure that
the longest sequence of components is listed first, so we
use the negative length as the primary key for sorting.
To make the tables easier to read, we use the component
sequence as the secondary key.
For example, this will sort (f,f,f) < (f,f,i) < (f,f) < (f,i) < (f,l).
"""
return (-len(components), components)
def build(self):
lookup = otTables.LigatureSubst()
lookup.Format = 1
lookup.ligatures = {}
for components in sorted(self.ligatures.keys(), key=self.make_key):
lig = otTables.Ligature()
lig.Component = components
lig.LigGlyph = self.ligatures[components]
lookup.ligatures.setdefault(components[0], []).append(lig)
return lookup
class SingleSubstBuilder(LookupBuilder):
def __init__(self, location, lookup_flag):
LookupBuilder.__init__(self, location, 'GSUB', 1, lookup_flag)
self.mapping = {}
def equals(self, other):
return (LookupBuilder.equals(self, other) and
self.mapping == other.mapping)
def build(self):
lookup = otTables.SingleSubst()
lookup.mapping = self.mapping
return lookup

View File

@ -0,0 +1,256 @@
from __future__ import print_function, division, absolute_import
from __future__ import unicode_literals
from fontTools.feaLib.builder import Builder, addOpenTypeFeatures
from fontTools.feaLib.builder import LigatureSubstBuilder
from fontTools.feaLib.error import FeatureLibError
from fontTools.ttLib import TTFont
import codecs
import difflib
import os
import shutil
import sys
import tempfile
import unittest
class BuilderTest(unittest.TestCase):
def __init__(self, methodName):
unittest.TestCase.__init__(self, methodName)
# Python 3 renamed assertRaisesRegexp to assertRaisesRegex,
# and fires deprecation warnings if a program uses the old name.
if not hasattr(self, "assertRaisesRegex"):
self.assertRaisesRegex = self.assertRaisesRegexp
def setUp(self):
self.tempdir = None
self.num_tempfiles = 0
def tearDown(self):
if self.tempdir:
shutil.rmtree(self.tempdir)
@staticmethod
def getpath(testfile):
path, _ = os.path.split(__file__)
return os.path.join(path, "testdata", testfile)
def temp_path(self, suffix):
if not self.tempdir:
self.tempdir = tempfile.mkdtemp()
self.num_tempfiles += 1
return os.path.join(self.tempdir,
"tmp%d%s" % (self.num_tempfiles, suffix))
def read_ttx(self, path):
lines = []
with codecs.open(path, "r", "utf-8") as ttx:
for line in ttx.readlines():
# Elide ttFont attributes because ttLibVersion may change,
# and use os-native line separators so we can run difflib.
if line.startswith("<ttFont "):
lines.append("<ttFont>" + os.linesep)
else:
lines.append(line.rstrip() + os.linesep)
return lines
def expect_ttx(self, font, expected_ttx):
path = self.temp_path(suffix=".ttx")
font.saveXML(path, quiet=True, tables=['GSUB', 'GPOS'])
actual = self.read_ttx(path)
expected = self.read_ttx(expected_ttx)
if actual != expected:
for line in difflib.unified_diff(
expected, actual, fromfile=path, tofile=expected_ttx):
sys.stdout.write(line)
self.fail("TTX output is different from expected")
def build(self, featureFile):
path = self.temp_path(suffix=".fea")
with codecs.open(path, "wb", "utf-8") as outfile:
outfile.write(featureFile)
font = TTFont()
addOpenTypeFeatures(path, font)
return font
def test_alternateSubst(self):
font = TTFont()
addOpenTypeFeatures(self.getpath("GSUB_3.fea"), font)
self.expect_ttx(font, self.getpath("GSUB_3.ttx"))
def test_alternateSubst_multipleSubstitutionsForSameGlyph(self):
self.assertRaisesRegex(
FeatureLibError,
"Already defined alternates for glyph \"A\"",
self.build,
"feature test {"
" sub A from [A.alt1 A.alt2];"
" sub B from [B.alt1 B.alt2 B.alt3];"
" sub A from [A.alt1 A.alt2];"
"} test;")
def test_singleSubst_multipleSubstitutionsForSameGlyph(self):
self.assertRaisesRegex(
FeatureLibError,
'Already defined rule for replacing glyph "e" by "E.sc"',
self.build,
"feature test {"
" sub [a-z] by [A.sc-Z.sc];"
" sub e by e.fina;"
"} test;")
def test_spec4h1(self):
# OpenType Feature File specification, section 4.h, example 1.
font = TTFont()
addOpenTypeFeatures(self.getpath("spec4h1.fea"), font)
self.expect_ttx(font, self.getpath("spec4h1.ttx"))
def test_spec5d1(self):
# OpenType Feature File specification, section 5.d, example 1.
font = TTFont()
addOpenTypeFeatures(self.getpath("spec5d1.fea"), font)
self.expect_ttx(font, self.getpath("spec5d1.ttx"))
def test_spec5d2(self):
# OpenType Feature File specification, section 5.d, example 2.
font = TTFont()
addOpenTypeFeatures(self.getpath("spec5d2.fea"), font)
self.expect_ttx(font, self.getpath("spec5d2.ttx"))
def test_languagesystem(self):
builder = Builder(None, TTFont())
builder.add_language_system(None, 'latn', 'FRA')
builder.add_language_system(None, 'cyrl', 'RUS')
builder.start_feature(location=None, name='test')
self.assertEqual(builder.language_systems,
{('latn', 'FRA'), ('cyrl', 'RUS')})
def test_languagesystem_duplicate(self):
self.assertRaisesRegex(
FeatureLibError,
'"languagesystem cyrl RUS" has already been specified',
self.build, "languagesystem cyrl RUS; languagesystem cyrl RUS;")
def test_languagesystem_none_specified(self):
builder = Builder(None, TTFont())
builder.start_feature(location=None, name='test')
self.assertEqual(builder.language_systems, {('DFLT', 'dflt')})
def test_languagesystem_DFLT_dflt_not_first(self):
self.assertRaisesRegex(
FeatureLibError,
"If \"languagesystem DFLT dflt\" is present, "
"it must be the first of the languagesystem statements",
self.build, "languagesystem latn TRK; languagesystem DFLT dflt;")
def test_script(self):
builder = Builder(None, TTFont())
builder.start_feature(location=None, name='test')
builder.set_script(location=None, script='cyrl')
self.assertEqual(builder.language_systems,
{('DFLT', 'dflt'), ('cyrl', 'dflt')})
def test_script_in_aalt_feature(self):
self.assertRaisesRegex(
FeatureLibError,
"Script statements are not allowed within \"feature aalt\"",
self.build, "feature aalt { script latn; } aalt;")
def test_script_in_lookup_block(self):
self.assertRaisesRegex(
FeatureLibError,
"Within a named lookup block, it is not allowed "
"to change the script",
self.build, "lookup Foo { script latn; } Foo;")
def test_script_in_size_feature(self):
self.assertRaisesRegex(
FeatureLibError,
"Script statements are not allowed within \"feature size\"",
self.build, "feature size { script latn; } size;")
def test_language(self):
builder = Builder(None, TTFont())
builder.add_language_system(None, 'latn', 'FRA ')
builder.start_feature(location=None, name='test')
builder.set_script(location=None, script='cyrl')
builder.set_language(location=None, language='RUS ',
include_default=False, required=False)
self.assertEqual(builder.language_systems, {('cyrl', 'RUS ')})
builder.set_language(location=None, language='BGR ',
include_default=True, required=False)
self.assertEqual(builder.language_systems,
{('latn', 'FRA '), ('cyrl', 'BGR ')})
def test_language_in_aalt_feature(self):
self.assertRaisesRegex(
FeatureLibError,
"Language statements are not allowed within \"feature aalt\"",
self.build, "feature aalt { language FRA; } aalt;")
def test_language_in_lookup_block(self):
self.assertRaisesRegex(
FeatureLibError,
"Within a named lookup block, it is not allowed "
"to change the language",
self.build, "lookup Foo { language RUS; } Foo;")
def test_language_in_size_feature(self):
self.assertRaisesRegex(
FeatureLibError,
"Language statements are not allowed within \"feature size\"",
self.build, "feature size { language FRA; } size;")
def test_language_required(self):
font = TTFont()
addOpenTypeFeatures(self.getpath("language_required.fea"), font)
self.expect_ttx(font, self.getpath("language_required.ttx"))
def test_language_required_duplicate(self):
self.assertRaisesRegex(
FeatureLibError,
r"Language FRA \(script latn\) has already specified "
"feature scmp as its required feature",
self.build,
"feature scmp {"
" script latn;"
" language FRA required;"
" language DEU required;"
" substitute [a-z] by [A.sc-Z.sc];"
"} scmp;"
"feature test {"
" language FRA required;"
" substitute [a-z] by [A.sc-Z.sc];"
"} test;")
def test_lookup_already_defined(self):
self.assertRaisesRegex(
FeatureLibError,
"Lookup \"foo\" has already been defined",
self.build, "lookup foo {} foo; lookup foo {} foo;")
def test_lookup_multiple_flags(self):
# TODO(sascha): As soon as we have a working implementation
# of the "lookupflag" statement, test whether the compiler
# rejects rules of the same lookup type but different flags.
pass
def test_lookup_multiple_types(self):
self.assertRaisesRegex(
FeatureLibError,
"Within a named lookup block, all rules must be "
"of the same lookup type and flag",
self.build,
"lookup foo {"
" sub f f i by f_f_i;"
" sub A from [A.alt1 A.alt2];"
"} foo;")
class LigatureSubstBuilderTest(unittest.TestCase):
def test_make_key(self):
self.assertEqual(LigatureSubstBuilder.make_key(('f', 'f', 'i')),
(-3, ('f', 'f', 'i')))
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,16 @@
from __future__ import print_function, division, absolute_import
from __future__ import unicode_literals
class FeatureLibError(Exception):
def __init__(self, message, location):
Exception.__init__(self, message)
self.location = location
def __str__(self):
message = Exception.__str__(self)
if self.location:
path, line, column = self.location
return "%s:%d:%d: %s" % (path, line, column, message)
else:
return message

View File

@ -0,0 +1,18 @@
from __future__ import print_function, division, absolute_import
from __future__ import unicode_literals
from fontTools.feaLib.error import FeatureLibError
import unittest
class FeatureLibErrorTest(unittest.TestCase):
def test_str(self):
err = FeatureLibError("Squeak!", ("foo.fea", 23, 42))
self.assertEqual(str(err), "foo.fea:23:42: Squeak!")
def test_str_nolocation(self):
err = FeatureLibError("Squeak!", None)
self.assertEqual(str(err), "Squeak!")
if __name__ == "__main__":
unittest.main()

View File

@ -1,23 +1,10 @@
from __future__ import print_function, division, absolute_import
from __future__ import unicode_literals
from fontTools.feaLib.error import FeatureLibError
import codecs
import os
class LexerError(Exception):
def __init__(self, message, location):
Exception.__init__(self, message)
self.location = location
def __str__(self):
message = Exception.__str__(self)
if self.location:
path, line, column = self.location
return "%s:%d:%d: %s" % (path, line, column, message)
else:
return message
class Lexer(object):
NUMBER = "NUMBER"
STRING = "STRING"
@ -90,11 +77,13 @@ class Lexer(object):
if self.mode_ is Lexer.MODE_FILENAME_:
if cur_char != "(":
raise LexerError("Expected '(' before file name", location)
raise FeatureLibError("Expected '(' before file name",
location)
self.scan_until_(")")
cur_char = text[self.pos_] if self.pos_ < limit else None
if cur_char != ")":
raise LexerError("Expected ')' after file name", location)
raise FeatureLibError("Expected ')' after file name",
location)
self.pos_ += 1
self.mode_ = Lexer.MODE_NORMAL_
return (Lexer.FILENAME, text[start + 1:self.pos_ - 1], location)
@ -108,9 +97,9 @@ class Lexer(object):
self.scan_over_(Lexer.CHAR_NAME_CONTINUATION_)
glyphclass = text[start + 1:self.pos_]
if len(glyphclass) < 1:
raise LexerError("Expected glyph class name", location)
raise FeatureLibError("Expected glyph class name", location)
if len(glyphclass) > 30:
raise LexerError(
raise FeatureLibError(
"Glyph class names must not be longer than 30 characters",
location)
return (Lexer.GLYPHCLASS, glyphclass, location)
@ -142,8 +131,10 @@ class Lexer(object):
self.pos_ += 1
return (Lexer.STRING, text[start + 1:self.pos_ - 1], location)
else:
raise LexerError("Expected '\"' to terminate string", location)
raise LexerError("Unexpected character: '%s'" % cur_char, location)
raise FeatureLibError("Expected '\"' to terminate string",
location)
raise FeatureLibError("Unexpected character: '%s'" % cur_char,
location)
def scan_over_(self, valid):
p = self.pos_
@ -179,15 +170,15 @@ class IncludingLexer(object):
if token_type is Lexer.NAME and token == "include":
fname_type, fname_token, fname_location = lexer.next()
if fname_type is not Lexer.FILENAME:
raise LexerError("Expected file name", fname_location)
raise FeatureLibError("Expected file name", fname_location)
semi_type, semi_token, semi_location = lexer.next()
if semi_type is not Lexer.SYMBOL or semi_token != ";":
raise LexerError("Expected ';'", semi_location)
raise FeatureLibError("Expected ';'", semi_location)
curpath, _ = os.path.split(lexer.filename_)
path = os.path.join(curpath, fname_token)
if len(self.lexers_) >= 5:
raise LexerError("Too many recursive includes",
fname_location)
raise FeatureLibError("Too many recursive includes",
fname_location)
self.lexers_.append(self.make_lexer_(path, fname_location))
continue
else:
@ -200,4 +191,4 @@ class IncludingLexer(object):
with codecs.open(filename, "rb", "utf-8") as f:
return Lexer(f.read(), filename)
except IOError as err:
raise LexerError(str(err), location)
raise FeatureLibError(str(err), location)

View File

@ -1,6 +1,7 @@
from __future__ import print_function, division, absolute_import
from __future__ import unicode_literals
from fontTools.feaLib.lexer import IncludingLexer, Lexer, LexerError
from fontTools.feaLib.error import FeatureLibError
from fontTools.feaLib.lexer import IncludingLexer, Lexer
import os
import unittest
@ -9,16 +10,6 @@ def lex(s):
return [(typ, tok) for (typ, tok, _) in Lexer(s, "test.fea")]
class LexerErrorTest(unittest.TestCase):
def test_str(self):
err = LexerError("Squeak!", ("foo.fea", 23, 42))
self.assertEqual(str(err), "foo.fea:23:42: Squeak!")
def test_str_nolocation(self):
err = LexerError("Squeak!", None)
self.assertEqual(str(err), "Squeak!")
class LexerTest(unittest.TestCase):
def __init__(self, methodName):
unittest.TestCase.__init__(self, methodName)
@ -43,9 +34,12 @@ class LexerTest(unittest.TestCase):
def test_glyphclass(self):
self.assertEqual(lex("@Vowel.sc"), [(Lexer.GLYPHCLASS, "Vowel.sc")])
self.assertRaisesRegex(LexerError, "Expected glyph class", lex, "@(a)")
self.assertRaisesRegex(LexerError, "Expected glyph class", lex, "@ A")
self.assertRaisesRegex(LexerError, "not be longer than 30 characters",
self.assertRaisesRegex(FeatureLibError,
"Expected glyph class", lex, "@(a)")
self.assertRaisesRegex(FeatureLibError,
"Expected glyph class", lex, "@ A")
self.assertRaisesRegex(FeatureLibError,
"not be longer than 30 characters",
lex, "@a123456789.a123456789.a123456789.x")
def test_include(self):
@ -59,8 +53,8 @@ class LexerTest(unittest.TestCase):
(Lexer.FILENAME, "foo"),
(Lexer.SYMBOL, ";")
])
self.assertRaises(LexerError, lex, "include blah")
self.assertRaises(LexerError, lex, "include (blah")
self.assertRaises(FeatureLibError, lex, "include blah")
self.assertRaises(FeatureLibError, lex, "include (blah")
def test_number(self):
self.assertEqual(lex("123 -456"),
@ -80,21 +74,22 @@ class LexerTest(unittest.TestCase):
def test_string(self):
self.assertEqual(lex('"foo" "bar"'),
[(Lexer.STRING, "foo"), (Lexer.STRING, "bar")])
self.assertRaises(LexerError, lambda: lex('"foo\n bar"'))
self.assertRaises(FeatureLibError, lambda: lex('"foo\n bar"'))
def test_bad_character(self):
self.assertRaises(LexerError, lambda: lex("123 \u0001"))
self.assertRaises(FeatureLibError, lambda: lex("123 \u0001"))
def test_newline(self):
lines = lambda s: [loc[1] for (_, _, loc) in Lexer(s, "test.fea")]
def lines(s):
return [loc[1] for (_, _, loc) in Lexer(s, "test.fea")]
self.assertEqual(lines("FOO\n\nBAR\nBAZ"), [1, 3, 4]) # Unix
self.assertEqual(lines("FOO\r\rBAR\rBAZ"), [1, 3, 4]) # Macintosh
self.assertEqual(lines("FOO\r\n\r\n BAR\r\nBAZ"), [1, 3, 4]) # Windows
self.assertEqual(lines("FOO\n\rBAR\r\nBAZ"), [1, 3, 4]) # mixed
def test_location(self):
locs = lambda s: ["%s:%d:%d" % loc
for (_, _, loc) in Lexer(s, "test.fea")]
def locs(s):
return ["%s:%d:%d" % loc for (_, _, loc) in Lexer(s, "test.fea")]
self.assertEqual(locs("a b # Comment\n12 @x"), [
"test.fea:1:1", "test.fea:1:3", "test.fea:2:1",
"test.fea:2:4"
@ -145,15 +140,15 @@ class IncludingLexerTest(unittest.TestCase):
def test_include_limit(self):
lexer = IncludingLexer(self.getpath("include6.fea"))
self.assertRaises(LexerError, lambda: list(lexer))
self.assertRaises(FeatureLibError, lambda: list(lexer))
def test_include_self(self):
lexer = IncludingLexer(self.getpath("includeself.fea"))
self.assertRaises(LexerError, lambda: list(lexer))
self.assertRaises(FeatureLibError, lambda: list(lexer))
def test_include_missing_file(self):
lexer = IncludingLexer(self.getpath("includemissingfile.fea"))
self.assertRaises(LexerError, lambda: list(lexer))
self.assertRaises(FeatureLibError, lambda: list(lexer))
if __name__ == "__main__":

View File

@ -1,25 +1,12 @@
from __future__ import print_function, division, absolute_import
from __future__ import unicode_literals
from fontTools.feaLib.error import FeatureLibError
from fontTools.feaLib.lexer import Lexer, IncludingLexer
import fontTools.feaLib.ast as ast
import os
import re
class ParserError(Exception):
def __init__(self, message, location):
Exception.__init__(self, message)
self.location = location
def __str__(self):
message = Exception.__str__(self)
if self.location:
path, line, column = self.location
return "%s:%d:%d: %s" % (path, line, column, message)
else:
return message
class Parser(object):
def __init__(self, path):
self.doc_ = ast.FeatureFile()
@ -54,9 +41,9 @@ class Parser(object):
statements.append(
self.parse_valuerecord_definition_(vertical=False))
else:
raise ParserError("Expected feature, languagesystem, "
"lookup, or glyph class definition",
self.cur_token_location_)
raise FeatureLibError("Expected feature, languagesystem, "
"lookup, or glyph class definition",
self.cur_token_location_)
return self.doc_
def parse_anchordef_(self):
@ -79,8 +66,8 @@ class Parser(object):
glyphs = self.parse_glyphclass_(accept_glyphname=False)
self.expect_symbol_(";")
if self.glyphclasses_.resolve(name) is not None:
raise ParserError("Glyph class @%s already defined" % name,
location)
raise FeatureLibError("Glyph class @%s already defined" % name,
location)
glyphclass = ast.GlyphClassDefinition(location, name, glyphs)
self.glyphclasses_.define(name, glyphclass)
return glyphclass
@ -94,8 +81,9 @@ class Parser(object):
self.advance_lexer_()
gc = self.glyphclasses_.resolve(self.cur_token_)
if gc is None:
raise ParserError("Unknown glyph class @%s" % self.cur_token_,
self.cur_token_location_)
raise FeatureLibError(
"Unknown glyph class @%s" % self.cur_token_,
self.cur_token_location_)
result.update(gc.glyphs)
return result
@ -116,12 +104,12 @@ class Parser(object):
elif self.cur_token_type_ is Lexer.GLYPHCLASS:
gc = self.glyphclasses_.resolve(self.cur_token_)
if gc is None:
raise ParserError(
raise FeatureLibError(
"Unknown glyph class @%s" % self.cur_token_,
self.cur_token_location_)
result.update(gc.glyphs)
else:
raise ParserError(
raise FeatureLibError(
"Expected glyph name, glyph range, "
"or glyph class reference",
self.cur_token_location_)
@ -147,13 +135,15 @@ class Parser(object):
if self.next_token_ == "lookup":
self.expect_keyword_("lookup")
if not marked:
raise ParserError("Lookups can only follow marked glyphs",
self.cur_token_location_)
raise FeatureLibError(
"Lookups can only follow marked glyphs",
self.cur_token_location_)
lookup_name = self.expect_name_()
lookup = self.lookups_.resolve(lookup_name)
if lookup is None:
raise ParserError('Unknown lookup "%s"' % lookup_name,
self.cur_token_location_)
raise FeatureLibError(
'Unknown lookup "%s"' % lookup_name,
self.cur_token_location_)
if marked:
lookups.append(lookup)
@ -171,11 +161,13 @@ class Parser(object):
prefix, glyphs, lookups, suffix = self.parse_glyph_pattern_()
self.expect_symbol_(";")
return ast.IgnoreSubstitutionRule(location, prefix, glyphs, suffix)
raise ParserError("Expected \"substitute\"", self.next_token_location_)
raise FeatureLibError(
"Expected \"substitute\"", self.next_token_location_)
def parse_language_(self):
assert self.is_cur_keyword_("language")
location, language = self.cur_token_location_, self.expect_tag_()
location = self.cur_token_location_
language = self.expect_language_tag_()
include_default, required = (True, False)
if self.next_token_ in {"exclude_dflt", "include_dflt"}:
include_default = (self.expect_name_() == "include_dflt")
@ -183,7 +175,7 @@ class Parser(object):
self.expect_keyword_("required")
required = True
self.expect_symbol_(";")
return ast.LanguageStatement(location, language.strip(),
return ast.LanguageStatement(location, language,
include_default, required)
def parse_lookup_(self, vertical):
@ -193,8 +185,8 @@ class Parser(object):
if self.next_token_ == ";":
lookup = self.lookups_.resolve(name)
if lookup is None:
raise ParserError("Unknown lookup \"%s\"" % name,
self.cur_token_location_)
raise FeatureLibError("Unknown lookup \"%s\"" % name,
self.cur_token_location_)
self.expect_symbol_(";")
return ast.LookupReferenceStatement(location, lookup)
@ -210,7 +202,7 @@ class Parser(object):
def parse_script_(self):
assert self.is_cur_keyword_("script")
location, script = self.cur_token_location_, self.expect_tag_()
location, script = self.cur_token_location_, self.expect_script_tag_()
self.expect_symbol_(";")
return ast.ScriptStatement(location, script)
@ -231,19 +223,58 @@ class Parser(object):
keyword = None
self.expect_symbol_(";")
if len(new) is 0 and not any(lookups):
raise ParserError(
raise FeatureLibError(
'Expected "by", "from" or explicit lookup references',
self.cur_token_location_)
# GSUB lookup type 3: Alternate substitution.
# Format: "substitute a from [a.1 a.2 a.3];"
if keyword == "from":
if len(old) != 1 or len(old[0]) != 1:
raise ParserError('Expected a single glyph before "from"',
location)
raise FeatureLibError(
'Expected a single glyph before "from"',
location)
if len(new) != 1:
raise ParserError('Expected a single glyphclass after "from"',
location)
raise FeatureLibError(
'Expected a single glyphclass after "from"',
location)
return ast.AlternateSubstitution(location, list(old[0])[0], new[0])
num_lookups = len([l for l in lookups if l is not None])
# GSUB lookup type 1: Single substitution.
# Format A: "substitute a by a.sc;"
# Format B: "substitute [one.fitted one.oldstyle] by one;"
# Format C: "substitute [a-d] by [A.sc-D.sc];"
if (len(old_prefix) == 0 and len(old_suffix) == 0 and
len(old) == 1 and len(new) == 1 and num_lookups == 0):
glyphs, replacements = sorted(list(old[0])), sorted(list(new[0]))
if len(replacements) == 1:
replacements = replacements * len(glyphs)
if len(glyphs) != len(replacements):
raise FeatureLibError(
'Expected a glyph class with %d elements after "by", '
'but found a glyph class with %d elements' %
(len(glyphs), len(replacements)), location)
return ast.SingleSubstitution(location,
dict(zip(glyphs, replacements)))
# GSUB lookup type 2: Multiple substitution.
# Format: "substitute f_f_i by f f i;"
if (len(old_prefix) == 0 and len(old_suffix) == 0 and
len(old) == 1 and len(old[0]) == 1 and
len(new) > 1 and max([len(n) for n in new]) == 1 and
num_lookups == 0):
return ast.MultipleSubstitution(location, tuple(old[0])[0],
tuple([list(n)[0] for n in new]))
# GSUB lookup type 4: Ligature substitution.
# Format: "substitute f f i by f_f_i;"
if (len(old_prefix) == 0 and len(old_suffix) == 0 and
len(old) > 1 and len(new) == 1 and len(new[0]) == 1 and
num_lookups == 0):
return ast.LigatureSubstitution(location, old, list(new[0])[0])
rule = ast.SubstitutionRule(location, old, new)
rule.old_prefix, rule.old_suffix = old_prefix, old_suffix
rule.lookups = lookups
@ -269,8 +300,8 @@ class Parser(object):
name = self.expect_name_()
vrd = self.valuerecords_.resolve(name)
if vrd is None:
raise ParserError("Unknown valueRecordDef \"%s\"" % name,
self.cur_token_location_)
raise FeatureLibError("Unknown valueRecordDef \"%s\"" % name,
self.cur_token_location_)
value = vrd.value
xPlacement, yPlacement = (value.xPlacement, value.yPlacement)
xAdvance, yAdvance = (value.xAdvance, value.yAdvance)
@ -295,8 +326,13 @@ class Parser(object):
def parse_languagesystem_(self):
assert self.cur_token_ == "languagesystem"
location = self.cur_token_location_
script, language = self.expect_tag_(), self.expect_tag_()
script = self.expect_script_tag_()
language = self.expect_language_tag_()
self.expect_symbol_(";")
if script == "DFLT" and language != "dflt":
raise FeatureLibError(
'For script "DFLT", the language must be "dflt"',
self.cur_token_location_)
return ast.LanguageSystemStatement(location, script, language)
def parse_feature_block_(self):
@ -342,7 +378,7 @@ class Parser(object):
elif self.is_cur_keyword_("valueRecordDef"):
statements.append(self.parse_valuerecord_definition_(vertical))
else:
raise ParserError(
raise FeatureLibError(
"Expected glyph class definition or statement",
self.cur_token_location_)
@ -352,8 +388,8 @@ class Parser(object):
name = self.expect_name_()
if name != block.name.strip():
raise ParserError("Expected \"%s\"" % block.name.strip(),
self.cur_token_location_)
raise FeatureLibError("Expected \"%s\"" % block.name.strip(),
self.cur_token_location_)
self.expect_symbol_(";")
def is_cur_keyword_(self, k):
@ -362,36 +398,53 @@ class Parser(object):
def expect_tag_(self):
self.advance_lexer_()
if self.cur_token_type_ is not Lexer.NAME:
raise ParserError("Expected a tag", self.cur_token_location_)
raise FeatureLibError("Expected a tag", self.cur_token_location_)
if len(self.cur_token_) > 4:
raise ParserError("Tags can not be longer than 4 characters",
self.cur_token_location_)
raise FeatureLibError("Tags can not be longer than 4 characters",
self.cur_token_location_)
return (self.cur_token_ + " ")[:4]
def expect_script_tag_(self):
tag = self.expect_tag_()
if tag == "dflt":
raise FeatureLibError(
'"dflt" is not a valid script tag; use "DFLT" instead',
self.cur_token_location_)
return tag
def expect_language_tag_(self):
tag = self.expect_tag_()
if tag == "DFLT":
raise FeatureLibError(
'"DFLT" is not a valid language tag; use "dflt" instead',
self.cur_token_location_)
return tag
def expect_symbol_(self, symbol):
self.advance_lexer_()
if self.cur_token_type_ is Lexer.SYMBOL and self.cur_token_ == symbol:
return symbol
raise ParserError("Expected '%s'" % symbol, self.cur_token_location_)
raise FeatureLibError("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_)
raise FeatureLibError("Expected \"%s\"" % keyword,
self.cur_token_location_)
def expect_name_(self):
self.advance_lexer_()
if self.cur_token_type_ is Lexer.NAME:
return self.cur_token_
raise ParserError("Expected a name", self.cur_token_location_)
raise FeatureLibError("Expected a name", self.cur_token_location_)
def expect_number_(self):
self.advance_lexer_()
if self.cur_token_type_ is Lexer.NUMBER:
return self.cur_token_
raise ParserError("Expected a number", self.cur_token_location_)
raise FeatureLibError("Expected a number", self.cur_token_location_)
def advance_lexer_(self):
self.cur_token_type_, self.cur_token_, self.cur_token_location_ = (
@ -402,14 +455,20 @@ class Parser(object):
except StopIteration:
self.next_token_type_, self.next_token_ = (None, None)
@staticmethod
def reverse_string_(s):
"""'abc' --> 'bca'"""
return ''.join(reversed(list(s)))
def make_glyph_range_(self, location, start, limit):
"""("a.sc", "d.sc") --> {"a.sc", "b.sc", "c.sc", "d.sc"}"""
result = set()
if len(start) != len(limit):
raise ParserError(
raise FeatureLibError(
"Bad range: \"%s\" and \"%s\" should have the same length" %
(start, limit), location)
rev = lambda s: ''.join(reversed(list(s))) # string reversal
rev = self.reverse_string_
prefix = os.path.commonprefix([start, limit])
suffix = rev(os.path.commonprefix([rev(start), rev(limit)]))
if len(suffix) > 0:
@ -420,8 +479,9 @@ class Parser(object):
limit_range = limit[len(prefix):]
if start_range >= limit_range:
raise ParserError("Start of range must be smaller than its end",
location)
raise FeatureLibError(
"Start of range must be smaller than its end",
location)
uppercase = re.compile(r'^[A-Z]$')
if uppercase.match(start_range) and uppercase.match(limit_range):
@ -442,7 +502,8 @@ class Parser(object):
result.add("%s%s%s" % (prefix, number, suffix))
return result
raise ParserError("Bad range: \"%s-%s\"" % (start, limit), location)
raise FeatureLibError("Bad range: \"%s-%s\"" % (start, limit),
location)
class SymbolTable(object):

View File

@ -1,7 +1,7 @@
from __future__ import print_function, division, absolute_import
from __future__ import unicode_literals
from fontTools.feaLib.lexer import LexerError
from fontTools.feaLib.parser import Parser, ParserError, SymbolTable
from fontTools.feaLib.error import FeatureLibError
from fontTools.feaLib.parser import Parser, SymbolTable
from fontTools.misc.py23 import *
import fontTools.feaLib.ast as ast
import codecs
@ -53,13 +53,13 @@ class ParserTest(unittest.TestCase):
def test_glyphclass_bad(self):
self.assertRaisesRegex(
ParserError,
FeatureLibError,
"Expected glyph name, glyph range, or glyph class reference",
self.parse, "@bad = [a 123];")
def test_glyphclass_duplicate(self):
self.assertRaisesRegex(
ParserError, "Glyph class @dup already defined",
FeatureLibError, "Glyph class @dup already defined",
self.parse, "@dup = [a b]; @dup = [x];")
def test_glyphclass_empty(self):
@ -96,17 +96,17 @@ class ParserTest(unittest.TestCase):
def test_glyphclass_range_bad(self):
self.assertRaisesRegex(
ParserError,
FeatureLibError,
"Bad range: \"a\" and \"foobar\" should have the same length",
self.parse, "@bad = [a-foobar];")
self.assertRaisesRegex(
ParserError, "Bad range: \"A.swash-z.swash\"",
FeatureLibError, "Bad range: \"A.swash-z.swash\"",
self.parse, "@bad = [A.swash-z.swash];")
self.assertRaisesRegex(
ParserError, "Start of range must be smaller than its end",
FeatureLibError, "Start of range must be smaller than its end",
self.parse, "@bad = [B.swash-A.swash];")
self.assertRaisesRegex(
ParserError, "Bad range: \"foo.1234-foo.9876\"",
FeatureLibError, "Bad range: \"foo.1234-foo.9876\"",
self.parse, "@bad = [foo.1234-foo.9876];")
def test_glyphclass_range_mixed(self):
@ -123,7 +123,7 @@ class ParserTest(unittest.TestCase):
self.assertEqual(vowels_uc.glyphs, set(list("AEIOU")))
self.assertEqual(vowels.glyphs, set(list("aeiouyAEIOUY")))
self.assertRaisesRegex(
ParserError, "Unknown glyph class @unknown",
FeatureLibError, "Unknown glyph class @unknown",
self.parse, "@bad = [@unknown];")
def test_glyphclass_scoping(self):
@ -159,7 +159,7 @@ class ParserTest(unittest.TestCase):
doc = self.parse("feature test {language DEU;} test;")
s = doc.statements[0].statements[0]
self.assertEqual(type(s), ast.LanguageStatement)
self.assertEqual(s.language, "DEU")
self.assertEqual(s.language, "DEU ")
self.assertTrue(s.include_default)
self.assertFalse(s.required)
@ -167,7 +167,7 @@ class ParserTest(unittest.TestCase):
doc = self.parse("feature test {language DEU exclude_dflt;} test;")
s = doc.statements[0].statements[0]
self.assertEqual(type(s), ast.LanguageStatement)
self.assertEqual(s.language, "DEU")
self.assertEqual(s.language, "DEU ")
self.assertFalse(s.include_default)
self.assertFalse(s.required)
@ -177,7 +177,7 @@ class ParserTest(unittest.TestCase):
"} test;")
s = doc.statements[0].statements[0]
self.assertEqual(type(s), ast.LanguageStatement)
self.assertEqual(s.language, "DEU")
self.assertEqual(s.language, "DEU ")
self.assertFalse(s.include_default)
self.assertTrue(s.required)
@ -185,7 +185,7 @@ class ParserTest(unittest.TestCase):
doc = self.parse("feature test {language DEU include_dflt;} test;")
s = doc.statements[0].statements[0]
self.assertEqual(type(s), ast.LanguageStatement)
self.assertEqual(s.language, "DEU")
self.assertEqual(s.language, "DEU ")
self.assertTrue(s.include_default)
self.assertFalse(s.required)
@ -195,10 +195,16 @@ class ParserTest(unittest.TestCase):
"} test;")
s = doc.statements[0].statements[0]
self.assertEqual(type(s), ast.LanguageStatement)
self.assertEqual(s.language, "DEU")
self.assertEqual(s.language, "DEU ")
self.assertTrue(s.include_default)
self.assertTrue(s.required)
def test_language_DFLT(self):
self.assertRaisesRegex(
FeatureLibError,
'"DFLT" is not a valid language tag; use "dflt" instead',
self.parse, "feature test { language DFLT; } test;")
def test_lookup_block(self):
[lookup] = self.parse("lookup Ligatures {} Ligatures;").statements
self.assertEqual(lookup.name, "Ligatures")
@ -211,7 +217,7 @@ class ParserTest(unittest.TestCase):
def test_lookup_block_name_mismatch(self):
self.assertRaisesRegex(
ParserError, 'Expected "Foo"',
FeatureLibError, 'Expected "Foo"',
self.parse, "lookup Foo {} Bar;")
def test_lookup_block_with_horizontal_valueRecordDef(self):
@ -247,7 +253,7 @@ class ParserTest(unittest.TestCase):
def test_lookup_reference_unknown(self):
self.assertRaisesRegex(
ParserError, 'Unknown lookup "Huh"',
FeatureLibError, 'Unknown lookup "Huh"',
self.parse, "feature liga {lookup Huh;} liga;")
def test_script(self):
@ -256,14 +262,17 @@ class ParserTest(unittest.TestCase):
self.assertEqual(type(s), ast.ScriptStatement)
self.assertEqual(s.script, "cyrl")
def test_script_dflt(self):
self.assertRaisesRegex(
FeatureLibError,
'"dflt" is not a valid script tag; use "DFLT" instead',
self.parse, "feature test {script dflt;} test;")
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]
self.assertEqual(sub.old_prefix, [])
self.assertEqual(sub.old, [{"a"}])
self.assertEqual(sub.old_suffix, [])
self.assertEqual(sub.new, [{"a.sc"}])
self.assertEqual(sub.lookups, [None])
self.assertEqual(type(sub), ast.SingleSubstitution)
self.assertEqual(sub.mapping, {"a": "a.sc"})
def test_substitute_single_format_b(self): # GSUB LookupType 1
doc = self.parse(
@ -271,11 +280,11 @@ class ParserTest(unittest.TestCase):
" substitute [one.fitted one.oldstyle] by one;"
"} smcp;")
sub = doc.statements[0].statements[0]
self.assertEqual(sub.old_prefix, [])
self.assertEqual(sub.old, [{"one.fitted", "one.oldstyle"}])
self.assertEqual(sub.old_suffix, [])
self.assertEqual(sub.new, [{"one"}])
self.assertEqual(sub.lookups, [None])
self.assertEqual(type(sub), ast.SingleSubstitution)
self.assertEqual(sub.mapping, {
"one.fitted": "one",
"one.oldstyle": "one"
})
def test_substitute_single_format_c(self): # GSUB LookupType 1
doc = self.parse(
@ -283,21 +292,27 @@ class ParserTest(unittest.TestCase):
" substitute [a-d] by [A.sc-D.sc];"
"} smcp;")
sub = doc.statements[0].statements[0]
self.assertEqual(sub.old_prefix, [])
self.assertEqual(sub.old, [{"a", "b", "c", "d"}])
self.assertEqual(sub.old_suffix, [])
self.assertEqual(sub.new, [{"A.sc", "B.sc", "C.sc", "D.sc"}])
self.assertEqual(sub.lookups, [None])
self.assertEqual(type(sub), ast.SingleSubstitution)
self.assertEqual(sub.mapping, {
"a": "A.sc",
"b": "B.sc",
"c": "C.sc",
"d": "D.sc"
})
def test_substitute_single_format_c_different_num_elements(self):
self.assertRaisesRegex(
FeatureLibError,
'Expected a glyph class with 4 elements after "by", '
'but found a glyph class with 26 elements',
self.parse, "feature smcp {sub [a-d] by [A.sc-Z.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]
self.assertEqual(type(sub), ast.SubstitutionRule)
self.assertEqual(sub.old_prefix, [])
self.assertEqual(sub.old, [{"f_f_i"}])
self.assertEqual(sub.old_suffix, [])
self.assertEqual(sub.new, [{"f"}, {"f"}, {"i"}])
self.assertEqual(sub.lookups, [None])
self.assertEqual(type(sub), ast.MultipleSubstitution)
self.assertEqual(sub.glyph, "f_f_i")
self.assertEqual(sub.replacement, ("f", "f", "i"))
def test_substitute_from(self): # GSUB LookupType 3
doc = self.parse("feature test {"
@ -321,11 +336,9 @@ class ParserTest(unittest.TestCase):
def test_substitute_ligature(self): # GSUB LookupType 4
doc = self.parse("feature liga {substitute f f i by f_f_i;} liga;")
sub = doc.statements[0].statements[0]
self.assertEqual(sub.old_prefix, [])
self.assertEqual(sub.old, [{"f"}, {"f"}, {"i"}])
self.assertEqual(sub.old_suffix, [])
self.assertEqual(sub.new, [{"f_f_i"}])
self.assertEqual(sub.lookups, [None, None, None])
self.assertEqual(type(sub), ast.LigatureSubstitution)
self.assertEqual(sub.glyphs, [{"f"}, {"f"}, {"i"}])
self.assertEqual(sub.replacement, "f_f_i")
def test_substitute_lookups(self):
doc = Parser(self.getpath("spec5fi.fea")).parse()
@ -335,7 +348,8 @@ class ParserTest(unittest.TestCase):
def test_substitute_missing_by(self):
self.assertRaisesRegex(
ParserError, 'Expected "by", "from" or explicit lookup references',
FeatureLibError,
'Expected "by", "from" or explicit lookup references',
self.parse, "feature liga {substitute f f i;} liga;")
def test_subtable(self):
@ -378,7 +392,7 @@ class ParserTest(unittest.TestCase):
def test_valuerecord_named_unknown(self):
self.assertRaisesRegex(
ParserError, "Unknown valueRecordDef \"unknown\"",
FeatureLibError, "Unknown valueRecordDef \"unknown\"",
self.parse, "valueRecordDef <unknown> foo;")
def test_valuerecord_scoping(self):
@ -396,14 +410,26 @@ class ParserTest(unittest.TestCase):
self.assertEqual(langsys.script, "latn")
self.assertEqual(langsys.language, "DEU ")
self.assertRaisesRegex(
ParserError, "Expected ';'",
FeatureLibError,
'For script "DFLT", the language must be "dflt"',
self.parse, "languagesystem DFLT DEU;")
self.assertRaisesRegex(
FeatureLibError,
'"dflt" is not a valid script tag; use "DFLT" instead',
self.parse, "languagesystem dflt dflt;")
self.assertRaisesRegex(
FeatureLibError,
'"DFLT" is not a valid language tag; use "dflt" instead',
self.parse, "languagesystem latn DFLT;")
self.assertRaisesRegex(
FeatureLibError, "Expected ';'",
self.parse, "languagesystem latn DEU")
self.assertRaisesRegex(
ParserError, "longer than 4 characters",
self.parse, "languagesystem foobar DEU")
FeatureLibError, "longer than 4 characters",
self.parse, "languagesystem foobar DEU;")
self.assertRaisesRegex(
ParserError, "longer than 4 characters",
self.parse, "languagesystem latn FOOBAR")
FeatureLibError, "longer than 4 characters",
self.parse, "languagesystem latn FOOBAR;")
def setUp(self):
self.tempdir = None

View File

@ -0,0 +1,13 @@
feature f1 {
sub A from [A.alt1 A.alt2];
sub B from [B.alt1 B.alt2 B.alt3];
sub C from [C.alt1];
} f1;
# Exact same content as feature f1; lookup should be shared.
feature f2 {
sub A from [A.alt1 A.alt2];
sub B from [B.alt1 B.alt2 B.alt3];
sub C from [C.alt1];
} f2;

View File

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<ttFont>
<GSUB>
<GSUB>
<Version value="1.0"/>
<ScriptList>
<!-- ScriptCount=1 -->
<ScriptRecord index="0">
<ScriptTag value="DFLT"/>
<Script>
<DefaultLangSys>
<ReqFeatureIndex value="65535"/>
<!-- FeatureCount=2 -->
<FeatureIndex index="0" value="0"/>
<FeatureIndex index="1" value="1"/>
</DefaultLangSys>
<!-- LangSysCount=0 -->
</Script>
</ScriptRecord>
</ScriptList>
<FeatureList>
<!-- FeatureCount=2 -->
<FeatureRecord index="0">
<FeatureTag value="f1 "/>
<Feature>
<!-- LookupCount=1 -->
<LookupListIndex index="0" value="0"/>
</Feature>
</FeatureRecord>
<FeatureRecord index="1">
<FeatureTag value="f2 "/>
<Feature>
<!-- LookupCount=1 -->
<LookupListIndex index="0" value="0"/>
</Feature>
</FeatureRecord>
</FeatureList>
<LookupList>
<!-- LookupCount=1 -->
<AlternateSubst index="0" Format="1">
<AlternateSet glyph="A">
<Alternate glyph="A.alt1"/>
<Alternate glyph="A.alt2"/>
</AlternateSet>
<AlternateSet glyph="B">
<Alternate glyph="B.alt1"/>
<Alternate glyph="B.alt2"/>
<Alternate glyph="B.alt3"/>
</AlternateSet>
<AlternateSet glyph="C">
<Alternate glyph="C.alt1"/>
</AlternateSet>
</AlternateSubst>
</LookupList>
</GSUB>
</GSUB>
<GPOS>
<GPOS>
<Version value="1.0"/>
<ScriptList>
<!-- ScriptCount=0 -->
</ScriptList>
<FeatureList>
<!-- FeatureCount=0 -->
</FeatureList>
<LookupList>
<!-- LookupCount=0 -->
</LookupList>
</GPOS>
</GPOS>
</ttFont>

View File

@ -0,0 +1,21 @@
languagesystem latn DEU;
languagesystem latn FRA;
languagesystem latn ITA;
feature hlig {
script latn;
language DEU exclude_dflt required;
sub D E U by D_E_U;
language FRA exclude_dflt;
sub F R A by D_E_U;
} hlig;
feature liga {
language ITA exclude_dflt required;
sub I T A by I_T_A;
} liga;
feature scmp {
sub [a-z] by [A.sc-Z.sc];
} scmp;

View File

@ -0,0 +1,136 @@
<?xml version="1.0" encoding="UTF-8"?>
<ttFont>
<GSUB>
<GSUB>
<Version value="1.0"/>
<ScriptList>
<!-- ScriptCount=1 -->
<ScriptRecord index="0">
<ScriptTag value="latn"/>
<Script>
<!-- LangSysCount=3 -->
<LangSysRecord index="0">
<LangSysTag value="DEU "/>
<LangSys>
<ReqFeatureIndex value="0"/>
<!-- FeatureCount=1 -->
<FeatureIndex index="0" value="1"/>
</LangSys>
</LangSysRecord>
<LangSysRecord index="1">
<LangSysTag value="FRA "/>
<LangSys>
<ReqFeatureIndex value="65535"/>
<!-- FeatureCount=2 -->
<FeatureIndex index="0" value="2"/>
<FeatureIndex index="1" value="1"/>
</LangSys>
</LangSysRecord>
<LangSysRecord index="2">
<LangSysTag value="ITA "/>
<LangSys>
<ReqFeatureIndex value="3"/>
<!-- FeatureCount=1 -->
<FeatureIndex index="0" value="1"/>
</LangSys>
</LangSysRecord>
</Script>
</ScriptRecord>
</ScriptList>
<FeatureList>
<!-- FeatureCount=4 -->
<FeatureRecord index="0">
<FeatureTag value="hlig"/>
<Feature>
<!-- LookupCount=1 -->
<LookupListIndex index="0" value="0"/>
</Feature>
</FeatureRecord>
<FeatureRecord index="1">
<FeatureTag value="scmp"/>
<Feature>
<!-- LookupCount=1 -->
<LookupListIndex index="0" value="3"/>
</Feature>
</FeatureRecord>
<FeatureRecord index="2">
<FeatureTag value="hlig"/>
<Feature>
<!-- LookupCount=1 -->
<LookupListIndex index="0" value="1"/>
</Feature>
</FeatureRecord>
<FeatureRecord index="3">
<FeatureTag value="liga"/>
<Feature>
<!-- LookupCount=1 -->
<LookupListIndex index="0" value="2"/>
</Feature>
</FeatureRecord>
</FeatureList>
<LookupList>
<!-- LookupCount=4 -->
<LigatureSubst index="0" Format="1">
<LigatureSet glyph="D">
<Ligature components="D,E,U" glyph="D_E_U"/>
</LigatureSet>
</LigatureSubst>
<LigatureSubst index="1" Format="1">
<LigatureSet glyph="F">
<Ligature components="F,R,A" glyph="D_E_U"/>
</LigatureSet>
</LigatureSubst>
<LigatureSubst index="2" Format="1">
<LigatureSet glyph="I">
<Ligature components="I,T,A" glyph="I_T_A"/>
</LigatureSet>
</LigatureSubst>
<SingleSubst index="3">
<Substitution in="a" out="A.sc"/>
<Substitution in="b" out="B.sc"/>
<Substitution in="c" out="C.sc"/>
<Substitution in="d" out="D.sc"/>
<Substitution in="e" out="E.sc"/>
<Substitution in="f" out="F.sc"/>
<Substitution in="g" out="G.sc"/>
<Substitution in="h" out="H.sc"/>
<Substitution in="i" out="I.sc"/>
<Substitution in="j" out="J.sc"/>
<Substitution in="k" out="K.sc"/>
<Substitution in="l" out="L.sc"/>
<Substitution in="m" out="M.sc"/>
<Substitution in="n" out="N.sc"/>
<Substitution in="o" out="O.sc"/>
<Substitution in="p" out="P.sc"/>
<Substitution in="q" out="Q.sc"/>
<Substitution in="r" out="R.sc"/>
<Substitution in="s" out="S.sc"/>
<Substitution in="t" out="T.sc"/>
<Substitution in="u" out="U.sc"/>
<Substitution in="v" out="V.sc"/>
<Substitution in="w" out="W.sc"/>
<Substitution in="x" out="X.sc"/>
<Substitution in="y" out="Y.sc"/>
<Substitution in="z" out="Z.sc"/>
</SingleSubst>
</LookupList>
</GSUB>
</GSUB>
<GPOS>
<GPOS>
<Version value="1.0"/>
<ScriptList>
<!-- ScriptCount=0 -->
</ScriptList>
<FeatureList>
<!-- FeatureCount=0 -->
</FeatureList>
<LookupList>
<!-- LookupCount=0 -->
</LookupList>
</GPOS>
</GPOS>
</ttFont>

View File

@ -0,0 +1,64 @@
# OpenType Feature File specification, section 4.h, example 1.
# http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html
languagesystem DFLT dflt;
languagesystem latn dflt;
languagesystem latn DEU;
languagesystem latn TRK;
languagesystem cyrl dflt;
feature smcp {
sub [a-z] by [A.sc-Z.sc];
# Since all the rules in this feature are of the same type, they
# will be grouped in a single lookup. Since no script or language
# keyword has been specified yet, the lookup will be registered
# for this feature under all the language systems.
} smcp;
feature liga {
sub f f by f_f;
sub f i by f_i;
sub f l by f_l;
# Since all the rules in this feature are of the same type, they
# will be grouped in a single lookup. Since no script or language
# keyword has been specified yet, the lookup will be registered
# for this feature under all the language systems.
script latn;
language dflt;
# lookupflag 0; (implicit)
sub c t by c_t;
sub c s by c_s;
# The rules above will be placed in a lookup that is registered
# for all the specified languages for the script latn, but not any
# other scripts.
language DEU;
# script latn; (stays the same)
# lookupflag 0; (stays the same)
sub c h by c_h;
sub c k by c_k;
# The rules above will be placed in a lookup that is registered
# only under the script latn, language DEU.
language TRK;
# This will inherit both the top level default rules - the rules
# defined before the first 'script' statement, and the
# script-level default rules for 'latn': all the lookups of this
# feature defined after the 'script latn' statement, and before
# the language DEU statement. If TRK were not named here, it
# would not inherit the default rules for the script latn.
} liga;
# TODO(sascha): Uncomment once we support 'pos' statements.
# feature kern {
# pos a y -150;
# # [more pos statements]
# # All the rules in this feature will be grouped in a single lookup
# # that is is registered under all the language systems.
# } kern;

View File

@ -0,0 +1,151 @@
<?xml version="1.0" encoding="UTF-8"?>
<ttFont>
<GSUB>
<GSUB>
<Version value="1.0"/>
<ScriptList>
<!-- ScriptCount=3 -->
<ScriptRecord index="0">
<ScriptTag value="DFLT"/>
<Script>
<DefaultLangSys>
<ReqFeatureIndex value="65535"/>
<!-- FeatureCount=2 -->
<FeatureIndex index="0" value="0"/>
<FeatureIndex index="1" value="1"/>
</DefaultLangSys>
<!-- LangSysCount=0 -->
</Script>
</ScriptRecord>
<ScriptRecord index="1">
<ScriptTag value="cyrl"/>
<Script>
<DefaultLangSys>
<ReqFeatureIndex value="65535"/>
<!-- FeatureCount=2 -->
<FeatureIndex index="0" value="0"/>
<FeatureIndex index="1" value="1"/>
</DefaultLangSys>
<!-- LangSysCount=0 -->
</Script>
</ScriptRecord>
<ScriptRecord index="2">
<ScriptTag value="latn"/>
<Script>
<DefaultLangSys>
<ReqFeatureIndex value="65535"/>
<!-- FeatureCount=2 -->
<FeatureIndex index="0" value="0"/>
<FeatureIndex index="1" value="1"/>
</DefaultLangSys>
<!-- LangSysCount=2 -->
<LangSysRecord index="0">
<LangSysTag value="DEU "/>
<LangSys>
<ReqFeatureIndex value="65535"/>
<!-- FeatureCount=2 -->
<FeatureIndex index="0" value="0"/>
<FeatureIndex index="1" value="1"/>
</LangSys>
</LangSysRecord>
<LangSysRecord index="1">
<LangSysTag value="TRK "/>
<LangSys>
<ReqFeatureIndex value="65535"/>
<!-- FeatureCount=2 -->
<FeatureIndex index="0" value="0"/>
<FeatureIndex index="1" value="1"/>
</LangSys>
</LangSysRecord>
</Script>
</ScriptRecord>
</ScriptList>
<FeatureList>
<!-- FeatureCount=2 -->
<FeatureRecord index="0">
<FeatureTag value="liga"/>
<Feature>
<!-- LookupCount=3 -->
<LookupListIndex index="0" value="1"/>
<LookupListIndex index="1" value="2"/>
<LookupListIndex index="2" value="3"/>
</Feature>
</FeatureRecord>
<FeatureRecord index="1">
<FeatureTag value="smcp"/>
<Feature>
<!-- LookupCount=1 -->
<LookupListIndex index="0" value="0"/>
</Feature>
</FeatureRecord>
</FeatureList>
<LookupList>
<!-- LookupCount=4 -->
<SingleSubst index="0">
<Substitution in="a" out="A.sc"/>
<Substitution in="b" out="B.sc"/>
<Substitution in="c" out="C.sc"/>
<Substitution in="d" out="D.sc"/>
<Substitution in="e" out="E.sc"/>
<Substitution in="f" out="F.sc"/>
<Substitution in="g" out="G.sc"/>
<Substitution in="h" out="H.sc"/>
<Substitution in="i" out="I.sc"/>
<Substitution in="j" out="J.sc"/>
<Substitution in="k" out="K.sc"/>
<Substitution in="l" out="L.sc"/>
<Substitution in="m" out="M.sc"/>
<Substitution in="n" out="N.sc"/>
<Substitution in="o" out="O.sc"/>
<Substitution in="p" out="P.sc"/>
<Substitution in="q" out="Q.sc"/>
<Substitution in="r" out="R.sc"/>
<Substitution in="s" out="S.sc"/>
<Substitution in="t" out="T.sc"/>
<Substitution in="u" out="U.sc"/>
<Substitution in="v" out="V.sc"/>
<Substitution in="w" out="W.sc"/>
<Substitution in="x" out="X.sc"/>
<Substitution in="y" out="Y.sc"/>
<Substitution in="z" out="Z.sc"/>
</SingleSubst>
<LigatureSubst index="1" Format="1">
<LigatureSet glyph="f">
<Ligature components="f,f" glyph="f_f"/>
<Ligature components="f,i" glyph="f_i"/>
<Ligature components="f,l" glyph="f_l"/>
</LigatureSet>
</LigatureSubst>
<LigatureSubst index="2" Format="1">
<LigatureSet glyph="c">
<Ligature components="c,s" glyph="c_s"/>
<Ligature components="c,t" glyph="c_t"/>
</LigatureSet>
</LigatureSubst>
<LigatureSubst index="3" Format="1">
<LigatureSet glyph="c">
<Ligature components="c,h" glyph="c_h"/>
<Ligature components="c,k" glyph="c_k"/>
</LigatureSet>
</LigatureSubst>
</LookupList>
</GSUB>
</GSUB>
<GPOS>
<GPOS>
<Version value="1.0"/>
<ScriptList>
<!-- ScriptCount=0 -->
</ScriptList>
<FeatureList>
<!-- FeatureCount=0 -->
</FeatureList>
<LookupList>
<!-- LookupCount=0 -->
</LookupList>
</GPOS>
</GPOS>
</ttFont>

View File

@ -0,0 +1,26 @@
# OpenType Feature File specification, section 5.d, example 1.
# http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html
feature F1 {
substitute [one one.oldstyle] [slash fraction] [two two.oldstyle] by onehalf;
} F1;
# Since the OpenType specification does not allow ligature substitutions
# to be specified on target sequences that contain glyph classes, the
# implementation software will enumerate all specific glyph sequences
# if glyph classes are detected in <glyph sequence>. Thus, the above
# example produces an identical representation in the font as if all
# the sequences were manually enumerated by the font editor:
feature F2 {
substitute one slash two by onehalf;
substitute one.oldstyle slash two by onehalf;
substitute one fraction two by onehalf;
substitute one.oldstyle fraction two by onehalf;
substitute one slash two.oldstyle by onehalf;
substitute one.oldstyle slash two.oldstyle by onehalf;
substitute one fraction two.oldstyle by onehalf;
substitute one.oldstyle fraction two.oldstyle by onehalf;
} F2;
# In the resulting OpenType GSUB table (spec5d1.ttx),
# we expect to see only one single lookup.

View File

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<ttFont>
<GSUB>
<GSUB>
<Version value="1.0"/>
<ScriptList>
<!-- ScriptCount=1 -->
<ScriptRecord index="0">
<ScriptTag value="DFLT"/>
<Script>
<DefaultLangSys>
<ReqFeatureIndex value="65535"/>
<!-- FeatureCount=2 -->
<FeatureIndex index="0" value="0"/>
<FeatureIndex index="1" value="1"/>
</DefaultLangSys>
<!-- LangSysCount=0 -->
</Script>
</ScriptRecord>
</ScriptList>
<FeatureList>
<!-- FeatureCount=2 -->
<FeatureRecord index="0">
<FeatureTag value="F1 "/>
<Feature>
<!-- LookupCount=1 -->
<LookupListIndex index="0" value="0"/>
</Feature>
</FeatureRecord>
<FeatureRecord index="1">
<FeatureTag value="F2 "/>
<Feature>
<!-- LookupCount=1 -->
<LookupListIndex index="0" value="0"/>
</Feature>
</FeatureRecord>
</FeatureList>
<LookupList>
<!-- LookupCount=1 -->
<LigatureSubst index="0" Format="1">
<LigatureSet glyph="one">
<Ligature components="one,fraction,two" glyph="onehalf"/>
<Ligature components="one,fraction,two.oldstyle" glyph="onehalf"/>
<Ligature components="one,slash,two" glyph="onehalf"/>
<Ligature components="one,slash,two.oldstyle" glyph="onehalf"/>
</LigatureSet>
<LigatureSet glyph="one.oldstyle">
<Ligature components="one.oldstyle,fraction,two" glyph="onehalf"/>
<Ligature components="one.oldstyle,fraction,two.oldstyle" glyph="onehalf"/>
<Ligature components="one.oldstyle,slash,two" glyph="onehalf"/>
<Ligature components="one.oldstyle,slash,two.oldstyle" glyph="onehalf"/>
</LigatureSet>
</LigatureSubst>
</LookupList>
</GSUB>
</GSUB>
<GPOS>
<GPOS>
<Version value="1.0"/>
<ScriptList>
<!-- ScriptCount=0 -->
</ScriptList>
<FeatureList>
<!-- FeatureCount=0 -->
</FeatureList>
<LookupList>
<!-- LookupCount=0 -->
</LookupList>
</GPOS>
</GPOS>
</ttFont>

View File

@ -0,0 +1,25 @@
# OpenType Feature File specification, section 5.d, example 2.
# http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html
# A contiguous set of ligature rules does not need to be ordered in
# any particular way by the font editor; the implementation software
# must do the appropriate sorting.
# So:
feature F1 {
sub f f by f_f;
sub f i by f_i;
sub f f i by f_f_i;
sub o f f i by o_f_f_i;
} F1;
# will produce an identical representation in the font as:
feature F2 {
sub o f f i by o_f_f_i;
sub f f i by f_f_i;
sub f f by f_f;
sub f i by f_i;
} F2;
# In the resulting OpenType GSUB table (spec5d2.ttx),
# we expect to see only one single lookup.

View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<ttFont>
<GSUB>
<GSUB>
<Version value="1.0"/>
<ScriptList>
<!-- ScriptCount=1 -->
<ScriptRecord index="0">
<ScriptTag value="DFLT"/>
<Script>
<DefaultLangSys>
<ReqFeatureIndex value="65535"/>
<!-- FeatureCount=2 -->
<FeatureIndex index="0" value="0"/>
<FeatureIndex index="1" value="1"/>
</DefaultLangSys>
<!-- LangSysCount=0 -->
</Script>
</ScriptRecord>
</ScriptList>
<FeatureList>
<!-- FeatureCount=2 -->
<FeatureRecord index="0">
<FeatureTag value="F1 "/>
<Feature>
<!-- LookupCount=1 -->
<LookupListIndex index="0" value="0"/>
</Feature>
</FeatureRecord>
<FeatureRecord index="1">
<FeatureTag value="F2 "/>
<Feature>
<!-- LookupCount=1 -->
<LookupListIndex index="0" value="0"/>
</Feature>
</FeatureRecord>
</FeatureList>
<LookupList>
<!-- LookupCount=1 -->
<LigatureSubst index="0" Format="1">
<LigatureSet glyph="f">
<Ligature components="f,f,i" glyph="f_f_i"/>
<Ligature components="f,f" glyph="f_f"/>
<Ligature components="f,i" glyph="f_i"/>
</LigatureSet>
<LigatureSet glyph="o">
<Ligature components="o,f,f,i" glyph="o_f_f_i"/>
</LigatureSet>
</LigatureSubst>
</LookupList>
</GSUB>
</GSUB>
<GPOS>
<GPOS>
<Version value="1.0"/>
<ScriptList>
<!-- ScriptCount=0 -->
</ScriptList>
<FeatureList>
<!-- FeatureCount=0 -->
</FeatureList>
<LookupList>
<!-- LookupCount=0 -->
</LookupList>
</GPOS>
</GPOS>
</ttFont>