Merge pull request #350 from brawer/feaLib
[feaLib] Implement GSUB lookup types 1, 3 and 4
This commit is contained in:
commit
91197a58a7
@ -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
|
||||
|
353
Lib/fontTools/feaLib/builder.py
Normal file
353
Lib/fontTools/feaLib/builder.py
Normal 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
|
256
Lib/fontTools/feaLib/builder_test.py
Normal file
256
Lib/fontTools/feaLib/builder_test.py
Normal 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()
|
16
Lib/fontTools/feaLib/error.py
Normal file
16
Lib/fontTools/feaLib/error.py
Normal 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
|
18
Lib/fontTools/feaLib/error_test.py
Normal file
18
Lib/fontTools/feaLib/error_test.py
Normal 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()
|
@ -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)
|
||||
|
@ -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__":
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
13
Lib/fontTools/feaLib/testdata/GSUB_3.fea
vendored
Normal file
13
Lib/fontTools/feaLib/testdata/GSUB_3.fea
vendored
Normal 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;
|
74
Lib/fontTools/feaLib/testdata/GSUB_3.ttx
vendored
Normal file
74
Lib/fontTools/feaLib/testdata/GSUB_3.ttx
vendored
Normal 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>
|
21
Lib/fontTools/feaLib/testdata/language_required.fea
vendored
Normal file
21
Lib/fontTools/feaLib/testdata/language_required.fea
vendored
Normal 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;
|
136
Lib/fontTools/feaLib/testdata/language_required.ttx
vendored
Normal file
136
Lib/fontTools/feaLib/testdata/language_required.ttx
vendored
Normal 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>
|
64
Lib/fontTools/feaLib/testdata/spec4h1.fea
vendored
Normal file
64
Lib/fontTools/feaLib/testdata/spec4h1.fea
vendored
Normal 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;
|
151
Lib/fontTools/feaLib/testdata/spec4h1.ttx
vendored
Normal file
151
Lib/fontTools/feaLib/testdata/spec4h1.ttx
vendored
Normal 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>
|
26
Lib/fontTools/feaLib/testdata/spec5d1.fea
vendored
Normal file
26
Lib/fontTools/feaLib/testdata/spec5d1.fea
vendored
Normal 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.
|
74
Lib/fontTools/feaLib/testdata/spec5d1.ttx
vendored
Normal file
74
Lib/fontTools/feaLib/testdata/spec5d1.ttx
vendored
Normal 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>
|
25
Lib/fontTools/feaLib/testdata/spec5d2.fea
vendored
Normal file
25
Lib/fontTools/feaLib/testdata/spec5d2.fea
vendored
Normal 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.
|
70
Lib/fontTools/feaLib/testdata/spec5d2.ttx
vendored
Normal file
70
Lib/fontTools/feaLib/testdata/spec5d2.ttx
vendored
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user