[diffoscope] 03/06: presenters: html: split index pages up if they get too big

Ximin Luo infinity0 at debian.org
Mon Jul 3 20:35:58 CEST 2017


This is an automated email from the git hooks/post-receive script.

infinity0 pushed a commit to branch WIP/humungous-diffs
in repository diffoscope.

commit cc8847d15f9810f2df7dec3dbea4b88917050e2d
Author: Ximin Luo <infinity0 at debian.org>
Date:   Mon Jun 19 22:46:32 2017 +0200

    presenters: html: split index pages up if they get too big
---
 diffoscope/config.py                    |   6 -
 diffoscope/difference.py                |   4 +-
 diffoscope/main.py                      |   9 +-
 diffoscope/presenters/html/html.py      | 337 +++++++++++++++++++++-----------
 diffoscope/presenters/html/templates.py |  55 +++---
 diffoscope/presenters/utils.py          |   4 +
 6 files changed, 263 insertions(+), 152 deletions(-)

diff --git a/diffoscope/config.py b/diffoscope/config.py
index d5f6007..5259a98 100644
--- a/diffoscope/config.py
+++ b/diffoscope/config.py
@@ -51,12 +51,6 @@ class Config(object):
             self.check_constraints()
 
     def check_constraints(self):
-        if self.max_diff_block_lines < self.max_diff_block_lines_parent:  # noqa
-            raise ValueError("max_diff_block_lines ({0.max_diff_block_lines}) "
-                "cannot be smaller than max_diff_block_lines_parent "
-                "({0.max_diff_block_lines_parent})".format(self),
-            )
-
         max_ = self.max_diff_block_lines_html_dir_ratio * \
             self.max_diff_block_lines
         if self.max_diff_block_lines_saved < max_:  # noqa
diff --git a/diffoscope/difference.py b/diffoscope/difference.py
index 9ded2d1..30db2f9 100644
--- a/diffoscope/difference.py
+++ b/diffoscope/difference.py
@@ -136,7 +136,7 @@ class Difference(object):
             queue.extend(top._details)
             yield from self.traverse_breadth(queue)
 
-    def traverse_heapq(self, scorer, queue=None):
+    def traverse_heapq(self, scorer, yield_score=False, queue=None):
         """Traverse the difference tree using a priority queue, where each node
         is scored according to a user-supplied function, and nodes with smaller
         scores are traversed first (after they have been added to the queue).
@@ -148,7 +148,7 @@ class Difference(object):
         queue = queue if queue is not None else [(scorer(self, None), self)]
         while queue:
             val, top = heapq.heappop(queue)
-            yield top
+            yield ((top, val) if yield_score else top)
             for d in top._details:
                 heapq.heappush(queue, (scorer(d, val), d))
 
diff --git a/diffoscope/main.py b/diffoscope/main.py
index 13606ab..8f064dd 100644
--- a/diffoscope/main.py
+++ b/diffoscope/main.py
@@ -97,10 +97,11 @@ def create_parser():
     group1.add_argument('--css', metavar='URL', dest='css_url',
                         help='Link to an extra CSS for the HTML report')
     group1.add_argument('--jquery', metavar='URL', dest='jquery_url',
-                        help='Link to the jQuery url, with --html-dir. Specify '
-                        '"disable" to disable JavaScript. When omitted '
-                        'diffoscope will try to create a symlink to a system '
-                        'installation. Known locations: %s' % ', '.join(JQUERY_SYSTEM_LOCATIONS))
+                        help='URL link to jQuery, for --html and --html-dir output. '
+                        'Specify "disable" to disable JavaScript. If this is a non-existent '
+                        'relative URL, or if this is omitted for --html-dir output, '
+                        'diffoscope will try to create a symlink to a system installation. '
+                        'Known locations: %s' % ', '.join(JQUERY_SYSTEM_LOCATIONS))
     group1.add_argument('--json', metavar='OUTPUT_FILE', dest='json_output',
                         help='Write JSON text output to given file (use - for stdout)')
     group1.add_argument('--markdown', metavar='OUTPUT_FILE', dest='markdown_output',
diff --git a/diffoscope/presenters/html/html.py b/diffoscope/presenters/html/html.py
index bb847a7..1648a36 100644
--- a/diffoscope/presenters/html/html.py
+++ b/diffoscope/presenters/html/html.py
@@ -37,9 +37,11 @@ import re
 import sys
 import html
 import codecs
+import contextlib
 import hashlib
 import logging
 import contextlib
+from urllib.parse import urlparse
 
 from diffoscope import VERSION
 from diffoscope.config import Config
@@ -47,7 +49,7 @@ from diffoscope.diff import SideBySideDiff, DIFFON, DIFFOFF
 
 from ..icon import FAVICON_BASE64
 from ..utils import PrintLimitReached, DiffBlockLimitReached, \
-    create_limited_print_func, Presenter, make_printer
+    create_limited_print_func, Presenter, make_printer, PartialString
 
 from . import templates
 
@@ -106,23 +108,18 @@ def convert(s, ponct=0, tag=''):
 
     return t.getvalue()
 
-def output_visual(print_func, visual, parents):
+def output_visual(visual, anchor, indentstr, indentnum):
     logger.debug('including image for %s', visual.source)
-    sources = parents + [visual.source]
-    print_func(u'<div class="difference">')
-    print_func(u'<div class="diffheader">')
-    print_func(u'<div class="diffcontrol">⊟</div>')
-    print_func(u'<div><span class="source">%s</span>'
-               % html.escape(visual.source))
-    anchor = escape_anchor('/'.join(sources[1:]))
-    print_func(
-        u' <a class="anchor" href="#%s" name="%s">\xb6</a>' % (anchor, anchor))
-    print_func(u"</div>")
-    print_func(u"</div>")
-    print_func(u'<div class="difference">'
-               u'<img src=\"data:%s,%s\" alt=\"compared images\" /></div>' %
-               (visual.data_type, visual.content))
-    print_func(u"</div>", force=True)
+    indent = tuple(indentstr * (indentnum + x) for x in range(3))
+    return u"""{0[0]}<div class="difference">
+{0[1]}<div class="diffheader">
+{0[1]}<div class="diffcontrol">⊟</div>
+{0[1]}<div><span class="source">{1}</span>
+{0[2]}<a class="anchor" href="#{2}" name="{2}">\xb6</a>
+{0[1]}</div>
+{0[1]}</div>
+{0[1]}<div class="difference"><img src=\"data:{3},{4}\" alt=\"compared images\" /></div>
+{0[0]}</div>""".format(indent, html.escape(visual.source), anchor, visual.data_type, visual.content)
 
 def escape_anchor(val):
     """
@@ -139,19 +136,89 @@ def escape_anchor(val):
 
     return val
 
-def output_header(css_url, print_func):
+def output_anchor(path):
+    return escape_anchor('/'.join(n.source1 for n in path[1:]))
+
+def output_node_frame(difference, path, indentstr, indentnum, body):
+    indent = tuple(indentstr * (indentnum + x) for x in range(3))
+    anchor = output_anchor(path)
+    dctrl_class, dctrl = ("diffcontrol", u'⊟') if difference.has_visible_children() else ("diffcontrol-nochildren", u'⊡')
+    if difference.source1 == difference.source2:
+        header = u"""{0[1]}<div class="{1}">{2}</div>
+{0[1]}<div><span class="source">{4}</span>
+{0[2]}<a class="anchor" href="#{3}" name="{3}">\xb6</a>
+{0[1]}</div>
+""".format(indent, dctrl_class, dctrl, anchor,
+       html.escape(PartialString.escape(difference.source1)))
+    else:
+        header = u"""{0[1]}<div class="{1} diffcontrol-double">{2}</div>
+{0[1]}<div><span class="source">{4}</span> vs.</div>
+{0[1]}<div><span class="source">{5}</span>
+{0[2]}<a class="anchor" href="#{3}" name="{3}">\xb6</a>
+{0[1]}</div>
+""".format(indent, dctrl_class, dctrl, anchor,
+       html.escape(PartialString.escape(difference.source1)),
+       html.escape(PartialString.escape(difference.source2)))
+
+    return u"""{0[1]}<div class="diffheader">
+{1}{0[1]}</div>
+{2}""".format(indent, header, body)
+
+def output_node(difference, path, indentstr, indentnum, css_url, directory):
+    indent = tuple(indentstr * (indentnum + x) for x in range(3))
+    t, cont = PartialString.cont()
+
+    if difference.comments:
+        comments = u'{0[1]}<div class="comment">\n{1}{0[1]}</div>\n'.format(
+            indent, "".join(u"{0[2]}{1}<br/>\n".format(indent, html.escape(x)) for x in difference.comments))
+    else:
+        comments = u""
+
+    visuals = u""
+    for visual in difference.visuals:
+        visuals += output_visual(visual, output_anchor(path), indentstr, indentnum+1)
+
+    udiff = io.StringIO()
+    if difference.unified_diff:
+        def print_func(x, force=False):
+            udiff.write(x)
+        HTMLPresenter().output_unified_diff(print_func, css_url, directory, difference.unified_diff, difference.has_internal_linenos)
+
+    # Construct a PartialString for this node
+    # {3} gets mapped to {-1}, a continuation hole for later child nodes
+    body = u"{0}{1}{2}{3}".format(t.escape(comments), t.escape(visuals), t.escape(udiff.getvalue()), "{-1}")
+    if len(path) == 1:
+        # root node, frame it
+        t = cont(t, output_node_frame(difference, path, indentstr, indentnum, body))
+    else:
+        t = cont(t, body)
+
+    # Add holes for child nodes
+    for d in difference.details:
+        # {0} hole, for the child node's contents
+        # {-1} continuation hole, for later child nodes
+        t = cont(t, u"""{0[1]}<div class="difference">
+{1}{0[1]}</div>
+{{-1}}""".format(indent, output_node_frame(d, path + [d], indentstr, indentnum+1, "{0}")), d)
+
+    return cont(t, u"")
+
+def output_header(css_url):
     if css_url:
         css_link = '<link href="%s" type="text/css" rel="stylesheet" />' % css_url
     else:
         css_link = ''
-    print_func(templates.HEADER % {'title': html.escape(' '.join(sys.argv)),
-                         'favicon': FAVICON_BASE64,
-                         'css_link': css_link,
-                        })
-
-def output_footer(print_func):
-    print_func(templates.FOOTER % {'version': VERSION}, force=True)
-
+    return templates.HEADER % {
+        'title': html.escape(' '.join(sys.argv)),
+        'favicon': FAVICON_BASE64,
+        'css_link': css_link,
+    }
+
+def output_footer(jquery_url=None):
+    footer = templates.FOOTER % {'version': VERSION}
+    if jquery_url:
+        return templates.SCRIPTS % {'jquery_url': html.escape(jquery_url)} + footer
+    return footer
 
 @contextlib.contextmanager
 def file_printer(directory, filename):
@@ -225,14 +292,14 @@ class HTMLPresenter(Presenter):
         self.spl_print_func = print_context.__enter__()
         _, _, css_url = rotation_params
         # Print file and table headers
-        output_header(css_url, self.spl_print_func)
+        self.spl_print_func(output_header(css_url))
 
     def spl_had_entered_child(self):
         return self.spl_print_ctrl and self.spl_print_ctrl[1] and self.spl_current_page > 0
 
     def spl_print_exit(self, *exc_info):
         if not self.spl_had_entered_child(): return False
-        output_footer(self.spl_print_func)
+        self.spl_print_func(output_footer(), force=True)
         _exit, _ = self.spl_print_ctrl
         self.spl_print_func = None
         self.spl_print_ctrl = None
@@ -300,6 +367,7 @@ class HTMLPresenter(Presenter):
                 u'<tr class="error">'
                 u'<td colspan="4">Max diff block lines reached; %s/%s bytes (%.2f%%) of diff not shown.'
                 u"</td></tr>" % (bytes_left, total, frac*100), force=True)
+            logger.debug('diff-block print limit reached')
             return False
         except PrintLimitReached:
             assert not self.spl_had_entered_child() # limit reached on the parent page
@@ -331,70 +399,114 @@ class HTMLPresenter(Presenter):
             text = "load diff (%s %s%s)" % (self.spl_current_page, noun, (", truncated" if truncated else ""))
             print_func(templates.UD_TABLE_FOOTER % {"filename": html.escape("%s-1.html" % mainname), "text": text}, force=True)
 
-    def output_difference(self, difference, print_func, css_url, directory, parents):
-        logger.debug('html output for %s', difference.source1)
-        sources = parents + [difference.source1]
-        print_func(u'<div class="difference">')
-        try:
-            print_func(u'<div class="diffheader">')
-            diffcontrol = ("diffcontrol", u'⊟') if difference.has_visible_children() else ("diffcontrol-nochildren", u'⊡')
-            if difference.source1 == difference.source2:
-                print_func(u'<div class="%s">%s</div>' % diffcontrol)
-                print_func(u'<div><span class="source">%s</span>'
-                           % html.escape(difference.source1))
+    def output_node_placeholder(self, anchor, lazy_load):
+        if lazy_load:
+            return templates.DIFFNODE_LAZY_LOAD % anchor
+        else:
+            return '<div class="error">Max report size reached</div>\n'
+
+    def output_difference(self, target, difference, css_url, jquery_url, single_page=False):
+        outputs = {} # nodes to their partial output
+        ancestors = {} # child nodes to ancestor nodes
+        placeholder_len = len(self.output_node_placeholder("XXXXXXXXXXXXXXXX", not single_page))
+
+        printers = {} # nodes to their printers
+        def maybe_print(node):
+            if outputs[node].holes:
+                return
+            printer_args = printers[node]
+            with printer_args[0](*printer_args[1:]) as printer:
+                printer(outputs[node].format())
+            del outputs[node]
+            del printers[node]
+
+        def smallest_first(node, parscore):
+            depth = parscore[0] + 1 if parscore else 0
+            parents = parscore[3] if parscore else []
+            # Difference is not comparable so use memory address in event of a tie
+            return depth, node.size_self(), id(node), parents + [node]
+
+        for node, score in difference.traverse_heapq(smallest_first, yield_score=True):
+            ancestor = ancestors.pop(node, None)
+            path = score[3]
+            anchor = output_anchor(path)
+            logger.debug('html output for %s', anchor)
+            node_output = output_node(node, path, "  ", len(path)-1, css_url, None if single_page else target)
+
+            if ancestor:
+                limit = Config().max_report_child_size
+                logger.debug("output size: %s, %s",
+                    outputs[ancestor].size(placeholder_len), node_output.size(placeholder_len))
             else:
-                print_func(u'<div class="%s diffcontrol-double">%s</div>' % diffcontrol)
-                print_func(u'<div><span class="source">%s</span> vs.</div>'
-                           % html.escape(difference.source1))
-                print_func(u'<div><span class="source">%s</span>'
-                           % html.escape(difference.source2))
-            anchor = escape_anchor('/'.join(sources[1:]))
-            print_func(u' <a class="anchor" href="#%s" name="%s">\xb6</a>' % (anchor, anchor))
-            print_func(u"</div>")
-            if difference.comments:
-                print_func(u'<div class="comment">%s</div>'
-                           % u'<br />'.join(map(html.escape, difference.comments)))
-            print_func(u"</div>")
-            if len(difference.visuals) > 0:
-                for visual in difference.visuals:
-                    output_visual(print_func, visual, sources)
-            elif difference.unified_diff:
-                self.output_unified_diff(print_func, css_url, directory, difference.unified_diff, difference.has_internal_linenos)
-            for detail in difference.details:
-                self.output_difference(detail, print_func, css_url, directory, sources)
-        except PrintLimitReached:
-            logger.debug('print limit reached')
-            raise
-        finally:
-            print_func(u"</div>", force=True)
+                limit = Config().max_report_size
 
-    def output_html(self, difference, css_url=None, print_func=None):
-        """
-        Default presenter, all in one HTML file
-        """
-        if print_func is None:
-            print_func = print
-        print_func = create_limited_print_func(print_func, Config().max_report_size)
-        try:
-            output_header(css_url, print_func)
-            self.output_difference(difference, print_func, css_url, None, [])
-        except PrintLimitReached:
-            logger.debug('print limit reached')
-            print_func(u'<div class="error">Max output size reached.</div>',
-                       force=True)
-        output_footer(print_func)
+            if ancestor and outputs[ancestor].size(placeholder_len) + node_output.size(placeholder_len) < limit:
+                # under limit, add it to an existing page
+                outputs[ancestor] = outputs[ancestor].pformat({node: node_output})
+                stored = ancestor
 
-    @classmethod
-    def run(cls, data, difference, parsed_args):
-        with make_printer(parsed_args.html_output) as fn:
-            cls().output_html(
-                difference,
-                css_url=parsed_args.css_url,
-                print_func=fn,
-            )
+            else:
+                # over limit (or root), new subpage
+                if ancestor:
+                    placeholder = self.output_node_placeholder(anchor, not single_page)
+                    outputs[ancestor] = outputs[ancestor].pformat({node: placeholder})
+                    maybe_print(ancestor)
+                    footer = output_footer()
+                    if single_page:
+                        if not outputs:
+                            # already output a single page, don't iterate through any more children
+                            break
+                        else:
+                            continue
+                else:
+                    assert node is difference
+                    footer = output_footer(jquery_url)
+                    anchor = "index"
+
+                outputs[node] = node_output.frame(
+                    output_header(css_url) + u'<div class="difference">\n',
+                    u'</div>\n' + footer)
+                printers[node] = (make_printer, target) if single_page else (file_printer, target, "%s.html" % anchor)
+                stored = node
+
+            for child in node.details:
+                ancestors[child] = stored
+
+            maybe_print(stored)
+
+        if outputs:
+            import pprint
+            pprint.pprint(outputs, indent=4)
+        assert not outputs
+
+    def ensure_jquery(self, jquery_url, basedir, default_override):
+        if jquery_url is None:
+            jquery_url = default_override
+        if jquery_url == 'disable' or not jquery_url:
+            return None
+
+        url = urlparse(jquery_url)
+        if url.scheme or url.netloc:
+            # remote path
+            return jquery_url
+
+        # local path
+        if os.path.isabs(url.path):
+            check_path = url.path
+        else:
+            check_path = os.path.join(basedir, url.path)
 
+        if os.path.lexists(check_path):
+            return url.path
 
-class HTMLDirectoryPresenter(HTMLPresenter):
+        for path in JQUERY_SYSTEM_LOCATIONS:
+            if os.path.exists(path):
+                os.symlink(path, check_path)
+                logger.debug('jquery found at %s and symlinked to %s', path, check_path)
+                return url.path
+        logger.warning('--jquery was specified as a relative path, but jQuery was not found in any known location. Disabling on-demand inline loading.')
+        logger.debug('Locations searched: %s', ', '.join(JQUERY_SYSTEM_LOCATIONS))
+        return None
 
     def output_html_directory(self, directory, difference, css_url=None, jquery_url=None):
         """
@@ -411,37 +523,28 @@ class HTMLDirectoryPresenter(HTMLPresenter):
         if not os.path.isdir(directory):
             raise ValueError("%s is not a directory" % directory)
 
-        if not jquery_url:
-            jquery_symlink = os.path.join(directory, "jquery.js")
-            if os.path.exists(jquery_symlink):
-                jquery_url = "./jquery.js"
-            else:
-                if os.path.lexists(jquery_symlink):
-                    os.unlink(jquery_symlink)
-                for path in JQUERY_SYSTEM_LOCATIONS:
-                    if os.path.exists(path):
-                        os.symlink(path, jquery_symlink)
-                        jquery_url = "./jquery.js"
-                        break
-                if not jquery_url:
-                    logger.warning('--jquery was not specified and jQuery was not found in any known location. Disabling on-demand inline loading.')
-                    logger.debug('Locations searched: %s', ', '.join(JQUERY_SYSTEM_LOCATIONS))
-        if jquery_url == 'disable':
-            jquery_url = None
-
-        with file_printer(directory, "index.html") as print_func:
-            print_func = create_limited_print_func(print_func, Config().max_report_size)
-            try:
-                output_header(css_url, print_func)
-                self.output_difference(difference, print_func, css_url, directory, [])
-            except PrintLimitReached:
-                logger.debug('print limit reached')
-                print_func(u'<div class="error">Max output size reached.</div>',
-                           force=True)
-            if jquery_url:
-                print_func(templates.SCRIPTS % {'jquery_url': html.escape(jquery_url)}, force=True)
-            output_footer(print_func)
+        jquery_url = self.ensure_jquery(jquery_url, directory, "jquery.js")
+        self.output_difference(directory, difference, css_url, jquery_url)
+
+
+    def output_html(self, target, difference, css_url=None, jquery_url=None):
+        """
+        Default presenter, all in one HTML file
+        """
+        jquery_url = self.ensure_jquery(jquery_url, os.getcwd(), None)
+        self.output_difference(target, difference, css_url, jquery_url, single_page=True)
+
+    @classmethod
+    def run(cls, data, difference, parsed_args):
+        cls().output_html(
+            parsed_args.html_output,
+            difference,
+            css_url=parsed_args.css_url,
+            jquery_url=parsed_args.jquery_url,
+        )
 
+
+class HTMLDirectoryPresenter(HTMLPresenter):
     @classmethod
     def run(cls, data, difference, parsed_args):
         cls().output_html_directory(
diff --git a/diffoscope/presenters/html/templates.py b/diffoscope/presenters/html/templates.py
index 713718a..d077ad5 100644
--- a/diffoscope/presenters/html/templates.py
+++ b/diffoscope/presenters/html/templates.py
@@ -110,12 +110,12 @@ HEADER = """<!DOCTYPE html>
     .diffoscope .diffheader:hover .anchor {
       display: inline;
     }
-    .diffoscope table.diff tr.ondemand td {
+    .diffoscope table.diff tr.ondemand td, .diffoscope div.ondemand-details {
       background: #f99;
       text-align: center;
       padding: 0.5em 0;
     }
-    .diffoscope table.diff tr.ondemand:hover td {
+    .diffoscope table.diff tr.ondemand:hover td, .diffoscope div.ondemand-details:hover {
       background: #faa;
       cursor: pointer;
     }
@@ -140,41 +140,47 @@ HEADER = """<!DOCTYPE html>
 <body class="diffoscope">
 """
 
-FOOTER = """
-<div class="footer">Generated by <a href="https://diffoscope.org" rel="noopener noreferrer" target="_blank">diffoscope</a> %(version)s</div>
+FOOTER = """<div class="footer">Generated by <a href="https://diffoscope.org" rel="noopener noreferrer" target="_blank">diffoscope</a> %(version)s</div>
 </body>
 </html>
 """
 
-SCRIPTS = """
-<script src="%(jquery_url)s"></script>
+SCRIPTS = """<script src="%(jquery_url)s"></script>
 <script type="text/javascript">
 $(function() {
-  var load_cont = function() {
-    var a = $(this).find("a");
+  // activate "loading" controls
+  var load_cont, load_generic = function(selector, target, getInfo, postLoad) {
+    return function() {
+        var a = $(this).find("a");
+        var filename = a.attr('href');
+        var info = getInfo ? getInfo(a) : null;
+        var button = a.parent();
+        button.text('... loading ...');
+        (target ? target(button) : button).load(filename + " " + selector, function() {
+            // https://stackoverflow.com/a/8452751/946226
+            var elems = $(this).children(':first').unwrap();
+            // set this behaviour for the next link too
+            var td = elems.parent().find(".ondemand td");
+            td.on('click', load_cont);
+            postLoad ? postLoad(td, info) : null;
+        });
+        return false;
+    };
+  };
+  load_cont = load_generic("tr", function(x) { return x.parent(); }, function(a) {
     var textparts = /^(.*)\((\d+) pieces?(.*)\)$/.exec(a.text());
     var numleft = Number.parseInt(textparts[2]) - 1;
     var noun = numleft == 1 ? "piece" : "pieces";
-    var newtext = textparts[1] + "(" + numleft + " " + noun + textparts[3] + ")";
-    var filename = a.attr('href');
-    var td = a.parent();
-    td.text('... loading ...');
-    td.parent().load(filename + " tr", function() {
-        // https://stackoverflow.com/a/8452751/946226
-        var elems = $(this).children(':first').unwrap();
-        // set this behaviour for the next link too
-        var td = elems.parent().find(".ondemand td");
-        td.find("a").text(newtext);
-        td.on('click', load_cont);
-    });
-    return false;
-  };
+    return textparts[1] + "(" + numleft + " " + noun + textparts[3] + ")";
+  }, function(td, info) { td.find("a").text(info); });
   $(".ondemand td").on('click', load_cont);
+  $(".ondemand-details").on('click', load_generic("div.difference > *"));
+  // activate [+]/[-] controls
   var diffcontrols = $(".diffcontrol");
   diffcontrols.on('click', function(evt) {
     var control = $(this);
     var parent = control.parent();
-    var target = $.merge(parent.siblings('table.diff, div.difference'), parent.find('div.comment'));
+    var target = parent.siblings('table.diff, div.difference, div.comment');
     var orig = target;
     if (evt.shiftKey) {
         var gparent = parent.parent();
@@ -196,6 +202,9 @@ $(function() {
 </script>
 """
 
+DIFFNODE_LAZY_LOAD = """<div class="ondemand-details">... <a href="%s.html">load details</a> ...</div>
+"""
+
 UD_TABLE_HEADER = u"""<table class="diff">
 <colgroup><col class="colines"/><col class="coldiff"/>
 <col class="colines"/><col class="coldiff"/></colgroup>
diff --git a/diffoscope/presenters/utils.py b/diffoscope/presenters/utils.py
index 4562e8b..6cc4edc 100644
--- a/diffoscope/presenters/utils.py
+++ b/diffoscope/presenters/utils.py
@@ -365,6 +365,10 @@ class PartialString(object):
                 return t.pformat({cont: cls(fmtstr, *(holes + (cont,)))})
         return cls("{0}", cont), cont
 
+    def frame(self, header, footer):
+        frame = self.__class__(self.escape(header) + "{0}" + self.escape(footer), None)
+        return frame.pformat({None: self})
+
 
 if __name__ == "__main__":
     import doctest

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/reproducible/diffoscope.git


More information about the diffoscope mailing list