Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
efbb6fd
test(reference): require code-styled object names in titles
has2k1 Aug 18, 2026
e4c83ce
refactor(post-render): remove unreachable reference-title rewrites
has2k1 Aug 18, 2026
f1ab92b
fix(reference): preserve one h1 and contiguous heading levels
has2k1 Aug 18, 2026
df2a022
fix(reference): protect decorated titles and limit title sizing
has2k1 Aug 19, 2026
762011e
test(reference): verify index group heading levels
has2k1 Aug 18, 2026
1720862
fix(reference): demote nav titles and share heading shifts
has2k1 Aug 19, 2026
8ddf9cf
test(reference): verify section and member heading levels
has2k1 Aug 19, 2026
d223902
fix(reference): emit headings at their final levels
has2k1 Aug 19, 2026
f0021ff
fix(assets): align the mobile reference nav title
has2k1 Aug 18, 2026
2b14a12
test(reference): require one h1 per rendered object page
has2k1 Aug 19, 2026
b4f08c3
fix(reference): preserve subtitles and nested fallback headings
has2k1 Aug 18, 2026
feee22c
fix(reference): preserve subtitle headings in machine-readable docs
has2k1 Aug 18, 2026
756adca
fix(post-render): keep chained fallback sections nested
has2k1 Aug 19, 2026
b52ca1f
docs(test-packages): count the subtitled reference section
has2k1 Aug 18, 2026
03b368d
test(reference): cover subtitle outputs and member separators
has2k1 Aug 18, 2026
88071f0
fix(post-render): demote nav titles without breadcrumbs
has2k1 Aug 19, 2026
a61f488
fix(reference): emit one title heading on CLI and MCP pages
has2k1 Aug 18, 2026
1e82c2a
fix(cli): read marked-up titles and process nested commands
has2k1 Aug 18, 2026
e8929e6
fix(styles): style docstring sections independently of depth
has2k1 Aug 18, 2026
1d5b856
fix(reference): preserve site TOC depth while listing members
has2k1 Aug 18, 2026
7f1c41a
docs(mcp): explain deeper source headings
has2k1 Aug 18, 2026
6a2a660
fix(cli): use section subtitles in verbose scan output
has2k1 Aug 18, 2026
e67f7af
refactor(reference): name generated pages by kind and family
has2k1 Aug 19, 2026
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
28 changes: 22 additions & 6 deletions great_docs/_apiref/_render/api_page.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

from dataclasses import dataclass
from functools import cached_property
from typing import TYPE_CHECKING, cast

Expand All @@ -22,11 +23,23 @@
from .doc import RenderDoc


# Member headings render at `h3`, so the table of contents must reach depth 3
# to list them.
_MIN_MEMBER_TOC_DEPTH = 3

# Use the default site depth when the caller does not provide the merged value.
_DEFAULT_SITE_TOC_DEPTH = 2


@dataclass
class __RenderAPIPage(RenderPageMixin, RenderBase):
"""
Render an API page object (`content.Page`)
"""

toc_depth: int = _DEFAULT_SITE_TOC_DEPTH
"""Merged site `toc-depth` used to select a page override"""

def __post_init__(self):
self.page = cast("Page", self.node)
"""Page in the documentation"""
Expand Down Expand Up @@ -69,12 +82,15 @@ def render_metadata(self) -> BlockContent:
# Derive the title of the page from the first (top-level) object
obj = self.render_objs[0]
title = obj._title # pyright: ignore[reportPrivateUsage]
return Meta(
{
"title": f"{title}",
"body-classes": "doc-api-page",
}
)
metadata: dict[str, object] = {
"title": f"{title}",
"body-classes": "doc-api-page doc-py-api-page",
"shift-heading-level-by": 0,
}
# Add a page override only when the site depth excludes members.
if self.toc_depth < _MIN_MEMBER_TOC_DEPTH:
metadata["toc-depth"] = _MIN_MEMBER_TOC_DEPTH
return Meta(metadata)

def render_body(self) -> BlockContent:
"""
Expand Down
19 changes: 11 additions & 8 deletions great_docs/_apiref/_render/reference_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,17 @@ def render_description(self) -> BlockContent:
)

def render_metadata(self) -> BlockContent:
return Meta(
{
"title": self.api_ref.title,
"body-classes": "doc-reference doc-reference-index",
"page-navigation": False,
"html-table-processing": "none",
}
)
metadata: dict[str, object] = {
"title": self.api_ref.title,
"body-classes": "doc-reference doc-py-reference",
"page-navigation": False,
"html-table-processing": "none",
"shift-heading-level-by": 0,
}
# Subtitle headings render at `h3`; preserve any deeper site setting.
if self.api_ref.site_toc_depth < 3:
metadata["toc-depth"] = 3
return Meta(metadata)

def render_body(self) -> BlockContent:
"""
Expand Down
95 changes: 87 additions & 8 deletions great_docs/_apiref/api_reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
# consumed; dropped before parsing so they neither reach `Settings` nor error.
_REMOVED_KEYS = {"style", "renderer", "render_interlinks"}

# Use the default site depth when a bare API reference config supplies no site
# settings.
_DEFAULT_SITE_TOC_DEPTH = 2


@dataclass
class Settings:
Expand Down Expand Up @@ -89,9 +93,12 @@ class APIReference:
options: SpecOptions | None
settings: Settings
items: list[InventoryItem]
site_toc_depth: int

def __init__(self, config: dict[str, Any] | str | Path) -> None:
block = self._select_block(config)
cfg = self._load_config(config)
self.site_toc_depth = self._read_site_toc_depth(cfg)
block = self._select_block(cfg)
block = {k: v for k, v in block.items() if k not in _REMOVED_KEYS}

self.settings = Settings.make(block)
Expand All @@ -112,13 +119,84 @@ def __init__(self, config: dict[str, Any] | str | Path) -> None:
self._resolver.current_package = self.package

@staticmethod
def _select_block(config: dict[str, Any] | str | Path) -> dict[str, Any]:
"""Select the `api-reference:` (or legacy `quartodoc:`) mapping from a config dict, file path, or full _quarto.yml"""
if isinstance(config, (str, Path)):
loaded = read_yaml(str(config))
cfg: dict[str, Any] = cast("dict[str, Any]", loaded) if isinstance(loaded, dict) else {}
else:
cfg = config
def _load_config(config: dict[str, Any] | str | Path) -> dict[str, Any]:
"""
Load a configuration mapping

Return mapping arguments unchanged. Read YAML paths and return an
empty mapping when the document's top-level value is not a mapping.

Parameters
----------
config
Configuration mapping or YAML file path.

Returns
-------
Configuration mapping.
"""
if not isinstance(config, (str, Path)):
return config
loaded = read_yaml(str(config))
return cast("dict[str, Any]", loaded) if isinstance(loaded, dict) else {}

@staticmethod
def _read_site_toc_depth(cfg: dict[str, Any]) -> int:
"""
Read the configured table-of-contents depth

Use the integer at `format.html.toc-depth` when available, then the
integer at `site.toc-depth`. Return the built-in default when neither
setting is an integer.

Parameters
----------
cfg
Configuration mapping.

Returns
-------
Configured depth or the built-in default.
"""
format_config = cfg.get("format")
html = (
cast("dict[str, Any]", format_config).get("html")
if isinstance(format_config, dict)
else None
)
depth = cast("dict[str, Any]", html).get("toc-depth") if isinstance(html, dict) else None
if isinstance(depth, int):
return depth

site = cfg.get("site")
depth = (
cast("dict[str, Any]", site).get("toc-depth") if isinstance(site, dict) else None
)
return depth if isinstance(depth, int) else _DEFAULT_SITE_TOC_DEPTH

@staticmethod
def _select_block(cfg: dict[str, Any]) -> dict[str, Any]:
"""
Select the API reference configuration

Select a non-empty `api-reference` mapping before the legacy
`quartodoc` mapping. Treat the full configuration as the API reference
mapping when both sections are empty or absent.

Parameters
----------
cfg
Configuration mapping.

Returns
-------
Copy of the selected API reference mapping.

Raises
------
KeyError
If the selected value is not a mapping or omits `package`.
"""
block = cfg.get("api-reference") or cfg.get("quartodoc") or cfg
if not isinstance(block, dict) or "package" not in block:
raise KeyError("No `api-reference:` section found in your _quarto.yml.")
Expand Down Expand Up @@ -182,6 +260,7 @@ def build(self, page_filter: str = "*") -> None:
rewrite_all_pages=s.rewrite_all_pages,
header_level=s.header_level,
page_filter=page_filter,
site_toc_depth=self.site_toc_depth,
)
write_typing_information(s.typing_module_paths, self)

Expand Down
6 changes: 5 additions & 1 deletion great_docs/_apiref/write.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ def write_pages(
rewrite_all_pages: bool,
header_level: int,
page_filter: str,
site_toc_depth: int,
) -> None:
"""Write API doc pages to `<dir>/<page.path><out_page_suffix>`

Expand All @@ -335,6 +336,9 @@ def write_pages(
page_filter :
Glob pattern; pages whose path does not match are neither rendered
nor written. `"*"` writes all pages.
site_toc_depth :
Merged site table-of-contents depth. A page overrides it only when
member headings require a greater depth.
"""
from ._render.api_page import RenderAPIPage

Expand All @@ -344,7 +348,7 @@ def write_pages(
continue

_log.info(f"Rendering {page.path}")
rendered = str(RenderAPIPage(page, header_level))
rendered = str(RenderAPIPage(page, header_level, toc_depth=site_toc_depth))

rendered = merge_frontmatter(
rendered, {"page-navigation": False, "html-table-processing": "none"}
Expand Down
26 changes: 14 additions & 12 deletions great_docs/_mcp_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,7 @@ def _generate_mcp_index_page(
# Front matter
lines.append("---")
lines.append(f'title: "{get_translation("mcp_reference", language)}"')
lines.append("body-classes: doc-reference doc-mcp-reference doc-reference-index")
lines.append("body-classes: doc-reference doc-mcp-reference")
lines.append("sidebar: mcp-reference")
lines.append("page-navigation: false")
lines.append("html-table-processing: none")
Expand Down Expand Up @@ -750,7 +750,7 @@ def _generate_tool_page(tool: dict[str, Any], server_name: str, language: str =
lines.append(f'title: "{name}"')
lines.append("title-block-style: none")
lines.append("bread-crumbs: false")
lines.append("body-classes: doc-api-page")
lines.append("body-classes: doc-api-page doc-mcp-api-page")
lines.append("sidebar: mcp-reference")
lines.append("page-navigation: false")
lines.append("html-table-processing: none")
Expand Down Expand Up @@ -807,9 +807,11 @@ def _generate_tool_page(tool: dict[str, Any], server_name: str, language: str =
lines.append(":::")
lines.append("")

# Parameters section — uses definition list format matching Python API style
# Match the Python API's definition-list format. Write this heading at `###`
# because MCP pages keep the site-wide shift. Disabling that shift prevents
# Quarto from hoisting the `.title` heading and creates a duplicate title.
if properties:
lines.append(f"## {get_translation('mcp_parameters', language)} {{.doc-parameters}}")
lines.append(f"### {get_translation('mcp_parameters', language)} {{.doc-parameters}}")
lines.append("")
lines.append("::: {.doc-definition-items}")
for param_name, param_info in properties.items():
Expand Down Expand Up @@ -863,7 +865,7 @@ def _generate_resource_page(
lines.append(f'title: "{name}"')
lines.append("title-block-style: none")
lines.append("bread-crumbs: false")
lines.append("body-classes: doc-api-page")
lines.append("body-classes: doc-api-page doc-mcp-api-page")
lines.append("sidebar: mcp-reference")
lines.append("page-navigation: false")
lines.append("html-table-processing: none")
Expand All @@ -878,7 +880,7 @@ def _generate_resource_page(
lines.append(":::")
lines.append("")

lines.append(f"## {get_translation('mcp_details', language)} {{.doc-parameters}}")
lines.append(f"### {get_translation('mcp_details', language)} {{.doc-parameters}}")
lines.append("")
lines.append(f"**URI:** `{uri}`")
lines.append("")
Expand All @@ -903,7 +905,7 @@ def _generate_resource_template_page(
lines.append(f'title: "{name}"')
lines.append("title-block-style: none")
lines.append("bread-crumbs: false")
lines.append("body-classes: doc-api-page")
lines.append("body-classes: doc-api-page doc-mcp-api-page")
lines.append("sidebar: mcp-reference")
lines.append("page-navigation: false")
lines.append("html-table-processing: none")
Expand All @@ -920,7 +922,7 @@ def _generate_resource_template_page(
lines.append(":::")
lines.append("")

lines.append(f"## {get_translation('mcp_details', language)} {{.doc-parameters}}")
lines.append(f"### {get_translation('mcp_details', language)} {{.doc-parameters}}")
lines.append("")
lines.append(f"**URI Template:** `{uri_template}`")
lines.append("")
Expand All @@ -934,7 +936,7 @@ def _generate_resource_template_page(
variables = _re.findall(r"\{(\w+)\}", uri_template)
if variables:
lines.append(
f"## {get_translation('mcp_template_variables', language)} {{.doc-parameters}}"
f"### {get_translation('mcp_template_variables', language)} {{.doc-parameters}}"
)
lines.append("")
lines.append("::: {.doc-definition-items}")
Expand Down Expand Up @@ -966,7 +968,7 @@ def _generate_prompt_page(prompt: dict[str, Any], server_name: str, language: st
lines.append(f'title: "{name}"')
lines.append("title-block-style: none")
lines.append("bread-crumbs: false")
lines.append("body-classes: doc-api-page")
lines.append("body-classes: doc-api-page doc-mcp-api-page")
lines.append("sidebar: mcp-reference")
lines.append("page-navigation: false")
lines.append("html-table-processing: none")
Expand All @@ -982,7 +984,7 @@ def _generate_prompt_page(prompt: dict[str, Any], server_name: str, language: st
lines.append("")

if arguments:
lines.append(f"## {get_translation('mcp_arguments', language)} {{.doc-parameters}}")
lines.append(f"### {get_translation('mcp_arguments', language)} {{.doc-parameters}}")
lines.append("")
lines.append("::: {.doc-definition-items}")
for arg in arguments:
Expand Down Expand Up @@ -1010,7 +1012,7 @@ def _generate_prompt_page(prompt: dict[str, Any], server_name: str, language: st

# Prompt message content
if messages:
lines.append(f"## {get_translation('mcp_prompt_text', language)}")
lines.append(f"### {get_translation('mcp_prompt_text', language)}")
lines.append("")
for msg in messages:
role = msg.get("role", "user")
Expand Down
2 changes: 1 addition & 1 deletion great_docs/assets/copy-page.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
if (document.body.classList.contains('gd-blog-index')) return;

// Skip reference index pages (API, CLI, MCP)
if (document.body.classList.contains('doc-reference-index')) return;
if (document.body.classList.contains('doc-reference')) return;

// Create the widget container
var widget = document.createElement('div');
Expand Down
Loading
Loading