json_encoders.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. """Class that pretty-prints QMK info.json files.
  2. """
  3. import json
  4. from decimal import Decimal
  5. _sentinel = object()
  6. newline = '\n'
  7. class QMKJSONEncoder(json.JSONEncoder):
  8. """Base class for all QMK JSON encoders.
  9. """
  10. container_types = (list, tuple, dict)
  11. indentation_char = " "
  12. def __init__(self, *args, **kwargs):
  13. super().__init__(*args, **kwargs)
  14. self.indentation_level = 0
  15. if not self.indent:
  16. self.indent = 4
  17. def encode_decimal(self, obj):
  18. """Encode a decimal object.
  19. """
  20. if obj == int(obj): # I can't believe Decimal objects don't have .is_integer()
  21. return int(obj)
  22. return float(obj)
  23. def encode_dict(self, obj, path):
  24. """Encode a dict-like object.
  25. """
  26. if obj:
  27. self.indentation_level += 1
  28. items = sorted(obj.items(), key=self.sort_dict) if self.sort_keys else obj.items()
  29. output = [self.indent_str + f"{json.dumps(key)}: {self.encode(value, path + [key])}" for key, value in items]
  30. self.indentation_level -= 1
  31. return "{\n" + ",\n".join(output) + "\n" + self.indent_str + "}"
  32. else:
  33. return "{}"
  34. def encode_dict_single_line(self, obj, path):
  35. """Encode a dict-like object onto a single line.
  36. """
  37. return "{" + ", ".join(f"{json.dumps(key)}: {self.encode(value, path + [key])}" for key, value in sorted(obj.items(), key=self.sort_layout)) + "}"
  38. def encode_list(self, obj, path):
  39. """Encode a list-like object.
  40. """
  41. if self.primitives_only(obj):
  42. return "[" + ", ".join(self.encode(value, path + [index]) for index, value in enumerate(obj)) + "]"
  43. else:
  44. self.indentation_level += 1
  45. if path[-1] in ('layout', 'rotary'):
  46. # These are part of a LED layout or encoder config, put them on a single line
  47. output = [self.indent_str + self.encode_dict_single_line(value, path + [index]) for index, value in enumerate(obj)]
  48. else:
  49. output = [self.indent_str + self.encode(value, path + [index]) for index, value in enumerate(obj)]
  50. self.indentation_level -= 1
  51. return "[\n" + ",\n".join(output) + "\n" + self.indent_str + "]"
  52. def encode(self, obj, path=_sentinel):
  53. """Encode JSON objects for QMK.
  54. """
  55. if path is _sentinel:
  56. path = []
  57. if isinstance(obj, Decimal):
  58. return self.encode_decimal(obj)
  59. elif isinstance(obj, (list, tuple)):
  60. return self.encode_list(obj, path)
  61. elif isinstance(obj, dict):
  62. return self.encode_dict(obj, path)
  63. else:
  64. return super().encode(obj)
  65. def primitives_only(self, obj):
  66. """Returns true if the object doesn't have any container type objects (list, tuple, dict).
  67. """
  68. if isinstance(obj, dict):
  69. obj = obj.values()
  70. return not any(isinstance(element, self.container_types) for element in obj)
  71. @property
  72. def indent_str(self):
  73. return self.indentation_char * (self.indentation_level * self.indent)
  74. class InfoJSONEncoder(QMKJSONEncoder):
  75. """Custom encoder to make info.json's a little nicer to work with.
  76. """
  77. def sort_layout(self, item):
  78. """Sorts the hashes in a nice way.
  79. """
  80. key = item[0]
  81. if key == 'label':
  82. return '00label'
  83. elif key == 'matrix':
  84. return '01matrix'
  85. elif key == 'x':
  86. return '02x'
  87. elif key == 'y':
  88. return '03y'
  89. elif key == 'w':
  90. return '04w'
  91. elif key == 'h':
  92. return '05h'
  93. elif key == 'flags':
  94. return '06flags'
  95. return key
  96. def sort_dict(self, item):
  97. """Forces layout to the back of the sort order.
  98. """
  99. key = item[0]
  100. if self.indentation_level == 1:
  101. if key == 'manufacturer':
  102. return '10manufacturer'
  103. elif key == 'keyboard_name':
  104. return '11keyboard_name'
  105. elif key == 'maintainer':
  106. return '12maintainer'
  107. elif key == 'community_layouts':
  108. return '97community_layouts'
  109. elif key == 'layout_aliases':
  110. return '98layout_aliases'
  111. elif key == 'layouts':
  112. return '99layouts'
  113. else:
  114. return '50' + str(key)
  115. return key
  116. class KeymapJSONEncoder(QMKJSONEncoder):
  117. """Custom encoder to make keymap.json's a little nicer to work with.
  118. """
  119. def encode_list(self, obj, path):
  120. """Encode a list-like object.
  121. """
  122. if self.indentation_level == 2:
  123. indent_level = self.indentation_level + 1
  124. # We have a list of keycodes
  125. layer = [[]]
  126. for key in obj:
  127. if key == 'JSON_NEWLINE':
  128. layer.append([])
  129. else:
  130. if isinstance(key, dict):
  131. # We have a macro
  132. # TODO: Add proper support for nicely formatting keymap.json macros
  133. layer[-1].append(f'{self.encode(key)}')
  134. else:
  135. layer[-1].append(f'"{key}"')
  136. layer = [f"{self.indent_str * indent_level}{', '.join(row)}" for row in layer]
  137. return f"{self.indent_str}[\n{newline.join(layer)}\n{self.indent_str * self.indentation_level}]"
  138. elif self.primitives_only(obj):
  139. return "[" + ", ".join(self.encode(element) for element in obj) + "]"
  140. else:
  141. self.indentation_level += 1
  142. output = [self.indent_str + self.encode(element) for element in obj]
  143. self.indentation_level -= 1
  144. return "[\n" + ",\n".join(output) + "\n" + self.indent_str + "]"
  145. def sort_dict(self, item):
  146. """Sorts the hashes in a nice way.
  147. """
  148. key = item[0]
  149. if self.indentation_level == 1:
  150. if key == 'version':
  151. return '00version'
  152. elif key == 'author':
  153. return '01author'
  154. elif key == 'notes':
  155. return '02notes'
  156. elif key == 'layers':
  157. return '98layers'
  158. elif key == 'documentation':
  159. return '99documentation'
  160. else:
  161. return '50' + str(key)
  162. return key
  163. class UserspaceJSONEncoder(QMKJSONEncoder):
  164. """Custom encoder to make userspace qmk.json's a little nicer to work with.
  165. """
  166. def sort_dict(self, item):
  167. """Sorts the hashes in a nice way.
  168. """
  169. key = item[0]
  170. if self.indentation_level == 1:
  171. if key == 'userspace_version':
  172. return '00userspace_version'
  173. if key == 'build_targets':
  174. return '01build_targets'
  175. return key
  176. class CommunityModuleJSONEncoder(QMKJSONEncoder):
  177. """Custom encoder to make qmk_module.json's a little nicer to work with.
  178. """
  179. def sort_dict(self, item):
  180. """Sorts the hashes in a nice way.
  181. """
  182. key = item[0]
  183. if self.indentation_level == 1:
  184. if key == 'module_name':
  185. return '00module_name'
  186. if key == 'maintainer':
  187. return '01maintainer'
  188. if key == 'license':
  189. return '02license'
  190. if key == 'url':
  191. return '03url'
  192. if key == 'features':
  193. return '04features'
  194. if key == 'keycodes':
  195. return '05keycodes'
  196. elif self.indentation_level == 3: # keycodes
  197. if key == 'key':
  198. return '00key'
  199. if key == 'aliases':
  200. return '01aliases'
  201. return key