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
63 changes: 52 additions & 11 deletions great_docs/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -10234,6 +10234,46 @@ def _build_metadata_margin(self) -> str:

return "\n".join(margin_sections) if margin_sections else ""

@staticmethod
def _bump_heading_levels(content: str) -> str:
"""
Bump every Markdown heading up by one level, skipping fenced code blocks.

Headings (`# ` … `###### `) are promoted one level deeper so that the
embedded README body sits below the homepage wrapper. Lines inside
fenced code blocks (delimited by ``` or ~~~) are left untouched, which
prevents the bump from turning Quarto cell options like
`#| code-fold: true` into `##| code-fold: true` (a plain comment Quarto
ignores).
"""
import re

fence_re = re.compile(r"^\s*(`{3,}|~{3,})")
heading_re = re.compile(r"^(#{1,6})(\s+)")

lines = content.split("\n")
in_fence = False
fence_char = ""
for i, line in enumerate(lines):
fence_match = fence_re.match(line)
if fence_match:
marker = fence_match.group(1)
if not in_fence:
in_fence = True
fence_char = marker[0]
elif marker[0] == fence_char:
in_fence = False
fence_char = ""
continue

if in_fence:
continue

if heading_re.match(line):
lines[i] = "#" + line

return "\n".join(lines)

def _create_index_from_readme(self, force_rebuild: bool = False) -> None:
"""
Create or update index.qmd from the best available source file.
Expand Down Expand Up @@ -10570,6 +10610,13 @@ def _create_index_from_readme(self, force_rebuild: bool = False) -> None:
if source_file.suffix.lower() == ".rst":
readme_content = self._convert_rst_to_markdown(source_file)

# Strip any leading YAML frontmatter from the source file. The
# wrapper template below supplies its own frontmatter, so embedding
# the source's frontmatter mid-document would render it as a
# horizontal rule plus visible raw YAML text (e.g. index.qmd files,
# which universally start with a `---` block).
_, readme_content = self._split_frontmatter(readme_content)

# Copy images referenced in the source file to the build directory
self._copy_readme_images(source_file)

Expand All @@ -10594,17 +10641,11 @@ def _create_index_from_readme(self, force_rebuild: bool = False) -> None:
readme_content = readme_content.replace(first_h1.group(0), "", 1).lstrip("\n")

if source_file is not None:
# Adjust heading levels: bump all headings up by one level
# This prevents h1 from becoming paragraphs and keeps proper hierarchy
# Replace headings from highest to lowest level to avoid double-replacement
import re

readme_content = re.sub(r"^######\s+", r"####### ", readme_content, flags=re.MULTILINE)
readme_content = re.sub(r"^#####\s+", r"###### ", readme_content, flags=re.MULTILINE)
readme_content = re.sub(r"^####\s+", r"##### ", readme_content, flags=re.MULTILINE)
readme_content = re.sub(r"^###\s+", r"#### ", readme_content, flags=re.MULTILINE)
readme_content = re.sub(r"^##\s+", r"### ", readme_content, flags=re.MULTILINE)
readme_content = re.sub(r"^#\s+", r"## ", readme_content, flags=re.MULTILINE)
# Adjust heading levels: bump all headings up by one level. This
# prevents h1 from becoming paragraphs and keeps proper hierarchy.
# Lines inside fenced code blocks are skipped so Quarto cell options
# (`#| code-fold: true`) and shell-style comments aren't mangled.
readme_content = self._bump_heading_levels(readme_content)

# Build margin content using the shared helper
margin_content = self._build_metadata_margin()
Expand Down
10 changes: 10 additions & 0 deletions test-packages/synthetic/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"gdtest_index_md", # 41
"gdtest_no_readme", # 42
"gdtest_index_wins", # 43
"gdtest_index_frontmatter", # 43b (issue #237 regression)
# 44–45: Supporting pages
"gdtest_full_extras", # 44
"gdtest_github_contrib", # 45
Expand Down Expand Up @@ -892,6 +893,15 @@
"text 'This index.qmd should take priority over README.md.' Tests the "
"full priority chain: index.qmd > index.md > README.md."
),
"gdtest_index_frontmatter": (
"Regression for issue #237. index.qmd carries its own YAML frontmatter "
"(title 'Embedded Frontmatter Title') plus a Quarto code cell with "
"`#| code-fold: true`. The landing page must NOT show the raw text "
"'title: Embedded Frontmatter Title' (frontmatter is stripped, not "
"embedded mid-document), and the code cell must stay folded (cell "
"options survive the heading bump). One function (hello) on the "
"Reference page."
),
"gdtest_full_extras": (
"Includes every supporting page type. The sidebar/nav should show "
"links to: License, Citation, Contributing, and Code of Conduct. "
Expand Down
87 changes: 87 additions & 0 deletions test-packages/synthetic/specs/gdtest_index_frontmatter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""
gdtest_index_frontmatter — index.qmd with YAML frontmatter + Quarto cell options.

Dimensions: A1, B1, C1, D1, E6, F6, G3, H7
Focus: Regression coverage for issue #237. The project root ``index.qmd`` has
its own YAML frontmatter block AND a Quarto code cell with hash-pipe
(``#|``) cell options. When Great Docs wraps this into the generated
homepage it must:

1. Strip the source file's leading frontmatter so it is NOT embedded
mid-document (otherwise the `---` block renders as a horizontal
rule with the raw YAML text visible).
2. Bump real Markdown headings one level, but leave fenced code blocks
untouched so `#| code-fold: true` is preserved (and not mangled
into `##| code-fold: true`, which Quarto ignores).
"""

SPEC = {
"name": "gdtest_index_frontmatter",
"description": "index.qmd frontmatter stripped; cell options preserved",
"dimensions": ["A1", "B1", "C1", "D1", "E6", "F6", "G3", "H7"],
"pyproject_toml": {
"project": {
"name": "gdtest-index-frontmatter",
"version": "0.1.0",
"description": "Synthetic test for index.qmd frontmatter + cell options",
},
"build-system": {
"requires": ["setuptools"],
"build-backend": "setuptools.build_meta",
},
},
"files": {
"gdtest_index_frontmatter/__init__.py": '''\
"""A test package whose homepage is an index.qmd with frontmatter."""

__version__ = "0.1.0"
__all__ = ["hello"]


def hello() -> str:
"""
Say hello.

Returns
-------
str
A greeting.
"""
return "Hello!"
''',
"index.qmd": """\
---
title: "Embedded Frontmatter Title"
toc: true
---

# Getting Started

Welcome to the homepage rendered from a Quarto `index.qmd`.

## Highlights

- First highlight
- Second highlight

```{python}
#| code-fold: true
#| code-summary: "Show the setup code"
# Greet the reader from inside a fenced code cell
print("hello from the homepage")
```
""",
},
"expected": {
"detected_name": "gdtest-index-frontmatter",
"detected_module": "gdtest_index_frontmatter",
"detected_parser": "numpy",
"export_names": ["hello"],
"num_exports": 1,
"section_titles": ["Functions"],
"has_user_guide": False,
"has_license_page": False,
"has_citation_page": False,
"coverage_exclude": ["nodoc", "bigcl", "ug", "supp", "hdg"],
},
}
77 changes: 77 additions & 0 deletions tests/test_gdg_rendered.py
Original file line number Diff line number Diff line change
Expand Up @@ -6328,6 +6328,83 @@ def test_DED_index_wins_ref_pages():
assert (_ref_dir(pkg) / "winner.html").exists(), "winner page missing"


# ───────────────────────────────────────────────────────────────────────────────
# DED: index.qmd frontmatter + cell options (issue #237 regression)
#
# The source index.qmd carries its own YAML frontmatter and a Quarto code cell
# with `#| code-fold` options. The generated homepage must strip the source
# frontmatter (so it is not embedded mid-document and rendered as raw text) and
# must bump real headings without mangling `#|` cell-option lines into `##|`.
# ───────────────────────────────────────────────────────────────────────────────
_INDEX_FM_PKG = "gdtest_index_frontmatter"


def _generated_index_qmd(pkg: str) -> Path:
"""Return the homepage index.qmd that Great Docs generated for *pkg*."""
return _RENDERED_DIR / pkg / "great-docs" / "index.qmd"


@pytest.mark.dedicated
def test_DED_index_frontmatter_stripped():
"""gdtest_index_frontmatter: source frontmatter is stripped, not embedded."""
pkg = _INDEX_FM_PKG
if not _has_rendered_site(pkg):
pytest.skip(f"{pkg} not rendered")

# Bug 1 root cause: the source file's `---` frontmatter block must not be
# embedded into the generated homepage body.
generated = _generated_index_qmd(pkg)
assert generated.exists(), "generated index.qmd missing"
qmd = generated.read_text(encoding="utf-8")
assert 'title: "Embedded Frontmatter Title"' not in qmd, (
"Source frontmatter was embedded verbatim into the generated index.qmd"
)

# End-to-end: the rendered homepage must not show the raw YAML as text, and
# the embedded title must not leak into the page <title>.
soup = _load_html(_site_dir(pkg) / "index.html")
text = soup.get_text()
assert "title: Embedded Frontmatter Title" not in text, (
"Raw frontmatter text leaked into the rendered homepage"
)
page_title = soup.find("title")
assert page_title is None or "Embedded Frontmatter Title" not in page_title.get_text(), (
"Embedded frontmatter title leaked into the page <title>"
)


@requires_bs4
@pytest.mark.dedicated
def test_DED_index_frontmatter_cell_options_preserved():
"""gdtest_index_frontmatter: `#|` cell options survive the heading bump."""
pkg = _INDEX_FM_PKG
if not _has_rendered_site(pkg):
pytest.skip(f"{pkg} not rendered")

# Bug 2 root cause: the heading bump must not touch lines inside fenced code
# blocks. A `#` comment inside the cell is what the old `^#\s+` regex
# mangled (it turned `# Greet ...` into `## Greet ...`); the hashpipe cell
# options must likewise stay intact.
qmd = _generated_index_qmd(pkg).read_text(encoding="utf-8")
assert "#| code-fold: true" in qmd, "cell option `#| code-fold: true` was lost"
assert "# Greet the reader from inside a fenced code cell" in qmd, (
"code-cell comment was lost"
)
assert "## Greet the reader from inside a fenced code cell" not in qmd, (
"heading bump mangled a `#` comment inside a fenced code block"
)

# Real headings outside the code fence should still be bumped one level.
assert "## Getting Started" in qmd, "expected `# Getting Started` to be bumped to `##`"

# End-to-end: code-fold renders a <details> disclosure with our summary.
soup = _load_html(_site_dir(pkg) / "index.html")
summaries = [s.get_text() for s in soup.find_all("summary")]
assert any("Show the setup code" in s for s in summaries), (
"code cell did not render folded — `#| code-fold` was not honored"
)


@pytest.mark.dedicated
def test_DED_no_readme_builds():
"""gdtest_no_readme: package with no README still builds."""
Expand Down
Loading