from spec.models import XMLElement, JSONObject
from spec.utils.relative_url import get_relative_url
import xml.sax
import json
INDENT_SIZE = 3
DIFF_ELEMENT = 'metadiff'
class DiffElementContentHandler(xml.sax.handler.ContentHandler):
def __init__(self, diffs_use_divs, *args, **kwargs):
super().__init__(*args, **kwargs)
self.diffs_use_divs = diffs_use_divs
self.result = []
self.pending_diff_class = None
self.saw_diff = False
def handle_start_diff_element(self):
self.pending_diff_class = 'diff'
self.saw_diff = True
def handle_end_diff_element(self):
self.pending_diff_class = 'nodiff'
def get_pending_diff_markup(self):
if self.pending_diff_class:
if self.diffs_use_divs:
result = f'
'
else:
result = f'
'
self.pending_diff_class = None
else:
result = ''
return result
def get_result(self):
html = '\n'.join(self.result)
extraclass = ' nodiff' if self.saw_diff else ''
if self.diffs_use_divs:
return f''
else:
return f''
class XMLAugmenter(DiffElementContentHandler):
def __init__(self, schema, current_url, diffs_use_divs, *args, **kwargs):
super().__init__(diffs_use_divs, *args, **kwargs)
self.schema = schema
self.current_url = current_url
self.element_stack = []
self.last_tag_opened = None
self.last_tag_opened_stack_size = 0
self.current_characters = []
self.preserve_whitespace = False
def get_element_obj(self, name):
xml_elements = XMLElement.objects.filter(schema=self.schema, name=name, is_abstract_element=False)
if self.element_stack:
if self.element_stack[-1]:
filtered_elements = []
for xml_element in xml_elements:
parents = [x.id for x in xml_element.get_parent_elements()]
if self.element_stack[-1] in parents:
filtered_elements.append(xml_element)
xml_elements = filtered_elements
else: # Previous element ID is None.
xml_elements = []
try:
obj = xml_elements[0]
except IndexError:
obj = None
return obj
def get_attribute_markup(self, element_obj, attrs):
result = []
if element_obj:
attribute_objs = element_obj.get_attributes()
for k, v in attrs.items():
start_tag = ''
end_tag = ''
if element_obj:
try:
attr_obj = [a for a in attribute_objs if a.name == k][0]
except IndexError:
attr_obj = None
if attr_obj:
start_tag = f''
end_tag = ''
result.append(f' {k}="{start_tag}{v}{end_tag}"')
return ''.join(result)
def startElement(self, name, attrs):
if name == DIFF_ELEMENT:
self.handle_start_diff_element()
return
obj = self.get_element_obj(name)
if obj:
start_tag = f''
end_tag = ''
else:
start_tag = ''
end_tag = ''
if attrs:
attr_string = self.get_attribute_markup(obj, attrs)
else:
attr_string = ''
self.preserve_whitespace = attrs.get('xml:space', '') == 'preserve'
space = ' ' * len(self.element_stack) * INDENT_SIZE
diff_html = self.get_pending_diff_markup()
self.result.append(f'{diff_html}{space}<{start_tag}{name}{end_tag}{attr_string}>')
self.last_tag_opened_stack_size = len(self.element_stack)
self.element_stack.append(obj.id if obj else None)
self.last_tag_opened = name
self.current_characters = []
def characters(self, content):
if not self.preserve_whitespace:
content = content.strip()
if content:
self.current_characters.append(content)
def endElement(self, name):
self.preserve_whitespace = False
if self.current_characters:
space = ' ' * len(self.element_stack) * INDENT_SIZE
content = ''.join(self.current_characters)
self.result.append(f'{space}{content}')
self.current_characters = []
if name == DIFF_ELEMENT:
self.handle_end_diff_element()
return
del self.element_stack[-1]
is_immediately_closing = self.last_tag_opened == name and len(self.element_stack) == self.last_tag_opened_stack_size
if is_immediately_closing and 'class="tag"' in self.result[-1]:
# As a nicety, make the element self-closing rather than
# outputting a separate closing element.
self.result[-1] = self.result[-1].replace('>', '/>')
else:
html = ''
if is_immediately_closing and 'class="xmltxt"' in self.result[-1]:
# If the element is immediately closing and contained text,
# put the text on the same line as the element.
# For example:
# Music
# Instead of:
#
# Music
#
element_contents = self.result.pop().strip()
html = self.result.pop() + element_contents
obj = self.get_element_obj(name)
if obj:
start_tag = f''
end_tag = ''
else:
start_tag = ''
end_tag = ''
if not html:
space = ' ' * len(self.element_stack) * INDENT_SIZE
else:
space = ''
diff_html = self.get_pending_diff_markup()
self.result.append(f'{html}{diff_html}{space}</{start_tag}{name}{end_tag}>')
def get_augmented_example(current_url, schema, raw_document, diffs_use_divs=True):
if schema.is_json:
return get_augmented_example_json(current_url, schema, raw_document, diffs_use_divs)
else:
return get_augmented_example_xml(current_url, schema, raw_document, diffs_use_divs)
def get_augmented_example_json(current_url, schema, raw_document, diffs_use_divs=True):
saw_diff = False # TODO: Implement this.
result = get_augmented_example_json_inner(
current_url,
json.loads(raw_document),
JSONObject.objects.get(schema=schema, name=JSONObject.ROOT_OBJECT_NAME),
indent_level=0
)
output_html = []
collapse_next = False
for indent_level, text, collapse in result:
if collapse_next and output_html:
output_html[-1] += ' ' + text
else:
output_html.append((' ' * INDENT_SIZE * indent_level) + str(text))
collapse_next = collapse
return (saw_diff, '' + '\n'.join(output_html) + '
')
def json_key_sorter(x):
"""
A sorting function (suitable for passing as the 'key' argument
to sorted()) that always puts the values "id", "mnx" and "type" first,
in that order.
We use this because it helps make the docs clearer if these keys
are listed first within a given object.
"""
return (x != 'id', x != 'mnx', x != 'type', x)
def get_augmented_example_json_inner(current_url, json_data, object_def=None, indent_level=0, add_comma=False):
result = []
if object_def is None:
result.append([
indent_level,
f'{json.dumps(json_data)}',
False
])
elif isinstance(json_data, (dict, list)) and not json_data:
# Special case: For empty dicts or empty lists, use "{}" and "[]"
# rather than splitting the opening/closing symbols over two lines.
result.append([indent_level, json.dumps(json_data), False])
elif isinstance(json_data, dict):
child_rels = {r.child_key: r for r in object_def.get_child_relationships()}
result.append([indent_level, '{', False])
keys = list(sorted(json_data.keys(), key=json_key_sorter))
for i, key in enumerate(keys):
result.append([
indent_level + 1,
f'"{key}":',
True
])
result.extend(get_augmented_example_json_inner(
current_url,
json_data[key],
child_rels[key].child if key in child_rels else None,
indent_level + 1,
add_comma=i != len(keys) - 1
))
result.append([indent_level, '}', False])
elif isinstance(json_data, list):
child_object_defs = [c.child for c in object_def.get_child_relationships()]
result.append([indent_level, '[', False])
for i, child_obj in enumerate(json_data):
child_object_def = JSONObject.get_jsonobject_for_data(child_obj, child_object_defs)
result.extend(get_augmented_example_json_inner(
current_url,
child_obj,
child_object_def,
indent_level + 1,
add_comma=i != len(json_data) - 1
))
result.append([indent_level, ']', False])
else:
value = json.dumps(json_data)
if object_def.has_docs_page():
value = f'{value}'
result.append([indent_level, value, False])
if add_comma:
result[-1][1] += ','
return result
def get_augmented_example_xml(current_url, schema, xml_string, diffs_use_divs=True):
reader = xml.sax.make_parser()
handler = XMLAugmenter(schema, current_url, diffs_use_divs)
xml.sax.parseString(xml_string, handler)
return (handler.saw_diff, handler.get_result())
class XMLPrettifier(DiffElementContentHandler):
def __init__(self, diffs_use_divs, *args, **kwargs):
super().__init__(diffs_use_divs, *args, **kwargs)
self.indent_level = 0
self.last_tag_opened = None
def startElement(self, name, attrs):
if name == DIFF_ELEMENT:
self.handle_start_diff_element()
return
html = [
self.get_pending_diff_markup(),
' ' * self.indent_level * INDENT_SIZE,
f'<{name}'
]
if attrs:
html.extend(f' {k}="{v}"' for (k, v) in attrs.items())
html.append('>')
self.result.append(''.join(html))
self.indent_level += 1
self.last_tag_opened = name
def characters(self, content):
if content and content.strip():
self.result.append((' ' * self.indent_level * INDENT_SIZE) + content.strip())
def endElement(self, name):
if name == DIFF_ELEMENT:
self.handle_end_diff_element()
return
self.indent_level -= 1
result = self.result
if name == self.last_tag_opened:
previous_line = result[-1].strip()
if previous_line.startswith('<'):
result[-1] += f'</{name}>'
else:
result[-2] += previous_line + f'</{name}>'
del result[-1]
else:
html = [
self.get_pending_diff_markup(),
' ' * self.indent_level * INDENT_SIZE,
f'</{name}>'
]
result.append(''.join(html))
def get_prettified_xml(xml_string):
reader = xml.sax.make_parser()
handler = XMLPrettifier(diffs_use_divs=True)
xml.sax.parseString(xml_string, handler)
return handler.get_result()
def htmlescape(html:str):
return html.replace('<', '<').replace('>', '>')
def get_element_subtree(current_url:str, el:XMLElement, els_seen:list):
el_url = get_relative_url(current_url, el.get_absolute_url())
result = [
f'{htmlescape(el.name_with_brackets())}'
]
if el.id not in els_seen:
els_seen.append(el.id)
children = el.get_child_elements()
if children:
result.append('')
for child in children:
result.append('- ')
result.extend(get_element_subtree(current_url, child, els_seen))
result.append('
')
result.append('
')
els_seen.pop(-1)
else:
result.append('(recursive)')
return result
def get_element_tree_html(current_url:str, root_el:XMLElement):
result = ['']
result.extend(get_element_subtree(current_url, root_el, []))
result.append('
')
return '\n'.join(result)