changeset 556:93b656f0af54

serve: Improve debug information in the preview server. Now the debug window only loads debug info on demand.
author Ludovic Chabant <ludovic@chabant.com>
date Wed, 12 Aug 2015 23:18:35 -0700
parents daf8df5ade7d
children 703ea5d76f33
files piecrust/data/builder.py piecrust/data/debug.py piecrust/data/piecrustdata.py piecrust/resources/server/piecrust-debug-info.css piecrust/resources/server/piecrust-debug-info.js piecrust/serving/middlewares.py
diffstat 6 files changed, 264 insertions(+), 123 deletions(-) [+]
line wrap: on
line diff
--- a/piecrust/data/builder.py	Wed Aug 12 23:04:46 2015 -0700
+++ b/piecrust/data/builder.py	Wed Aug 12 23:18:35 2015 -0700
@@ -60,7 +60,7 @@
     # displayed in the debugger window.
     if (app.config.get('site/show_debug_info') and
             not app.config.get('baker/is_baking')):
-        pc_data._enableDebugInfo(page, data)
+        pc_data.enableDebugInfo(page)
 
     return data
 
--- a/piecrust/data/debug.py	Wed Aug 12 23:04:46 2015 -0700
+++ b/piecrust/data/debug.py	Wed Aug 12 23:18:35 2015 -0700
@@ -14,75 +14,38 @@
 css_id_re = re.compile(r'[^\w\d\-]+')
 
 
-# CSS for the debug window.
-CSS_DEBUGWINDOW = """
-text-align: left;
-font-family: serif;
-font-style: normal;
-font-weight: normal;
-position: fixed;
-width: 50%;
-bottom: 0;
-right: 0;
-overflow: auto;
-max-height: 50%;
-box-shadow: 0 0 10px #633;
-"""
-
-CSS_PIPELINESTATUS = """
-background: #fff;
-color: #a22;
-"""
+CSS_DATA = 'piecrust-debug-info-data'
+CSS_DATABLOCK = 'piecrust-debug-info-datablock'
+CSS_VALUE = 'piecrust-debug-info-datavalue'
+CSS_DOC = 'piecrust-debug-info-doc'
+CSS_BIGHEADER = 'piecrust-debug-info-header1'
+CSS_HEADER = 'piecrust-debug-info-header2'
 
-CSS_DEBUGINFO = """
-padding: 1em;
-background: #a42;
-color: #fff;
-"""
-
-# HTML elements.
-CSS_P = 'margin: 0; padding: 0;'
-CSS_A = 'color: #fff; text-decoration: none;'
-
-# Headers.
-CSS_BIGHEADER = 'margin: 0.5em 0; font-weight: bold;'
-CSS_HEADER = 'margin: 0.5em 0; font-weight: bold;'
-
-# Data block elements.
-CSS_DATA = 'font-family: Courier, sans-serif; font-size: 0.9em;'
-CSS_DATABLOCK = 'margin-left: 2em;'
-CSS_VALUE = 'color: #fca;'
-CSS_DOC = 'color: #fa8; font-size: 0.9em;'
 
 # 'Baked with PieCrust' text
 BRANDING_TEXT = 'Baked with <em><a href="%s">PieCrust</a> %s</em>.' % (
         PIECRUST_URL, APP_VERSION)
 
 
-def build_debug_info(page, data):
+def build_debug_info(page):
     """ Generates HTML debug info for the given page's data.
     """
     output = io.StringIO()
     try:
-        _do_build_debug_info(page, data, output)
+        _do_build_debug_info(page, output)
         return output.getvalue()
     finally:
         output.close()
 
 
-def _do_build_debug_info(page, data, output):
+def _do_build_debug_info(page, output):
     app = page.app
 
-    print('<div id="piecrust-debug-info" style="%s">' % CSS_DEBUGWINDOW,
+    print('<div id="piecrust-debug-window" class="piecrust-debug-window">',
           file=output)
 
-    print('<div id="piecrust-debug-info-pipeline-status" style="%s">' %
-          CSS_PIPELINESTATUS, file=output)
-    print('</div>', file=output)
-
-    print('<div style="%s">' % CSS_DEBUGINFO, file=output)
-    print('<p style="%s"><strong>PieCrust %s</strong> &mdash; ' %
-          (CSS_P, APP_VERSION), file=output)
+    print('<div><strong>PieCrust %s</strong> &mdash; ' % APP_VERSION,
+          file=output)
 
     # If we have some execution info in the environment,
     # add more information.
@@ -101,46 +64,78 @@
 
     output.write(', ')
     if app.env.start_time != 0:
-        output.write('in __PIECRUST_TIMING_INFORMATION__')
+        output.write('in __PIECRUST_TIMING_INFORMATION__. ')
     else:
-        output.write('no timing information available')
+        output.write('no timing information available. ')
 
-    print('</p>', file=output)
+    print('<a class="piecrust-debug-expander" hred="#">[+]</a>',
+          file=output)
+
     print('</div>', file=output)
 
-    if data:
-        print('<div style="%s padding-top: 0;">' % CSS_DEBUGINFO, file=output)
-        print(('<p style="%s cursor: pointer;" onclick="var l = '
-                         'document.getElementById(\'piecrust-debug-details\'); '
-                         'if (l.style.display == \'none\') l.style.display = '
-                         '\'block\'; else l.style.display = \'none\';">' % CSS_P), file=output)
-        print(('<span style="%s">Template engine data</span> '
-                         '&mdash; click to toggle</a>.</p>' % CSS_BIGHEADER), file=output)
-
-        print('<div id="piecrust-debug-details" style="display: none;">', file=output)
-        print(('<p style="%s">The following key/value pairs are '
-                         'available in the layout\'s markup, and most are '
-                         'available in the page\'s markup.</p>' % CSS_DOC), file=output)
-
-        filtered_data = dict(data)
-        for k in list(filtered_data.keys()):
-            if k.startswith('__'):
-                del filtered_data[k]
-
-        renderer = DebugDataRenderer(output)
-        renderer.external_docs['data-site'] = (
-                "This section comes from the site configuration file.")
-        renderer.external_docs['data-page'] = (
-                "This section comes from the page's configuration header.")
-        renderer.renderData(filtered_data)
-
-        print('</div>', file=output)
-        print('</div>', file=output)
+    print('<div class="piecrust-debug-info piecrust-debug-info-unloaded">',
+          file=output)
+    print('</div>', file=output)
 
     print('</div>', file=output)
 
     print('<script src="/__piecrust_static/piecrust-debug-info.js"></script>',
-            file=output)
+          file=output)
+
+
+def build_var_debug_info(data, var_path=None):
+    output = io.StringIO()
+    try:
+        _do_build_var_debug_info(data, output, var_path)
+        return output.getvalue()
+    finally:
+        output.close()
+
+
+def _do_build_var_debug_info(data, output, var_path=None):
+    if False:
+        print('<html>', file=output)
+        print('<head>', file=output)
+        print('<meta charset="utf-8" />', file=output)
+        print('<link rel="stylesheet" type="text/css" '
+              'href="/__piecrust_static/piecrust-debug-info.css" />',
+              file=output)
+        print('</head>', file=output)
+        print('<body class="piecrust-debug-page">', file=output)
+
+    #print('<div class="piecrust-debug-info">', file=output)
+    #print(('<p style="cursor: pointer;" onclick="var l = '
+    #       'document.getElementById(\'piecrust-debug-details\'); '
+    #       'if (l.style.display == \'none\') l.style.display = '
+    #       '\'block\'; else l.style.display = \'none\';">'),
+    #      file=output)
+    #print(('<span class="%s">Template engine data</span> '
+    #       '&mdash; click to toggle</a>.</p>' % CSS_BIGHEADER), file=output)
+
+    #print('<div id="piecrust-debug-details" style="display: none;">',
+    #      file=output)
+    #print(('<p class="%s">The following key/value pairs are '
+    #       'available in the layout\'s markup, and most are '
+    #       'available in the page\'s markup.</p>' % CSS_DOC), file=output)
+
+    filtered_data = dict(data)
+    for k in list(filtered_data.keys()):
+        if k.startswith('__'):
+            del filtered_data[k]
+
+    renderer = DebugDataRenderer(output)
+    renderer.external_docs['data-site'] = (
+            "This section comes from the site configuration file.")
+    renderer.external_docs['data-page'] = (
+            "This section comes from the page's configuration header.")
+    renderer.renderData(filtered_data)
+
+    print('</div>', file=output)
+    #print('</div>', file=output)
+
+    if False:
+        print('</body>', file=output)
+        print('</html>', file=output)
 
 
 class DebugDataRenderer(object):
@@ -154,7 +149,7 @@
     def renderData(self, data):
         if not isinstance(data, dict):
             raise Exception("Expected top level data to be a dict.")
-        self._writeLine('<div style="%s">' % CSS_DATA)
+        self._writeLine('<div class="%s">' % CSS_DATA)
         self._renderDict(data, 'data')
         self._writeLine('</div>')
 
@@ -179,16 +174,16 @@
 
         data_type = type(data)
         if data_type is bool:
-            self._write('<span style="%s">%s</span>' % (CSS_VALUE,
-                'true' if bool(data) else 'false'))
+            self._write('<span class="%s">%s</span>' % (CSS_VALUE,
+                        'true' if bool(data) else 'false'))
             return
 
         if data_type is int:
-            self._write('<span style="%s">%d</span>' % (CSS_VALUE, data))
+            self._write('<span class="%s">%d</span>' % (CSS_VALUE, data))
             return
 
         if data_type is float:
-            self._write('<span style="%s">%4.2f</span>' % (CSS_VALUE, data))
+            self._write('<span class="%s">%4.2f</span>' % (CSS_VALUE, data))
             return
 
         if data_type is str:
@@ -196,7 +191,7 @@
                 data = data[:DebugDataRenderer.MAX_VALUE_LENGTH - 5]
                 data += '[...]'
             data = html.escape(data)
-            self._write('<span style="%s">%s</span>' % (CSS_VALUE, data))
+            self._write('<span class="%s">%s</span>' % (CSS_VALUE, data))
             return
 
         self._renderCollapsableValueStart(path)
@@ -205,24 +200,27 @@
         self._renderCollapsableValueEnd()
 
     def _renderList(self, data, path):
-        self._writeLine('<div style="%s">' % CSS_DATABLOCK)
+        self._writeLine('<div class="%s">' % CSS_DATABLOCK)
         self._renderDoc(data, path)
         self._renderAttributes(data, path)
-        rendered_count = self._renderIterable(data, path, lambda d: enumerate(d))
+        rendered_count = self._renderIterable(
+                data, path, lambda d: enumerate(d))
         if (rendered_count == 0 and
                 not hasattr(data.__class__, 'debug_render_not_empty')):
-            self._writeLine('<p style="%s %s">(empty array)</p>' % (CSS_P, CSS_DOC))
+            self._writeLine('<p class="%s">(empty array)</p>' % (CSS_DOC))
         self._writeLine('</div>')
 
     def _renderDict(self, data, path):
-        self._writeLine('<div style="%s">' % CSS_DATABLOCK)
+        self._writeLine('<div class="%s">' % CSS_DATABLOCK)
         self._renderDoc(data, path)
         self._renderAttributes(data, path)
-        rendered_count = self._renderIterable(data, path,
+        rendered_count = self._renderIterable(
+                data, path,
                 lambda d: sorted(iter(d.items()), key=lambda i: i[0]))
         if (rendered_count == 0 and
                 not hasattr(data.__class__, 'debug_render_not_empty')):
-            self._writeLine('<p style="%s %s">(empty dictionary)</p>' % (CSS_P, CSS_DOC))
+            self._writeLine('<p class="%s %s">(empty dictionary)</p>' %
+                            CSS_DOC)
         self._writeLine('</div>')
 
     def _renderObject(self, data, path):
@@ -234,22 +232,22 @@
             self._renderValue(value, path)
             return
 
-        self._writeLine('<div style="%s">' % CSS_DATABLOCK)
+        self._writeLine('<div class="%s">' % CSS_DATABLOCK)
         self._renderDoc(data, path)
         rendered_attrs = self._renderAttributes(data, path)
 
         if (hasattr(data, '__iter__') and
                 hasattr(data.__class__, 'debug_render_items') and
                 data.__class__.debug_render_items):
-            rendered_count = self._renderIterable(data, path,
-                    lambda d: enumerate(d))
+            rendered_count = self._renderIterable(
+                    data, path, lambda d: enumerate(d))
             if (rendered_count == 0 and
                     not hasattr(data.__class__, 'debug_render_not_empty')):
-                self._writeLine('<p style="%s %s">(empty)</p>' % (CSS_P, CSS_DOC))
+                self._writeLine('<p class="%s">(empty)</p>' % CSS_DOC)
 
         elif (rendered_attrs == 0 and
                 not hasattr(data.__class__, 'debug_render_not_empty')):
-            self._writeLine('<p style="%s %s">(empty)</p>' % (CSS_P, CSS_DOC))
+            self._writeLine('<p class="%s">(empty)</p>' % CSS_DOC)
 
         self._writeLine('</div>')
 
@@ -267,20 +265,20 @@
 
     def _renderDoc(self, data, path):
         if hasattr(data.__class__, 'debug_render_doc'):
-            self._writeLine('<span style="%s">&ndash; %s</span>' %
-                    (CSS_DOC, data.__class__.debug_render_doc))
+            self._writeLine('<span class="%s">&ndash; %s</span>' %
+                            (CSS_DOC, data.__class__.debug_render_doc))
 
         if hasattr(data.__class__, 'debug_render_doc_dynamic'):
             drdd = data.__class__.debug_render_doc_dynamic
             for ng in drdd:
                 doc = getattr(data, ng)
-                self._writeLine('<span style="%s">&ndash; %s</span>' %
-                        (CSS_DOC, doc()))
+                self._writeLine('<span class="%s">&ndash; %s</span>' %
+                                (CSS_DOC, doc()))
 
         doc = self.external_docs.get(path)
         if doc is not None:
-            self._writeLine('<span style="%s">&ndash; %s</span>' %
-                    (CSS_DOC, doc))
+            self._writeLine('<span class="%s">&ndash; %s</span>' %
+                            (CSS_DOC, doc))
 
     def _renderAttributes(self, data, path):
         if not hasattr(data.__class__, 'debug_render'):
@@ -355,19 +353,19 @@
 
     def _renderCollapsableValueStart(self, path):
         self._writeLine('<span style="cursor: pointer;" onclick="var l = '
-                    'document.getElementById(\'piecrust-debug-data-%s\'); '
-                    'if (l.style.display == \'none\') {'
-                    '  l.style.display = \'block\';'
-                    '  this.innerHTML = \'[-]\';'
-                    '} else {'
-                    '  l.style.display = \'none\';'
-                    '  this.innerHTML = \'[+]\';'
-                    '}">'
-                    '[+]'
-                    '</span>' %
-                    path)
+                        'document.getElementById(\'piecrust-debug-data-%s\'); '
+                        'if (l.style.display == \'none\') {'
+                        '  l.style.display = \'block\';'
+                        '  this.innerHTML = \'[-]\';'
+                        '} else {'
+                        '  l.style.display = \'none\';'
+                        '  this.innerHTML = \'[+]\';'
+                        '}">'
+                        '[+]'
+                        '</span>' %
+                        path)
         self._writeLine('<div style="display: none"'
-                         'id="piecrust-debug-data-%s">' % path)
+                        'id="piecrust-debug-data-%s">' % path)
 
     def _renderCollapsableValueEnd(self):
         self._writeLine('</div>')
--- a/piecrust/data/piecrustdata.py	Wed Aug 12 23:04:46 2015 -0700
+++ b/piecrust/data/piecrustdata.py	Wed Aug 12 23:18:35 2015 -0700
@@ -17,22 +17,20 @@
         self.branding = 'Baked with <em><a href="%s">PieCrust</a> %s</em>.' % (
                 'http://bolt80.com/piecrust/', APP_VERSION)
         self._page = None
-        self._data = None
 
     @property
     def debug_info(self):
-        if self._page is not None and self._data is not None:
+        if self._page is not None:
             try:
-                return build_debug_info(self._page, self._data)
+                return build_debug_info(self._page)
             except Exception as ex:
                 logger.exception(ex)
                 return ('An error occured while generating debug info. '
                         'Please check the logs.')
         return ''
 
-    def _enableDebugInfo(self, page, data):
+    def enableDebugInfo(self, page):
         self._page = page
-        self._data = data
 
     def _debugRenderDebugInfo(self):
         return "The very thing you're looking at!"
--- a/piecrust/resources/server/piecrust-debug-info.css	Wed Aug 12 23:04:46 2015 -0700
+++ b/piecrust/resources/server/piecrust-debug-info.css	Wed Aug 12 23:18:35 2015 -0700
@@ -1,3 +1,6 @@
+
+/*****************************************************************************/
+
 @keyframes slideNotification {
     0% { opacity: 1; }
     100% { opacity: 0; right: -11em; }
@@ -30,3 +33,65 @@
     border: 2px solid #810B0B;
 }
 
+/*****************************************************************************/
+
+.piecrust-debug-window {
+    padding: 1em;
+    text-align: left;
+    font-family: serif;
+    font-style: normal;
+    font-size: 1rem;
+    font-weight: normal;
+    background: #a42;
+    color: #fff;
+    position: fixed;
+    width: 50%;
+    bottom: 0;
+    right: 0;
+    overflow: auto;
+    max-height: 50%;
+    box-shadow: 0 0 10px #633;
+}
+
+.piecrust-debug-info {
+}
+
+.piecrust-debug-info a {
+    color: #fff;
+    text-decoration: none;
+}
+
+.piecrust-debug-icon {
+    height: 2em;
+}
+
+.piecrust-debug-info-header1 {
+    margin: 0.5em 0;
+    font-weight: bold;
+}
+
+.piecrust-debug-info-header2 {
+    margin: 0.5em 0;
+    font-weight: bold;
+}
+
+.piecrust-debug-info-data {
+    font-family: Courier, sans-serif;
+    font-size: 0.9em;
+}
+
+.piecrust-debug-info-datablock {
+    margin-left: 2em;
+}
+
+.piecrust-debug-info-datavalue {
+    color: #fca;
+}
+
+.piecrust-debug-info-doc {
+    color: #fa8;
+    font-size: 0.9em;
+}
+
+/*****************************************************************************/
+
--- a/piecrust/resources/server/piecrust-debug-info.js	Wed Aug 12 23:04:46 2015 -0700
+++ b/piecrust/resources/server/piecrust-debug-info.js	Wed Aug 12 23:18:35 2015 -0700
@@ -263,6 +263,46 @@
 
 ///////////////////////////////////////////////////////////////////////////////
 
+function toggleDebugInfo() {
+    var info = document.querySelector('.piecrust-debug-info');
+    if (info.classList.contains('piecrust-debug-info-unloaded')) {
+        loadDebugInfo();
+        info.classList.remove('piecrust-debug-info-unloaded');
+    }
+    if (this.innerHTML == '[+]') {
+        this.innerHTML = '[-]';
+        info.style = "";
+    } else {
+        this.innerHTML = '[+]';
+        info.style = "display: none;";
+    }
+}
+
+function loadDebugInfo() {
+    var xmlHttp = new XMLHttpRequest();
+
+    xmlHttp.onreadystatechange = function() {
+        if (xmlHttp.readyState == XMLHttpRequest.DONE) {
+            var info = document.querySelector('.piecrust-debug-info');
+            if(xmlHttp.status == 200) {
+                info.innerHTML = xmlHttp.responseText;
+            }
+            else if(xmlHttp.status == 400) {
+                info.innerHTML = "Error fetching debug info.";
+            }
+            else {
+                info.innerHTML = "Unknown error.";
+            }
+        }
+    }
+
+    var pageUrl = window.location.pathname;
+    xmlHttp.open("GET", "/__piecrust_debug/debug_info?page=" + pageUrl, true);
+    xmlHttp.send();
+}
+
+///////////////////////////////////////////////////////////////////////////////
+
 var notification = new NotificationArea();
 var assetReloader = new AssetReloader();
 
@@ -274,6 +314,9 @@
     style.type = 'text/css';
     style.href = '/__piecrust_static/piecrust-debug-info.css' + cacheBust;
     document.head.appendChild(style);
+
+    var expander = document.querySelector('.piecrust-debug-expander');
+    expander.onclick = toggleDebugInfo;
 };
 
 
--- a/piecrust/serving/middlewares.py	Wed Aug 12 23:04:46 2015 -0700
+++ b/piecrust/serving/middlewares.py	Wed Aug 12 23:18:35 2015 -0700
@@ -1,8 +1,15 @@
 import os.path
+from werkzeug.exceptions import NotFound, Forbidden
 from werkzeug.wrappers import Request, Response
 from werkzeug.wsgi import ClosingIterator
 from piecrust import RESOURCES_DIR, CACHE_DIR
-from piecrust.serving.util import make_wrapped_file_response
+from piecrust.data.builder import (
+        DataBuildingContext, build_page_data)
+from piecrust.data.debug import build_var_debug_info
+from piecrust.routing import RouteNotFoundError
+from piecrust.serving.util import (
+        make_wrapped_file_response, get_requested_page, get_app_for_server)
+from piecrust.sources.pageref import PageNotFoundError
 
 
 class StaticResourcesMiddleware(object):
@@ -38,12 +45,14 @@
         self.app = app
         self.root_dir = root_dir
         self.debug = debug
+        self.sub_cache_dir = sub_cache_dir
         self.run_sse_check = run_sse_check
         self._proc_loop = None
         self._out_dir = os.path.join(root_dir, CACHE_DIR, 'server')
         if sub_cache_dir:
             self._out_dir = os.path.join(sub_cache_dir, 'server')
         self._handlers = {
+                'debug_info': self._getDebugInfo,
                 'werkzeug_shutdown': self._shutdownWerkzeug,
                 'pipeline_status': self._startSSEProvider}
 
@@ -70,6 +79,34 @@
 
         return self.app(environ, start_response)
 
+    def _getDebugInfo(self, request, start_response):
+        app = get_app_for_server(self.root_dir, debug=self.debug,
+                                 sub_cache_dir=self.sub_cache_dir)
+        if not app.config.get('site/enable_debug_info'):
+            return Forbidden()
+
+        found = False
+        page_path = request.args.get('page')
+        try:
+            req_page = get_requested_page(app, page_path)
+            found = (req_page is not None)
+        except (RouteNotFoundError, PageNotFoundError):
+            pass
+        if not found:
+            return NotFound("No such page: %s" % page_path)
+
+        ctx = DataBuildingContext(req_page.qualified_page,
+                                  page_num=req_page.page_num)
+        data = build_page_data(ctx)
+
+        var_path = request.args.getlist('var')
+        if not var_path:
+            var_path = None
+        output = build_var_debug_info(data, var_path)
+
+        response = Response(output, mimetype='text/html')
+        return response(request.environ, start_response)
+
     def _shutdownWerkzeug(self, request, start_response):
         shutdown_func = request.environ.get('werkzeug.server.shutdown')
         if shutdown_func is None: