/usr/lib/python3/dist-packages/glyphsLib/parser.py is in python3-glyphslib 2.2.1-1.
This file is owned by root:root, with mode 0o644.
The actual contents of the file can be viewed below.
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 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 | # 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.
from __future__ import (print_function, division, absolute_import,
unicode_literals)
from fontTools.misc.py23 import tounicode, unichr, unicode
from collections import OrderedDict
from io import open
import re
import logging
import sys
import glyphsLib
logger = logging.getLogger(__name__)
class Parser(object):
"""Parses Python dictionaries from Glyphs source files."""
value_re = r'(".*?(?<!\\)"|[-_./$A-Za-z0-9]+)'
start_dict_re = re.compile(r'\s*{')
end_dict_re = re.compile(r'\s*}')
dict_delim_re = re.compile(r'\s*;')
start_list_re = re.compile(r'\s*\(')
end_list_re = re.compile(r'\s*\)')
list_delim_re = re.compile(r'\s*,')
attr_re = re.compile(r'\s*%s\s*=' % value_re, re.DOTALL)
value_re = re.compile(r'\s*%s' % value_re, re.DOTALL)
def __init__(self, current_type=OrderedDict):
self.current_type = current_type
def parse(self, text):
"""Do the parsing."""
text = tounicode(text, encoding='utf-8')
result, i = self._parse(text, 0)
if text[i:].strip():
self._fail('Unexpected trailing content', text, i)
return result
def parse_into_object(self, res, text):
"""Parse data into an existing GSFont instance."""
text = tounicode(text, encoding='utf-8')
m = self.start_dict_re.match(text, 0)
if m:
i = self._parse_dict_into_object(res, text, 1)
else:
self._fail('not correct file format', text, i)
if text[i:].strip():
self._fail('Unexpected trailing content', text, i)
return i
def _guess_current_type(self, parsed, value):
if value.lower() in ('infinity', 'inf', 'nan'):
# Those values would be accepted by `float()`
# But `infinity` is a glyph name
return unicode
if parsed[-1] != '"':
try:
float_val = float(value)
if float_val.is_integer():
current_type = int
else:
current_type = float
except:
current_type = unicode
else:
current_type = unicode
return current_type
def _parse(self, text, i):
"""Recursive function to parse a single dictionary, list, or value."""
m = self.start_dict_re.match(text, i)
if m:
parsed = m.group(0)
i += len(parsed)
return self._parse_dict(text, i)
m = self.start_list_re.match(text, i)
if m:
parsed = m.group(0)
i += len(parsed)
return self._parse_list(text, i)
m = self.value_re.match(text, i)
if m:
parsed, value = m.group(0), self._trim_value(m.group(1))
i += len(parsed)
if hasattr(self.current_type, "read"):
reader = self.current_type()
# Give the escaped value to `read` to be symetrical with
# `plistValue` which handles the escaping itself.
value = reader.read(m.group(1))
return value, i
if (self.current_type is None or
self.current_type in (dict, OrderedDict)):
self.current_type = self._guess_current_type(parsed, value)
if self.current_type == bool:
value = bool(int(value)) # bool(u'0') returns True
return value, i
value = self.current_type(value)
return value, i
else:
self._fail('Unexpected content', text, i)
def _parse_dict(self, text, i):
"""Parse a dictionary from source text starting at i."""
old_current_type = self.current_type
new_type = self.current_type
if new_type is None:
# customparameter.value needs to be set from the found value
new_type = dict
elif type(new_type) == list:
new_type = new_type[0]
res = new_type()
i = self._parse_dict_into_object(res, text, i)
self.current_type = old_current_type
return res, i
def _parse_dict_into_object(self, res, text, i):
end_match = self.end_dict_re.match(text, i)
while not end_match:
old_current_type = self.current_type
m = self.attr_re.match(text, i)
if not m:
self._fail('Unexpected dictionary content', text, i)
parsed, name = m.group(0), self._trim_value(m.group(1))
if hasattr(res, "classForName"):
self.current_type = res.classForName(name)
i += len(parsed)
result = self._parse(text, i)
try:
res[name], i = result
except:
res = {} # ugly, this fixes nested dicts in customparameters
res[name], i = result
m = self.dict_delim_re.match(text, i)
if not m:
self._fail('Missing delimiter in dictionary before content',
text, i)
parsed = m.group(0)
i += len(parsed)
end_match = self.end_dict_re.match(text, i)
self.current_type = old_current_type
parsed = end_match.group(0)
i += len(parsed)
return i
def _parse_list(self, text, i):
"""Parse a list from source text starting at i."""
res = []
end_match = self.end_list_re.match(text, i)
old_current_type = self.current_type
while not end_match:
list_item, i = self._parse(text, i)
res.append(list_item)
end_match = self.end_list_re.match(text, i)
if not end_match:
m = self.list_delim_re.match(text, i)
if not m:
self._fail('Missing delimiter in list before content',
text, i)
parsed = m.group(0)
i += len(parsed)
self.current_type = old_current_type
parsed = end_match.group(0)
i += len(parsed)
return res, i
# glyphs only supports octal escapes between \000 and \077 and hexadecimal
# escapes between \U0000 and \UFFFF
_unescape_re = re.compile(r'(\\0[0-7]{2})|(\\U[0-9a-fA-F]{4})')
@staticmethod
def _unescape_fn(m):
if m.group(1):
return unichr(int(m.group(1)[1:], 8))
return unichr(int(m.group(2)[2:], 16))
def _trim_value(self, value):
"""Trim double quotes off the ends of a value, un-escaping inner
double quotes.
Also convert escapes to unicode.
"""
if value[0] == '"':
assert value[-1] == '"'
value = value[1:-1].replace('\\"', '"')
return Parser._unescape_re.sub(Parser._unescape_fn, value)
def _fail(self, message, text, i):
"""Raise an exception with given message and text at i."""
raise ValueError('%s:\n%s' % (message, text[i:i + 79]))
def load(fp):
"""Read a .glyphs file. 'fp' should be (readable) file object.
Return a GSFont object.
"""
return loads(fp.read())
def loads(s):
"""Read a .glyphs file from a (unicode) str object, or from
a UTF-8 encoded bytes object.
Return a GSFont object.
"""
p = Parser(current_type=glyphsLib.classes.GSFont)
logger.info('Parsing .glyphs file')
data = p.parse(s)
return data
def main(args=None):
"""Roundtrip the .glyphs file given as an argument."""
for arg in args:
glyphsLib.dump(load(open(arg, 'r', encoding='utf-8')), sys.stdout)
if __name__ == '__main__':
main(sys.argv[1:])
|