Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/src/theme-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,20 @@ Python, Anaconda, and JFrog logos are bundled from
[Simple Icons](https://github.com/simple-icons/simple-icons) (CC0). All navigation
icons are inline SVGs that inherit the theme's text color.

## LLM documentation menu

When Yardang's llms output is enabled, a robot button appears in the sidebar
toolbar and header. Its menu links to `llms.txt` and, when `full-build` is enabled,
`llms-full.txt`. Links work from nested documentation pages.

The menu appears automatically when the `yardang.sphinx.llms` extension is loaded.
For projects built with Yardang, this is controlled by:

```toml
[tool.yardang.llms]
enabled = true
```

## Section navigator

Each `sidebar_tabs` mapping accepts these fields:
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ develop = [
# tests
"myst-parser",
"sphinx-design",
"yardang",
]

[project.scripts]
Expand Down
6 changes: 6 additions & 0 deletions sphinx_fuma/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ def _on_page_context(app, pagename, templatename, context, doctree) -> None:
context["fuma_sidebar"] = sidebar_tree(app, pagename)
context["fuma_toc"] = page_toc(app, pagename)
context["fuma_breadcrumbs"] = breadcrumbs(context)
context["fuma_llms_links"] = []
if "yardang.sphinx.llms" in app.extensions:
filenames = ["llms.txt"]
if app.config.yardang_llms_full_build:
filenames.append("llms-full.txt")
context["fuma_llms_links"] = [{"title": filename, "url": context["pathto"](filename, resource=True)} for filename in filenames]


def setup(app) -> dict[str, object]:
Expand Down
1 change: 1 addition & 0 deletions sphinx_fuma/partials/icons.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"album": '<rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><polyline points="11 3 11 11 14 8 17 11 17 3"/>',
"blocks": '<rect width="7" height="7" x="14" y="3" rx="1"/><path d="M10 21V8a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1H3"/>',
"book": '<path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"/>',
"bot": '<path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2m16 0h2m-13-1v2m6-2v2"/>',
"bookmark": '<path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/>',
"box": '<path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/>',
"braces": '<path d="M8 3H7a2 2 0 0 0-2 2v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5c0 1.1.9 2 2 2h1"/><path d="M16 21h1a2 2 0 0 0 2-2v-5c0-1.1.9-2 2-2a2 2 0 0 1-2-2V5a2 2 0 0 0-2-2h-1"/>',
Expand Down
13 changes: 13 additions & 0 deletions sphinx_fuma/partials/llms.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{% import "partials/icons.html" as icons with context %}
{%- if fuma_llms_links %}
<details class="fd-llms">
<summary class="fd-icon-button" aria-label="{{ _('LLM documentation') }}" title="{{ _('LLM documentation') }}">
{{ icons.named("bot", "fd-icon-bot") }}
</summary>
<nav class="fd-llms-menu" aria-label="{{ _('LLM documentation') }}">
{%- for link in fuma_llms_links %}
<a href="{{ link.url|e }}">{{ link.title }}</a>
{%- endfor %}
</nav>
</details>
{%- endif %}
1 change: 1 addition & 0 deletions sphinx_fuma/partials/sidebar.html
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
{%- if fuma_github_url %}
<a class="fd-toolbar-item" href="{{ fuma_github_url|e }}" target="_blank" rel="noopener" aria-label="GitHub">{{ icons.github() }}</a>
{%- endif %}
{%- include "partials/llms.html" %}
<div class="fd-theme-switch" role="group" aria-label="{{ _('Theme') }}">
<button type="button" class="fd-theme-option" data-fd-theme="light" aria-label="{{ _('Light theme') }}">{{ icons.sun() }}</button>
<button type="button" class="fd-theme-option" data-fd-theme="dark" aria-label="{{ _('Dark theme') }}">{{ icons.moon() }}</button>
Expand Down
1 change: 1 addition & 0 deletions sphinx_fuma/partials/subnav.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
{%- if fuma_github_url %}
<a class="fd-icon-button" href="{{ fuma_github_url|e }}" target="_blank" rel="noopener" aria-label="GitHub">{{ icons.github() }}</a>
{%- endif %}
{%- include "partials/llms.html" %}
<button type="button" class="fd-icon-button fd-theme-toggle" aria-label="{{ _('Toggle theme') }}">
{{ icons.sun("fd-icon-light") }}{{ icons.moon("fd-icon-dark") }}
</button>
Expand Down
46 changes: 46 additions & 0 deletions sphinx_fuma/static/fuma.css
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,52 @@ body[data-layout="notebook"] #fd-subnav {
background: var(--color-fd-accent);
}

.fd-llms {
position: relative;
flex-shrink: 0;
}

.fd-llms summary {
list-style: none;
}

.fd-llms summary::-webkit-details-marker {
display: none;
}

.fd-llms-menu {
position: absolute;
z-index: 20;
inset-inline-start: 0;
bottom: calc(100% + 0.25rem);
min-width: 10rem;
padding: 0.25rem;
border: 1px solid var(--color-fd-border);
border-radius: var(--fd-radius);
background: var(--color-fd-popover);
color: var(--color-fd-popover-foreground);
box-shadow: 0 8px 24px rgb(0 0 0 / 0.16);
}

#fd-subnav .fd-llms-menu {
inset-inline-start: auto;
inset-inline-end: 0;
top: calc(100% + 0.25rem);
bottom: auto;
}

.fd-llms-menu a {
display: block;
padding: 0.5rem;
border-radius: 0.375rem;
font-size: 0.875rem;
}

.fd-llms-menu a:hover,
.fd-llms-menu a:focus-visible {
background: var(--color-fd-accent);
}

.fd-theme-toggle .fd-icon-dark,
html.dark .fd-theme-toggle .fd-icon-light {
display: none;
Expand Down
13 changes: 13 additions & 0 deletions sphinx_fuma/static/fuma.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@
var body = document.body;
var docEl = document.documentElement;

document.querySelectorAll(".fd-llms").forEach(function (menu) {
document.addEventListener("click", function (event) {
if (!menu.contains(event.target) || event.target.closest("a")) menu.open = false;
});
menu.addEventListener("keydown", function (event) {
if (event.key === "Escape" && menu.open) {
menu.open = false;
menu.querySelector("summary").focus();
event.preventDefault();
}
});
});

/* Theme -------------------------------------------------------------- */

function isDark() {
Expand Down
40 changes: 40 additions & 0 deletions sphinx_fuma/tests/test_llms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import re
from pathlib import Path

import pytest
from sphinx.application import Sphinx


@pytest.fixture(scope="module", params=[True, False])
def built_llms(tmp_path_factory, request) -> tuple[Path, bool]:
src = tmp_path_factory.mktemp("llms")
(src / "conf.py").write_text(
'extensions = ["myst_parser", "sphinx_fuma", "yardang.sphinx.llms"]\n'
'project = "Demo"\nhtml_theme = "fuma"\n'
f"yardang_llms_full_build = {request.param}\n"
)
(src / "index.md").write_text("# Demo\n\n```{toctree}\n\nguides/start\n```\n")
(src / "guides").mkdir()
(src / "guides/start.md").write_text("# Getting started\n\nInstall the package.\n")
out = src / "_build"
Sphinx(str(src), str(src), str(out), str(out / ".doctrees"), "html", freshenv=True, status=None).build()
return out, request.param


@pytest.mark.parametrize("page", ["index.html", "guides/start.html"])
@pytest.mark.parametrize("region", ["fd-subnav", "fd-sidebar"])
def test_llms_menu_links_to_generated_files(built_llms, page, region):
out, full_build = built_llms
html = (out / page).read_text()
region_html = html.split(f'id="{region}"', 1)[1].split("</header>" if region == "fd-subnav" else "</aside>", 1)[0]
menu = re.search(r'<details class="fd-llms">(.*?)</details>', region_html, re.DOTALL)
assert menu is not None
assert 'aria-label="LLM documentation"' in menu.group(1)
assert 'title="LLM documentation"' in menu.group(1)
assert "fd-icon-bot" in menu.group(1)
links = re.findall(r'<a href="([^"]+)">([^<]+)</a>', menu.group(1))
filenames = ["llms.txt", "llms-full.txt"] if full_build else ["llms.txt"]
assert [label for _, label in links] == filenames
for href, label in links:
assert (out / page).parent.joinpath(href).resolve() == out / label
assert (out / label).is_file()
3 changes: 3 additions & 0 deletions sphinx_fuma/tests/test_theme.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,9 @@ def test_no_edit_url_without_a_page(self):


class TestRenderedOutput:
def test_llms_menu_is_absent_without_llms_output(self, built):
assert 'class="fd-llms"' not in (built / "index.html").read_text()

@pytest.mark.parametrize(
("region", "closing_tag", "links_container"),
[("fd-subnav", "header", "fd-nav-links"), ("fd-sidebar", "aside", "fd-toolbar")],
Expand Down