diff --git a/docs/authentication.rst b/docs/authentication.rst
index 2f72e89a..e658e78b 100644
--- a/docs/authentication.rst
+++ b/docs/authentication.rst
@@ -1093,7 +1093,7 @@ All three endpoints support both HTML and JSON responses. Visit the endpoint dir
.. _PermissionRulesView:
Permission rules view
-======================
+=====================
The ``/-/rules`` endpoint displays all permission rules (both allow and deny) for each candidate resource for the requested action.
@@ -1106,7 +1106,7 @@ Pass ``?action=`` as a query parameter to specify which action to check.
.. _PermissionCheckView:
Permission check view
-======================
+=====================
The ``/-/check`` endpoint evaluates a single action/resource pair and returns information indicating whether the access was allowed along with diagnostic information.
@@ -1212,7 +1212,7 @@ Default *allow*.
.. _permissions_view_database_download:
view-database-download
------------------------
+----------------------
Actor is allowed to download a database, e.g. https://latest.datasette.io/fixtures.db
diff --git a/docs/changelog.rst b/docs/changelog.rst
index c21ba0a9..35b3c3ac 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -577,7 +577,7 @@ Documentation
.. _v0_62:
0.62 (2022-08-14)
--------------------
+-----------------
Datasette can now run entirely in your browser using WebAssembly. Try out `Datasette Lite `__, take a look `at the code `__ or read more about it in `Datasette Lite: a server-side Python web application running in a browser `__.
diff --git a/docs/deploying.rst b/docs/deploying.rst
index 3754267d..95b4b52e 100644
--- a/docs/deploying.rst
+++ b/docs/deploying.rst
@@ -79,7 +79,7 @@ Datasette will not be accessible from outside the server because it is listening
.. _deploying_openrc:
Running Datasette using OpenRC
-===============================
+==============================
OpenRC is the service manager on non-systemd Linux distributions like `Alpine Linux `__ and `Gentoo `__.
Create an init script at ``/etc/init.d/datasette`` with the following contents:
diff --git a/docs/internals.rst b/docs/internals.rst
index d71c3906..3f94f361 100644
--- a/docs/internals.rst
+++ b/docs/internals.rst
@@ -419,7 +419,7 @@ For legacy string/tuple based permission checking, use :ref:`datasette_permissio
.. _datasette_ensure_permission:
await .ensure_permission(action, resource=None, actor=None)
-------------------------------------------------------------
+-----------------------------------------------------------
``action`` - string
The action to check. See :ref:`permissions` for a list of available actions.
@@ -1047,7 +1047,7 @@ These methods each return a ``datasette.utils.PrefixedUrlString`` object, which
.. _internals_permission_classes:
Permission classes and utilities
-=================================
+================================
.. _internals_permission_sql:
@@ -1296,7 +1296,7 @@ Example usage:
.. _database_execute_write:
await db.execute_write(sql, params=None, block=True)
------------------------------------------------------
+----------------------------------------------------
SQLite only allows one database connection to write at a time. Datasette handles this for you by maintaining a queue of writes to be executed against a given database. Plugins can submit write operations to this queue and they will be executed in the order in which they are received.
@@ -1313,7 +1313,7 @@ Each call to ``execute_write()`` will be executed inside a transaction.
.. _database_execute_write_script:
await db.execute_write_script(sql, block=True)
------------------------------------------------
+----------------------------------------------
Like ``execute_write()`` but can be used to send multiple SQL statements in a single string separated by semicolons, using the ``sqlite3`` `conn.executescript() `__ method.
@@ -1322,7 +1322,7 @@ Each call to ``execute_write_script()`` will be executed inside a transaction.
.. _database_execute_write_many:
await db.execute_write_many(sql, params_seq, block=True)
----------------------------------------------------------
+--------------------------------------------------------
Like ``execute_write()`` but uses the ``sqlite3`` `conn.executemany() `__ method. This will efficiently execute the same SQL statement against each of the parameters in the ``params_seq`` iterator, for example:
diff --git a/docs/plugin_hooks.rst b/docs/plugin_hooks.rst
index 979baa84..b1615e27 100644
--- a/docs/plugin_hooks.rst
+++ b/docs/plugin_hooks.rst
@@ -780,7 +780,7 @@ The plugin hook can then be used to register the new facet class like this:
.. _plugin_register_permissions:
register_permissions(datasette)
---------------------------------
+-------------------------------
.. note::
This hook is deprecated. Use :ref:`plugin_register_actions` instead, which provides a more flexible resource-based permission system.
@@ -830,7 +830,7 @@ The fields of the ``Permission`` class are as follows:
.. _plugin_register_actions:
register_actions(datasette)
-----------------------------
+---------------------------
If your plugin needs to register actions that can be checked with Datasette's new resource-based permission system, return a list of those actions from this hook.
@@ -931,7 +931,7 @@ The fields of the ``Action`` dataclass are as follows:
- Have an ``__init__`` method that accepts appropriate parameters and calls ``super().__init__(parent=..., child=...)``
The ``resources_sql()`` method
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The ``resources_sql()`` classmethod is crucial to Datasette's permission system. It returns a SQL query that lists all resources of that type that exist in the system.
@@ -1445,7 +1445,7 @@ Example: `datasette-permissions-sql = 5 and
+ len(set(next_line)) == 1 and
+ next_line[0] in underline_chars):
+ # Skip if the previous line is empty (blank line before underline)
+ if not current_line:
+ continue
+
+ # Check if this is an overline+underline style heading
+ # Look at the line before current_line to see if it's also an underline
+ if i > 0:
+ prev_line = lines[i - 1]
+ if (prev_line and
+ len(prev_line) >= 5 and
+ len(set(prev_line)) == 1 and
+ prev_line[0] in underline_chars and
+ len(prev_line) == len(next_line)):
+ # This is overline+underline style, skip it
+ continue
+
+ # This is a heading underline
+ title_length = len(current_line)
+ underline_length = len(next_line)
+
+ if title_length != underline_length:
+ errors.append(
+ f"{rst_file.name}:{i+1}: Title length {title_length} != underline length {underline_length}\n"
+ f" Title: {current_line!r}\n"
+ f" Underline: {next_line!r}"
+ )
+
+ if errors:
+ raise AssertionError(
+ f"Found {len(errors)} RST heading(s) with mismatched underline length:\n\n" +
+ "\n\n".join(errors)
+ )
+
+
# Tests for testing_plugins.rst documentation
# fmt: off