diff --git a/great_docs/_apiref/_render/html_table.py b/great_docs/_apiref/_render/html_table.py
index c3c952d0..7fb2ddc4 100644
--- a/great_docs/_apiref/_render/html_table.py
+++ b/great_docs/_apiref/_render/html_table.py
@@ -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 `
`
+ (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 ` | `
+ 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 `` 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'| {html.escape(h)} | '
+ for h, w in zip(headers, col_widths)
+ )
+ else:
+ header_cells = "".join(f"{html.escape(h)} | " for h in headers)
+
+ body_rows = [
+ " " + "".join(f"| {cell} | " for cell in row) + " "
+ for row in rows # type: ignore[union-attr]
+ ]
+
+ return (
+ f'\n'
+ f'\n \n\n'
+ f"\n" + "\n".join(body_rows) + "\n\n "
+ )
+
+ # ── 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)
@@ -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" \n | {name} | \n {desc} | \n ")
+ body_rows_2col.append(f" \n | {name} | \n {desc} | \n ")
- 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"""
+ return f"""
| Name |
@@ -87,5 +129,3 @@ def html_table(
{table_body}
"""
-
- return html_out
diff --git a/great_docs/assets/great-docs.scss b/great_docs/assets/great-docs.scss
index 3a47f525..ff0480e7 100644
--- a/great_docs/assets/great-docs.scss
+++ b/great_docs/assets/great-docs.scss
@@ -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 )
+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
diff --git a/great_docs/core.py b/great_docs/core.py
index 72c4616b..a088efc2 100644
--- a/great_docs/core.py
+++ b/great_docs/core.py
@@ -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
@@ -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"{_html.escape(dep['specifier'])}" if dep["specifier"] else "—"
+ date_str = _html.escape(pypi_dates.get(name, "—"))
+ pypi_cell = (
+ f'{_link_svg}'
+ )
+ pkg_cell = f'{_html.escape(name)}'
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"{_html.escape(dep['marker'])}" 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 ─────────────────────────────────────
@@ -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")
@@ -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
diff --git a/tests/test_great_docs.py b/tests/test_great_docs.py
index eaba06a3..8ca2a3d6 100644
--- a/tests/test_great_docs.py
+++ b/tests/test_great_docs.py
@@ -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' in content
+ assert ">=1.24" in content
+ assert 'class="gd-no-link">pandas' in content
assert "pypi.org/project/numpy" in content
assert "pypi.org/project/pandas" in content
assert "Runtime Dependencies" in content
@@ -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
@@ -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():
| |