Skip to content

Commit

Permalink
Improved new docs system MusicXML comparisons to highlight relevant s…
Browse files Browse the repository at this point in the history
…ections.

The /comparisons/musicxml/ page now has the fancy
'Show relevant section' vs. 'Show full document' switcher
for all relevant examples. This was ported from the old
MNX By Example page and was the remaining feature to port.

The markup for that old page was done manually, in an
error-prone way. This new system automatically handles
outputting the markup correctly, based on the data in the
document. It's rather fussy due to being a combination of
HTML-escaped XML elements, proper HTML markup for formatting/
links and the new HTML markup for locating the diffs.

It works by looking for a special element <metadiff> in the
MNX and MusicXML examples. If an example has at least one
<metadiff>, it will automatically get the diff UI on the
/comparisons/musicxml/ page. Any sections of the document
wrapped within <metadiff> </metadiff> will get highlighted
as the 'relevant sections' of the example.

All ExampleDocuments in the system have had the <metadiff>
elements added appropriately, using the old MNX By Example
page as a guide.

One subtle change: In the previous page, we had arbitrary
control over where the diffs started and ended, but this
new page requires the diffs to start/end cleanly around
an XML element. Thus it's no longer possible to highlight
just a starting element without its accompanying end
element. But that's not a big deal. We can always add an
extra hook to highlight start elements (or attributes)
if needed.

The example_detail pages are unaffected by this change.
They do not display 'Relevant sections.' This is an area
for potential future improvement.
  • Loading branch information
adrianholovaty committed Feb 4, 2021
1 parent 028c8b9 commit 8a02d35
Show file tree
Hide file tree
Showing 100 changed files with 928 additions and 485 deletions.
100 changes: 50 additions & 50 deletions docgenerator/data.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docgenerator/media/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ p, ul, table {
margin-right: auto;
}
.xmlmarkup {
display: block;
white-space: pre;
font: 14px "courier new",courier,fixed-width;
color: #333;
Expand Down
2 changes: 2 additions & 0 deletions docgenerator/spec/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,7 @@
{% block content %}{% endblock %}
</div>
{% endblock %}

{% block finalscript %}{% endblock %}
</body>
</html>
2 changes: 1 addition & 1 deletion docgenerator/spec/templates/example_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@ <h1>MNX example: {{ example.name }}</h1>
<p>See also: {% for comparison in comparisons %}<a href="{% relative_url_string comparison.get_absolute_url %}">This same example in {{ comparison.doc_format.name }}</a> {% endfor %}</p>
{% endif %}

<div class="xmlmarkup">{{ augmented_doc|safe }}</div>
{{ augmented_doc|safe }}
{% endblock %}
41 changes: 37 additions & 4 deletions docgenerator/spec/templates/format_comparison_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
.showui { margin: 0 0 15px; }
.onlydiff .nodiff { font-size: 0; }
.onlydiff .nodiff:after { content: "..."; font-size: 18px; }
.markupexamples:not(.onlydiff) .diff { font-weight: bold; color: blue; }
.markupexamples:not(.onlydiff) .diff, .markupexamples:not(.onlydiff) .diff * { font-weight: bold; color: blue !important; }
</style>
{% endblock %}

Expand All @@ -75,6 +75,7 @@ <h1>Comparing MNX and {{ other_format.name }}</h1>
<p>On this page, you’ll find several examples of documents encoded both in MNX and {{ other_format.name }}. The goal is to give you a sense of the differences, especially if you’re already familiar with {{ other_format.name }}.</p>
</section>

<form id="comparisonform">
{% for comparison in comparisons %}
<section class="example" id="{{ comparison.example.slug }}">
<div class="center">
Expand All @@ -83,21 +84,53 @@ <h2>{{ comparison.example.name }}</h2>
{% if comparison.preamble_html %}
{{ comparison.preamble_html|safe }}
{% endif %}
{% if comparison.highlight_diffs %}
<p class="showui">Show:
<label><input type="radio" name="toggle{{ forloop.counter }}" class="switchdiff" data-example="{{ forloop.counter }}" value="1" checked> Relevant section</label>
<label><input type="radio" name="toggle{{ forloop.counter }}" class="switchdiff" data-example="{{ forloop.counter }}" value="2"> Full document</label>
</p>
{% endif %}
</div>
<div class="markupexamples">
<div class="markupexamples{% if comparison.highlight_diffs %} onlydiff{% endif %}" id="markupexamples{{ forloop.counter }}">
<div class="markupexample">
<h3>{{ other_format.name }}</h3>
<div class="markupcode">
<div class="xmlmarkup">{{ comparison.other_document_html|safe }}</div>
{{ comparison.other_document_html|safe }}
</div>
</div>
<div class="markupexample">
<h3>MNX</h3>
<div class="markupcode">
<div class="xmlmarkup">{{ comparison.document_html|safe }}</div>
{{ comparison.document_html|safe }}
</div>
</div>
</div>
</section>
{% endfor %}
</form>
{% endblock %}

{% block finalscript %}
<script>
function toggleDiff(e) {
var onlyDiff = e.target.value === '1',
exampleNumber = e.target.dataset.example,
exampleEl = document.getElementById('markupexamples' + exampleNumber);
if (onlyDiff) {
exampleEl.classList.add('onlydiff');
}
else {
exampleEl.classList.remove('onlydiff');
}
}

document.addEventListener('DOMContentLoaded', function() {
var els = document.querySelectorAll('.switchdiff'),
i = els.length;
while (i--) {
els[i].addEventListener('change', toggleDiff);
}
document.getElementById('comparisonform').reset();
});
</script>
{% endblock %}
5 changes: 4 additions & 1 deletion docgenerator/spec/utils/datautils.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
from spec.models import ExampleDocumentElement
import xml.sax

ELEMENTS_TO_IGNORE = {'metadiff'}

class ElementCollector(xml.sax.handler.ContentHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.result = set()

def startElement(self, name, attrs):
self.result.add(name)
if name not in ELEMENTS_TO_IGNORE:
self.result.add(name)

def update_example_elements(example):
"""
Expand Down
85 changes: 68 additions & 17 deletions docgenerator/spec/utils/htmlutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,42 @@
import xml.sax

INDENT_SIZE = 3
DIFF_ELEMENT = 'metadiff'

class XMLAugmenter(xml.sax.handler.ContentHandler):
def __init__(self, current_url, *args, **kwargs):
class DiffElementContentHandler(xml.sax.handler.ContentHandler):
def __init__(self, add_diffs, *args, **kwargs):
super().__init__(*args, **kwargs)
self.current_url = current_url
self.add_diffs = add_diffs
self.result = []
self.pending_diff_class = None
self.saw_diff = False

def handle_start_diff_element(self):
if self.add_diffs:
self.pending_diff_class = 'diff'
self.saw_diff = True

def handle_end_diff_element(self):
if self.add_diffs:
self.pending_diff_class = 'nodiff'

def get_pending_diff_markup(self):
if self.pending_diff_class:
result = f'</div><div class="xmlmarkup {self.pending_diff_class}">'
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 ''
return f'<div class="xmlmarkup{extraclass}">{html}</div>'

class XMLAugmenter(DiffElementContentHandler):
def __init__(self, current_url, add_diffs, *args, **kwargs):
super().__init__(add_diffs, *args, **kwargs)
self.current_url = current_url
self.element_stack = []
self.last_tag_opened = None
self.last_tag_opened_stack_size = 0
Expand Down Expand Up @@ -46,6 +76,9 @@ def get_attribute_markup(self, element_obj, attrs):
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'<a class="tag" href="{get_relative_url(self.current_url, obj.get_absolute_url())}">'
Expand All @@ -58,7 +91,8 @@ def startElement(self, name, attrs):
else:
attr_string = ''
space = ' ' * len(self.element_stack) * INDENT_SIZE
self.result.append(f'{space}&lt;{start_tag}{name}{end_tag}{attr_string}&gt;')
diff_html = self.get_pending_diff_markup()
self.result.append(f'{diff_html}{space}&lt;{start_tag}{name}{end_tag}{attr_string}&gt;')
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
Expand All @@ -69,6 +103,9 @@ def characters(self, content):
self.result.append(f'{space}<span class="xmltxt">{content.strip()}</span>')

def endElement(self, name):
if name == DIFF_ELEMENT:
self.handle_end_diff_element()
return
del self.element_stack[-1]
if self.last_tag_opened == name and 'class="tag"' in self.result[-1] and len(self.element_stack) == self.last_tag_opened_stack_size:
# As a nicety, make the element self-closing rather than
Expand All @@ -83,24 +120,30 @@ def endElement(self, name):
start_tag = '<span class="tag">'
end_tag = '</span>'
space = ' ' * len(self.element_stack) * INDENT_SIZE
self.result.append(f'{space}&lt;/{start_tag}{name}{end_tag}&gt;')
diff_html = self.get_pending_diff_markup()
self.result.append(f'{diff_html}{space}&lt;/{start_tag}{name}{end_tag}&gt;')

def get_augmented_xml(current_url, xml_string):
def get_augmented_xml(current_url, xml_string, add_diffs=False):
reader = xml.sax.make_parser()
handler = XMLAugmenter(current_url)
handler = XMLAugmenter(current_url, add_diffs)
xml.sax.parseString(xml_string, handler)
return '\n'.join(handler.result)
return (handler.saw_diff, handler.get_result())

class XMLPrettifier(xml.sax.handler.ContentHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.result = []
class XMLPrettifier(DiffElementContentHandler):
def __init__(self, add_diffs, *args, **kwargs):
super().__init__(add_diffs, *args, **kwargs)
self.indent_level = 0
self.last_tag_opened = None

def startElement(self, name, attrs):
html = [' ' * self.indent_level * INDENT_SIZE]
html.append(f'&lt;{name}')
if name == DIFF_ELEMENT:
self.handle_start_diff_element()
return
html = [
self.get_pending_diff_markup(),
' ' * self.indent_level * INDENT_SIZE,
f'&lt;{name}'
]
if attrs:
html.extend(f' {k}="{v}"' for (k, v) in attrs.items())
html.append('&gt;')
Expand All @@ -113,6 +156,9 @@ def characters(self, content):
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:
Expand All @@ -123,13 +169,18 @@ def endElement(self, name):
result[-2] += previous_line + f'&lt;/{name}&gt;'
del result[-1]
else:
result.append((' ' * self.indent_level * INDENT_SIZE) + f'&lt;/{name}&gt;')
html = [
self.get_pending_diff_markup(),
' ' * self.indent_level * INDENT_SIZE,
f'&lt;/{name}&gt;'
]
result.append(''.join(html))

def get_prettified_xml(xml_string):
reader = xml.sax.make_parser()
handler = XMLPrettifier()
handler = XMLPrettifier(add_diffs=True)
xml.sax.parseString(xml_string, handler)
return '\n'.join(handler.result)
return handler.get_result()

def htmlescape(html:str):
return html.replace('<', '&lt;').replace('>', '&gt;')
Expand Down
6 changes: 4 additions & 2 deletions docgenerator/spec/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def example_detail(request, slug):
example = get_object_or_404(ExampleDocument, slug=slug)
return render(request, 'example_detail.html', {
'example': example,
'augmented_doc': htmlutils.get_augmented_xml(request.path, example.document),
'augmented_doc': htmlutils.get_augmented_xml(request.path, example.document)[1],
'concepts': ExampleDocumentConcept.objects.filter(example=example).order_by('example__name'),
'comparisons': ExampleDocumentComparison.objects.filter(example=example).select_related('doc_format'),
})
Expand All @@ -79,11 +79,13 @@ def format_comparison_detail(request, slug):
comparisons = []
# TODO: Ordering.
for edc in ExampleDocumentComparison.objects.filter(doc_format=other_format).select_related('example'):
highlight_diffs, doc_html = htmlutils.get_augmented_xml(request.path, edc.example.document, True)
comparisons.append({
'example': edc.example,
'preamble_html': edc.preamble_html(),
'document_html': htmlutils.get_augmented_xml(request.path, edc.example.document),
'document_html': doc_html,
'other_document_html': htmlutils.get_prettified_xml(edc.document),
'highlight_diffs': highlight_diffs,
})
return render(request, 'format_comparison_detail.html', {
'other_format': other_format,
Expand Down
Loading

0 comments on commit 8a02d35

Please sign in to comment.