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
70 changes: 55 additions & 15 deletions great_docs/_apiref/_render/html_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,31 +37,74 @@ def replace_link(match: re.Match[str]) -> str:


def html_table(
rows: Sequence[tuple[Stringable, Stringable | None]],
rows: Sequence[tuple[Stringable, Stringable | None]] | Sequence[Sequence[str]],
*,
headers: Sequence[str] | None = None,
col_widths: Sequence[int] | None = None,
table_class: str = "gd-summary-table",
) -> str:
"""
Render rows as an HTML table
Render rows as an HTML table.

Styling is handled by the .gd-summary-table class in great-docs.scss,
which overrides Bootstrap defaults for a cleaner appearance.
When `headers=` is provided the table switches to **multi-column mode**: the thead row is
visible, optional per-column widths are applied via inline `style="width: X%"` on each `<th>`
(the CSS class must set `table-layout: fixed`), and Bootstrap's base `caption-top table` classes
are added alongside `table_class=` so the output matches what Quarto emits for Markdown tables.

When `headers=` is `None` the function operates in legacy two-column mode: rows are
`(name, description)` tuples, the `name` cell undergoes Markdown-link-to-HTML conversion, and
styling is handled entirely by `table_class=` (default `gd-summary-table`).

Parameters
----------
rows
Sequence of (name, description) tuples. Name can contain markdown links.
Multi-column mode: sequence of rows, each a sequence of HTML-ready cell strings (caller is
responsible for `html.escape` on text values). Legacy mode: sequence of
`(name, description)` tuples where `name` may contain Pandoc-style Markdown links.
headers
Column header labels (plain text). Providing this activates multi-column mode.
col_widths
Integer percentage widths for each column (multi-column mode only). When given, each `<th>`
gets an inline `style="width: X%"`. The CSS class must set `table-layout: fixed` for the
widths to take effect.
table_class
CSS class to apply to the table for styling.
Extra CSS class applied to the `<table>` element.

Returns
-------
str
HTML string with table markup.
"""
# Build table rows
body_rows: list[str] = []
for name, desc in rows:
if headers is not None:
# ── Multi-column mode ──────────────────────────────────────────
# Mirror the classes Quarto emits for markdown tables so Bootstrap
# base styles apply, then tack on the caller's custom class.
full_class = f"caption-top table {table_class}"

if col_widths is not None:
header_cells = "".join(
f'<th style="width: {w}%">{html.escape(h)}</th>'
for h, w in zip(headers, col_widths)
)
else:
header_cells = "".join(f"<th>{html.escape(h)}</th>" for h in headers)

body_rows = [
" <tr>" + "".join(f"<td>{cell}</td>" for cell in row) + "</tr>"
for row in rows # type: ignore[union-attr]
]

return (
f'<table class="{full_class}" data-quarto-disable-processing="true">\n'
f'<thead>\n <tr class="header">{header_cells}</tr>\n</thead>\n'
f"<tbody>\n" + "\n".join(body_rows) + "\n</tbody>\n</table>"
)

# ── Legacy two-column mode ─────────────────────────────────────────
# Styling is handled by the .gd-summary-table class in great-docs.scss,
# which overrides Bootstrap defaults for a cleaner appearance.
body_rows_2col: list[str] = []
for name, desc in rows: # type: ignore[misc]
name, desc = str(name), str(desc) if desc is not None else ""
# Convert markdown links to HTML
name = _md_link_to_html(name)
Expand All @@ -71,12 +114,11 @@ def html_table(
desc = " ".join(line.strip() for line in desc.split("\n") if line.strip())
else:
desc = ""
body_rows.append(f" <tr>\n <td>{name}</td>\n <td>{desc}</td>\n </tr>")
body_rows_2col.append(f" <tr>\n <td>{name}</td>\n <td>{desc}</td>\n </tr>")

table_body = "\n".join(body_rows)
table_body = "\n".join(body_rows_2col)

# Assemble full HTML (no inline styles needed - SCSS handles styling)
html_out = f"""<table class="{table_class}">
return f"""<table class="{table_class}">
<thead>
<tr>
<th>Name</th>
Expand All @@ -87,5 +129,3 @@ def html_table(
{table_body}
</tbody>
</table>"""

return html_out
6 changes: 6 additions & 0 deletions great_docs/assets/great-docs.scss
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,12 @@ body.quarto-dark table.gd-summary-table td:last-child {
color: var(--bs-body-color);
}

// Dependency tables on the Package Info page (fixed column widths via <th style="width: X%">)
table.gd-dep-table {
table-layout: fixed;
width: 100%;
}

.table > tbody {
// quarto adds a bottom color that is different from the border color,
// that looks good the default
Expand Down
106 changes: 53 additions & 53 deletions great_docs/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -11160,6 +11160,9 @@ def _generate_package_info_page(self) -> str | None:
if not self._config.is_python_project:
return None

import html as _html

from ._apiref._render.html_table import html_table
from ._icons import get_icon_svg
from ._translations import get_translation

Expand Down Expand Up @@ -11192,36 +11195,49 @@ def _generate_package_info_page(self) -> str | None:
all_dep_names |= {d["name"] for d in deps}
pypi_dates = self._fetch_pypi_dates(all_dep_names)

sections: list[str] = []
# Column widths (%)
_COL_WIDTHS_4 = (35, 30, 25, 10) # Package / Version / Last Published / PyPI
_COL_WIDTHS_5 = (28, 24, 20, 20, 8) # … + Environment Marker

# ── Runtime Dependencies ──────────────────────────────────────
if runtime_deps:
has_markers = any(d.get("marker") for d in runtime_deps)
header_cols = f"| {_pkg_col} | {_ver_col} | Last Published | PyPI |"
separator = "|----|----|----|----|"
if has_markers:
header_cols = f"| {_pkg_col} | {_ver_col} | {_marker_col} | Last Published | PyPI |"
separator = "|----|----|----|----|----|"

rows: list[str] = []
for dep in runtime_deps:
def _build_dep_rows(deps: list[dict], has_markers: bool) -> list[list[str]]:
dep_rows: list[list[str]] = []
for dep in deps:
name = dep["name"]
spec = f"`{dep['specifier']}`" if dep["specifier"] else "—"
date_str = pypi_dates.get(name, "—")
pypi_cell = f"[{_link_svg}](https://pypi.org/project/{name}/){{.gd-pypi-link}}"
spec = f"<code>{_html.escape(dep['specifier'])}</code>" if dep["specifier"] else "—"
date_str = _html.escape(pypi_dates.get(name, "—"))
pypi_cell = (
f'<a href="https://pypi.org/project/{_html.escape(name)}/"'
f' class="gd-pypi-link">{_link_svg}</a>'
)
pkg_cell = f'<code class="gd-no-link">{_html.escape(name)}</code>'
if has_markers:
marker = f"`{dep['marker']}`" if dep.get("marker") else ""
rows.append(
f"| `{name}`{{.gd-no-link}} | {spec} | {marker}"
f" | {date_str} | {pypi_cell} |"
marker = (
f"<code>{_html.escape(dep['marker'])}</code>" if dep.get("marker") else ""
)
dep_rows.append([pkg_cell, spec, marker, date_str, pypi_cell])
else:
rows.append(f"| `{name}`{{.gd-no-link}} | {spec} | {date_str} | {pypi_cell} |")
dep_rows.append([pkg_cell, spec, date_str, pypi_cell])
return dep_rows

sections: list[str] = []

# ── Runtime Dependencies ──────────────────────────────────────
if runtime_deps:
has_markers = any(d.get("marker") for d in runtime_deps)
headers = (
[_pkg_col, _ver_col, _marker_col, "Last Published", "PyPI"]
if has_markers
else [_pkg_col, _ver_col, "Last Published", "PyPI"]
)
col_widths = _COL_WIDTHS_5 if has_markers else _COL_WIDTHS_4
table_html = html_table(
_build_dep_rows(runtime_deps, has_markers),
headers=headers,
col_widths=col_widths,
table_class="gd-dep-table",
)
sections.append(f"## {_runtime}\n")
sections.append(header_cols)
sections.append(separator)
sections.extend(rows)
sections.append(table_html)
sections.append("")

# ── Optional Dependencies ─────────────────────────────────────
Expand All @@ -11233,34 +11249,19 @@ def _generate_package_info_page(self) -> str | None:

if group_deps:
has_markers = any(d.get("marker") for d in group_deps)
header_cols = f"| {_pkg_col} | {_ver_col} | Last Published | PyPI |"
separator = "|----|----|----|----|"
if has_markers:
header_cols = (
f"| {_pkg_col} | {_ver_col} | {_marker_col} | Last Published | PyPI |"
)
separator = "|----|----|----|----|----|"

sections.append(header_cols)
sections.append(separator)

for dep in group_deps:
name = dep["name"]
spec = f"`{dep['specifier']}`" if dep["specifier"] else "—"
date_str = pypi_dates.get(name, "—")
pypi_cell = (
f"[{_link_svg}](https://pypi.org/project/{name}/){{.gd-pypi-link}}"
)
if has_markers:
marker = f"`{dep['marker']}`" if dep.get("marker") else ""
sections.append(
f"| `{name}`{{.gd-no-link}} | {spec} | {marker}"
f" | {date_str} | {pypi_cell} |"
)
else:
sections.append(
f"| `{name}`{{.gd-no-link}} | {spec} | {date_str} | {pypi_cell} |"
)
headers = (
[_pkg_col, _ver_col, _marker_col, "Last Published", "PyPI"]
if has_markers
else [_pkg_col, _ver_col, "Last Published", "PyPI"]
)
col_widths = _COL_WIDTHS_5 if has_markers else _COL_WIDTHS_4
table_html = html_table(
_build_dep_rows(group_deps, has_markers),
headers=headers,
col_widths=col_widths,
table_class="gd-dep-table",
)
sections.append(table_html)
sections.append("")
else:
sections.append("*No dependencies declared.*\n")
Expand Down Expand Up @@ -16440,8 +16441,7 @@ class Result:
d2_modified = _render_d2(self.project_path, cache_dir=d2_cache)
if d2_modified:
log.detail(
f"Rendered d2 diagrams in {len(d2_modified)} page(s): "
+ ", ".join(d2_modified)
f"Rendered d2 diagrams in {len(d2_modified)} page(s): " + ", ".join(d2_modified)
)

# Get environment with QUARTO_PYTHON set
Expand Down
14 changes: 7 additions & 7 deletions tests/test_great_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -42320,9 +42320,9 @@ def test_generate_package_info_page_basic():

content = pkg_info.read_text(encoding="utf-8")
assert "Package Info" in content
assert "`numpy`{.gd-no-link}" in content
assert "`>=1.24`" in content
assert "`pandas`{.gd-no-link}" in content
assert 'class="gd-no-link">numpy</code>' in content
assert "&gt;=1.24" in content
assert 'class="gd-no-link">pandas</code>' in content
assert "pypi.org/project/numpy" in content
assert "pypi.org/project/pandas" in content
assert "Runtime Dependencies" in content
Expand Down Expand Up @@ -42366,9 +42366,9 @@ def test_generate_package_info_page_with_optional_deps():
assert "Optional Dependencies" in content
assert "### `dev`" in content
assert "### `docs`" in content
assert "`pytest`{.gd-no-link}" in content or "`pytest`" in content
assert "`coverage`{.gd-no-link}" in content or "`coverage`" in content
assert "`quartodoc`{.gd-no-link}" in content or "`quartodoc`" in content
assert "pytest" in content
assert "coverage" in content
assert "quartodoc" in content
# Summary counts
assert "3 groups" in content or "2 groups" in content

Expand Down Expand Up @@ -42397,7 +42397,7 @@ def test_generate_package_info_page_with_markers():
content = (gd_dir / "package-info.qmd").read_text(encoding="utf-8")
assert "Environment Marker" in content
assert "python_version" in content
assert "`typing-extensions`" in content or "`typing_extensions`" in content
assert "typing-extensions" in content or "typing_extensions" in content


def test_generate_package_info_page_disabled():
Expand Down
Loading