You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by va...@apache.org on 2022/09/13 02:50:26 UTC

[couchdb] branch main updated: Upgrade to latest Sphinx 5.1.1 and fix the top Edit on Github link

This is an automated email from the ASF dual-hosted git repository.

vatamane pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/couchdb.git


The following commit(s) were added to refs/heads/main by this push:
     new f1b7ce637 Upgrade to latest Sphinx 5.1.1 and fix the top Edit on Github link
f1b7ce637 is described below

commit f1b7ce6370621634ebab4e27218654efac059472
Author: Nick Vatamaniuc <va...@gmail.com>
AuthorDate: Mon Sep 12 18:37:51 2022 -0400

    Upgrade to latest Sphinx 5.1.1 and fix the top Edit on Github link
    
    "Edit on Github" link wasn't working and pointing to a non-existent "index"
    file. This was most likely because we override the index.html template, so the
    source renderer doesn't know what to link it to. Instead opt to make the top
    level "Edit on Github" point the docs readme page. Individual doc pages will
    point to their respective source pages.
    
    In addition, we were quite a bit behind on on the sphinx version 1.5 vs 5.1.1
    this has caused quite a bit of headaches over the years as we had to pin
    jinja2, docutils and other libraries versions to avoid breaking things.
    
    The main issue after the upgrade was that the http domain became a bit more
    strict. After vendoring it in, we had to make a few multipart http examples use
    plaintext. On the positive side, it found broken references in the admin guide,
    so those were fixed. Since we're using the latest 5.1.1 opt to just use a
    python3 venv with a short setup.sh script. That should make it easier for
    contributors to build docs locally.
---
 Makefile                               |   2 +-
 build-aux/Jenkinsfile.pr               |   2 +-
 src/docs/Makefile                      |   2 +-
 src/docs/ext/github.py                 |  46 ---
 src/docs/ext/httpdomain.py             | 712 ---------------------------------
 src/docs/requirements.txt              |   5 +-
 src/docs/setup.sh                      |  13 +
 src/docs/src/api/ddoc/search.rst       |   5 +-
 src/docs/src/api/document/common.rst   |  10 +-
 src/docs/src/api/server/common.rst     |  16 +-
 src/docs/src/conf.py                   |  28 +-
 src/docs/src/ddocs/search.rst          |   4 +-
 src/docs/src/intro/security.rst        |   9 +-
 src/docs/src/replication/conflicts.rst |   2 +-
 src/docs/src/replication/protocol.rst  |   4 +-
 src/docs/src/whatsnew/3.1.rst          |   2 +-
 src/docs/templates/breadcrumbs.html    |  11 +
 src/docs/templates/pages/download.html |   2 +-
 18 files changed, 63 insertions(+), 812 deletions(-)

diff --git a/Makefile b/Makefile
index 82c2b335b..34562d1b9 100644
--- a/Makefile
+++ b/Makefile
@@ -488,7 +488,7 @@ config.erl:
 
 src/docs/build:
 ifeq ($(with_docs), 1)
-	@cd src/docs; $(MAKE)
+	@cd src/docs; ./setup.sh ; $(MAKE)
 endif
 
 
diff --git a/build-aux/Jenkinsfile.pr b/build-aux/Jenkinsfile.pr
index 0a5996833..a51e2e20a 100644
--- a/build-aux/Jenkinsfile.pr
+++ b/build-aux/Jenkinsfile.pr
@@ -149,7 +149,7 @@ pipeline {
       }
       steps {
         sh '''
-           (cd src/docs && make html)
+           (cd src/docs && ./setup.sh ; make html)
          '''
       }
       post {
diff --git a/src/docs/Makefile b/src/docs/Makefile
index d9b157a6e..85833e2ae 100644
--- a/src/docs/Makefile
+++ b/src/docs/Makefile
@@ -10,7 +10,7 @@
 ## License for the specific language governing permissions and limitations under
 ## the License.
 
-SPHINXBUILD  := sphinx-build
+SPHINXBUILD  := ./.venv/bin/sphinx-build
 TEX          := tex
 PDFLATEX     := pdflatex
 MAKEINFO     := makeinfo
diff --git a/src/docs/ext/github.py b/src/docs/ext/github.py
deleted file mode 100644
index f812d9e6b..000000000
--- a/src/docs/ext/github.py
+++ /dev/null
@@ -1,46 +0,0 @@
-## Licensed under the Apache License, Version 2.0 (the "License"); you may not
-## use this file except in compliance with the License. You may obtain a copy of
-## the License at
-##
-##   http://www.apache.org/licenses/LICENSE-2.0
-##
-## Unless required by applicable law or agreed to in writing, software
-## distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-## WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-## License for the specific language governing permissions and limitations under
-## the License.
-
-import os
-
-
-def get_github_url(app, view, path):
-    return "https://github.com/{project}/{view}/{branch}/{path}".format(
-        project=app.config.github_project,
-        view=view,
-        branch=app.config.github_branch,
-        path=path,
-    )
-
-
-def html_page_context(app, pagename, templatename, context, doctree):
-    # base template for common sphinx pages like search or genindex
-    # there is no need to provide github show/edit links for them
-    if templatename != "page.html":
-        return
-
-    # ok, I'm aware about that this is wrong way to concat url segments
-    # but this is one is most portable between 2.x and 3.x versions
-    # plus it fits our current requirements. But still, patches are welcome (:
-    path = os.path.join(
-        app.config.github_docs_path,
-        os.path.relpath(doctree.get("source"), app.builder.srcdir),
-    )
-    context["github_show_url"] = get_github_url(app, "blob", path)
-    context["github_edit_url"] = get_github_url(app, "edit", path)
-
-
-def setup(app):
-    app.add_config_value("github_project", "", True)
-    app.add_config_value("github_branch", "master", True)
-    app.add_config_value("github_docs_path", "", True)
-    app.connect("html-page-context", html_page_context)
diff --git a/src/docs/ext/httpdomain.py b/src/docs/ext/httpdomain.py
deleted file mode 100644
index 5e8803d29..000000000
--- a/src/docs/ext/httpdomain.py
+++ /dev/null
@@ -1,712 +0,0 @@
-"""
-    sphinxcontrib.httpdomain
-    ~~~~~~~~~~~~~~~~~~~~~~~~
-
-    The HTTP domain for documenting RESTful HTTP APIs.
-
-    :copyright: Copyright 2011 by Hong Minhee
-    :license: BSD, see LICENSE for details.
-
-"""
-
-import re
-
-from docutils import nodes
-from docutils.parsers.rst.roles import set_classes
-
-from pygments.lexer import RegexLexer, bygroups
-from pygments.lexers import get_lexer_by_name
-from pygments.token import Literal, Text, Operator, Keyword, Name, Number
-from pygments.util import ClassNotFound
-
-from sphinx import addnodes
-from sphinx.roles import XRefRole
-from sphinx.domains import Domain, ObjType, Index
-from sphinx.directives import ObjectDescription, directives
-from sphinx.util.nodes import make_refnode
-from sphinx.util.docfields import GroupedField, TypedField
-
-
-class DocRef(object):
-    """Represents a link to an RFC which defines an HTTP method."""
-
-    def __init__(self, base_url, anchor, section):
-        """Stores the specified attributes which represent a URL which links to
-        an RFC which defines an HTTP method.
-
-        """
-        self.base_url = base_url
-        self.anchor = anchor
-        self.section = section
-
-    def __repr__(self):
-        """Returns the URL which this object represents, which points to the
-        location of the RFC which defines some HTTP method.
-
-        """
-        return "{0}#{1}{2}".format(self.base_url, self.anchor, self.section)
-
-
-class RFC2616Ref(DocRef):
-    def __init__(self, section):
-        url = "http://www.w3.org/Protocols/rfc2616/rfc2616-sec{0:d}.html"
-        url = url.format(int(section))
-        super(RFC2616Ref, self).__init__(url, "sec", section)
-
-
-class IETFRef(DocRef):
-    def __init__(self, rfc, section):
-        url = "http://tools.ietf.org/html/rfc{0:d}".format(rfc)
-        super(IETFRef, self).__init__(url, "section-", section)
-
-
-class EventSourceRef(DocRef):
-    def __init__(self, section):
-        url = "http://www.w3.org/TR/eventsource/"
-        super(EventSourceRef, self).__init__(url, section, "")
-
-
-#: Mapping from lowercase HTTP method name to :class:`DocRef` object which
-#: maintains the URL which points to the section of the RFC which defines that
-#: HTTP method.
-METHOD_REFS = {
-    "patch": IETFRef(5789, 2),
-    "options": RFC2616Ref(9.2),
-    "get": RFC2616Ref(9.3),
-    "head": RFC2616Ref(9.4),
-    "post": RFC2616Ref(9.5),
-    "put": RFC2616Ref(9.6),
-    "delete": RFC2616Ref(9.7),
-    "trace": RFC2616Ref(9.8),
-    "connect": RFC2616Ref(9.9),
-    "copy": IETFRef(2518, 8.8),
-    "any": "",
-}
-
-#: Mapping from HTTP header name to :class:`DocRef` object which
-#: maintains the URL which points to the related section of the RFC.
-HEADER_REFS = {
-    "Accept": RFC2616Ref(14.1),
-    "Accept-Charset": RFC2616Ref(14.2),
-    "Accept-Encoding": RFC2616Ref(14.3),
-    "Accept-Language": RFC2616Ref(14.4),
-    "Accept-Ranges": RFC2616Ref(14.5),
-    "Age": RFC2616Ref(14.6),
-    "Allow": RFC2616Ref(14.7),
-    "Authorization": RFC2616Ref(14.8),
-    "Cache-Control": RFC2616Ref(14.9),
-    "Cookie": IETFRef(2109, "4.3.4"),
-    "Connection": RFC2616Ref(14.10),
-    "Content-Encoding": RFC2616Ref(14.11),
-    "Content-Language": RFC2616Ref(14.12),
-    "Content-Length": RFC2616Ref(14.13),
-    "Content-Location": RFC2616Ref(14.14),
-    "Content-MD5": RFC2616Ref(14.15),
-    "Content-Range": RFC2616Ref(14.16),
-    "Content-Type": RFC2616Ref(14.17),
-    "Date": RFC2616Ref(14.18),
-    "Destination": IETFRef(2518, 9.3),
-    "ETag": RFC2616Ref(14.19),
-    "Expect": RFC2616Ref(14.20),
-    "Expires": RFC2616Ref(14.21),
-    "From": RFC2616Ref(14.22),
-    "Host": RFC2616Ref(14.23),
-    "If-Match": RFC2616Ref(14.24),
-    "If-Modified-Since": RFC2616Ref(14.25),
-    "If-None-Match": RFC2616Ref(14.26),
-    "If-Range": RFC2616Ref(14.27),
-    "If-Unmodified-Since": RFC2616Ref(14.28),
-    "Last-Event-ID": EventSourceRef("last-event-id"),
-    "Last-Modified": RFC2616Ref(14.29),
-    "Location": RFC2616Ref(14.30),
-    "Max-Forwards": RFC2616Ref(14.31),
-    "Pragma": RFC2616Ref(14.32),
-    "Proxy-Authenticate": RFC2616Ref(14.33),
-    "Proxy-Authorization": RFC2616Ref(14.34),
-    "Range": RFC2616Ref(14.35),
-    "Referer": RFC2616Ref(14.36),
-    "Retry-After": RFC2616Ref(14.37),
-    "Server": RFC2616Ref(14.38),
-    "Set-Cookie": IETFRef(2109, "4.2.2"),
-    "TE": RFC2616Ref(14.39),
-    "Trailer": RFC2616Ref(14.40),
-    "Transfer-Encoding": RFC2616Ref(14.41),
-    "Upgrade": RFC2616Ref(14.42),
-    "User-Agent": RFC2616Ref(14.43),
-    "Vary": RFC2616Ref(14.44),
-    "Via": RFC2616Ref(14.45),
-    "Warning": RFC2616Ref(14.46),
-    "WWW-Authenticate": RFC2616Ref(14.47),
-}
-
-
-HTTP_STATUS_CODES = {
-    100: "Continue",
-    101: "Switching Protocols",
-    102: "Processing",
-    200: "OK",
-    201: "Created",
-    202: "Accepted",
-    203: "Non Authoritative Information",
-    204: "No Content",
-    205: "Reset Content",
-    206: "Partial Content",
-    207: "Multi Status",
-    226: "IM Used",  # see RFC 3229
-    300: "Multiple Choices",
-    301: "Moved Permanently",
-    302: "Found",
-    303: "See Other",
-    304: "Not Modified",
-    305: "Use Proxy",
-    307: "Temporary Redirect",
-    400: "Bad Request",
-    401: "Unauthorized",
-    402: "Payment Required",  # unused
-    403: "Forbidden",
-    404: "Not Found",
-    405: "Method Not Allowed",
-    406: "Not Acceptable",
-    407: "Proxy Authentication Required",
-    408: "Request Timeout",
-    409: "Conflict",
-    410: "Gone",
-    411: "Length Required",
-    412: "Precondition Failed",
-    413: "Request Entity Too Large",
-    414: "Request URI Too Long",
-    415: "Unsupported Media Type",
-    416: "Requested Range Not Satisfiable",
-    417: "Expectation Failed",
-    418: "I'm a teapot",  # see RFC 2324
-    422: "Unprocessable Entity",
-    423: "Locked",
-    424: "Failed Dependency",
-    426: "Upgrade Required",
-    449: "Retry With",  # proprietary MS extension
-    500: "Internal Server Error",
-    501: "Not Implemented",
-    502: "Bad Gateway",
-    503: "Service Unavailable",
-    504: "Gateway Timeout",
-    505: "HTTP Version Not Supported",
-    507: "Insufficient Storage",
-    510: "Not Extended",
-}
-
-http_sig_param_re = re.compile(
-    r"\((?:(?P<type>[^:)]+):)?(?P<name>[\w_]+)\)", re.VERBOSE
-)
-
-
-def sort_by_method(entries):
-    def cmp(item):
-        order = ["HEAD", "GET", "POST", "PUT", "DELETE", "COPY", "OPTIONS"]
-        method = item[0].split(" ", 1)[0]
-        if method in order:
-            return order.index(method)
-        return 100
-
-    return sorted(entries, key=cmp)
-
-
-def http_resource_anchor(method, path):
-    path = re.sub(r"[{}]", "", re.sub(r"[<>:/]", "-", path))
-    return method.lower() + "-" + path
-
-
-class HTTPResource(ObjectDescription):
-
-    doc_field_types = [
-        TypedField(
-            "parameter",
-            label="Parameters",
-            names=("param", "parameter", "arg", "argument"),
-            typerolename="obj",
-            typenames=("paramtype", "type"),
-        ),
-        TypedField(
-            "jsonobject",
-            label="JSON Object",
-            names=("jsonparameter", "jsonparam", "json"),
-            typerolename="obj",
-            typenames=("jsonparamtype", "jsontype"),
-        ),
-        TypedField(
-            "requestjsonobject",
-            label="Request JSON Object",
-            names=("reqjsonobj", "reqjson", "<jsonobj", "<json"),
-            typerolename="obj",
-            typenames=("reqjsontype", "<jsontype"),
-        ),
-        TypedField(
-            "requestjsonarray",
-            label="Request JSON Array of Objects",
-            names=("reqjsonarr", "<jsonarr"),
-            typerolename="obj",
-            typenames=("reqjsonarrtype", "<jsonarrtype"),
-        ),
-        TypedField(
-            "responsejsonobject",
-            label="Response JSON Object",
-            names=("resjsonobj", "resjson", ">jsonobj", ">json"),
-            typerolename="obj",
-            typenames=("resjsontype", ">jsontype"),
-        ),
-        TypedField(
-            "responsejsonarray",
-            label="Response JSON Array of Objects",
-            names=("resjsonarr", ">jsonarr"),
-            typerolename="obj",
-            typenames=("resjsonarrtype", ">jsonarrtype"),
-        ),
-        TypedField(
-            "queryparameter",
-            label="Query Parameters",
-            names=("queryparameter", "queryparam", "qparam", "query"),
-            typerolename="obj",
-            typenames=("queryparamtype", "querytype", "qtype"),
-        ),
-        GroupedField(
-            "formparameter",
-            label="Form Parameters",
-            names=("formparameter", "formparam", "fparam", "form"),
-        ),
-        GroupedField(
-            "requestheader",
-            label="Request Headers",
-            rolename="mailheader",
-            names=("<header", "reqheader", "requestheader"),
-        ),
-        GroupedField(
-            "responseheader",
-            label="Response Headers",
-            rolename="mailheader",
-            names=(">header", "resheader", "responseheader"),
-        ),
-        GroupedField(
-            "statuscode",
-            label="Status Codes",
-            rolename="statuscode",
-            names=("statuscode", "status", "code"),
-        ),
-    ]
-
-    option_spec = {
-        "deprecated": directives.flag,
-        "noindex": directives.flag,
-        "synopsis": lambda x: x,
-    }
-
-    method = NotImplemented
-
-    def handle_signature(self, sig, signode):
-        method = self.method.upper() + " "
-        signode += addnodes.desc_name(method, method)
-        offset = 0
-        path = None
-        for match in http_sig_param_re.finditer(sig):
-            path = sig[offset : match.start()]
-            signode += addnodes.desc_name(path, path)
-            params = addnodes.desc_parameterlist()
-            typ = match.group("type")
-            if typ:
-                typ += ": "
-                params += addnodes.desc_annotation(typ, typ)
-            name = match.group("name")
-            params += addnodes.desc_parameter(name, name)
-            signode += params
-            offset = match.end()
-        if offset < len(sig):
-            path = sig[offset : len(sig)]
-            signode += addnodes.desc_name(path, path)
-        if path is None:
-            assert False, "no matches for sig: %s" % sig
-        fullname = self.method.upper() + " " + path
-        signode["method"] = self.method
-        signode["path"] = sig
-        signode["fullname"] = fullname
-        return (fullname, self.method, sig)
-
-    def needs_arglist(self):
-        return False
-
-    def add_target_and_index(self, name_cls, sig, signode):
-        signode["ids"].append(http_resource_anchor(*name_cls[1:]))
-        if "noindex" not in self.options:
-            self.env.domaindata["http"][self.method][sig] = (
-                self.env.docname,
-                self.options.get("synopsis", ""),
-                "deprecated" in self.options,
-            )
-
-    def get_index_text(self, modname, name):
-        return ""
-
-
-class HTTPOptions(HTTPResource):
-
-    method = "options"
-
-
-class HTTPHead(HTTPResource):
-
-    method = "head"
-
-
-class HTTPPatch(HTTPResource):
-
-    method = "patch"
-
-
-class HTTPPost(HTTPResource):
-
-    method = "post"
-
-
-class HTTPGet(HTTPResource):
-
-    method = "get"
-
-
-class HTTPPut(HTTPResource):
-
-    method = "put"
-
-
-class HTTPDelete(HTTPResource):
-
-    method = "delete"
-
-
-class HTTPTrace(HTTPResource):
-
-    method = "trace"
-
-
-class HTTPCopy(HTTPResource):
-
-    method = "copy"
-
-
-class HTTPAny(HTTPResource):
-
-    method = "any"
-
-
-def http_statuscode_role(
-    name, rawtext, text, lineno, inliner, options=None, content=None
-):
-    if options is None:
-        options = {}
-    if content is None:
-        content = []
-    if text.isdigit():
-        code = int(text)
-        try:
-            status = HTTP_STATUS_CODES[code]
-        except KeyError:
-            msg = inliner.reporter.error(
-                "%d is invalid HTTP status code" % code, lineno=lineno
-            )
-            prb = inliner.problematic(rawtext, rawtext, msg)
-            return [prb], [msg]
-    else:
-        try:
-            code, status = re.split(r"\s", text.strip(), 1)
-            code = int(code)
-        except ValueError:
-            msg = inliner.reporter.error(
-                "HTTP status code must be an integer (e.g. `200`) or "
-                "start with an integer (e.g. `200 OK`); %r is invalid" % text,
-                line=lineno,
-            )
-            prb = inliner.problematic(rawtext, rawtext, msg)
-            return [prb], [msg]
-    nodes.reference(rawtext)
-    if code == 226:
-        url = "http://www.ietf.org/rfc/rfc3229.txt"
-    elif code == 418:
-        url = "http://www.ietf.org/rfc/rfc2324.txt"
-    elif code == 449:
-        url = "http://msdn.microsoft.com/en-us/library/dd891478(v=prot.10).aspx"
-    elif code in HTTP_STATUS_CODES:
-        url = "http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html" "#sec10." + (
-            "%d.%d" % (code // 100, 1 + code % 100)
-        )
-    else:
-        url = ""
-    set_classes(options)
-    node = nodes.reference(rawtext, "%d %s" % (code, status), refuri=url, **options)
-    return [node], []
-
-
-def http_method_role(name, rawtext, text, lineno, inliner, options=None, content=None):
-    if options is None:
-        options = {}
-    if content is None:
-        content = []
-    method = str(text).lower()
-    if method not in METHOD_REFS:
-        msg = inliner.reporter.error(
-            "%s is not valid HTTP method" % method, lineno=lineno
-        )
-        prb = inliner.problematic(rawtext, rawtext, msg)
-        return [prb], [msg]
-    url = str(METHOD_REFS[method])
-    node = nodes.reference(rawtext, method.upper(), refuri=url, **options)
-    return [node], []
-
-
-def http_header_role(name, rawtext, text, lineno, inliner, options=None, content=None):
-    if options is None:
-        options = {}
-    if content is None:
-        content = []
-    header = str(text)
-    if header not in HEADER_REFS:
-        header = header.title()
-    if header not in HEADER_REFS:
-        if header.startswith(("X-Couch-", "Couch-")):
-            return [nodes.strong(header, header)], []
-        msg = inliner.reporter.error(
-            "%s is not unknown HTTP header" % header, lineno=lineno
-        )
-        prb = inliner.problematic(rawtext, rawtext, msg)
-        return [prb], [msg]
-    url = str(HEADER_REFS[header])
-    node = nodes.reference(rawtext, header, refuri=url, **options)
-    return [node], []
-
-
-class HTTPXRefRole(XRefRole):
-    def __init__(self, method, **kwargs):
-        XRefRole.__init__(self, **kwargs)
-        self.method = method
-
-    def process_link(self, env, refnode, has_explicit_title, title, target):
-        if not target.startswith("/"):
-            pass
-        if not has_explicit_title:
-            title = self.method.upper() + " " + title
-        return title, target
-
-
-class HTTPIndex(Index):
-
-    name = "api"
-    localname = "API Quick Reference"
-    shortname = "API Reference"
-
-    def generate(self, docnames=None):
-        content = {}
-        items = (
-            (method, path, info)
-            for method, routes in self.domain.routes.items()
-            for path, info in routes.items()
-        )
-        items = sorted(items, key=lambda item: item[1])
-        for method, path, info in items:
-            entries = content.setdefault(path, [])
-            entry_name = method.upper() + " " + path
-            entries.append(
-                [
-                    entry_name,
-                    0,
-                    info[0],
-                    http_resource_anchor(method, path),
-                    "",
-                    "Deprecated" if info[2] else "",
-                    info[1],
-                ]
-            )
-        items = sorted(
-            (path, sort_by_method(entries)) for path, entries in content.items()
-        )
-        return (items, True)
-
-
-class HTTPDomain(Domain):
-    """HTTP domain."""
-
-    name = "http"
-    label = "HTTP"
-
-    object_types = {
-        "options": ObjType("options", "options", "obj"),
-        "head": ObjType("head", "head", "obj"),
-        "post": ObjType("post", "post", "obj"),
-        "get": ObjType("get", "get", "obj"),
-        "put": ObjType("put", "put", "obj"),
-        "patch": ObjType("patch", "patch", "obj"),
-        "delete": ObjType("delete", "delete", "obj"),
-        "trace": ObjType("trace", "trace", "obj"),
-        "copy": ObjType("copy", "copy", "obj"),
-        "any": ObjType("any", "any", "obj"),
-    }
-
-    directives = {
-        "options": HTTPOptions,
-        "head": HTTPHead,
-        "post": HTTPPost,
-        "get": HTTPGet,
-        "put": HTTPPut,
-        "patch": HTTPPatch,
-        "delete": HTTPDelete,
-        "trace": HTTPTrace,
-        "copy": HTTPCopy,
-        "any": HTTPAny,
-    }
-
-    roles = {
-        "options": HTTPXRefRole("options"),
-        "head": HTTPXRefRole("head"),
-        "post": HTTPXRefRole("post"),
-        "get": HTTPXRefRole("get"),
-        "put": HTTPXRefRole("put"),
-        "patch": HTTPXRefRole("patch"),
-        "delete": HTTPXRefRole("delete"),
-        "trace": HTTPXRefRole("trace"),
-        "copy": HTTPXRefRole("copy"),
-        "all": HTTPXRefRole("all"),
-        "statuscode": http_statuscode_role,
-        "method": http_method_role,
-        "header": http_header_role,
-    }
-
-    initial_data = {
-        "options": {},  # path: (docname, synopsis)
-        "head": {},
-        "post": {},
-        "get": {},
-        "put": {},
-        "patch": {},
-        "delete": {},
-        "trace": {},
-        "copy": {},
-        "any": {},
-    }
-
-    indices = [HTTPIndex]
-
-    @property
-    def routes(self):
-        return dict((key, self.data[key]) for key in self.object_types)
-
-    def clear_doc(self, docname):
-        for typ, routes in self.routes.items():
-            for path, info in list(routes.items()):
-                if info[0] == docname:
-                    del routes[path]
-
-    def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode):
-        try:
-            info = self.data[str(typ)][target]
-        except KeyError:
-            text = contnode.rawsource
-            if typ == "statuscode":
-                return http_statuscode_role(None, text, text, None, None)[0][0]
-            elif typ == "mailheader":
-                return http_header_role(None, text, text, None, None)[0][0]
-            else:
-                return nodes.emphasis(text, text)
-        else:
-            anchor = http_resource_anchor(typ, target)
-            title = typ.upper() + " " + target
-            return make_refnode(builder, fromdocname, info[0], anchor, contnode, title)
-
-    def get_objects(self):
-        for method, routes in self.routes.items():
-            for path, info in routes.items():
-                anchor = http_resource_anchor(method, path)
-                yield (path, path, method, info[0], anchor, 1)
-
-
-class HTTPLexer(RegexLexer):
-    """Lexer for HTTP sessions."""
-
-    name = "HTTP"
-    aliases = ["http"]
-
-    flags = re.DOTALL
-
-    def header_callback(self, match):
-        if match.group(1).lower() == "content-type":
-            content_type = match.group(5).strip()
-            if ";" in content_type:
-                content_type = content_type[: content_type.find(";")].strip()
-            self.content_type = content_type
-        yield match.start(1), Name.Attribute, match.group(1)
-        yield match.start(2), Text, match.group(2)
-        yield match.start(3), Operator, match.group(3)
-        yield match.start(4), Text, match.group(4)
-        yield match.start(5), Literal, match.group(5)
-        yield match.start(6), Text, match.group(6)
-
-    def continuous_header_callback(self, match):
-        yield match.start(1), Text, match.group(1)
-        yield match.start(2), Literal, match.group(2)
-        yield match.start(3), Text, match.group(3)
-
-    def content_callback(self, match):
-        content_type = getattr(self, "content_type", None)
-        content = match.group()
-        offset = match.start()
-        if content_type:
-            from pygments.lexers import get_lexer_for_mimetype
-
-            try:
-                lexer = get_lexer_for_mimetype(content_type)
-            except ClassNotFound:
-                pass
-            else:
-                for idx, token, value in lexer.get_tokens_unprocessed(content):
-                    yield offset + idx, token, value
-                return
-        yield offset, Text, content
-
-    tokens = {
-        "root": [
-            (
-                r"(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|TRACE|COPY)"
-                r"( +)([^ ]+)( +)"
-                r"(HTTPS?)(/)(1\.[01])(\r?\n|$)",
-                bygroups(
-                    Name.Function,
-                    Text,
-                    Name.Namespace,
-                    Text,
-                    Keyword.Reserved,
-                    Operator,
-                    Number,
-                    Text,
-                ),
-                "headers",
-            ),
-            (
-                r"(HTTPS?)(/)(1\.[01])( +)(\d{3})( +)([^\r\n]+)(\r?\n|$)",
-                bygroups(
-                    Keyword.Reserved,
-                    Operator,
-                    Number,
-                    Text,
-                    Number,
-                    Text,
-                    Name.Exception,
-                    Text,
-                ),
-                "headers",
-            ),
-        ],
-        "headers": [
-            (r"([^\s:]+)( *)(:)( *)([^\r\n]+)(\r?\n|$)", header_callback),
-            (r"([\t ]+)([^\r\n]+)(\r?\n|$)", continuous_header_callback),
-            (r"\r?\n", Text, "content"),
-        ],
-        "content": [(r".+", content_callback)],
-    }
-
-
-def setup(app):
-    app.add_domain(HTTPDomain)
-    app.add_lexer("http", HTTPLexer())
diff --git a/src/docs/requirements.txt b/src/docs/requirements.txt
index d1ac8cef0..f5755bb0d 100644
--- a/src/docs/requirements.txt
+++ b/src/docs/requirements.txt
@@ -1,3 +1,4 @@
-Sphinx==2.4.4
+Sphinx==5.1.1
 sphinx-rtd-theme==1.0.0
-jinja2<3.1
\ No newline at end of file
+sphinxcontrib-httpdomain==1.8.0
+
diff --git a/src/docs/setup.sh b/src/docs/setup.sh
new file mode 100755
index 000000000..4f56b1775
--- /dev/null
+++ b/src/docs/setup.sh
@@ -0,0 +1,13 @@
+#!/bin/sh
+
+set -e
+
+if [ ! -f ./.venv/bin/activate ]; then
+    rm -rf ./.venv
+    python3 -m venv .venv
+    . ./.venv/bin/activate
+    pip install --upgrade pip
+    pip install -r requirements.txt
+else
+    . ./.venv/bin/activate
+fi
diff --git a/src/docs/src/api/ddoc/search.rst b/src/docs/src/api/ddoc/search.rst
index 3a4802181..37b47a154 100644
--- a/src/docs/src/api/ddoc/search.rst
+++ b/src/docs/src/api/ddoc/search.rst
@@ -154,7 +154,10 @@
 
 **Response**:
 
-.. code-block:: javascript
+.. code-block:: http
+
+    HTTP/1.1 200 OK
+    Content-Type: application/json
 
     {
         "name": "_design/cookbook/ingredients",
diff --git a/src/docs/src/api/document/common.rst b/src/docs/src/api/document/common.rst
index b8e5cd178..03ea29064 100644
--- a/src/docs/src/api/document/common.rst
+++ b/src/docs/src/api/document/common.rst
@@ -378,7 +378,7 @@
 
     **Request**:
 
-    .. code-block:: http
+    .. code-block:: text
 
         COPY /recipes/SpaghettiWithMeatballs HTTP/1.1
         Accept: application/json
@@ -636,7 +636,7 @@ To solve this problem, CouchDB allows to get documents in
 
 **Response**:
 
-.. code-block:: http
+.. code-block:: text
 
     HTTP/1.1 200 OK
     Content-Length: 538
@@ -773,7 +773,7 @@ The subsequent MIME bodies are the attachments.
 
 **Request**:
 
-.. code-block:: http
+.. code-block:: text
 
     PUT /temp/somedoc HTTP/1.1
     Accept: application/json
@@ -1150,7 +1150,7 @@ or :header:`If-Match`:
 
 **Request**:
 
-.. code-block:: http
+.. code-block:: text
 
     COPY /recipes/SpaghettiWithMeatballs HTTP/1.1
     Accept: application/json
@@ -1188,7 +1188,7 @@ for the target document by appending the ``rev`` parameter to the
 
 **Request**:
 
-.. code-block:: http
+.. code-block:: text
 
     COPY /recipes/SpaghettiWithMeatballs?rev=8-6f5ad8db0f34af24a6e0984cd1a6cfb9 HTTP/1.1
     Accept: application/json
diff --git a/src/docs/src/api/server/common.rst b/src/docs/src/api/server/common.rst
index fa1c1beba..c73bd7c7a 100644
--- a/src/docs/src/api/server/common.rst
+++ b/src/docs/src/api/server/common.rst
@@ -1948,8 +1948,7 @@ See :ref:`Configuration of Prometheus Endpoint <config/prometheus>` for details.
 
         {
           "uptime": 259,
-          "memory": {
-          ...
+          "memory": {}
         }
 
     These statistics are generally intended for CouchDB developers only.
@@ -2352,8 +2351,7 @@ You can verify the change by obtaining a list of UUIDs:
                             "detail": "initial_copy",
                             "timestamp": "2019-03-28T15:28:02Z",
                             "type": "running"
-                        },
-                        ...
+                        }
                     ],
                     "id": "001-171d1211418996ff47bd610b1d1257fc4ca2628868def4a05e63e8f8fe50694a",
                     "job_state": "completed",
@@ -2368,8 +2366,7 @@ You can verify the change by obtaining a list of UUIDs:
                     ],
                     "type": "split",
                     "update_time": "2019-03-28T15:28:08Z"
-                },
-                ...
+                }
             ],
             "offset": 0,
             "total_rows": 24
@@ -2446,8 +2443,7 @@ You can verify the change by obtaining a list of UUIDs:
                     "detail": "initial_copy",
                     "timestamp": "2019-03-28T15:28:02Z",
                     "type": "running"
-                },
-                ...
+                }
             ]
         }
 
@@ -2505,11 +2501,11 @@ You can verify the change by obtaining a list of UUIDs:
         Accept: application/json
         Content-Type: application/json
 
-       {
+        {
            "db": "db3",
            "range": "80000000-ffffffff",
            "type": "split"
-       }
+        }
 
     **Response**:
 
diff --git a/src/docs/src/conf.py b/src/docs/src/conf.py
index c0da58004..01f197b1a 100644
--- a/src/docs/src/conf.py
+++ b/src/docs/src/conf.py
@@ -18,18 +18,15 @@ import sphinx_rtd_theme
 
 sys.path.insert(0, os.path.abspath("../ext"))
 
-needs_sphinx = "1.5"
+needs_sphinx = "5.1.1"
 
 extensions = [
     "sphinx.ext.todo",
     "sphinx.ext.extlinks",
-    "github",
-    "httpdomain",
+    "sphinxcontrib.httpdomain",
     "configdomain",
 ]
 
-source_suffix = ".rst"
-
 nitpicky = True
 
 # should be over-written using rebar-inherited settings
@@ -51,7 +48,7 @@ pygments_style = "sphinx"
 html_theme = "sphinx_rtd_theme"
 html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
 
-html_theme_options = {"canonical_url": "http://docs.couchdb.org/en/stable/"}
+html_theme_options = {"canonical_url": "https://docs.couchdb.org/en/stable/"}
 
 templates_path = ["../templates"]
 
@@ -59,8 +56,6 @@ html_static_path = ["../static"]
 
 html_title = " ".join([project, version, "Documentation"])
 
-# html_style = "css/rtd_theme.css"
-
 html_logo = "../images/logo.png"
 
 html_favicon = "../images/favicon.ico"
@@ -75,13 +70,12 @@ html_context = {
     "display_github": False,
     # Set the following variables to generate the resulting github URL for each page.
     # Format Template: https://{{ github_host|default("github.com") }}/{{ github_user }}/{{ github_repo }}/blob/{{ github_version }}{{ conf_py_path }}{{ pagename }}{{ suffix }}
+    "github_version": "main",
+    "conf_py_path": "/src/docs/src/",
     "github_user": "apache",
     "github_repo": "couchdb",
-    "github_version": "main/src/",
 }
 
-master_doc = "index"
-
 text_newlines = "native"
 
 latex_documents = [("index", "CouchDB.tex", project, "", "manual", True)]
@@ -102,20 +96,14 @@ texinfo_documents = [
 ]
 
 extlinks = {
-    "issue": ("%s-%%s" % "https://issues.apache.org/jira/browse/COUCHDB", "COUCHDB-"),
-    "ghissue": ("https://github.com/apache/couchdb/issues/%s", "#"),
+    "issue": ("https://issues.apache.org/jira/browse/COUCHDB-%s", "COUCHDB-%s"),
+    "ghissue": ("https://github.com/apache/couchdb/issues/%s", "#%s"),
     "commit": (
         "https://git-wip-us.apache.org/repos/asf?p=couchdb.git;a=commit;h=%s",
-        "#",
+        "#%s",
     ),
 }
 
-github_project = "apache/couchdb"
-
-html_context["git_branch"] = github_branch = "main"
-
-github_docs_path = "src/docs/src"
-
 
 def setup(app):
     app.add_css_file("css/rtd_theme.css")
diff --git a/src/docs/src/ddocs/search.rst b/src/docs/src/ddocs/search.rst
index 5dbc99057..42e0d9f22 100644
--- a/src/docs/src/ddocs/search.rst
+++ b/src/docs/src/ddocs/search.rst
@@ -784,7 +784,7 @@ results for each unique value of each named field.
 
 *Example of a query using the counts facet syntax:*
 
-.. code-block:: http
+.. code-block:: text
 
     ?q=*:*&counts=["type"]
 
@@ -857,7 +857,7 @@ brackets (``[``, ``]``). Exclusive range queries are denoted by curly brackets (
 
 *Example of a request that uses faceted search for matching ranges:*
 
-.. code-block:: http
+.. code-block:: text
 
     ?q=*:*&ranges={"price":{"cheap":"[0 TO 100]","expensive":"{100 TO Infinity}"}}
 
diff --git a/src/docs/src/intro/security.rst b/src/docs/src/intro/security.rst
index 0a4aebbbe..8b4a575a4 100644
--- a/src/docs/src/intro/security.rst
+++ b/src/docs/src/intro/security.rst
@@ -43,12 +43,9 @@ identification for certain requests:
   </{db}/_design/{ddoc}>`)
 - Triggering compaction (:post:`POST /database/_compact </{db}/_compact>`)
 - Reading the task status list (:get:`GET /_active_tasks </_active_tasks>`)
-- Restarting the server on a given node
-  (:post:`POST /_node/{node-name}/_restart </_restart>`)
-- Reading the active configuration
-  (:get:`GET /_node/{node-name}/_config </_config>`)
-- Updating the active configuration
-  (:put:`PUT /_node/{node-name}/_config/section/key </_config/{section}/{key}>`)
+- Restarting the server on a given node (:post:`/_node/{node-name}/_restart`)
+- Reading the active configuration (:get:`/_node/{node-name}/_config`)
+- Updating the active configuration (:put:`/_node/{node-name}/_config/{section}/{key}`)
 
 Creating a New Admin User
 -------------------------
diff --git a/src/docs/src/replication/conflicts.rst b/src/docs/src/replication/conflicts.rst
index f876400ef..13544830a 100644
--- a/src/docs/src/replication/conflicts.rst
+++ b/src/docs/src/replication/conflicts.rst
@@ -191,7 +191,7 @@ arbitrarily large.
 Working with conflicting documents
 ==================================
 
-The basic :get:`/{doc}/{docid}` operation will not show you any
+The basic :get:`/{db}/{docid}` operation will not show you any
 information about conflicts. You see only the deterministically-chosen winner,
 and get no indication as to whether other conflicting revisions exist or not:
 
diff --git a/src/docs/src/replication/protocol.rst b/src/docs/src/replication/protocol.rst
index 90101c9d1..bee76253c 100644
--- a/src/docs/src/replication/protocol.rst
+++ b/src/docs/src/replication/protocol.rst
@@ -1181,7 +1181,7 @@ Documents-Attachments and may handle it as stream with lesser memory footprint.
 
     **Response**:
 
-    .. code-block:: http
+    .. code-block:: text
 
         HTTP/1.1 200 OK
         Content-Type: multipart/mixed; boundary="7b1596fc4940bc1be725ad67f11ec1c4"
@@ -1429,7 +1429,7 @@ one by one without any serialization overhead.
 
     **Request**:
 
-    .. code-block:: http
+    .. code-block:: text
 
         PUT /target/SpaghettiWithMeatballs?new_edits=false HTTP/1.1
         Accept: application/json
diff --git a/src/docs/src/whatsnew/3.1.rst b/src/docs/src/whatsnew/3.1.rst
index 5715b42ab..3d1a8d511 100644
--- a/src/docs/src/whatsnew/3.1.rst
+++ b/src/docs/src/whatsnew/3.1.rst
@@ -142,6 +142,6 @@ Performance
 * :ghissue:`2754`: Optimized compactor performance, resulting in a 40% speed improvement
   when document revisions approach the ``revs_limit``. The fixes also include additional
   metrics on size tracking during the sort and copy phases, accessible via the
-  :get:`GET /_active_tasks </active_tasks>` endpoint.
+  :get:`/_active_tasks` endpoint.
 
 * A big bowl of candy! OK, no, not really. If you got this far...thank you for reading.
diff --git a/src/docs/templates/breadcrumbs.html b/src/docs/templates/breadcrumbs.html
new file mode 100644
index 000000000..65259a177
--- /dev/null
+++ b/src/docs/templates/breadcrumbs.html
@@ -0,0 +1,11 @@
+{%- extends "sphinx_rtd_theme/breadcrumbs.html" %}
+
+{% block breadcrumbs_aside %}
+  {% if pagename != 'index' %}
+    {{ super() }}
+  {% else %}
+   <li class="wy-breadcrumbs-aside">
+      <a href="https://github.com/apache/couchdb/tree/main/src/docs" class="fa fa-github"> Edit on GitHub</a>
+   </li>
+  {% endif %}
+{% endblock %}
diff --git a/src/docs/templates/pages/download.html b/src/docs/templates/pages/download.html
index 76fe93daf..481ec9fac 100644
--- a/src/docs/templates/pages/download.html
+++ b/src/docs/templates/pages/download.html
@@ -16,7 +16,7 @@ specific language governing permissions and limitations under the License.
 {% extends "layout.html" %}
 {% set title = 'Download' %}
 {% set url = 'https://media.readthedocs.org/%s/couchdb/%s/couchdb.%s' %}
-{% if git_branch == 'master' %}
+{% if git_branch == 'main' %}
   {% set rtd_ver = 'latest' %}
 {% else %}
   {% set rtd_ver = git_branch %}