summaryrefslogtreecommitdiff
path: root/scripts/lib
diff options
context:
space:
mode:
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()