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
54 changes: 52 additions & 2 deletions great_docs/assets/page-tags.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,36 @@
return null;
}

/**
* Insert the tag container at the bottom of the page, after page metadata
* (if present) or at the end of main content under a horizontal rule.
* @param {HTMLElement} container - The tag pills container element
*/
function insertTagsBottom(container) {
// Look for existing page metadata element
var metadataEl = document.querySelector(".gd-page-metadata");
if (metadataEl) {
// Insert tags after the metadata block
if (metadataEl.nextSibling) {
metadataEl.parentNode.insertBefore(container, metadataEl.nextSibling);
} else {
metadataEl.parentNode.appendChild(container);
}
} else {
// No page metadata: append to end of main content with a horizontal rule
var mainContent = document.querySelector("#quarto-content > main");
if (!mainContent) {
mainContent = document.querySelector("main");
}
if (mainContent) {
var hr = document.createElement("hr");
hr.className = "gd-tags-rule";
mainContent.appendChild(hr);
mainContent.appendChild(container);
}
}
}

/**
* Render tag pills into the DOM, below the page title and subtitle.
* @param {string[]} tags - List of tag names
Expand Down Expand Up @@ -151,7 +181,16 @@
container.appendChild(pill);
});

// Insert after the title and subtitle (if present), but before description
// Determine placement: per-page override → global default → "top"
var pageLocations = tagsData.page_tag_locations || {};
var location = pageLocations[matchedKey] || tagsData.default_location || "top";

if (location === "bottom") {
insertTagsBottom(container);
return;
}

// Default "top" placement: after the title and subtitle, before description
// Structure: .quarto-title contains h1 + optional p.subtitle
// Description is in a sibling div outside .quarto-title
var insertAfter = titleEl;
Expand All @@ -177,7 +216,18 @@
if (!data || !data.page_tags) return;

var result = findPageTags(data.page_tags);
if (result && result.tags.length > 0) {
if (!result || result.tags.length === 0) return;

// Determine if this page uses bottom placement
var pageLocations = data.page_tag_locations || {};
var location = pageLocations[result.key] || data.default_location || "top";

if (location === "bottom") {
// Defer so page-metadata.js (also on DOMContentLoaded) inserts first
requestAnimationFrame(function () {
renderTagPills(result.tags, data, result.key);
});
} else {
renderTagPills(result.tags, data, result.key);
}
}
Expand Down
8 changes: 8 additions & 0 deletions great_docs/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1194,6 +1194,14 @@ def tags_show_on_pages(self) -> bool:
"""Check if tags should be rendered above page titles."""
return self.tags_enabled and self.get("tags.show_on_pages", True)

@property
def tags_location(self) -> str:
"""Get the default tag pill placement: ``"top"`` or ``"bottom"``."""
val = self.get("tags.location", "top")
if val in ("top", "bottom"):
return val
return "top"

@property
def tags_hierarchical(self) -> bool:
"""Check if hierarchical tags (using '/') are supported."""
Expand Down
20 changes: 18 additions & 2 deletions great_docs/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1878,8 +1878,17 @@ def _generate_tags_json(self, tag_index: dict[str, list[dict[str, str]]]) -> Non
page_tags.setdefault(page["href"], []).append(tag_name)

# Also collect shadow tags per page (so they get meta tags but no pills)
# Re-scan for shadow-tagged pages separately
for scan_dir_name in ("user-guide", "recipes"):
# and per-page tag_location overrides from frontmatter
# Re-scan tagged directories
page_tag_locations: dict[str, str] = {}
scan_dir_names = ["user-guide", "recipes"]
# Include custom section directories
for section_cfg in self._config.sections:
title = section_cfg.get("title", "")
slug = re.sub(r"[^a-z0-9]+", "-", title.lower()).strip("-") if title else ""
if slug and slug not in scan_dir_names:
scan_dir_names.append(slug)
for scan_dir_name in scan_dir_names:
scan_dir = self.project_path / scan_dir_name
if not scan_dir.is_dir():
continue
Expand All @@ -1901,6 +1910,11 @@ def _generate_tags_json(self, tag_index: dict[str, list[dict[str, str]]]) -> Non
page_tags.setdefault(href, [])
# Shadow tags are NOT added to the visible list

# Collect per-page tag-location override
tag_loc = fm.get("tag-location")
if tag_loc in ("top", "bottom"):
page_tag_locations[href] = tag_loc

# Build per-tag metadata (page count + sections) for tooltips
tag_meta: dict[str, dict] = {}
for tag_name, pages in tag_index.items():
Expand Down Expand Up @@ -1937,6 +1951,8 @@ def _generate_tags_json(self, tag_index: dict[str, list[dict[str, str]]]) -> Non
"icons": resolved_icons,
"shadow": list(shadow_tags),
"hierarchical": self._config.tags_hierarchical,
"default_location": self._config.tags_location,
"page_tag_locations": page_tag_locations,
}
tags_path = self.project_path / "_tags.json"
with open(tags_path, "w", encoding="utf-8") as f:
Expand Down
12 changes: 12 additions & 0 deletions test-packages/synthetic/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,8 @@
"gdtest_page_tags", # 162
# 163: Page status badges
"gdtest_page_status", # 163
# 164: Tag location (top vs. bottom)
"gdtest_tag_location", # 164
]


Expand Down Expand Up @@ -545,6 +547,7 @@
"K53": {"axis": "config", "label": "nav_icons config"},
# Page tags axes
"T1": {"axis": "tags", "label": "Page tags with hierarchy + shadow"},
"T3": {"axis": "tags", "label": "Tag location top vs. bottom with per-page overrides"},
# Page status axes
"T2": {"axis": "status", "label": "Page status badges in sidebar + pages"},
}
Expand Down Expand Up @@ -1859,6 +1862,15 @@
"headings on the tags page. Tag icons are configured for Python, "
"Tutorial, and API."
),
"gdtest_tag_location": (
"Six user-guide pages testing tag_location placement. The global default "
"is set to 'bottom' so tags appear after page metadata. Three pages have "
"frontmatter dates (date_created, last_update) with show_dates enabled "
"to test bottom-placed tags appearing after the metadata block. Two "
"pages override to 'top' via tag-location frontmatter, one page "
"explicitly sets 'bottom', one page has no dates (tags under <hr>), "
"and one page has no tags at all."
),
"gdtest_page_status": (
"Seven user-guide pages exercising page status badges. Six pages carry "
"a status frontmatter value (new, updated, beta, deprecated, experimental, "
Expand Down
214 changes: 214 additions & 0 deletions test-packages/synthetic/specs/gdtest_tag_location.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
"""
gdtest_tag_location — Tag pills at top vs. bottom of pages.

Dimensions: T3
Focus: Tests the tag_location feature which controls placement of tag pills.
Pages can show tags at the "top" (default, below title) or "bottom"
(after page metadata / end of content). The global default is set in
great-docs.yml and individual pages can override via tag-location
frontmatter. This site exercises both global "bottom" default with
per-page "top" overrides, and pages that inherit the global default.
"""

SPEC = {
"name": "gdtest_tag_location",
"description": "Tag pills at top vs. bottom with per-page overrides",
"dimensions": ["T3"],
"pyproject_toml": {
"project": {
"name": "gdtest-tag-location",
"version": "0.1.0",
"description": "A test package for tag_location placement",
},
"build-system": {
"requires": ["setuptools"],
"build-backend": "setuptools.build_meta",
},
},
"config": {
"display_name": "Tag Location Demo",
"site": {
"show_dates": True,
},
"tags": {
"enabled": True,
"index_page": True,
"show_on_pages": True,
"hierarchical": True,
"location": "bottom",
"icons": {
"Setup": "download",
"Python": "code",
"API": "plug",
},
},
},
"files": {
"gdtest_tag_location/__init__.py": '''\
"""A test package for the tag location feature."""

__version__ = "0.1.0"
__all__ = ["Gadget", "make_gadget"]


class Gadget:
"""
A simple gadget.

Parameters
----------
label
Display label for the gadget.
"""

def __init__(self, label: str):
self.label = label

def activate(self) -> str:
"""
Activate the gadget.

Returns
-------
str
Activation message.
"""
return f"Gadget {self.label} activated"


def make_gadget(label: str) -> Gadget:
"""
Create a new gadget.

Parameters
----------
label
Display label for the gadget.

Returns
-------
Gadget
A new gadget instance.
"""
return Gadget(label)
''',
# Page 1: inherits global "bottom" — has page metadata dates
"user_guide/01-intro.qmd": """\
---
title: Introduction
tags: [Setup, Python]
date_created: "2025-06-01"
last_update:
date: "2026-03-15"
author: "Alice"
---

Welcome! This page inherits the global tag location (bottom)
and has page metadata (dates + author). Tags should appear
*after* the metadata block at the bottom.
""",
# Page 2: explicit override to "top" — also has dates
"user_guide/02-api-guide.qmd": """\
---
title: API Guide
tags: [API, Python]
tag-location: top
date_created: "2025-07-10"
last_update:
date: "2026-02-20"
---

This page overrides the global setting and places tags at the top,
right below the title. Page metadata still appears at the bottom.

## Using the API

Import and use gadgets in your code.
""",
# Page 3: inherits global "bottom" — has dates + subtitle
"user_guide/03-advanced.qmd": """\
---
title: Advanced Patterns
subtitle: Power-user techniques
tags: [Python, API]
date_created: "2025-08-22"
last_update:
date: "2026-04-01"
author: "Bob"
---

This page inherits the global tag location (bottom) and has
both a subtitle and page metadata. Tags should appear after
the metadata block.

## Multi-Gadget Workflows

Combine multiple gadgets for complex tasks.
""",
# Page 4: explicit "bottom" — no dates (tags under <hr>)
"user_guide/04-setup.qmd": """\
---
title: Setup Guide
tags: [Setup]
tag-location: bottom
---

This page explicitly sets tag-location to bottom and has
no date metadata. Tags should appear under a horizontal
rule at the end of content.

## Installation

Install the package via pip.
""",
# Page 5: explicit override to "top"
"user_guide/05-tips.qmd": """\
---
title: Tips and Tricks
description: Handy shortcuts and lesser-known features.
tags: [Python, Setup]
tag-location: top
---

This page overrides to top and has a description field.

## Shortcuts

Use keyboard shortcuts for faster workflows.
""",
# Page 6: no tags at all
"user_guide/06-faq.qmd": """\
---
title: FAQ
---

This page has no tags. No tag pills should appear anywhere.
""",
"README.md": """\
# gdtest-tag-location

A test package demonstrating the tag_location feature.

Tags can appear at the top (below title) or bottom (after metadata)
of each page. The global default is set in great-docs.yml and
individual pages can override via tag-location frontmatter.
""",
},
"expected": {
"detected_name": "gdtest-tag-location",
"detected_module": "gdtest_tag_location",
"detected_parser": "numpy",
"export_names": ["Gadget", "make_gadget"],
"num_exports": 2,
"section_titles": ["Classes", "Functions"],
"has_user_guide": True,
"user_guide_files": [
"01-intro.qmd",
"02-api-guide.qmd",
"03-advanced.qmd",
"04-setup.qmd",
"05-tips.qmd",
"06-faq.qmd",
],
},
}
Loading
Loading