From dba2471215de7196635337ce1ee9a5f5e15a3adf Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Sun, 28 Jun 2026 16:13:43 -0400 Subject: [PATCH 1/2] Better adjust heading levels --- great_docs/core.py | 63 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 52 insertions(+), 11 deletions(-) diff --git a/great_docs/core.py b/great_docs/core.py index 1949052a..15ad6d07 100644 --- a/great_docs/core.py +++ b/great_docs/core.py @@ -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. @@ -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) @@ -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() From 8facddce71d5fe1e165eec0a60e36c7e528341ac Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Sun, 28 Jun 2026 16:14:10 -0400 Subject: [PATCH 2/2] Add GDG site and dedicated test (DED) --- test-packages/synthetic/catalog.py | 10 +++ .../specs/gdtest_index_frontmatter.py | 87 +++++++++++++++++++ tests/test_gdg_rendered.py | 77 ++++++++++++++++ 3 files changed, 174 insertions(+) create mode 100644 test-packages/synthetic/specs/gdtest_index_frontmatter.py diff --git a/test-packages/synthetic/catalog.py b/test-packages/synthetic/catalog.py index aeef7ec3..bac5886d 100644 --- a/test-packages/synthetic/catalog.py +++ b/test-packages/synthetic/catalog.py @@ -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 @@ -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. " diff --git a/test-packages/synthetic/specs/gdtest_index_frontmatter.py b/test-packages/synthetic/specs/gdtest_index_frontmatter.py new file mode 100644 index 00000000..be150bcf --- /dev/null +++ b/test-packages/synthetic/specs/gdtest_index_frontmatter.py @@ -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"], + }, +} diff --git a/tests/test_gdg_rendered.py b/tests/test_gdg_rendered.py index 7156f20d..83c1b6df 100644 --- a/tests/test_gdg_rendered.py +++ b/tests/test_gdg_rendered.py @@ -6294,6 +6294,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 . + 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."""