kopia lustrzana https://github.com/simonw/datasette
CSS styling hooks as classes on the body
Refs #153 Every template now gets CSS classes in the body designed to support custom styling. The index template (the top level page at /) gets this: <body class="index"> The database template (/dbname/) gets this: <body class="db db-dbname"> The table template (/dbname/tablename) gets: <body class="table db-dbname table-tablename"> The row template (/dbname/tablename/rowid) gets: <body class="row db-dbname table-tablename"> The db-x and table-x classes use the database or table names themselves IF they are valid CSS identifiers. If they aren't, we strip any invalid characters out and append a 6 character md5 digest of the original name, in order to ensure that multiple tables which resolve to the same stripped character version still have different CSS classes. Some examples (extracted from the unit tests): "simple" => "simple" "MixedCase" => "MixedCase" "-no-leading-hyphens" => "no-leading-hyphens-65bea6" "_no-leading-underscores" => "no-leading-underscores-b921bc" "no spaces" => "no-spaces-7088d7" "-" => "336d5e" "no $ characters" => "no--characters-59e024"sanic-07
rodzic
b67890d15d
commit
8ab3a169d4
|
@ -31,6 +31,7 @@ from .utils import (
|
|||
path_with_added_args,
|
||||
path_with_ext,
|
||||
sqlite_timelimit,
|
||||
to_css_class,
|
||||
validate_sql_select,
|
||||
)
|
||||
from .version import __version__
|
||||
|
@ -897,6 +898,7 @@ class Datasette:
|
|||
self.jinja.add_env('escape_css_string', escape_css_string, 'filters')
|
||||
self.jinja.add_env('quote_plus', lambda u: urllib.parse.quote_plus(u), 'filters')
|
||||
self.jinja.add_env('escape_table_name', escape_sqlite_table_name, 'filters')
|
||||
self.jinja.add_env('to_css_class', to_css_class, 'filters')
|
||||
app.add_route(IndexView.as_view(self), '/<as_json:(.jsono?)?$>')
|
||||
# TODO: /favicon.ico and /-/static/ deserve far-future cache expires
|
||||
app.add_route(favicon, '/favicon.ico')
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
{% endfor %}
|
||||
{% block extra_head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<body class="{% block body_class %}{% endblock %}">
|
||||
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -21,6 +21,8 @@
|
|||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block body_class %}db db-{{ database|to_css_class }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="hd"><a href="/">home</a>{% if query %} / <a href="/{{ database }}-{{ database_hash }}">{{ database }}</a>{% endif %}</div>
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
{% block title %}{{ metadata.title or "Datasette" }}: {% for database in databases %}{{ database.name }}{% if not loop.last %}, {% endif %}{% endfor %}{% endblock %}
|
||||
|
||||
{% block body_class %}index{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ metadata.title or "Datasette" }}</h1>
|
||||
{% if metadata.license or metadata.source_url %}
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block body_class %}row db-{{ database|to_css_class }} table-{{ table|to_css_class }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="hd"><a href="/">home</a> / <a href="/{{ database }}-{{ database_hash }}">{{ database }}</a> / <a href="/{{ database }}-{{ database_hash }}/{{ table|quote_plus }}">{{ table }}</a></div>
|
||||
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block body_class %}table db-{{ database|to_css_class }} table-{{ table|to_css_class }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="hd"><a href="/">home</a> / <a href="/{{ database }}-{{ database_hash }}">{{ database }}</a></div>
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from contextlib import contextmanager
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
|
@ -456,3 +457,30 @@ def is_url(value):
|
|||
if whitespace_re.search(value):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
css_class_re = re.compile(r'^[a-zA-Z]+[_a-zA-Z0-9-]*$')
|
||||
css_invalid_chars_re = re.compile(r'[^a-zA-Z0-9_\-]')
|
||||
|
||||
|
||||
def to_css_class(s):
|
||||
"""
|
||||
Given a string (e.g. a table name) returns a valid unique CSS class.
|
||||
For simple cases, just returns the string again. If the string is not a
|
||||
valid CSS class (we disallow - and _ prefixes even though they are valid
|
||||
as they may be confused with browser prefixes) we strip invalid characters
|
||||
and add a 6 char md5 sum suffix, to make sure two tables with identical
|
||||
names after stripping characters don't end up with the same CSS class.
|
||||
"""
|
||||
if css_class_re.match(s):
|
||||
return s
|
||||
md5_suffix = hashlib.md5(s.encode('utf8')).hexdigest()[:6]
|
||||
# Strip leading _, -
|
||||
s = s.lstrip('_').lstrip('-')
|
||||
# Replace any whitespace with hyphens
|
||||
s = '-'.join(s.split())
|
||||
# Remove any remaining invalid characters
|
||||
s = css_invalid_chars_re.sub('', s)
|
||||
# Attach the md5 suffix
|
||||
bits = [b for b in (s, md5_suffix) if b]
|
||||
return '-'.join(bits)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from datasette.app import Datasette
|
||||
import os
|
||||
import pytest
|
||||
import re
|
||||
import sqlite3
|
||||
import tempfile
|
||||
import time
|
||||
|
@ -421,6 +422,25 @@ def test_empty_search_parameter_gets_removed(app_client):
|
|||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('path,expected_classes', [
|
||||
('/', ['index']),
|
||||
('/test_tables', ['db', 'db-test_tables']),
|
||||
('/test_tables/simple_primary_key', [
|
||||
'table', 'db-test_tables', 'table-simple_primary_key'
|
||||
]),
|
||||
('/test_tables/table%2Fwith%2Fslashes.csv', [
|
||||
'table', 'db-test_tables', 'table-tablewithslashescsv-fa7563'
|
||||
]),
|
||||
('/test_tables/simple_primary_key/1', [
|
||||
'row', 'db-test_tables', 'table-simple_primary_key'
|
||||
]),
|
||||
])
|
||||
def test_css_classes_on_body(app_client, path, expected_classes):
|
||||
response = app_client.get(path, gather_request=False)
|
||||
classes = re.search(r'<body class="(.*)">', response.text).group(1).split()
|
||||
assert classes == expected_classes
|
||||
|
||||
|
||||
TABLES = '''
|
||||
CREATE TABLE simple_primary_key (
|
||||
pk varchar(30) primary key,
|
||||
|
|
|
@ -161,3 +161,16 @@ def test_detect_fts():
|
|||
])
|
||||
def test_is_url(url, expected):
|
||||
assert expected == utils.is_url(url)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('s,expected', [
|
||||
('simple', 'simple'),
|
||||
('MixedCase', 'MixedCase'),
|
||||
('-no-leading-hyphens', 'no-leading-hyphens-65bea6'),
|
||||
('_no-leading-underscores', 'no-leading-underscores-b921bc'),
|
||||
('no spaces', 'no-spaces-7088d7'),
|
||||
('-', '336d5e'),
|
||||
('no $ characters', 'no--characters-59e024'),
|
||||
])
|
||||
def test_to_css_class(s, expected):
|
||||
assert expected == utils.to_css_class(s)
|
||||
|
|
Ładowanie…
Reference in New Issue