json_encoders.py 6.4 KB

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