diff --git a/datasette/hookspecs.py b/datasette/hookspecs.py
index 9546eebf..233a9aa0 100644
--- a/datasette/hookspecs.py
+++ b/datasette/hookspecs.py
@@ -28,3 +28,8 @@ def extra_js_urls():
@hookspec
def publish_subcommand(publish):
"Subcommands for 'datasette publish'"
+
+
+@hookspec(firstresult=True)
+def render_cell(value):
+ "Customize rendering of HTML table cell values"
diff --git a/datasette/views/base.py b/datasette/views/base.py
index 45bc8183..fbb9b173 100644
--- a/datasette/views/base.py
+++ b/datasette/views/base.py
@@ -493,14 +493,20 @@ class BaseView(RenderMixin):
display_row = []
for value in row:
display_value = value
- if value in ("", None):
- display_value = jinja2.Markup(" ")
- elif is_url(str(value).strip()):
- display_value = jinja2.Markup(
- '{url}'.format(
- url=jinja2.escape(value.strip())
+ # Let the plugins have a go
+ from datasette.app import pm
+ plugin_value = pm.hook.render_cell(value=value)
+ if plugin_value is not None:
+ display_value = plugin_value
+ else:
+ if value in ("", None):
+ display_value = jinja2.Markup(" ")
+ elif is_url(str(display_value).strip()):
+ display_value = jinja2.Markup(
+ '{url}'.format(
+ url=jinja2.escape(value.strip())
+ )
)
- )
display_row.append(display_value)
display_rows.append(display_row)
return {
diff --git a/docs/plugins.rst b/docs/plugins.rst
index fc351bf6..add20669 100644
--- a/docs/plugins.rst
+++ b/docs/plugins.rst
@@ -267,3 +267,52 @@ command. Datasette uses this hook internally to implement the default ``now``
and ``heroku`` subcommands, so you can read
`their source `_
to see examples of this hook in action.
+
+render_cell(value)
+~~~~~~~~~~~~~~~~~~
+
+Lets you customize the display of values within table cells in the HTML table view.
+
+``value`` is the value that was loaded from the database.
+
+If your hook returns ``None``, it will be ignored. Use this to indicate that your hook is not able to custom render this particular value.
+
+If the hook returns a string, that string will be rendered in the table cell.
+
+If you want to return HTML markup you can do so by returning a ``jinja2.Markup`` object.
+
+Here is an example of a custom ``render_cell()`` plugin which looks for values that are a JSON string matching the following format::
+
+ {"href": "https://www.example.com/", "label": "Name"}
+
+If the value matches that pattern, the plugin returns an HTML link element:
+
+.. code-block:: python
+
+ from datasette import hookimpl
+ import jinja2
+ import json
+
+
+ @hookimpl
+ def render_cell(value):
+ # Render {"href": "...", "label": "..."} as link
+ stripped = value.strip()
+ if not stripped.startswith("{") and stripped.endswith("}"):
+ return None
+ try:
+ data = json.loads(value)
+ except ValueError:
+ return None
+ if set(data.keys()) != {"href", "label"}:
+ return None
+ href = data["href"]
+ if not (
+ href.startswith("/") or href.startswith("http://")
+ or href.startswith("https://")
+ ):
+ return None
+ return jinja2.Markup('{label}'.format(
+ href=jinja2.escape(data["href"]),
+ label=jinja2.escape(data["label"] or "") or " "
+ ))
diff --git a/tests/fixtures.py b/tests/fixtures.py
index 6335494a..955b155d 100644
--- a/tests/fixtures.py
+++ b/tests/fixtures.py
@@ -208,6 +208,8 @@ def extra_js_urls():
PLUGIN2 = '''
from datasette import hookimpl
+import jinja2
+import json
@hookimpl
@@ -216,6 +218,30 @@ def extra_js_urls():
'url': 'https://example.com/jquery.js',
'sri': 'SRIHASH',
}, 'https://example.com/plugin2.js']
+
+
+@hookimpl
+def render_cell(value):
+ # Render {"href": "...", "label": "..."} as link
+ stripped = value.strip()
+ if not stripped.startswith("{") and stripped.endswith("}"):
+ return None
+ try:
+ data = json.loads(value)
+ except ValueError:
+ return None
+ if set(data.keys()) != {"href", "label"}:
+ return None
+ href = data["href"]
+ if not (
+ href.startswith("/") or href.startswith("http://")
+ or href.startswith("https://")
+ ):
+ return None
+ return jinja2.Markup('{label}'.format(
+ href=jinja2.escape(data["href"]),
+ label=jinja2.escape(data["label"] or "") or " "
+ ))
'''
TABLES = '''
diff --git a/tests/test_plugins.py b/tests/test_plugins.py
index 487aa10c..fee27c24 100644
--- a/tests/test_plugins.py
+++ b/tests/test_plugins.py
@@ -3,6 +3,7 @@ from .fixtures import ( # noqa
app_client,
)
import pytest
+import urllib
def test_plugins_dir_plugin(app_client):
@@ -67,3 +68,20 @@ def test_plugins_with_duplicate_js_urls(app_client):
) < srcs.index(
'https://example.com/plugin2.js'
)
+
+
+def test_plugins_render_cell(app_client):
+ sql = """
+ select '{"href": "http://example.com/", "label":"Example"}'
+ """.strip()
+ path = "/fixtures?" + urllib.parse.urlencode({
+ "sql": sql,
+ })
+ response = app_client.get(path)
+ td = Soup(
+ response.body, "html.parser"
+ ).find("table").find("tbody").find("td")
+ a = td.find("a")
+ assert a is not None, str(a)
+ assert a.attrs["href"] == "http://example.com/"
+ assert a.text == "Example"