Commits

Kirill Simonov committed 3e3fd30

Added drafts of HTML, text, JSON and CSV renderers.

The request interface was updated, but in no way is in the final form.

  • Participants
  • Parent commits 66b7838

Comments (0)

Files changed (24)

File src/htsql/application.py

         """
         with self:
             wsgi = WSGI()
-            return wsgi(environ, start_response)
+            body = wsgi(environ, start_response)
+            for chunk in body:
+                yield chunk
 
 

File src/htsql/ctl/request.py

         assert isinstance(remote_user, maybe(str))
         assert isinstance(content_type, maybe(str))
         assert isinstance(content_body, maybe(oneof(str, filelike())))
-        assert isinstance(extra_headers, maybe(listof(tupleof(str, str))))
+        assert isinstance(extra_headers, maybe(dictof(str, str)))
         if method == 'GET':
             assert content_type is None
             assert content_body is None

File src/htsql/export.py

 from connect import connect_adapters
 from split_sql import split_sql_adapters
 from introspect import introspect_adapters
-from translate import translate_adapters
-from produce import produce_adapters
-from render import render_adapters
+from request import request_adapters
 from tr.binder import bind_adapters
 from tr.lookup import lookup_adapters
 from tr.encoder import encode_adapters
 from tr.outliner import outline_adapters
 from tr.compiler import compile_adapters
 from tr.serializer import serialize_adapters
+from fmt import fmt_adapters
+from fmt.format import format_adapters
+from fmt.json import json_adapters
+from fmt.spreadsheet import spreadsheet_adapters
+from fmt.text import text_adapters
+from fmt.html import html_adapters
 
 
 class HTSQL_CORE(Addon):
                 outline_adapters +
                 compile_adapters +
                 serialize_adapters +
-                translate_adapters +
-                produce_adapters +
-                render_adapters)
+                request_adapters +
+                fmt_adapters +
+                format_adapters +
+                json_adapters +
+                spreadsheet_adapters +
+                text_adapters +
+                html_adapters)
 
 

File src/htsql/fmt/__init__.py

+#
+# Copyright (c) 2006-2010, Prometheus Research, LLC
+# Authors: Clark C. Evans <cce@clarkevans.com>,
+#          Kirill Simonov <xi@resolvent.net>
+#
+
+
+"""
+:mod:`htsql.fmt`
+================
+
+This package implements product formatters.
+"""
+
+
+from ..adapter import find_adapters
+from .format import FindRenderer
+from .json import JSONRenderer
+from .spreadsheet import CSVRenderer
+from .html import HTMLRenderer
+from .text import TextRenderer
+
+
+class FindStandardRenderer(FindRenderer):
+
+    def get_renderers(self):
+        return ([CSVRenderer, JSONRenderer, HTMLRenderer, TextRenderer]
+                + super(FindStandardRenderer, self).get_renderers())
+
+
+fmt_adapters = find_adapters()
+
+

File src/htsql/fmt/format.py

+#
+# Copyright (c) 2006-2010, Prometheus Research, LLC
+# Authors: Clark C. Evans <cce@clarkevans.com>,
+#          Kirill Simonov <xi@resolvent.net>
+#
+
+
+"""
+:mod:`htsql.format`
+===================
+
+This module implements the format adapter.
+"""
+
+
+from ..util import setof
+from ..adapter import Adapter, Utility, adapts, find_adapters
+from ..domain import Domain
+
+
+class Renderer(object):
+
+    name = None
+    aliases = []
+
+    @classmethod
+    def names(cls):
+        if cls.name is not None:
+            yield cls.name
+        for name in cls.aliases:
+            yield name
+
+    def render(self, product):
+        raise NotImplementedError()
+
+
+class Formatter(Adapter):
+
+    adapts(Renderer)
+
+    def __init__(self, renderer):
+        self.renderer = renderer
+
+    def format(self, value, domain):
+        format = Format(self.renderer, domain, self)
+        return format(value)
+
+
+class Format(Adapter):
+
+    adapts(Renderer, Domain)
+
+    def __init__(self, renderer, domain, tool):
+        self.renderer = renderer
+        self.domain = domain
+        self.tool = tool
+
+    def __call__(self, value):
+        raise NotImplementedError()
+
+
+class FindRenderer(Utility):
+
+    def get_renderers(self):
+        return []
+
+    def __call__(self, names):
+        assert isinstance(names, setof(str))
+        for renderer_class in self.get_renderers():
+            for name in renderer_class.names():
+                if name in names:
+                    return renderer_class
+        return None
+
+
+format_adapters = find_adapters()
+
+

File src/htsql/fmt/html.py

+#
+# Copyright (c) 2006-2010, Prometheus Research, LLC
+# Authors: Clark C. Evans <cce@clarkevans.com>,
+#          Kirill Simonov <xi@resolvent.net>
+#
+
+
+"""
+:mod:`htsql.fmt.html`
+=====================
+
+This module implements the HTML renderer.
+"""
+
+
+from ..adapter import adapts, find_adapters
+from .format import Format, Formatter, Renderer
+from ..domain import (Domain, BooleanDomain, NumberDomain,
+                      StringDomain, EnumDomain, DateDomain)
+import cgi
+
+
+class HTMLRenderer(Renderer):
+
+    name = 'text/html'
+
+    def render(self, product):
+        status = self.generate_status(product)
+        headers = self.generate_headers(product)
+        body = list(self.generate_body(product))
+        return status, headers, body
+
+    def generate_status(self, product):
+        return "200 OK"
+
+    def generate_headers(self, product):
+        return [('Content-Type', 'text/html; charset=UTF-8')]
+
+    def calculate_layout(self, product, formats):
+        segment = product.profile.binding.segment
+        caption = str(segment.base.syntax).decode('utf-8')
+        headers = [str(element.syntax).decode('utf-8')
+                   for element in segment.elements]
+        column_widths = [len(header) for header in headers]
+        total = 0
+        for record in product:
+            for idx, (format, value) in enumerate(zip(formats, record)):
+                width = format.measure(value)
+                column_widths[idx] = max(column_widths[idx], width)
+            total += 1
+        table_width = len(caption)
+        if total == 0:
+            total = u"(no rows)"
+        elif total == 1:
+            total = u"(1 row)"
+        else:
+            total = u"(%s rows)" % total
+        table_width = max(table_width, len(total)-2)
+        if formats:
+            columns_width = sum(column_widths)+3*(len(formats)-1)
+            table_width = max(table_width, columns_width)
+            if columns_width < table_width:
+                extra = table_width-columns_width
+                inc = extra/len(formats)
+                rem = extra - inc*len(formats)
+                for idx in range(len(formats)):
+                    column_widths[idx] += inc
+                    if idx < rem:
+                        column_widths[idx] += 1
+        caption = (u"%*s" % (-table_width, caption)).encode('utf-8')
+        headers = [(u"%*s" % (-width, header)).encode('utf-8')
+                   for width, header in zip(column_widths, headers)]
+        total = (u"%*s" % (table_width+4, total)).encode('utf-8')
+        return Layout(caption, headers, total, table_width, column_widths)
+
+    def generate_body(self, product):
+        for chunk in self.serialize_html(product):
+            yield chunk
+
+    def serialize_html(self, product):
+        yield "<!DOCTYPE HTML PUBLIC"
+        yield " \"-//W3C//DTD HTML 4.01 Transitional//EN\""
+        yield " \"http://www.w3.org/TR/html4/loose.dtd\">\n"
+        yield "<html>\n"
+        yield "<head>\n"
+        for chunk in self.serialize_head(product):
+            yield chunk
+        yield "</head>\n"
+        yield "<body>\n"
+        for chunk in self.serialize_body(product):
+            yield chunk
+        yield "</body>\n"
+        yield "</html>\n"
+
+    def serialize_head(self, product):
+        title = str(product.profile.syntax)
+        yield "<meta http-equiv=\"Content-Type\""
+        yield " content=\"text/html; charset=UTF-8\">\n"
+        yield "<title>%s</title>\n" % cgi.escape(title)
+        yield "<style type=\"text/css\">\n"
+        for chunk in self.serialize_style():
+            yield chunk
+        yield "</style>\n"
+
+    def serialize_style(self):
+        yield "body { font-family: sans-serif; font-size: 90%;"
+        yield " color: #515151; background: #ffffff }\n"
+        yield "a:link, a:visited { color: #1f4884; text-decoration: none }\n"
+        yield "a:hover { text-decoration: underline }\n"
+        yield "table { border-collapse: collapse;"
+        yield " margin: 0.5em auto; width: 100% }\n"
+        yield "table, tr { border-style: solid; border-width: 0 }\n"
+        yield "td, th { padding: 0.2em 0.5em; vertical-align: top;"
+        yield " text-align: left }\n"
+        yield "div.tab { position: relative; left: -1px; margin-right: 60%;"
+        yield " padding: 0.2em 0.5em; background: #ffffff;"
+        yield " border-style: solid; border-width: 5px 1px 0;"
+        yield " border-top-left-radius: 10px; border-top-right-radius: 10px;"
+        yield " -moz-border-radius-topleft: 10px;"
+        yield " -moz-border-radius-topright: 10px;"
+        yield " -webkit-border-top-left-radius: 10px;"
+        yield " -webkit-border-top-right-radius: 10px }\n"
+        yield "table.page { border: 0; padding: 1em; width: auto }\n"
+        yield "tr.content { padding: 1em 1em 0.5em }\n"
+        yield "tr.footer { padding: 0 1em 1em; text-align: left;"
+        yield " font-style: italic }\n"
+        yield "table.chart .number { text-align: right }\n"
+        yield "tr.caption { font-size: 105%; background: transparent }\n"
+        yield "tr.caption th { padding: 0 }\n"
+        yield "div.tab { border-color: #6f9ad3 #c3c3c3 }\n"
+        yield "tr.header { background: #dae3ea; border-color: #c3c3c3;"
+        yield " border-width: 1px 1px 0 }\n"
+        yield "tr.odd { background: #ffffff; border-color: #c3c3c3;"
+        yield " border-width: 0 1px }\n"
+        yield "tr.even { background: #f2f2f2; border-color: #c3c3c3;"
+        yield " border-width: 0 1px }\n"
+        yield "tr.odd:hover, tr.even:hover { background: #ffe3bd }\n"
+        yield "tr.total { background: transparent;"
+        yield "border-color: #c3c3c3; border-width: 1px 0 0 }\n"
+        yield "tr.total td { text-align: right; font-size: 75%;"
+        yield " font-style: italic; padding: 0.3em 0.5em 0 }\n"
+        yield "table.void { text-align: center;"
+        yield" border-color: #c3c3c3; border-width: 1px 0 }\n"
+
+    def serialize_body(self, product):
+        title = str(product.profile.syntax)
+        yield "<table class=\"page\" summary=\"%s\">\n" % cgi.escape(title)
+        yield "<tr>\n"
+        yield "<td class=\"content\">\n"
+        if product:
+            for chunk in self.serialize_content(product):
+                yield chunk
+        else:
+            for chunk in self.serialize_no_content():
+                yield chunk
+        yield "</td>\n"
+        yield "</tr>\n"
+        yield "<tr><td class=\"footer\">%s</td></tr>\n" % cgi.escape(title)
+        yield "</table>\n"
+
+    def serialize_no_content(self):
+        yield "<table class=\"void\">\n"
+        yield "<tr><td>no data</td></tr>\n"
+        yield "</table>\n"
+
+    def serialize_content(self, product):
+        caption = str(product.profile.segment.binding.base.syntax)
+        headers = [str(element.syntax)
+                   for element in product.profile.segment.elements]
+        width = len(product.profile.segment.elements)
+        domains = [element.domain
+                   for element in product.profile.segment.elements]
+        tool = HTMLFormatter(self)
+        formats = [Format(self, domain, tool) for domain in domains]
+        colspan = " colspan=\"%s\"" % width if width > 1 else ""
+        yield "<table class=\"chart\" summary=\"%s\">\n" % cgi.escape(caption)
+        yield "<tr class=\"caption\">"
+        yield ("<th%s><div class=\"tab\">%s</div></th>"
+                % (colspan, cgi.escape(caption)))
+        yield "</tr>\n"
+        if width:
+            yield "<tr class=\"header\">"
+            for (header, format) in zip(headers, formats):
+                style = (" class=\"%s\"" % format.style
+                         if format.style is not None else "")
+                yield "<th%s>%s</th>" % (style, cgi.escape(header))
+            yield "</tr>\n"
+        is_odd = False
+        total = 0
+        for record in product:
+            total += 1
+            is_odd = not is_odd
+            if width:
+                if is_odd:
+                    style = " class=\"odd\""
+                else:
+                    style = " class=\"even\""
+                yield "<tr%s>" % style
+                for value, format in zip(record, formats):
+                    style = (" class=\"%s\"" % format.style
+                             if format.style is not None else "")
+                    output = format(value)
+                    yield "<td%s>%s</td>" % (style, output)
+                yield "</tr>\n"
+        if total == 0:
+            total = "(no rows)"
+        elif total == 1:
+            total = "(1 row)"
+        else:
+            total = "(%s rows)" % total
+        yield "<tr class=\"total\"><td%s>%s</td></tr>" % (colspan, total)
+        yield "</table>"
+
+
+class HTMLFormatter(Formatter):
+
+    adapts(HTMLRenderer)
+
+
+class FormatDomain(Format):
+
+    adapts(HTMLRenderer, Domain)
+
+    style = None
+
+    def format_null(self):
+        return "<em>&mdash;</em>"
+
+    def __call__(self, value):
+        if value is None:
+            return self.format_null()
+        return "<em>?</em>"
+
+
+class FormatBoolean(Format):
+
+    adapts(HTMLRenderer, BooleanDomain)
+
+    def __call__(self, value):
+        if value is None:
+            return self.format_null()
+        if value is True:
+            return "<em>true</em>"
+        if value is False:
+            return "<em>false</em>"
+
+
+class FormatNumber(Format):
+
+    adapts(HTMLRenderer, NumberDomain)
+
+    style = 'number'
+
+    def __call__(self, value):
+        if value is None:
+            return self.format_null()
+        return str(value)
+
+
+class FormatString(Format):
+
+    adapts(HTMLRenderer, StringDomain)
+
+    def __call__(self, value):
+        if value is None:
+            return self.format_null()
+        return cgi.escape(value)
+
+
+class FormatEnum(Format):
+
+    adapts(HTMLRenderer, EnumDomain)
+
+    def __call__(self, value):
+        if value is None:
+            return self.format_null()
+        return cgi.escape(value)
+
+
+class FormatDate(Format):
+
+    adapts(HTMLRenderer, DateDomain)
+
+    def __call__(self, value):
+        if value is None:
+            return self.format_null()
+        return cgi.escape(value)
+
+
+html_adapters = find_adapters()
+
+

File src/htsql/fmt/json.py

+#
+# Copyright (c) 2006-2010, Prometheus Research, LLC
+# Authors: Clark C. Evans <cce@clarkevans.com>,
+#          Kirill Simonov <xi@resolvent.net>
+#
+
+
+"""
+:mod:`htsql.fmt.json`
+=====================
+
+This module implements the JSON renderer.
+"""
+
+
+from ..adapter import adapts, find_adapters
+from .format import Format, Formatter, Renderer
+from ..domain import (Domain, BooleanDomain, NumberDomain, FloatDomain,
+                      StringDomain, EnumDomain, DateDomain)
+import re
+
+
+class JSONRenderer(Renderer):
+
+    # Note: see `http://www.ietf.org/rfc/rfc4627.txt`.
+    name = 'application/json'
+    aliases = []
+
+    def render(self, product):
+        status = self.generate_status(product)
+        headers = self.generate_headers(product)
+        body = self.generate_body(product)
+        return status, headers, body
+
+    def generate_status(self, product):
+        return "200 OK"
+
+    def generate_headers(self, product):
+        return [('Content-Type', 'application/json')]
+
+    def generate_body(self, product):
+        if not product:
+            yield "[]\n"
+            return
+        domains = [element.domain
+                   for element in product.profile.segment.elements]
+        tool = JSONFormatter(self)
+        formats = [Format(self, domain, tool) for domain in domains]
+        record = None
+        for next_record in product:
+            if record is not None:
+                items = [format(value)
+                         for format, value in zip(formats, record)]
+                yield "  [%s],\n" % ", ".join(items)
+            else:
+                yield "[\n"
+            record = next_record
+        if record is not None:
+            items = [format(value)
+                     for format, value in zip(formats, record)]
+            yield "  [%s]\n" % ", ".join(items)
+            yield "]\n"
+        else:
+            yield "[]\n"
+
+
+class JSONFormatter(Formatter):
+
+    adapts(JSONRenderer)
+
+
+class FormatDomain(Format):
+
+    adapts(JSONRenderer, Domain)
+
+    escape_pattern = r"""[\x00-\x1F\\/"]"""
+    escape_regexp = re.compile(escape_pattern)
+    escape_table = {
+            '"': '"',
+            '\\': '\\',
+            '/': '/',
+            '\x08': 'b',
+            '\x0C': 'f',
+            '\x0A': 'n',
+            '\x0D': 'r',
+            '\x09': 't',
+    }
+
+    def replace(self, match):
+        char = match.group()
+        if char in self.escape_table:
+            return '\\'+self.escape_table[char]
+        return '\\u%04X' % ord(char)
+
+    def escape(self, value):
+        value = value.decode('utf-8')
+        value = self.escape_regexp.sub(self.replace, value)
+        value = value.encode('utf-8')
+        return '"%s"' % value
+
+    def __call__(self, value):
+        if value is None:
+            return "null"
+        return "\"?\""
+
+
+class FormatBoolean(Format):
+
+    adapts(JSONRenderer, BooleanDomain)
+
+    def __call__(self, value):
+        if value is None:
+            return "null"
+        if value is True:
+            return "true"
+        if value is False:
+            return "false"
+
+
+class FormatNumber(Format):
+
+    adapts(JSONRenderer, NumberDomain)
+
+    def __call__(self, value):
+        if value is None:
+            return "null"
+        return str(value)
+
+
+class FormatFloat(Format):
+
+    adapts(JSONRenderer, FloatDomain)
+
+    def __call__(self, value):
+        if value is None:
+            return "null"
+        return repr(value)
+
+
+class FormatString(Format):
+
+    adapts(JSONRenderer, StringDomain)
+
+    def __call__(self, value):
+        if value is None:
+            return "null"
+        return self.escape(value)
+
+
+class FormatEnum(Format):
+
+    adapts(JSONRenderer, EnumDomain)
+
+    def __call__(self, value):
+        if value is None:
+            return "null"
+        return self.escape(value)
+
+
+class FormatDate(Format):
+
+    adapts(JSONRenderer, DateDomain)
+
+    def __call__(self, value):
+        if value is None:
+            return "null"
+        return str(value)
+
+
+json_adapters = find_adapters()
+
+

File src/htsql/fmt/spreadsheet.py

+#
+# Copyright (c) 2006-2010, Prometheus Research, LLC
+# Authors: Clark C. Evans <cce@clarkevans.com>,
+#          Kirill Simonov <xi@resolvent.net>
+#
+
+
+"""
+:mod:`htsql.fmt.spreadsheet`
+============================
+
+This module implements the CSV renderer.
+"""
+
+
+from ..adapter import adapts, find_adapters
+from .format import Format, Formatter, Renderer
+from ..domain import (Domain, BooleanDomain, NumberDomain, FloatDomain,
+                      StringDomain, EnumDomain, DateDomain)
+import csv
+import cStringIO
+
+
+class CSVRenderer(Renderer):
+
+    name = 'text/csv'
+    aliases = []
+
+    def render(self, product):
+        status = self.generate_status(product)
+        headers = self.generate_headers(product)
+        body = self.generate_body(product)
+        return status, headers, body
+
+    def generate_status(self, product):
+        return "200 OK"
+
+    def generate_headers(self, product):
+        return [('Content-Type', 'text/csv; charset=UTF-8')]
+
+    def generate_body(self, product):
+        if not product:
+            return
+        titles = [str(element.syntax)
+                  for element in product.profile.segment.elements]
+        domains = [element.domain
+                   for element in product.profile.segment.elements]
+        tool = Formatter(self)
+        formats = [Format(self, domain, tool) for domain in domains]
+        output = cStringIO.StringIO()
+        writer = csv.writer(output)
+        writer.writerow(titles)
+        yield output.getvalue()
+        output.seek(0)
+        output.truncate()
+        for record in product:
+            items = [format(value)
+                     for format, value in zip(formats, record)]
+            writer.writerow(items)
+            yield output.getvalue()
+            output.seek(0)
+            output.truncate()
+
+
+class CSVFormatter(Formatter):
+
+    adapts(CSVRenderer)
+
+
+class FormatDomain(Format):
+
+    adapts(CSVRenderer, Domain)
+
+    def __call__(self, value):
+        if value is None:
+            return ""
+        return "\"?\""
+
+
+class FormatBoolean(Format):
+
+    adapts(CSVRenderer, BooleanDomain)
+
+    def __call__(self, value):
+        if value is None:
+            return ""
+        if value is True:
+            return "true"
+        if value is False:
+            return "false"
+
+
+class FormatNumber(Format):
+
+    adapts(CSVRenderer, NumberDomain)
+
+    def __call__(self, value):
+        if value is None:
+            return ""
+        return str(value)
+
+
+class FormatFloat(Format):
+
+    adapts(CSVRenderer, FloatDomain)
+
+    def __call__(self, value):
+        if value is None:
+            return ""
+        return repr(value)
+
+
+class FormatString(Format):
+
+    adapts(CSVRenderer, StringDomain)
+
+    def __call__(self, value):
+        if value is None:
+            return ""
+        return value
+
+
+class FormatEnum(Format):
+
+    adapts(CSVRenderer, EnumDomain)
+
+    def __call__(self, value):
+        if value is None:
+            return ""
+        return value
+
+
+class FormatDate(Format):
+
+    adapts(CSVRenderer, DateDomain)
+
+    def __call__(self, value):
+        if value is None:
+            return ""
+        return str(value)
+
+
+spreadsheet_adapters = find_adapters()
+
+

File src/htsql/fmt/text.py

+#
+# Copyright (c) 2006-2010, Prometheus Research, LLC
+# Authors: Clark C. Evans <cce@clarkevans.com>,
+#          Kirill Simonov <xi@resolvent.net>
+#
+
+
+"""
+:mod:`htsql.fmt.text`
+=====================
+
+This module implements the plain text renderer.
+"""
+
+
+from ..adapter import adapts, find_adapters
+from .format import Format, Formatter, Renderer
+from ..domain import (Domain, BooleanDomain, NumberDomain, FloatDomain,
+                      StringDomain, EnumDomain, DateDomain)
+import re
+
+
+class Layout(object):
+
+    def __init__(self, caption, headers, total, table_width, column_widths):
+        self.caption = caption
+        self.headers = headers
+        self.total = total
+        self.table_width = table_width
+        self.column_widths = column_widths
+
+
+class TextRenderer(Renderer):
+
+    name = 'text/plain'
+    aliases = ['']
+
+    def render(self, product):
+        status = self.generate_status(product)
+        headers = self.generate_headers(product)
+        body = list(self.generate_body(product))
+        return status, headers, body
+
+    def generate_status(self, product):
+        return "200 OK"
+
+    def generate_headers(self, product):
+        return [('Content-Type', 'text/plain; charset=UTF-8')]
+
+    def calculate_layout(self, product, formats):
+        segment = product.profile.binding.segment
+        caption = str(segment.base.syntax).decode('utf-8')
+        headers = [str(element.syntax).decode('utf-8')
+                   for element in segment.elements]
+        column_widths = [len(header) for header in headers]
+        total = 0
+        for record in product:
+            for idx, (format, value) in enumerate(zip(formats, record)):
+                width = format.measure(value)
+                column_widths[idx] = max(column_widths[idx], width)
+            total += 1
+        table_width = len(caption)
+        if total == 0:
+            total = u"(no rows)"
+        elif total == 1:
+            total = u"(1 row)"
+        else:
+            total = u"(%s rows)" % total
+        table_width = max(table_width, len(total)-2)
+        if formats:
+            columns_width = sum(column_widths)+3*(len(formats)-1)
+            table_width = max(table_width, columns_width)
+            if columns_width < table_width:
+                extra = table_width-columns_width
+                inc = extra/len(formats)
+                rem = extra - inc*len(formats)
+                for idx in range(len(formats)):
+                    column_widths[idx] += inc
+                    if idx < rem:
+                        column_widths[idx] += 1
+        caption = (u"%*s" % (-table_width, caption)).encode('utf-8')
+        headers = [(u"%*s" % (-width, header)).encode('utf-8')
+                   for width, header in zip(column_widths, headers)]
+        total = (u"%*s" % (table_width+4, total)).encode('utf-8')
+        return Layout(caption, headers, total, table_width, column_widths)
+
+    def generate_body(self, product):
+        request_title = str(product.profile.syntax)
+        if not product:
+            yield "(no data)\n"
+            yield "\n"
+            yield " ----\n"
+            yield " %s\n" % request_title
+            return
+        domains = [element.domain
+                   for element in product.profile.segment.elements]
+        tool = TextFormatter(self)
+        formats = [Format(self, domain, tool) for domain in domains]
+        layout = self.calculate_layout(product, formats)
+        yield " | " + layout.caption + " |\n"
+        yield "-+-" + "-"*layout.table_width + "-+-\n"
+        if product.profile.segment.elements:
+            yield (" | " +
+                   " | ".join(header for header in layout.headers) +
+                   " |\n")
+            yield ("-+-" +
+                   "-+-".join("-"*width for width in layout.column_widths) +
+                   "-+-\n")
+            for record in product:
+                columns = [format(value, width)
+                           for format, value, width
+                                in zip(formats, record, layout.column_widths)]
+                height = max(len(column) for column in columns)
+                for row_idx in range(height):
+                    if row_idx == 0:
+                        left, mid, right = " | ", " | ", " |\n"
+                    else:
+                        left, mid, right = " : ", " : ", " :\n"
+                    cells = []
+                    for idx, column in enumerate(columns):
+                        if row_idx < len(column):
+                            cell = column[row_idx]
+                        else:
+                            cell = " "*layout.column_widths[idx]
+                        cells.append(cell)
+                    if row_idx == 0:
+                        yield " | " + " | ".join(cells) + " |\n"
+                    else:
+                        yield " : " + " : ".join(cells) + " :\n"
+        yield " " + layout.total + "\n"
+        yield "\n"
+        yield " ----\n"
+        yield " %s\n" % request_title
+
+
+class TextFormatter(Formatter):
+
+    adapts(TextRenderer)
+
+
+class FormatDomain(Format):
+
+    adapts(TextRenderer, Domain)
+
+    unescaped_pattern = ur"""^(?=[^ "])[^\x00-\x1F]+(?<=[^ "])$"""
+    unescaped_regexp = re.compile(unescaped_pattern)
+
+    escape_pattern = ur"""[\x00-\x1F"\\]"""
+    escape_regexp = re.compile(escape_pattern)
+    escape_table = {
+            u'\\': u'\\\\',
+            u'"': u'\\"',
+            u'\b': u'\\b',
+            u'\f': u'\\f',
+            u'\n': u'\\n',
+            u'\r': u'\\r',
+            u'\t': u'\\t',
+    }
+
+    def escape_replace(self, match):
+        char = match.group()
+        if char in self.escape_table:
+            return self.escape_table[char]
+        return u"%%%02x" % ord(char)
+
+    def escape_string(self, value):
+        if self.unescaped_regexp.match(value):
+            return value
+        return u'"%s"' % self.escape_regexp.sub(self.escape_replace, value)
+
+    def format_null(self, width):
+        return [" "*width]
+
+    def measure(self, value):
+        if value is None:
+            return 0
+        return 1
+
+    def __call__(self, value, width):
+        if value is None:
+            return self.format_null(width)
+        return ["%*s" % (-width, "?")]
+
+
+class FormatBoolean(Format):
+
+    adapts(TextRenderer, BooleanDomain)
+
+    def measure(self, value):
+        if value is None:
+            return 0
+        if value is True:
+            return 4
+        if value is False:
+            return 5
+
+    def __call__(self, value, width):
+        if value is None:
+            return self.format_null(width)
+        if value is True:
+            return ["%*s" % (-width, "true")]
+        if value is False:
+            return ["%*s" % (-width, "false")]
+
+
+class FormatNumber(Format):
+
+    adapts(TextRenderer, NumberDomain)
+
+    def measure(self, value):
+        if value is None:
+            return 0
+        return len(str(value))
+
+    def __call__(self, value, width):
+        if value is None:
+            return self.format_null(width)
+        return ["%*s" % (width, value)]
+
+
+class FormatString(Format):
+
+    adapts(TextRenderer, StringDomain)
+
+    threshold = 32
+
+    boundary_pattern = u"""(?<=\S) (?=\S)"""
+    boundary_regexp = re.compile(boundary_pattern)
+
+    def measure(self, value):
+        if value is None:
+            return 0
+        value = value.decode('utf-8')
+        value = self.escape_string(value)
+        if len(value) <= self.threshold:
+            return len(value)
+        chunks = self.boundary_regexp.split(value)
+        max_length = max(len(chunk) for chunk in chunks)
+        if max_length >= self.threshold:
+            return max_length
+        max_length = length = 0
+        start = end = 0
+        while end < len(chunks):
+            length += len(chunks[end])
+            if end != 0:
+                length += 1
+            end += 1
+            while length > self.threshold:
+                length -= len(chunks[start])
+                if start != 0:
+                    length -= 1
+                start += 1
+            assert start < end
+            if length > max_length:
+                max_length = length
+        return max_length
+
+    def __call__(self, value, width):
+        if value is None:
+            return self.format_null(width)
+        value = value.decode('utf-8')
+        value = self.escape_string(value)
+        if len(value) <= width:
+            line = u"%*s" % (-width, value)
+            return [line.encode('utf-8')]
+        chunks = self.boundary_regexp.split(value)
+        best_badnesses = []
+        best_lengths = []
+        best_sizes = []
+        for idx in range(len(chunks)):
+            chunk = chunks[idx]
+            best_badness = None
+            best_size = None
+            best_length = None
+            length = len(chunk)
+            size = 1
+            while length <= width and idx-size >= -1:
+                if size > idx:
+                    badness = 0
+                else:
+                    tail = width - best_lengths[idx-size]
+                    badness = best_badnesses[idx-size] + tail*tail
+                if best_badness is None or best_badness > badness:
+                    best_badness = badness
+                    best_size = size
+                    best_length = length
+                if idx >= size:
+                    length += len(chunks[idx-size]) + 1
+                size += 1
+            assert best_badness is not None and best_length <= width
+            best_badnesses.append(best_badness)
+            best_lengths.append(best_length)
+            best_sizes.append(best_size)
+        lines = []
+        idx = len(chunks)
+        while idx > 0:
+            size = best_sizes[idx-1]
+            group = u" ".join(chunks[idx-size:idx])
+            assert len(group) <= width
+            line = u"%*s" % (-width, group)
+            lines.insert(0, line.encode('utf-8'))
+            idx -= size
+        return lines
+
+
+class FormatEnum(Format):
+
+    adapts(TextRenderer, EnumDomain)
+
+    def measure(self, value):
+        if value is None:
+            return 0
+        value = value.decode('utf-8')
+        value = self.escape_string(value)
+        return len(value)
+
+    def __call__(self, value, width):
+        if value is None:
+            return self.format_null(width)
+        value = value.decode('utf-8')
+        value = self.escape_string(value)
+        line = u"%*s" % (-width, value)
+        return [line.encode('utf-8')]
+
+
+class FormatDate(Format):
+
+    adapts(TextRenderer, DateDomain)
+
+    def measure(self, value):
+        if value is None:
+            return 0
+        return 10
+
+    def __call__(self, value, width):
+        if value is None:
+            return self.format_null(width)
+        return ["%*s" % (-width, value)]
+
+
+text_adapters = find_adapters()
+
+

File src/htsql/produce.py

-#
-# Copyright (c) 2006-2010, Prometheus Research, LLC
-# Authors: Clark C. Evans <cce@clarkevans.com>,
-#          Kirill Simonov <xi@resolvent.net>
-#
-
-
-"""
-:mod:`htsql.produce`
-====================
-
-This module implements the produce utility.
-"""
-
-
-from .adapter import Utility, find_adapters
-from .connect import DBError, Connect, Normalize
-
-
-class Profile(object):
-
-    def __init__(self, plan):
-        self.plan = plan
-        self.frame = plan.frame
-        self.sketch = plan.sketch
-        self.term = plan.term
-        self.code = plan.code
-        self.binding = plan.binding
-        self.syntax = plan.syntax
-
-
-class Product(object):
-
-    def __init__(self, profile, records=None):
-        self.profile = profile
-        self.records = records
-
-    def __iter__(self):
-        if self.records is not None:
-            return iter(self.records)
-        else:
-            return iter([])
-
-    def __nonzero__(self):
-        return (self.records is not None)
-
-
-class Produce(Utility):
-
-    def __call__(self, plan):
-        profile = Profile(plan)
-        records = None
-        if plan.sql:
-            try:
-                connect = Connect()
-                connection = connect()
-                cursor = connection.cursor()
-                cursor.execute(plan.sql)
-                rows = cursor.fetchall()
-                connection.close()
-            except DBError, exc:
-                raise EngineError(str(exc))
-            records = []
-            select = plan.frame.segment.select
-            normalizers = []
-            for phrase in plan.frame.segment.select:
-                normalize = Normalize(phrase.domain)
-                normalizers.append(normalize)
-            for row in rows:
-                values = []
-                for item, normalize in zip(row, normalizers):
-                    value = normalize(item)
-                    values.append(value)
-                records.append((values))
-        return Product(profile, records)
-
-
-produce_adapters = find_adapters()
-
-

File src/htsql/render.py

-#
-# Copyright (c) 2006-2010, Prometheus Research, LLC
-# Authors: Clark C. Evans <cce@clarkevans.com>,
-#          Kirill Simonov <xi@resolvent.net>
-#
-
-
-"""
-:mod:`htsql.render`
-===================
-
-This module implements the render utility.
-"""
-
-
-from .adapter import Utility, find_adapters
-
-
-class Output(object):
-
-    def __init__(self, status, headers, body):
-        self.status = status
-        self.headers = headers
-        self.body = body
-
-
-class Render(Utility):
-
-    def __call__(self, product, environ):
-        status = "200 OK"
-        headers = [('Content-Type', 'text/plain; charset=UTF-8')]
-        body = self.render_product(product)
-        return Output(status, headers, body)
-
-    def render_product(self, product):
-        if not product:
-            yield "(no data)\n"
-        else:
-            for row in product:
-                yield ", ".join(str(value) for value in row)+"\n"
-
-
-render_adapters = find_adapters()
-
-

File src/htsql/request.py

+#
+# Copyright (c) 2006-2010, Prometheus Research, LLC
+# Authors: Clark C. Evans <cce@clarkevans.com>,
+#          Kirill Simonov <xi@resolvent.net>
+#
+
+
+"""
+:mod:`htsql.request`
+====================
+
+This module implements the request utility.
+"""
+
+
+from .adapter import Utility, find_adapters
+from .connect import DBError, Connect, Normalize
+from .error import EngineError
+from .tr.parser import QueryParser
+from .tr.binder import Binder
+from .tr.encoder import Encoder
+from .tr.assembler import Assembler
+from .tr.outliner import Outliner
+from .tr.compiler import Compiler
+from .tr.serializer import Serializer
+#from .fmt.text import TextRenderer
+#from .fmt.spreadsheet import CSVRenderer
+from .fmt.format import FindRenderer
+import urllib
+
+
+class ElementProfile(object):
+
+    def __init__(self, binding):
+        self.binding = binding
+        self.domain = binding.domain
+        self.syntax = binding.syntax
+        self.mark = binding.mark
+
+
+class SegmentProfile(object):
+
+    def __init__(self, binding):
+        self.binding = binding
+        self.syntax = binding.syntax
+        self.mark = binding.mark
+        self.elements = [ElementProfile(element)
+                         for element in binding.elements]
+
+
+class RequestProfile(object):
+
+    def __init__(self, plan):
+        self.plan = plan
+        self.binding = plan.binding
+        self.syntax = plan.syntax
+        self.mark = plan.mark
+        self.segment = None
+        if plan.frame.segment is not None:
+            self.segment = SegmentProfile(plan.binding.segment)
+
+
+class Product(Utility):
+
+    def __init__(self, profile, records=None):
+        self.profile = profile
+        self.records = records
+
+    def __iter__(self):
+        if self.records is not None:
+            return iter(self.records)
+        else:
+            return iter([])
+
+    def __nonzero__(self):
+        return (self.records is not None)
+
+
+class Request(Utility):
+
+    @classmethod
+    def build(cls, environ):
+        # FIXME: override `classmethod` in `htsql.adapter`?
+        if not cls.is_realized:
+            cls = cls.realize()
+            return cls.build(environ)
+        path_info = environ['PATH_INFO']
+        query_string = environ.get('QUERY_STRING')
+        uri = urllib.quote(path_info)
+        if query_string:
+            uri += '?'+query_string
+        return cls(uri)
+
+    def __init__(self, uri):
+        self.uri = uri
+
+    def translate(self):
+        parser = QueryParser(self.uri)
+        syntax = parser.parse()
+        binder = Binder()
+        binding = binder.bind_one(syntax)
+        encoder = Encoder()
+        code = encoder.encode(binding)
+        assembler = Assembler()
+        term = assembler.assemble(code)
+        outliner = Outliner()
+        sketch = outliner.outline(term)
+        compiler = Compiler()
+        frame = compiler.compile(sketch)
+        serializer = Serializer()
+        plan = serializer.serialize(frame)
+        return plan
+
+    def produce(self):
+        plan = self.translate()
+        profile = RequestProfile(plan)
+        records = None
+        if plan.sql:
+            try:
+                connect = Connect()
+                connection = connect()
+                cursor = connection.cursor()
+                cursor.execute(plan.sql)
+                rows = cursor.fetchall()
+                connection.close()
+            except DBError, exc:
+                raise EngineError(str(exc), plan.mark)
+            records = []
+            select = plan.frame.segment.select
+            normalizers = []
+            for phrase in plan.frame.segment.select:
+                normalize = Normalize(phrase.domain)
+                normalizers.append(normalize)
+            for row in rows:
+                values = []
+                for item, normalize in zip(row, normalizers):
+                    value = normalize(item)
+                    values.append(value)
+                records.append((values))
+        return Product(profile, records)
+
+    def render(self, environ):
+        accept = set([''])
+        if 'HTTP_ACCEPT' in environ:
+            for name in environ['HTTP_ACCEPT'].split(','):
+                if ';' in name:
+                    name = name.split(';', 1)[0]
+                name = name.strip()
+                accept.add(name)
+        find_renderer = FindRenderer()
+        renderer_class = find_renderer(accept)
+        assert renderer_class is not None
+        renderer = renderer_class()
+        product = self.produce()
+        return renderer.render(product)
+
+    def __call__(self, environ):
+        return self.render(environ)
+
+
+request_adapters = find_adapters()
+
+

File src/htsql/tr/serializer.py

     adapts(LeafFrame, Serializer)
 
     def serialize(self):
-        return self.format.name(self.frame.table.name)
+        parent = self.format.name(self.frame.table.schema_name)
+        child = self.format.name(self.frame.table.name)
+        return self.format.attr(parent, child)
 
     def call(self):
         return self.frame.table.name

File src/htsql/translate.py

 from .tr.serializer import Serializer
 
 
-
 class Translate(Utility):
 
     def __call__(self, uri):

File src/htsql/util.py

                 all(isinstance(item, self.item_type) for item in value))
 
 
+class setof(object):
+    """
+    Checks if a value is a set containing elements of the specified type.
+
+    Usage::
+    
+        isinstance(value, setof(T))
+    """
+
+    # For Python 2.5, we can't use `__instancecheck__`; in this case,
+    # we let ``isinstance(setof(...)) == isinstance(list)``.
+    if sys.version_info < (2, 6):
+        def __new__(cls, *args, **kwds):
+            return set
+
+    def __init__(self, item_type):
+        self.item_type = item_type
+
+    def __instancecheck__(self, value):
+        return (isinstance(value, set) and
+                all(isinstance(item, self.item_type) for item in value))
+
+
 class tupleof(object):
     """
     Checks if a value is a tuple with the fixed number of elements

File src/htsql/wsgi.py

 """
 
 from .adapter import Utility, weights, find_adapters
+from .request import Request
 from .error import HTTPError
-from .translate import Translate
-from .produce import Produce
-from .render import Render
-import urllib
 
 
 class WSGI(Utility):
         Implements the WSGI entry point.
         """
         # Process the query.
+        request = Request.build(environ)
+        try:
+            status, headers, body = request.render(environ)
+        except HTTPError, exc:
+            return exc(environ, start_response)
+        start_response(status, headers)
+        return body
+
+    def request(self, environ):
         path_info = environ['PATH_INFO']
         query_string = environ.get('QUERY_STRING')
         uri = urllib.quote(path_info)
         if query_string:
             uri += '?'+query_string
-        try:
-            translate = Translate()
-            plan = translate(uri)
-            produce = Produce()
-            product = produce(plan)
-            render = Render()
-            output = render(product, environ)
-        except HTTPError, exc:
-            return exc(environ, start_response)
-        start_response(output.status, output.headers)
-        return output.body
+        return uri
 
 
 wsgi_adapters = find_adapters()

File src/htsql_sqlite/connect.py

 """
 
 
-from htsql.connect import Connect, DBError
-from htsql.adapter import find_adapters
+from htsql.connect import Connect, Normalize, DBError
+from htsql.adapter import adapts, find_adapters
 from htsql.context import context
+from htsql.domain import StringDomain
 # In Python 2.6, the `sqlite3` module is built-in, but
 # for Python 2.5, we need to import a third-party module.
 try:
         return super(SQLiteConnect, self).normalize_error(exception)
 
 
+class NormalizeSQLiteString(Normalize):
+
+    adapts(StringDomain)
+
+    def __call__(self, value):
+        if isinstance(value, unicode):
+            value = value.encode('utf-8')
+        return value
+
+
 connect_adapters = find_adapters()
 
 

File src/htsql_sqlite/export.py

 from .connect import connect_adapters
 from .split_sql import split_sql_adapters
 from .introspect import introspect_adapters
+from .tr.serializer import serializer_adapters
 
 
 class ENGINE_SQLITE(Addon):
     # List of adapters exported by the addon.
     adapters = (connect_adapters +
                 split_sql_adapters +
-                introspect_adapters)
+                introspect_adapters +
+                serializer_adapters)
 
 

File src/htsql_sqlite/tr/__init__.py

+#
+# Copyright (c) 2006-2010, Prometheus Research, LLC
+# Authors: Clark C. Evans <cce@clarkevans.com>,
+#          Kirill Simonov <xi@resolvent.net>
+#
+
+
+"""
+:mod:`htsql_sqlite.tr`
+======================
+
+This package adapts the HTSQL-to-SQL translator for SQLite.
+"""
+
+

File src/htsql_sqlite/tr/serializer.py

+#
+# Copyright (c) 2006-2010, Prometheus Research, LLC
+# Authors: Clark C. Evans <cce@clarkevans.com>,
+#          Kirill Simonov <xi@resolvent.net>
+#
+
+
+"""
+:mod:`htsql_sqlite.tr.serializer`
+=================================
+
+This module adapts the SQL serializer for SQLite.
+"""
+
+
+from htsql.adapter import adapts, find_adapters
+from htsql.tr.frame import LeafFrame
+from htsql.tr.serializer import Serializer, SerializeLeaf
+
+
+class SQLiteSerializeLeaf(SerializeLeaf):
+
+    adapts(LeafFrame, Serializer)
+
+    def serialize(self):
+        return self.format.name(self.frame.table.name)
+
+
+serializer_adapters = find_adapters()
+
+

File test/input/pgsql.yaml

   - uri: /test{id}
   - uri: /test{data}?id=1
     skip: true
+  - uri: /test
+    headers:
+      Accept: application/json
+  - uri: /test
+    headers:
+      Accept: text/csv
+  - uri: /test
+    headers:
+      Accept: text/plain
+  - uri: /test
+    headers:
+      Accept: text/html
 

File test/input/sqlite.yaml

   - uri: /test{id}
   - uri: /test{data}?id=1
     skip: true
+  - uri: /test
+    headers:
+      Accept: application/json
+  - uri: /test
+    headers:
+      Accept: text/csv
+  - uri: /test
+    headers:
+      Accept: text/plain
+  - uri: /test
+    headers:
+      Accept: text/html
 

File test/output/pgsql.yaml

     - [Content-Type, text/plain; charset=UTF-8]
     body: |
       (no data)
+
+       ----
+       /
   - uri: /{2+2}
     status: 200 OK
     headers:
     status: 200 OK
     headers:
     - [Content-Type, text/plain; charset=UTF-8]
-    body: |
-      1, one
-      2, two
-      3, three
+    body: |2
+       | test       |
+      -+------------+-
+       | id | data  |
+      -+----+-------+-
+       |  1 | one   |
+       |  2 | two   |
+       |  3 | three |
+             (3 rows)
+
+       ----
+       /test
   - uri: /test{id}
     status: 200 OK
     headers:
     - [Content-Type, text/plain; charset=UTF-8]
+    body: |2
+       | test   |
+      -+--------+-
+       | id     |
+      -+--------+-
+       |      1 |
+       |      2 |
+       |      3 |
+         (3 rows)
+
+       ----
+       /test{id}
+  - uri: /test
+    status: 200 OK
+    headers:
+    - [Content-Type, application/json]
     body: |
-      1
-      2
-      3
+      [
+        [1, "one"],
+        [2, "two"],
+        [3, "three"]
+      ]
+  - uri: /test
+    status: 200 OK
+    headers:
+    - [Content-Type, text/csv; charset=UTF-8]
+    body: "id,data\r\n1,one\r\n2,two\r\n3,three\r\n"
+  - uri: /test
+    status: 200 OK
+    headers:
+    - [Content-Type, text/plain; charset=UTF-8]
+    body: |2
+       | test       |
+      -+------------+-
+       | id | data  |
+      -+----+-------+-
+       |  1 | one   |
+       |  2 | two   |
+       |  3 | three |
+             (3 rows)
+
+       ----
+       /test
+  - uri: /test
+    status: 200 OK
+    headers:
+    - [Content-Type, text/html; charset=UTF-8]
+    body: |
+      <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+      <html>
+      <head>
+      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+      <title>/test</title>
+      <style type="text/css">
+      body { font-family: sans-serif; font-size: 90%; color: #515151; background: #ffffff }
+      a:link, a:visited { color: #1f4884; text-decoration: none }
+      a:hover { text-decoration: underline }
+      table { border-collapse: collapse; margin: 0.5em auto; width: 100% }
+      table, tr { border-style: solid; border-width: 0 }
+      td, th { padding: 0.2em 0.5em; vertical-align: top; text-align: left }
+      div.tab { position: relative; left: -1px; margin-right: 60%; padding: 0.2em 0.5em; background: #ffffff; border-style: solid; border-width: 5px 1px 0; border-top-left-radius: 10px; border-top-right-radius: 10px; -moz-border-radius-topleft: 10px; -moz-border-radius-topright: 10px; -webkit-border-top-left-radius: 10px; -webkit-border-top-right-radius: 10px }
+      table.page { border: 0; padding: 1em; width: auto }
+      tr.content { padding: 1em 1em 0.5em }
+      tr.footer { padding: 0 1em 1em; text-align: left; font-style: italic }
+      table.chart .number { text-align: right }
+      tr.caption { font-size: 105%; background: transparent }
+      tr.caption th { padding: 0 }
+      div.tab { border-color: #6f9ad3 #c3c3c3 }
+      tr.header { background: #dae3ea; border-color: #c3c3c3; border-width: 1px 1px 0 }
+      tr.odd { background: #ffffff; border-color: #c3c3c3; border-width: 0 1px }
+      tr.even { background: #f2f2f2; border-color: #c3c3c3; border-width: 0 1px }
+      tr.odd:hover, tr.even:hover { background: #ffe3bd }
+      tr.total { background: transparent;border-color: #c3c3c3; border-width: 1px 0 0 }
+      tr.total td { text-align: right; font-size: 75%; font-style: italic; padding: 0.3em 0.5em 0 }
+      table.void { text-align: center; border-color: #c3c3c3; border-width: 1px 0 }
+      </style>
+      </head>
+      <body>
+      <table class="page" summary="/test">
+      <tr>
+      <td class="content">
+      <table class="chart" summary="test">
+      <tr class="caption"><th colspan="2"><div class="tab">test</div></th></tr>
+      <tr class="header"><th class="number">id</th><th>data</th></tr>
+      <tr class="odd"><td class="number">1</td><td>one</td></tr>
+      <tr class="even"><td class="number">2</td><td>two</td></tr>
+      <tr class="odd"><td class="number">3</td><td>three</td></tr>
+      <tr class="total"><td colspan="2">(3 rows)</td></tr></table></td>
+      </tr>
+      <tr><td class="footer">/test</td></tr>
+      </table>
+      </body>
+      </html>
   - uri: /test{data}?id=1
     status: 200 OK
     headers:

File test/output/sqlite.yaml

     - [Content-Type, text/plain; charset=UTF-8]
     body: |
       (no data)
+
+       ----
+       /
   - uri: /{2+2}
     status: 200 OK
     headers:
     status: 200 OK
     headers:
     - [Content-Type, text/plain; charset=UTF-8]
-    body: |
-      1, one
-      2, two
-      3, three
+    body: |2
+       | test       |
+      -+------------+-
+       | id | data  |
+      -+----+-------+-
+       |  1 | one   |
+       |  2 | two   |
+       |  3 | three |
+             (3 rows)
+
+       ----
+       /test
   - uri: /test{id}
     status: 200 OK
     headers:
     - [Content-Type, text/plain; charset=UTF-8]
+    body: |2
+       | test   |
+      -+--------+-
+       | id     |
+      -+--------+-
+       |      1 |
+       |      2 |
+       |      3 |
+         (3 rows)
+
+       ----
+       /test{id}
+  - uri: /test
+    status: 200 OK
+    headers:
+    - [Content-Type, application/json]
     body: |
-      1
-      2
-      3
+      [
+        [1, "one"],
+        [2, "two"],
+        [3, "three"]
+      ]
+  - uri: /test
+    status: 200 OK
+    headers:
+    - [Content-Type, text/csv; charset=UTF-8]
+    body: "id,data\r\n1,one\r\n2,two\r\n3,three\r\n"
+  - uri: /test
+    status: 200 OK
+    headers:
+    - [Content-Type, text/plain; charset=UTF-8]
+    body: |2
+       | test       |
+      -+------------+-
+       | id | data  |
+      -+----+-------+-
+       |  1 | one   |
+       |  2 | two   |
+       |  3 | three |
+             (3 rows)
+
+       ----
+       /test
+  - uri: /test
+    status: 200 OK
+    headers:
+    - [Content-Type, text/html; charset=UTF-8]
+    body: |
+      <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+      <html>
+      <head>
+      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+      <title>/test</title>
+      <style type="text/css">
+      body { font-family: sans-serif; font-size: 90%; color: #515151; background: #ffffff }
+      a:link, a:visited { color: #1f4884; text-decoration: none }
+      a:hover { text-decoration: underline }
+      table { border-collapse: collapse; margin: 0.5em auto; width: 100% }
+      table, tr { border-style: solid; border-width: 0 }
+      td, th { padding: 0.2em 0.5em; vertical-align: top; text-align: left }
+      div.tab { position: relative; left: -1px; margin-right: 60%; padding: 0.2em 0.5em; background: #ffffff; border-style: solid; border-width: 5px 1px 0; border-top-left-radius: 10px; border-top-right-radius: 10px; -moz-border-radius-topleft: 10px; -moz-border-radius-topright: 10px; -webkit-border-top-left-radius: 10px; -webkit-border-top-right-radius: 10px }
+      table.page { border: 0; padding: 1em; width: auto }
+      tr.content { padding: 1em 1em 0.5em }
+      tr.footer { padding: 0 1em 1em; text-align: left; font-style: italic }
+      table.chart .number { text-align: right }
+      tr.caption { font-size: 105%; background: transparent }
+      tr.caption th { padding: 0 }
+      div.tab { border-color: #6f9ad3 #c3c3c3 }
+      tr.header { background: #dae3ea; border-color: #c3c3c3; border-width: 1px 1px 0 }
+      tr.odd { background: #ffffff; border-color: #c3c3c3; border-width: 0 1px }
+      tr.even { background: #f2f2f2; border-color: #c3c3c3; border-width: 0 1px }
+      tr.odd:hover, tr.even:hover { background: #ffe3bd }
+      tr.total { background: transparent;border-color: #c3c3c3; border-width: 1px 0 0 }
+      tr.total td { text-align: right; font-size: 75%; font-style: italic; padding: 0.3em 0.5em 0 }
+      table.void { text-align: center; border-color: #c3c3c3; border-width: 1px 0 }
+      </style>
+      </head>
+      <body>
+      <table class="page" summary="/test">
+      <tr>
+      <td class="content">
+      <table class="chart" summary="test">
+      <tr class="caption"><th colspan="2"><div class="tab">test</div></th></tr>
+      <tr class="header"><th class="number">id</th><th>data</th></tr>
+      <tr class="odd"><td class="number">1</td><td>one</td></tr>
+      <tr class="even"><td class="number">2</td><td>two</td></tr>
+      <tr class="odd"><td class="number">3</td><td>three</td></tr>
+      <tr class="total"><td colspan="2">(3 rows)</td></tr></table></td>
+      </tr>
+      <tr><td class="footer">/test</td></tr>
+      </table>
+      </body>
+      </html>
   - uri: /test{data}?id=1
     status: 200 OK
     headers: