summaryrefslogtreecommitdiff
path: root/scripts/lib
diff options
context:
space:
mode:
authorJames Godfrey-Kittle <jamesgk@google.com>2015-03-04 15:23:47 -0800
committerJames Godfrey-Kittle <jamesgk@google.com>2015-04-16 12:16:32 -0700
commit15a678c34e3606e65c436c77c74b04539e2e14bb (patch)
tree6e249c44eec066fb91435e516a8b8b1d5dc563c8 /scripts/lib
parent8946a70ea8811561170ba23611b5029963777e30 (diff)
Use feaTools in features.py.
Use an existing library for parsing OpenType features, which should be more robust (though this library does have some syntax parsing issues, addressed in our code).
Diffstat (limited to 'scripts/lib')
-rw-r--r--scripts/lib/fontbuild/Build.py8
-rwxr-xr-xscripts/lib/fontbuild/features.py258
2 files changed, 154 insertions, 112 deletions
diff --git a/scripts/lib/fontbuild/Build.py b/scripts/lib/fontbuild/Build.py
index a3c7d54..c524e7a 100644
--- a/scripts/lib/fontbuild/Build.py
+++ b/scripts/lib/fontbuild/Build.py
@@ -6,7 +6,7 @@ from fontbuild.convertCurves import glyphCurvesToQuadratic
from fontbuild.mitreGlyph import mitreGlyph
from fontbuild.generateGlyph import generateGlyph
from fontTools.misc.transform import Transform
-from fontbuild.features import generateFeatureFile, readFeatureFile, readGlyphClasses, writeFeatureFile
+from fontbuild.features import readFeatureFile, writeFeatureFile
from fontbuild.markFeature import GenerateFeature_mark
from fontbuild.mkmkFeature import GenerateFeature_mkmk
from fontbuild.decomposeGlyph import decomposeGlyph
@@ -142,8 +142,7 @@ class FontProject:
log(">> Generating glyphs")
generateGlyphs(f, self.diacriticList, self.adobeGlyphList)
log(">> Copying features")
- readGlyphClasses(f, self.ot_classes)
- readFeatureFile(f, self.basefont.features.text)
+ readFeatureFile(f, self.ot_classes + self.basefont.features.text)
log(">> Decomposing")
for gname in self.decompose:
if f.has_key(gname):
@@ -156,10 +155,9 @@ class FontProject:
if kern:
log(">> Generating kern classes")
- readGlyphClasses(f, self.ot_kerningclasses, update=False)
+ readFeatureFile(f, self.ot_kerningclasses)
log(">> Generating font files")
- generateFeatureFile(f)
ufoName = self.generateOutputPath(f, "ufo")
f.save(ufoName)
diff --git a/scripts/lib/fontbuild/features.py b/scripts/lib/fontbuild/features.py
index fb1f1c9..3b52f76 100755
--- a/scripts/lib/fontbuild/features.py
+++ b/scripts/lib/fontbuild/features.py
@@ -1,120 +1,164 @@
-"""Functions for parsing and validating RoboFab RFont feature files."""
-
-
import re
-
-# feature file syntax rules from:
-# http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html
-_glyphNameChars = r"[A-Za-z_][\w.]"
-_glyphName = r"%s{,30}" % _glyphNameChars
-_className = r"@%s{,29}" % _glyphNameChars
-_classValToken = (
- r"(?:%s|%s(?:\s*-\s*%s)?)" % (_className, _glyphName, _glyphName))
-_classVal = r"\[\s*(%s(?:\s+%s)*)\s*\]" % (_classValToken, _classValToken)
-_classDef = re.compile(r"(%s)\s*=\s*%s\s*;" % (_className, _classVal))
-_featureDef = re.compile(
- r"(feature\s+(?P<tag>[A-Za-z]{4})\s+\{.*?\}\s+(?P=tag)\s*;)",
- re.DOTALL)
-_subRuleToken = r"(?:%s|%s)'?" % (_glyphName, _className)
-_subRuleTokenList = (
- r"\[?\s*(%s(?:\s+%s)*)\s*\]?" % (_subRuleToken, _subRuleToken))
-_subRule = re.compile(
- r"(\s*)sub(?:stitute)?\s+%s\s+by\s+%s\s*;" %
- (_subRuleTokenList, _subRuleTokenList))
-_systemDef = re.compile(r"languagesystem\s+([A-Za-z]+)\s+([A-Za-z]+)\s*;")
-_comment = re.compile(r"\s*#.*")
+from feaTools import parser
+from feaTools.writers.fdkSyntaxWriter import FDKSyntaxFeatureWriter
+
+
+# fix some regular expressions used by feaTools
+# we may want to push these fixes upstream
+# allow dashes in glyph class content, for glyph ranges
+parser.classDefinitionRE = re.compile(
+ "([\s;\{\}]|^)" # whitepace, ; {, } or start of line
+ "@" # @
+ "([\w\d_.]+)" # name
+ "\s*=\s*" # =
+ "\[" # [
+ "([\w\d\s\-_.@]+)" # content
+ "\]" # ]
+ "\s*;" # ;
+ , re.M
+ )
+parser.classContentRE = re.compile(
+ "([\w\d\-_.@]+)"
+ )
+
+# allow apostrophes in feature/lookup content
+parser.sequenceInlineClassRE = re.compile(
+ "\[" # [
+ "([\w\d\s_.@']+)" # content
+ "\]" # ]
+ )
+
+# allow apostrophes in the target and replacement of a substitution
+parser.subType1And4RE = re.compile(
+ "([\s;\{\}]|^)" # whitepace, ; {, } or start of line
+ "substitute|sub\s+" # sub
+ "([\w\d\s_.@\[\]']+)" # target
+ "\s+by\s+" # by
+ "([\w\d\s_.@\[\]']+)" # replacement
+ "\s*;" # ;
+ )
+
+# don't be greedy when matching feature/lookup content (may be duplicates)
+parser.featureContentRE[3] = parser.featureContentRE[3].replace('*', '*?')
+parser.lookupContentRE[3] = parser.lookupContentRE[3].replace('*', '*?')
+
+
+class FilterFeatureWriter(FDKSyntaxFeatureWriter):
+ """Feature writer to detect invalid references and duplicate definitions."""
+
+ def __init__(self, refs=set(), name=None, isFeature=False):
+ """Initializes the set of known references, empty by default."""
+ self.refs = refs
+ self.featureNames = set()
+ self.lookupNames = set()
+ self.languageSystems = set()
+ super(FilterFeatureWriter, self).__init__(
+ name=name, isFeature=isFeature)
+
+ # error to print when undefined reference is found in glyph class
+ self.classErr = ('Undefined reference "%s" removed from glyph class '
+ 'definition %s.')
+
+ # error to print when undefined reference is found in sub or pos rule
+ subErr = ['Substitution rule with undefined reference "%s" removed']
+ if self._name:
+ subErr.append(" from ")
+ subErr.append("feature" if self._isFeature else "lookup")
+ subErr.append(' "%s"' % self._name)
+ subErr.append(".")
+ self.subErr = "".join(subErr)
+ self.posErr = self.subErr.replace("Substitution", "Positioning")
+
+ def _subwriter(self, name, isFeature):
+ """Use this class for nested expressions e.g. in feature definitions."""
+ return FilterFeatureWriter(self.refs, name, isFeature)
+
+ def _checkRefs(self, refs, errorMsg):
+ """Check a list of references found in a sub or pos rule."""
+ for ref in refs:
+ # trailing apostrophes should be ignored
+ if ref[-1] == "'":
+ ref = ref[:-1]
+ if ref not in self.refs:
+ print errorMsg % ref
+ # insert an empty instruction so that we can't end up with an
+ # empty block, which is illegal syntax
+ super(FilterFeatureWriter, self).rawText(";")
+ return False
+ return True
+
+ def classDefinition(self, name, contents):
+ """Check that contents are valid, then add name to known references."""
+ if name in self.refs:
+ return
+ newContents = []
+ for ref in contents:
+ if ref not in self.refs and ref != "-":
+ print self.classErr % (ref, name)
+ else:
+ newContents.append(ref)
+ self.refs.add(name)
+ super(FilterFeatureWriter, self).classDefinition(name, newContents)
+
+ def gsubType1(self, target, replacement):
+ """Check a sub rule with one-to-one replacement."""
+ if type(target) == str:
+ target, replacement = [target], [replacement]
+ if self._checkRefs(target + replacement, self.subErr):
+ super(FilterFeatureWriter, self).gsubType1(target, replacement)
+
+ def gsubType4(self, target, replacement):
+ """Check a sub rule with many-to-one replacement."""
+ if self._checkRefs(target + [replacement], self.subErr):
+ super(FilterFeatureWriter, self).gsubType4(target, replacement)
+
+ def gposType1(self, target, value):
+ """Check a positioning rule."""
+ if self._checkRefs([target], self.posErr):
+ super(FilterFeatureWriter, self).gposType1(target, value)
+
+ # these rules may contain references, but they aren't present in Roboto
+ def gsubType3(self, target, replacement):
+ raise NotImplementedError
+ def gsubType6(self, precedingContext, target, trailingContext, replacement):
+ raise NotImplementedError
+ def gposType2(self, target, value):
+ raise NotImplementedError
+
+ def feature(self, name):
+ """Adds a feature definition only once."""
+ if name not in self.featureNames:
+ self.featureNames.add(name)
+ return super(FilterFeatureWriter, self).feature(name)
+ # we must return a new writer even if we don't add it to this one
+ return FDKSyntaxFeatureWriter(name, True)
+
+ def lookup(self, name):
+ """Adds a lookup block only once."""
+ if name not in self.lookupNames:
+ self.lookupNames.add(name)
+ return super(FilterFeatureWriter, self).lookup(name)
+ # we must return a new writer even if we don't add it to this one
+ return FDKSyntaxFeatureWriter(name, False)
+
+ def languageSystem(self, langTag, scriptTag):
+ """Adds a language system instruction only once."""
+ system = (langTag, scriptTag)
+ if system not in self.languageSystems:
+ self.languageSystems.add(system)
+ super(FilterFeatureWriter, self).languageSystem(langTag, scriptTag)
def readFeatureFile(font, text):
"""Incorporate valid definitions from feature text into font."""
-
- readGlyphClasses(font, text)
- text = "\n".join([l for l in text.splitlines() if not _comment.match(l)])
-
- # filter out substitution rules with invalid references
- errorMsg = "feature definition %s (substitution rule removed)"
- if not hasattr(font.features, "tags"):
- font.features.tags = []
- font.features.values = {}
- for value, tag in _featureDef.findall(text):
- lines = value.splitlines()
- for i in range(len(lines)):
- match = _subRule.match(lines[i])
- if not match:
- continue
- indentation, subbed, sub = match.groups()
- refs = subbed.split() + sub.split()
- invalid = None
- for ref in refs:
- if ref[-1] == "'":
- ref = ref[:-1]
- if not invalid and not _isValidRef(errorMsg % tag, ref, font):
- invalid = ref
- if invalid:
- lines[i] = ("%s; # substitution rule removed for invalid "
- "reference %s" % (indentation, invalid))
- font.features.tags.append(tag)
- font.features.values[tag] = "\n".join(lines)
-
-
-def readGlyphClasses(font, text, update=True):
- """Incorporate valid glyph classes from feature text into font."""
-
- text = "\n".join([l for l in text.splitlines() if not _comment.match(l)])
-
- # filter out invalid references from glyph class definitions
- errorMsg = "glyph class definition %s (reference removed)"
- if not hasattr(font, "classNames"):
- font.classNames = []
- font.classVals = {}
- for name, value in _classDef.findall(text):
- if name in font.classNames:
- if not update:
- continue
- font.classNames.remove(name)
- refs = value.split()
- refs = [r for r in refs if _isValidRef(errorMsg % name, r, font)]
- font.classNames.append(name)
- font.classVals[name] = " ".join(refs)
-
- if not hasattr(font, "languageSystems"):
- font.languageSystems = []
- for system in _systemDef.findall(text):
- if system not in font.languageSystems:
- font.languageSystems.append(system)
-
-
-def _isValidRef(referencer, ref, font):
- """Check if a reference is valid for a font."""
-
- if ref.startswith("@"):
- if not font.classVals.has_key(ref):
- print "Undefined class %s referenced in %s." % (ref, referencer)
- return False
- else:
- for r in ref.split("-"):
- if r and not font.has_key(r):
- print "Undefined glyph %s referenced in %s." % (r, referencer)
- return False
- return True
-
-
-def generateFeatureFile(font):
- """Populate a font's feature file text from its classes and features."""
-
- classes = "\n".join(
- ["%s = [%s];" % (n, font.classVals[n]) for n in font.classNames])
- systems = "\n".join(
- ["languagesystem %s %s;" % (s[0], s[1]) for s in font.languageSystems])
- fea = "\n\n".join([font.features.values[t] for t in font.features.tags])
- font.features.text = "\n\n".join([classes, systems, fea])
+ writer = FilterFeatureWriter(set(font.keys()))
+ parser.parseFeatures(writer, text + font.features.text)
+ font.features.text = writer.write()
def writeFeatureFile(font, path):
"""Write the font's features to an external file."""
-
- generateFeatureFile(font)
fout = open(path, "w")
fout.write(font.features.text)
fout.close()