summaryrefslogtreecommitdiffstats
path: root/lib/silfont/scripts/psfcheckclassorders.py
blob: 12654c97a2e4e41321a7e4f652a65436b8d01b94 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
#!/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__ = 'http://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2019 SIL International  (http://www.sil.org)'
__license__ = 'Released under the MIT License (http://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()