-
Notifications
You must be signed in to change notification settings - Fork 0
/
generate.py
254 lines (196 loc) · 7.92 KB
/
generate.py
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
251
252
253
254
"""
This file is the actual generation script for the website. Run this to generate
a new index.html, then preview in your browser.
"""
from textwrap import indent
import tag_builder as tb
import json
import traceback
def smart_get(data, key, definitions, default=None):
"""
Returns the value of a key in a dictionary, or a default value if the key does not exist.
Has the ability to follow schema references *but only if the schema follows a special format*.
"""
result = data.get('properties', {}).get(key, {})
if "$ref" in result.keys():
result = definitions.get(result.get('$ref').split('/')[-1], {})
return result
def force_array(data):
"""
Returns a list of data, even if it is a single item.
"""
if isinstance(data, list):
return data
else:
return [data]
def resolve_schema_references(schema, definitions):
"""
Resolves the references of a schema, at a single 'level'.
"""
if isinstance(schema, dict) and "$ref" in schema.keys():
schema = definitions.get(schema.get("$ref").split("/")[-1], {})
return schema
def type_convert(type):
"""
Converts a type to a more readable format.
"""
type_map = {
"number": "float",
None: "filter",
}
return type_map.get(type, type)
def fetch_compound_types(schema):
"""
Returns a list of compound types in a schema.
This is a form of "look ahead", where we look at the next schema
to see what types we are dealing with.
"""
compound_types = []
for option in schema.get("oneOf"):
compound_types.append(type_convert(option.get("type")))
return compound_types
def get_type_name(schema_branch):
"""
Returns the type display-name of the schema branch.
"""
simple_type = schema_branch.get("type")
if simple_type == "string" and "enum" in schema_branch:
return "exact-string"
if schema_branch.get("oneOf"):
return "compound"
elif simple_type == "string":
return "string"
elif simple_type == "number":
return "float"
elif simple_type == "boolean":
return "boolean"
elif simple_type == "object":
return "object"
elif simple_type == "array":
return "array"
elif simple_type == "integer":
return "integer"
else:
return "filter"
def generate_recursive(parent: tb.TagBuilder, property_name: str, schema: dict, indent: int, inside_array, definitions : dict, classes, full_path, required=False) -> tb.TagBuilder:
"""
Recursive method to generate html based on a json schema.
"""
try:
schema = resolve_schema_references(schema, definitions)
# TODO: Handle this being an array
data_type = get_type_name(schema)
description = schema.get('description', 'Unknown Description')
default = schema.get('default', None)
# The 'tag' is the head of the HTML object that we will fill.
tag = parent.insert_tag('div', style=f"indent indent-{indent} {' '.join(classes)} {full_path}")
# Comment
tag.append_tag("div", f"# {description}", style="token comment spacer")
if default:
tag.append_tag("div", f"# Default: {default}", style="token comment")
if "enum" in schema:
tag.append_tag("div", f"# Options: {schema.get('enum')}", style="token comment")
if required:
tag.append_tag("div", f"# Required: True", style="token comment")
if "items" in schema and isinstance(schema["items"], list):
tag.append_tag("div", f"# Must contain exactly {len(schema.get('items'))} elements.", style="token comment")
is_one_of = schema.get('oneOf') is not None
# Property
if not inside_array:
tag.append_tag("span", f'"{property_name}"', style='token property')
tag.append_tag("span", ":", style="token operator")
elif not is_one_of:
tag.append_tag("span", f'{data_type}', style=f"token {data_type}")
# Compound types
if is_one_of:
# A preview of the compound types
compound_types = fetch_compound_types(schema)
first = True
for compound_type in compound_types:
if not first:
tag.append_tag("span", "or", style="token comment")
first = False
tag.append_tag("span", f"{compound_type}", style=f"token {compound_type}")
# Recurse into each compound type
inner_tag = tb.TagBuilder("div", style="compound-block")
for option in schema.get("oneOf"):
generate_recursive(inner_tag, property_name, option, indent + 1, True, definitions, ["compound"], full_path, required=False)
tag.insert(inner_tag)
return tag
# Simple Types, which are not compound
if data_type != 'object' and data_type != 'array':
if not inside_array:
tag.append_tag("span", f'{data_type}', style=f"token {data_type}")
# Object Types
if data_type == 'object':
tag.append_tag("span", '{', style='token punctuation open-block')
tag.append_tag("span", '...', style='token comment invisible extender')
required_list = schema.get('required', [])
# The generated child properties
inner_tag =tb.TagBuilder("div", style="block")
for property_name, value in schema.get('properties', {}).items():
required = property_name in required_list
generate_recursive(inner_tag, property_name, value, indent + 1, False, definitions, ["object"], f"{full_path}.{property_name}", required=required)
tag.insert(inner_tag)
# The final part
tag.append_tag("span", '}', style='token punctuation close-block')
# Array Types
if data_type == 'array':
items = schema.get('items', [])
tag.append_tag("span", '[', style='token punctuation open-block')
tag.append_tag("span", '...', style='token comment invisible extender')
# The generated child properties
inner_tag =tb.TagBuilder("div", style="block")
for item in force_array(items):
generate_recursive(inner_tag, property_name, item, indent + 1, True, definitions, ["array"], full_path, required=False)
tag.insert(inner_tag)
# The final part
tag.append_tag("span", ']', style='token punctuation close-block')
return tag
except Exception as e:
print("Failed on: ", property_name)
print(e)
print(traceback.format_exc())
return parent.insert_tag("div", f"unknown: {e}", style="token unknown indent")
def generate_html(data, definitions):
"""
Expects a dictionary containing component names and schema data
"""
components = tb.TagBuilder("div").decorate("class", "site-content")
for component_name, schema in data.get('properties').items():
schema = resolve_schema_references(schema, definitions)
component = components.insert_tag("div", style=f"component {component_name}")
title_row = component.insert_tag("div" , style="title-row")
title_row.insert_tag("h3", component_name, style="title").decorate("id", component_name)
title_row.insert_tag("a", "#", style="anchor").decorate("href", "#" + component_name)
button_row = component.insert_tag("span", style="button-row")
button_row.insert_tag("a", "bedrock.dev", style="code-container-button"). \
decorate("href", f"https://bedrock.dev/docs/stable/Entities#{component_name.replace(':', '%3A')}")
# Put copy button, if possible
examples = force_array(schema.get('examples', []))
if len(examples) > 0:
example_json = examples[0]
if example_json:
example_json = f'"{component_name}": {json.dumps(example_json, indent=2)}'
button_row.insert_tag("button", "copy", style="code-container-button copy-button")
button_row.insert_tag("span", str(example_json), style="hidden")
code_container = component.insert_tag("div", style="code-container")
code = code_container.insert_tag("code", style="code")
generate_recursive(code, component_name, schema, 0, False, definitions, [], component_name, required=False)
with open('templates/base.html', 'r') as f:
html = f.read()
html = html.replace('<gen/>', components.generate())
return html
def main():
print("Running generation script. Open docs/index.html to view.")
with open('schemas/schema.json') as f:
schema = json.load(f)
# TODO make this smart-get smarter.
definitions = schema.get('definitions')
data = smart_get(schema, 'minecraft:entity', definitions)
data = smart_get(data, 'components', definitions)
html = generate_html(data, definitions)
with open('docs/index.html', 'w') as f:
f.write(html)
if __name__ == "__main__":
main()