manifest.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. import json
  2. import os
  3. from orx.orx import Orx
  4. import orx.parse
  5. import util
  6. class Manifest(Orx):
  7. """
  8. Class representing all of the data within the input manifest.
  9. """
  10. def __init__(self, homedir='.', filename=None, string=None):
  11. self.homedir = homedir
  12. self.classes = {}
  13. self.colormaps = {}
  14. self.defines = {}
  15. self.emoji = []
  16. self.palettes = {}
  17. self.shortcodes = {}
  18. self.codepoints = {}
  19. self.license = {}
  20. # get the actual data for this stuff
  21. if filename is not None:
  22. self.load_and_parse(filename)
  23. def add_emoji(self, emoji):
  24. """
  25. Adds emoji to the manifest class. Will check for duplicate shortcodes
  26. and codepoints and will throw an error if there are any.
  27. """
  28. self.emoji.append(emoji)
  29. if 'short' in emoji:
  30. if emoji['short'] in self.shortcodes:
  31. raise ValueError('Shortcode already in use: ' + emoji['short'])
  32. self.shortcodes[emoji['short']] = emoji
  33. if 'code' in emoji and '!' not in emoji['code']:
  34. if emoji['code'] in self.codepoints:
  35. raise ValueError('Codepoint already in use: ' +
  36. util.uni_to_hex_hash(emoji['code']))
  37. self.codepoints[emoji['code']] = emoji
  38. def compile_emoji(self, kwargs, color=None):
  39. """
  40. Takes the basic output of `exec_emoji`, validates it, and returns a completed, compiled emoji object.
  41. kwargs: parameters from the manifest
  42. color: an attached colour for recolouring. (This is given by exec_emoji())
  43. """
  44. res = dict(kwargs)
  45. # if the input doesn't have colour, then the output doesnt
  46. if not color and 'color' in res:
  47. del res['color']
  48. elif color:
  49. # make sure whatever colour this emoji has is in the manifest.
  50. if color not in self.colormaps:
  51. raise ValueError('Undefined colormap: ' + color)
  52. res['color'] = color
  53. # formatting shortcode substitution
  54. # (for parameter values)
  55. #
  56. # this is performed first before other checks and stuff are performed to
  57. # the parameter contents.
  58. for k, v in res.items():
  59. # CM shortcode insertion (basic)
  60. if '%c' in v:
  61. if not color:
  62. raise ValueError('%c without colormap')
  63. try:
  64. res[k] = v.replace('%c', self.colormaps[color]['short'])
  65. except KeyError:
  66. raise ValueError('Shortcode not defined for colormap: ' +
  67. color)
  68. # CM shortcode insertion (prepends and underscore if the shortcode isn't empty)
  69. if '%C' in v:
  70. if not color:
  71. raise ValueError('%C without colormap')
  72. try:
  73. subst = self.colormaps[color]['short']
  74. except KeyError:
  75. raise ValueError('Shortcode not defined for colormap: ' +
  76. color)
  77. if subst:
  78. subst = '_' + subst
  79. res[k] = v.replace('%C', subst)
  80. # CM codepoint insertion (basic)
  81. if '%u' in v:
  82. if not color:
  83. raise ValueError('%u without colormap')
  84. try:
  85. res[k] = v.replace('%u', self.colormaps[color]['code'])
  86. except KeyError:
  87. raise ValueError('Codepoint not defined for colormap: ' +
  88. color)
  89. # CM codepoint insertion (prepend ZWJ if the shortcoode isn't empty)
  90. if '%U' in v:
  91. if not color:
  92. raise ValueError('%U without colormap')
  93. try:
  94. color_code = self.colormaps[color]['code']
  95. except KeyError:
  96. raise ValueError('Codepoint not defined for colormap: ' +
  97. color)
  98. if color_code:
  99. res[k] = v.replace('%U', '#200D ' + color_code)
  100. else:
  101. res[k] = v.replace('%U', '')
  102. # param insertion
  103. # %(<param>)
  104. idx = 0
  105. while idx < len(v):
  106. idx = v.find('%(', idx)
  107. if idx == -1:
  108. break
  109. end = v.find(')', idx+2)
  110. if end == -1:
  111. raise ValueError('No matching parenthesis')
  112. prop = v[idx+2:end]
  113. if prop not in res:
  114. raise ValueError('Undefined property: ' + prop)
  115. res[k] = v[:idx] + res[prop] + v[end+1:]
  116. idx += 1
  117. v = res[k]
  118. if 'code' in res:
  119. # it's either explicitly empty, or it's not
  120. if '!' in res['code']:
  121. res['code'] = '!'
  122. else:
  123. # attempt to interpret each part of the codepoint sequence as an int
  124. codeseq_list = []
  125. for codepoint in res['code'].split():
  126. try:
  127. if codepoint[0] == '#':
  128. codeseq_list.append(int(codepoint[1:], 16))
  129. else:
  130. codeseq_list.append(int(codepoint))
  131. except ValueError:
  132. raise ValueError('Expected a number: ' + codepoint)
  133. res['code'] = tuple(codeseq_list)
  134. if 'desc' in res:
  135. # insert color modifier name at the end of the description.
  136. # ie. 'thumbs up (dark skin tone)'
  137. if color:
  138. try:
  139. color_desc = self.colormaps[color]['desc']
  140. except KeyError:
  141. raise ValueError('Description not defined for colormap: ' +
  142. color)
  143. if color_desc:
  144. res['desc'] += f' ({color_desc})'
  145. if 'bundle' in res and color:
  146. # replace the emoji bundle with the color bundle.
  147. # if the colormap doesn't have a bundle, do nothing.
  148. if 'bundle' in self.colormaps[color]:
  149. res['bundle'] = self.colormaps[color]['bundle']
  150. # assume the shortcode is the same as the root if there are no modifiers going on.
  151. if 'root' not in res and not color and 'morph' not in res and 'short' in res:
  152. res['root'] = res['short']
  153. return res
  154. def exec_class(self, args, kwargs):
  155. """
  156. Executes an orx manifest `class` statement.
  157. """
  158. if not args:
  159. raise ValueError('Missing id')
  160. if args[0] in self.classes:
  161. raise ValueError('Already defined: ' + args[0])
  162. if 'class' in kwargs:
  163. raise ValueError('Illegal recursion in class definition')
  164. res = {}
  165. for parent in args[1:]:
  166. if parent not in self.classes:
  167. raise ValueError('Parent class is undefined: ' + parent)
  168. res.update(self.classes[parent])
  169. res.update(kwargs)
  170. self.classes[args[0]] = res
  171. def exec_colormap(self, args, kwargs):
  172. """
  173. Executes an orx manifest `colormap` statement.
  174. """
  175. if not args:
  176. raise ValueError('Missing id')
  177. if len(args) > 1:
  178. raise ValueError('Multiple ids')
  179. if args[0] in self.colormaps:
  180. raise ValueError('Already defined: ' + args[0])
  181. if 'src' not in kwargs:
  182. raise ValueError('Missing src')
  183. if 'dst' not in kwargs:
  184. raise ValueError('Missing dst')
  185. if kwargs['src'] not in self.palettes:
  186. raise ValueError('Undefined source palette: ' + kwargs['src'])
  187. if kwargs['dst'] not in self.palettes:
  188. raise ValueError('Undefined target palette: ' + kwargs['dst'])
  189. self.colormaps[args[0]] = kwargs
  190. def exec_emoji(self, args, kwargs):
  191. """
  192. Executes an orx manifest `emoji` statement.
  193. """
  194. emoji_args = {}
  195. for c in kwargs.get('class', '').split():
  196. if c not in self.classes:
  197. raise ValueError('Undefined class: ' + c)
  198. emoji_args.update(self.classes[c])
  199. emoji_args.update(kwargs)
  200. if 'src' not in emoji_args:
  201. raise ValueError('Missing src')
  202. # if the emoji has a 'color' parameter, duplicate it based on
  203. # the number of colourmaps are in that parameter, and attach
  204. # those colormaps to the dupes.
  205. if 'color' in emoji_args:
  206. for color in emoji_args['color'].split():
  207. self.add_emoji(self.compile_emoji(emoji_args, color))
  208. else:
  209. self.add_emoji(self.compile_emoji(emoji_args))
  210. def exec_license(self, args, kwargs):
  211. """
  212. Executes an orx manifest `license` statement.
  213. (Takes a license statement, verifies it and stores it in the manifest structure.)
  214. """
  215. for k, v in kwargs.items():
  216. path = os.path.join(self.homedir, v)
  217. # try to load the license files that were given.
  218. if k == 'svg':
  219. try:
  220. self.license['svg'] = open(path, 'r').read()
  221. except OSError:
  222. raise Exception('Failed to load license file: ' + path)
  223. elif k == 'exif':
  224. try:
  225. self.license['exif'] = json.load(open(path, 'r'))
  226. except OSError:
  227. raise Exception('Failed to load license file: ' + path)
  228. except ValueError:
  229. raise ValueError('Failed to parse JSON in file: ' + path)
  230. def exec_palette(self, args, kwargs):
  231. """
  232. Executes an orx manifest `palette` statement.
  233. (Takes a palette statement, verifies it and stores it in the manifest structure.)
  234. """
  235. if not args:
  236. raise ValueError('Missing id')
  237. if len(args) > 1:
  238. raise ValueError('Multiple ids')
  239. if args[0] in self.palettes:
  240. raise ValueError('Already defined: ' + args[0])
  241. self.palettes[args[0]] = kwargs
  242. def exec_expr(self, expr):
  243. """
  244. Executes an orx manifest expression.
  245. """
  246. # finds all of the constants from `define` expressions and fills
  247. # in their actual value
  248. final_expr = orx.parse.subst_consts(expr, self.defines)
  249. # now parse all of the expressions
  250. #
  251. # head: the expression name ('emoji', 'define', 'color', etc.)
  252. # args: the arguments without parameters
  253. # kwargs: the arguments with parameters
  254. try:
  255. head, args, kwargs = orx.parse.parse_expr(final_expr)
  256. except Exception:
  257. raise ValueError('Syntax error')
  258. # execute each expression based on the head.
  259. # (or do nothing if there's no head)
  260. if head is None:
  261. return
  262. elif head == 'define':
  263. self.exec_define(args, kwargs)
  264. elif head == 'include':
  265. self.exec_include(args, kwargs)
  266. elif head == 'class':
  267. self.exec_class(args, kwargs)
  268. elif head == 'colormap':
  269. self.exec_colormap(args, kwargs)
  270. elif head == 'emoji':
  271. self.exec_emoji(args, kwargs)
  272. elif head == 'license':
  273. self.exec_license(args, kwargs)
  274. elif head == 'palette':
  275. self.exec_palette(args, kwargs)
  276. else:
  277. raise ValueError('Unknown expression type: ' + head)