189 lines
7.4 KiB
Python
Executable file
189 lines
7.4 KiB
Python
Executable file
# Copyright 2015 Google Inc. All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
|
|
import re
|
|
|
|
from feaTools import parser
|
|
from feaTools.writers.fdkSyntaxWriter import FDKSyntaxFeatureWriter
|
|
|
|
|
|
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.tableNames = 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 _flattenRefs(self, refs, flatRefs):
|
|
"""Flatten a list of references."""
|
|
for ref in refs:
|
|
if type(ref) == list:
|
|
self._flattenRefs(ref, flatRefs)
|
|
elif ref != "'": # ignore contextual class markings
|
|
flatRefs.append(ref)
|
|
|
|
def _checkRefs(self, refs, errorMsg):
|
|
"""Check a list of references found in a sub or pos rule."""
|
|
flatRefs = []
|
|
self._flattenRefs(refs, flatRefs)
|
|
for ref in flatRefs:
|
|
# 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 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 gsubType6(self, precedingContext, target, trailingContext, replacement):
|
|
"""Check a sub rule with contextual replacement."""
|
|
refs = [precedingContext, target, trailingContext, replacement]
|
|
if self._checkRefs(refs, self.subErr):
|
|
super(FilterFeatureWriter, self).gsubType6(
|
|
precedingContext, target, trailingContext, replacement)
|
|
|
|
def gposType1(self, target, value):
|
|
"""Check a single positioning rule."""
|
|
if self._checkRefs([target], self.posErr):
|
|
super(FilterFeatureWriter, self).gposType1(target, value)
|
|
|
|
def gposType2(self, target, value, needEnum=False):
|
|
"""Check a pair positioning rule."""
|
|
if self._checkRefs(target, self.posErr):
|
|
super(FilterFeatureWriter, self).gposType2(target, value, needEnum)
|
|
|
|
# these rules may contain references, but they aren't present in Roboto
|
|
def gsubType3(self, target, replacement):
|
|
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 table(self, name, data):
|
|
"""Adds a table only once."""
|
|
if name in self.tableNames:
|
|
return
|
|
self.tableNames.add(name)
|
|
self._instructions.append("table %s {" % name)
|
|
self._instructions.extend([" %s %s;" % line for line in data])
|
|
self._instructions.append("} %s;" % name)
|
|
|
|
|
|
def compileFeatureRE(name):
|
|
"""Compiles a feature-matching regex."""
|
|
|
|
# this is the pattern used internally by feaTools:
|
|
# https://github.com/typesupply/feaTools/blob/master/Lib/feaTools/parser.py
|
|
featureRE = list(parser.featureContentRE)
|
|
featureRE.insert(2, name)
|
|
featureRE.insert(6, name)
|
|
return re.compile("".join(featureRE))
|
|
|
|
|
|
def updateFeature(font, name, value):
|
|
"""Add a feature definition, or replace existing one."""
|
|
featureRE = compileFeatureRE(name)
|
|
if featureRE.search(font.features.text):
|
|
font.features.text = featureRE.sub(value, font.features.text)
|
|
else:
|
|
font.features.text += "\n" + value
|
|
|
|
|
|
def readFeatureFile(font, text, prepend=True):
|
|
"""Incorporate valid definitions from feature text into font."""
|
|
writer = FilterFeatureWriter(set(font.keys()))
|
|
if prepend:
|
|
text += font.features.text
|
|
else:
|
|
text = font.features.text + text
|
|
parser.parseFeatures(writer, text)
|
|
font.features.text = writer.write()
|
|
|
|
|
|
def writeFeatureFile(font, path):
|
|
"""Write the font's features to an external file."""
|
|
fout = open(path, "w")
|
|
fout.write(font.features.text)
|
|
fout.close()
|