142 lines
5.7 KiB
Python
142 lines
5.7 KiB
Python
#!/usr/bin/env python3
|
|
'''verify classes defined in xml have correct ordering where needed
|
|
|
|
Looks for comment lines in the classes.xml file that match the string:
|
|
*NEXT n CLASSES MUST MATCH*
|
|
where n is the number of upcoming class definitions that must result in the
|
|
same glyph alignment when glyph names are sorted by TTF order (as described
|
|
in the glyph_data.csv file).
|
|
'''
|
|
__url__ = 'https://github.com/silnrsi/pysilfont'
|
|
__copyright__ = 'Copyright (c) 2019 SIL International (https://www.sil.org)'
|
|
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
|
|
__author__ = 'Bob Hallissy'
|
|
|
|
import re
|
|
import types
|
|
from xml.etree import ElementTree as ET
|
|
from silfont.core import execute
|
|
|
|
argspec = [
|
|
('classes', {'help': 'class definition in XML format', 'nargs': '?', 'default': 'classes.xml'}, {'type': 'infile'}),
|
|
('glyphdata', {'help': 'Glyph info csv file', 'nargs': '?', 'default': 'glyph_data.csv'}, {'type': 'incsv'}),
|
|
('--gname', {'help': 'Column header for glyph name', 'default': 'glyph_name'}, {}),
|
|
('--sort', {'help': 'Column header(s) for sort order', 'default': 'sort_final'}, {}),
|
|
]
|
|
|
|
# Dictionary of glyphName : sortValue
|
|
sorts = dict()
|
|
|
|
# Keep track of glyphs mentioned in classes but not in glyph_data.csv
|
|
missingGlyphs = set()
|
|
|
|
def doit(args):
|
|
logger = args.logger
|
|
|
|
# Read input csv to get glyph sort order
|
|
incsv = args.glyphdata
|
|
fl = incsv.firstline
|
|
if fl is None: logger.log("Empty input file", "S")
|
|
if args.gname in fl:
|
|
glyphnpos = fl.index(args.gname)
|
|
else:
|
|
logger.log("No" + args.gname + "field in csv headers", "S")
|
|
if args.sort in fl:
|
|
sortpos = fl.index(args.sort)
|
|
else:
|
|
logger.log('No "' + args.sort + '" heading in csv headers"', "S")
|
|
next(incsv.reader, None) # Skip first line with containing headers
|
|
for line in incsv:
|
|
glyphn = line[glyphnpos]
|
|
if len(glyphn) == 0:
|
|
continue # No need to include cases where name is blank
|
|
sorts[glyphn] = float(line[sortpos])
|
|
|
|
# RegEx we are looking for in comments
|
|
matchCountRE = re.compile("\*NEXT ([1-9]\d*) CLASSES MUST MATCH\*")
|
|
|
|
# parse classes.xml but include comments
|
|
class MyTreeBuilder(ET.TreeBuilder):
|
|
def comment(self, data):
|
|
res = matchCountRE.search(data)
|
|
if res:
|
|
# record the count of classes that must match
|
|
self.start(ET.Comment, {})
|
|
self.data(res.group(1))
|
|
self.end(ET.Comment)
|
|
doc = ET.parse(args.classes, parser=ET.XMLParser(target=MyTreeBuilder())).getroot()
|
|
|
|
# process results looking for both class elements and specially formatted comments
|
|
matchCount = 0
|
|
refClassList = None
|
|
refClassName = None
|
|
|
|
for child in doc:
|
|
if isinstance(child.tag, types.FunctionType):
|
|
# Special type used for comments
|
|
if matchCount > 0:
|
|
logger.log("Unexpected match request '{}': matching {} is not yet complete".format(child.text, refClassName), "E")
|
|
ref = None
|
|
matchCount = int(child.text)
|
|
# print "Match count = {}".format(matchCount)
|
|
|
|
elif child.tag == 'class':
|
|
l = orderClass(child, logger) # Do this so we record classes whether we match them or not.
|
|
if matchCount > 0:
|
|
matchCount -= 1
|
|
className = child.attrib['name']
|
|
if refClassName is None:
|
|
refClassList = l
|
|
refLen = len(refClassList)
|
|
refClassName = className
|
|
else:
|
|
# compare ref list and l
|
|
if len(l) != refLen:
|
|
logger.log("Class {} (length {}) and {} (length {}) have unequal length".format(refClassName, refLen, className, len(l)), "E")
|
|
else:
|
|
errCount = 0
|
|
for i in range(refLen):
|
|
if l[i][0] != refClassList[i][0]:
|
|
logger.log ("Class {} and {} inconsistent order glyphs {} and {}".format(refClassName, className, refClassList[i][2], l[i][2]), "E")
|
|
errCount += 1
|
|
if errCount > 5:
|
|
logger.log ("Abandoning compare between Classes {} and {}".format(refClassName, className), "E")
|
|
break
|
|
if matchCount == 0:
|
|
refClassName = None
|
|
|
|
# List glyphs mentioned in classes.xml but not present in glyph_data:
|
|
if len(missingGlyphs):
|
|
logger.log('Glyphs mentioned in classes.xml but not present in glyph_data: ' + ', '.join(sorted(missingGlyphs)), 'W')
|
|
|
|
|
|
classes = {} # Keep record of all classes we've seen so we can flatten references
|
|
|
|
def orderClass(classElement, logger):
|
|
# returns a list of tuples, each containing (indexWithinClass, sortOrder, glyphName)
|
|
# list is sorted by sortOrder
|
|
glyphList = classElement.text.split()
|
|
res = []
|
|
for i in range(len(glyphList)):
|
|
token = glyphList[i]
|
|
if token.startswith('@'):
|
|
# Nested class
|
|
cname = token[1:]
|
|
if cname in classes:
|
|
res.extend(classes[cname])
|
|
else:
|
|
logger.log("Invalid fea: class {} referenced before being defined".format(cname),"S")
|
|
else:
|
|
# simple glyph name -- make sure it is in glyph_data:
|
|
if token in sorts:
|
|
res.append((i, sorts[token], token))
|
|
else:
|
|
missingGlyphs.add(token)
|
|
|
|
classes[classElement.attrib['name']] = res
|
|
return sorted(res, key=lambda x: x[1])
|
|
|
|
|
|
|
|
def cmd() : execute(None,doit,argspec)
|
|
if __name__ == "__main__": cmd()
|