Skip to content

Commit

Permalink
feat(toc): add collapsible toc
Browse files Browse the repository at this point in the history
.. versionadded:: 2024.09.09

    [1] Added support for collapsible table of content in the sidebar
        and post processing HTML capabilities.

Signed-off-by: Akshay Mestry <[email protected]>
  • Loading branch information
xames3 committed Sep 10, 2024
1 parent 422c332 commit b436ffb
Show file tree
Hide file tree
Showing 3 changed files with 213 additions and 11 deletions.
15 changes: 11 additions & 4 deletions coeus_sphinx_theme/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
Author: Akshay Mestry <[email protected]>
Created on: Sunday, August 11 2024
Last updated on: Saturday, September 07 2024
Last updated on: Tuesday, September 10 2024
This module defines the extensions for Coeus Sphinx Theme, providing
utilities and configuration for integrating a custom theme into Sphinx
Expand Down Expand Up @@ -86,6 +86,8 @@
[1] Added support for "Open Graph Protocol" using another extension,
called `sphinxext.opengraph`.
[2] Added support for collapsible table of content in the sidebar
and post processing HTML capabilities.
.. versionchanged:: 2024.09.09
Expand Down Expand Up @@ -120,6 +122,8 @@

from coeus_sphinx_theme.extensions import directives
from coeus_sphinx_theme.extensions import roles
from coeus_sphinx_theme.utils import post_process_build
from coeus_sphinx_theme.utils import read_env_docs

if t.TYPE_CHECKING:
import docutils.nodes as nodes
Expand Down Expand Up @@ -243,9 +247,9 @@ def setup(app: Sphinx) -> dict[str, t.Any]:
for default, new in coeus_theme_default_mapping.items():
setattr(config, default, getattr(config, new))
app.add_html_theme(name=theme_name, theme_path=here)
app.add_css_file("theme.css", priority=900)
app.add_js_file("theme.js", loading_method="defer")
app.connect("html-page-context", update_html_context)
app.add_css_file("coeus.css", priority=900)
app.add_js_file("main.js", loading_method="defer")
app.add_js_file("coeus.js", loading_method="defer")
for directive in directives:
app.add_node(fix(directive), html=(directive.visit, directive.depart))
app.add_directive(directive.name, directive.directive)
Expand All @@ -255,4 +259,7 @@ def setup(app: Sphinx) -> dict[str, t.Any]:
if role.endswith("_role"):
_role = role[:-5].replace("_", "-")
rst.roles.register_local_role(_role, getattr(roles, role))
app.connect("env-before-read-docs", read_env_docs)
app.connect("build-finished", post_process_build)
app.connect("html-page-context", update_html_context)
return {"parallel_read_safe": True, "parallel_write_safe": True}
31 changes: 24 additions & 7 deletions coeus_sphinx_theme/static/coeus.css
Original file line number Diff line number Diff line change
Expand Up @@ -989,7 +989,7 @@ table tbody td:not(:first-child) {
border-radius: .5rem;
border-radius: var(--radius);
color: #0f1729;
color: var(--foreground);
color: var(--color-text);
font-size: 1.1rem;
border-style: solid;
border-width: 2px;
Expand All @@ -1002,7 +1002,6 @@ table tbody td:not(:first-child) {

.admonition p:not(.admonition-title) {
margin-top: 1.3rem;
color: #1d1d1f;
line-height: 1.5;
}

Expand All @@ -1027,7 +1026,7 @@ table tbody td:not(:first-child) {
--coeus-bg-opacity: 1;
background-color: #f0f9ff;
background-color: rgba(245, 245, 247, var(--coeus-bg-opacity));
--coeus-text-opacity: 1;
--coeus-text-opacity: 0.7;
color: #0c4a6e;
color: rgba(32, 32, 32, var(--coeus-text-opacity));
font-size: 1.1rem;
Expand Down Expand Up @@ -1806,10 +1805,28 @@ table:not(.does-not-exist):hover .navbarlink>* {
justify-content: space-between;
}

#left-sidebar a.expandable.expanded>button>svg {
--coeus-rotate: 90deg;
transform: translate(var(--coeus-translate-x), var(--coeus-translate-y)) rotate(90deg) skewX(var(--coeus-skew-x)) skewY(var(--coeus-skew-y)) scaleX(var(--coeus-scale-x)) scaleY(var(--coeus-scale-y));
transform: translate(var(--coeus-translate-x), var(--coeus-translate-y)) rotate(var(--coeus-rotate)) skewX(var(--coeus-skew-x)) skewY(var(--coeus-skew-y)) scaleX(var(--coeus-scale-x)) scaleY(var(--coeus-scale-y));
#left-sidebar a>button>i {
opacity: 0.5;
color: #636365;
display: inline-block;
text-rendering: auto;
-webkit-font-smoothing: antialiased;
font-size: 1rem !important;
font: var(--fa-font-solid);
transition: transform 0.3s ease-in-out;
}

#left-sidebar a.expandable>button>i::before {
content: "\f055";
}

#left-sidebar a.expandable.expanded>button>i::before {
content: "\f056";
}

#left-sidebar a.expandable.expanded>button>i {
transform: rotate(180deg);
transition: transform 0.3s ease-in-out;
}

#right-sidebar ul {
Expand Down
178 changes: 178 additions & 0 deletions coeus_sphinx_theme/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
"""\
Coeus Sphinx Theme
==================
Author: Akshay Mestry <[email protected]>
Created on: Tuesday, September 10 2024
Last updated on: Tuesday, September 10 2024
This module defines the utilities for customizing or post processing
Coeus Sphinx Theme.
.. versionadded:: 2024.09.09
"""

from __future__ import annotations

import typing as t

import bs4
from sphinx.util.display import status_iterator

if t.TYPE_CHECKING:
from sphinx.application import Sphinx
from sphinx.environment import BuildEnvironment


def make_toc_collapsible(tree: bs4.BeautifulSoup) -> None:
"""Enhance the navigation sidebar by making the child elements
collapsible.
The function searches for the links within the left sidebar that
have sibling `ul` elements (indicating child navigation items). It
modifies these links and their parent elements to be collapsible
using a toggle mechanism that simulates expansion and contraction.
:param tree: Parsed HTML tree representing the document structure.
"""
for link in tree.select("#left-sidebar a"):
children = link.find_next_sibling("ul")
if children:
link.parent["x-data"] = (
"{ expanded: $el.classList.contains('current') }"
)
link["@click"] = "expanded = !expanded"
link["class"].append("expandable")
link[":class"] = "{ 'expanded': expanded }"
children["x-show"] = "expanded"
button = tree.new_tag(
"button",
type="button",
**{"@click.prevent.stop": "expanded = !expanded"},
)
label = tree.new_tag("span", class_="sr-only")
button.append(label)
i = tree.new_tag("i", attrs={"class": "fa-solid fa-circle-plus"})
button.append(i)
link.append(button)


def read_env_docs(
app: Sphinx, _: BuildEnvironment, docnames: list[str]
) -> None:
"""Return the list of modified or changed documents into the Sphinx
environment object for post-processing.
This is essential for optimizing performance during post-processing,
ensuring that only the modified documents are processed further in
the build pipeline.
:param app: Sphinx application object.
:param _: Current build environment object.
:param docnames: List of document names that were changed.
"""
app.env.coeus_files = docnames


def remove_empty_toctree_wrappers(tree: bs4.BeautifulSoup) -> None:
"""Removes empty toctree wrapper divs from the parsed HTML tree to
clean up the document structure.
This function searches for empty `toctree-wrapper` divs, which may
occur when using the `hidden` option in Sphinx toctrees. These empty
divs, which typically contain only whitespace or an end-of-line
character, are removed to ensure the final HTML is free from
redundant empty elements.
:param tree: Parsed HTML tree representing the document structure.
"""
for div in tree.select("div.toctree-wrapper"):
if len(div.contents) == 1 and not div.contents[0].strip():
div.decompose()


def enhance_header_links_for_copy(tree: bs4.BeautifulSoup) -> None:
"""Adds functionality to header links, enabling them to copy their
URL to the clipboard when clicked.
This function modifies all anchor links with the class `headerlink`
by binding a click event to each link. When the link is clicked,
its URL is copied to the clipboard, and a temporary tooltip displays
"Copied!" before reverting back to the default "Copy link" message.
:param tree: Parsed HTML tree representing the document structure.
"""
for link in tree.select("a.headerlink"):
link["@click.prevent"] = (
"window.navigator.clipboard.writeText($el.href); "
"$el.setAttribute('data-tooltip', 'Copied!'); "
"setTimeout(() => $el.setAttribute('data-tooltip', 'Copy link'),"
" 2000)"
)
del link["title"]
link["aria-label"] = "Copy link to this element"
link["data-tooltip"] = "Copy link"


def remove_html_comments(tree: bs4.BeautifulSoup) -> None:
"""Removes all HTML comments from the parsed document to ensure
cleaner final HTML output.
This function identifies and strips out all HTML comments (i.e., text
nodes enclosed in `<!-- -->`), which may be present in the document
and are not needed in the final output.
:param tree: Parsed HTML tree representing the document structure.
"""
for comment in tree.find_all(string=lambda t: isinstance(t, bs4.Comment)):
comment.extract()


def modify_single_html_document(html: str) -> None:
"""Parses, modifies, and rewrites a single HTML file to apply various
post-processing transformations.
This function takes a file path to an HTML document, parses it into a
BeautifulSoup tree, applies several transformations (such as
collapsible navigation links, scrollspy, removing comments, etc.),
and writes the modified tree back into the original file.
:param html: HTML document to be modified.
"""
with open(html, encoding="utf-8") as f:
tree = bs4.BeautifulSoup(f, "html.parser")
make_toc_collapsible(tree)
enhance_header_links_for_copy(tree)
remove_empty_toctree_wrappers(tree)
remove_html_comments(tree)
with open(html, "w", encoding="utf-8") as f:
f.write(str(tree))


def post_process_build(app: Sphinx, exc: Exception | None) -> None:
"""Post-processes HTML documents after the Sphinx build, applying
final modifications to the output files.
This function is triggered after the build process is completed. It
checks if there are any errors, and if the builder is set to produce
`HTML` or `dirhtml` output. It then applies final transformations
to the list of modified documents stored in the environment, such as
collapsible navigation, and comment removal.
:param app: Sphinx application object.
:param exc: Any exception raised during the build process, or None
if no exceptions occurred.
"""
if exc or app.builder.name not in {"html", "dirhtml"}:
return
files = [app.builder.get_outfilename(doc) for doc in app.env.coeus_files]
if not files:
return
for doc in status_iterator(
files,
"Postprocessing... ",
"darkgreen",
len(files),
app.verbosity,
):
modify_single_html_document(doc)

0 comments on commit b436ffb

Please sign in to comment.