From fc2b513193b250cfc09d2da735fffcc144591864 Mon Sep 17 00:00:00 2001 From: Julia Hoang Date: Mon, 6 Jul 2026 13:42:46 -0700 Subject: [PATCH 01/30] feat: add per-library Slack channel URL with cppalliance fallback --- core/constants.py | 1 + .../migrations/0042_library_slack_url.py | 22 +++++++++++++++++++ libraries/models.py | 8 +++++++ libraries/views.py | 4 ++-- 4 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 libraries/migrations/0042_library_slack_url.py diff --git a/core/constants.py b/core/constants.py index 2969cd612..18a6aa7a4 100644 --- a/core/constants.py +++ b/core/constants.py @@ -28,6 +28,7 @@ class BadgeToken(StrEnum): SLACK_URL = "https://cpplang.slack.com" +SLACK_JOIN_URL = "https://cppalliance.org/slack/" # URL to join the Slack workspace, not the Slack workspace itself SLACK_MEMBER_COUNT = "24,000+" STATIC_CONTENT_EARLY_EXIT_PATH_PREFIXES = ("releases/",) # possible library versions are: boost_1_53_0_beta1, 1_82_0, 1_55_0b1 diff --git a/libraries/migrations/0042_library_slack_url.py b/libraries/migrations/0042_library_slack_url.py new file mode 100644 index 000000000..199da8233 --- /dev/null +++ b/libraries/migrations/0042_library_slack_url.py @@ -0,0 +1,22 @@ +# Generated by Django 6.0.2 on 2026-07-06 20:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("libraries", "0041_category_short_description"), + ] + + operations = [ + migrations.AddField( + model_name="library", + name="slack_url", + field=models.URLField( + blank=True, + help_text="URL of the dedicated Slack channel for this library. Falls back to the general Boost Slack when blank.", + max_length=500, + ), + ), + ] diff --git a/libraries/models.py b/libraries/models.py index f6da98e97..31006949d 100644 --- a/libraries/models.py +++ b/libraries/models.py @@ -273,6 +273,14 @@ class Library(models.Model): null=True, help_text="The URL of the library's GitHub repository.", ) + slack_url = models.URLField( + max_length=500, + blank=True, + help_text=( + "URL of the dedicated Slack channel for this library. " + "Falls back to the general Boost Slack when blank." + ), + ) versions = models.ManyToManyField( "versions.Version", through="libraries.LibraryVersion", related_name="libraries" ) diff --git a/libraries/views.py b/libraries/views.py index d1561543c..c6d945738 100644 --- a/libraries/views.py +++ b/libraries/views.py @@ -13,7 +13,7 @@ from django.views.decorators.csrf import csrf_exempt from django.views.generic import DetailView, ListView, FormView, TemplateView -from core.constants import SLACK_URL +from core.constants import SLACK_JOIN_URL from core.githubhelper import GithubAPIClient from core.install_commands import INSTALL_PKG_MANAGERS, INSTALL_SYSTEM_INSTALL from core.mixins import V3Mixin @@ -522,7 +522,7 @@ def get_v3_context_data(self, **kwargs): context["install_card_system_install"] = INSTALL_SYSTEM_INSTALL context["library_about_code"] = SharedResources.library_about_code context["library_install_code"] = SharedResources.library_install_code - context["slack_url"] = SLACK_URL + context["slack_url"] = self.object.slack_url or SLACK_JOIN_URL context["category_tags_v3"] = [ { From 93053c722f876243fa51a07752d1007b278209a5 Mon Sep 17 00:00:00 2001 From: Julia Hoang Date: Mon, 6 Jul 2026 14:39:34 -0700 Subject: [PATCH 02/30] feat: wire up documentation card --- static/css/v3/library-subpage.css | 12 ++++++++++++ templates/v3/libraries/library-subpage.html | 6 ++++++ 2 files changed, 18 insertions(+) diff --git a/static/css/v3/library-subpage.css b/static/css/v3/library-subpage.css index ec6800494..791ef969a 100644 --- a/static/css/v3/library-subpage.css +++ b/static/css/v3/library-subpage.css @@ -100,11 +100,23 @@ body:has(.is_flagship) .library-subpage-container { .card-masonry-bottom > div > .basic-card, .card-masonry-bottom > div > .card-group, .card-masonry-bottom > div > .post-card, +.library-subpage__documentation > .markdown-card, .library-subpage__all-contributors > .contributors-list { max-width: 100%; width: 100%; } +.library-subpage__documentation > .markdown-card { + max-height: 572px; + display: flex; + flex-direction: column; +} + +.library-subpage__documentation > .markdown-card .markdown-content { + min-height: 0; + overflow-y: auto; +} + /* ── Desktop (≥ 1280px): show col-3, hide tablet-only duplicates ─────────── */ @media (min-width: 1280px) { diff --git a/templates/v3/libraries/library-subpage.html b/templates/v3/libraries/library-subpage.html index 05855e965..42dd2ccfd 100644 --- a/templates/v3/libraries/library-subpage.html +++ b/templates/v3/libraries/library-subpage.html @@ -45,6 +45,12 @@ + {% if description %} +
+ {% include "v3/includes/_markdown_card.html" with title="Documentation" html=description %} +
+ {% endif %} +
{% include "v3/includes/_contributors_list.html" with title="Contributors: This Release" variant="all" contributors=this_release_contributors heading_level="2" %} From e0d39b748a54cd34ebb51340841d60c81655ef46 Mon Sep 17 00:00:00 2001 From: Julia Hoang Date: Mon, 6 Jul 2026 14:59:30 -0700 Subject: [PATCH 03/30] feat: version-aware all contributors with identity-based dedup --- libraries/mixins.py | 37 ++++++++++++++++ libraries/tests/test_mixins.py | 80 +++++++++++++++++++++++++++++++++- libraries/views.py | 14 ++++-- 3 files changed, 126 insertions(+), 5 deletions(-) diff --git a/libraries/mixins.py b/libraries/mixins.py index ead9d41fd..e392c2492 100644 --- a/libraries/mixins.py +++ b/libraries/mixins.py @@ -340,3 +340,40 @@ def get_previous_contributors(self, library_version, exclude=None): if exclude: qs = qs.exclude(id__in=exclude) return qs + + def build_all_contributors(self, library_version, authors, maintainers): + """All Contributors: authors + maintainers + commit contributors from the + first release up to and including the selected version. + + Deduplication is identity-based only: linked authors/maintainers are + excluded from the commit-author rows by CommitAuthor id (never by name), + so an author who hasn't yet claimed their commit email may appear twice + until linked. `authors` and `maintainers` must already be patched via patch_commit_authors. + """ + author_ca_ids = [ + u.commitauthor.id + for u in authors + if getattr(getattr(u, "commitauthor", None), "id", None) + ] + maintainer_ca_ids = [ + u.commitauthor.id + for u in maintainers + if getattr(getattr(u, "commitauthor", None), "id", None) + ] + library_versions = LibraryVersion.objects.filter( + library=library_version.library, + version__in=Version.objects.minor_versions().filter( + version_array__lte=library_version.version.cleaned_version_parts_int + ), + ) + contributors = ( + CommitAuthor.humans.filter(commit__library_version__in=library_versions) + .exclude(id__in=author_ca_ids + maintainer_ca_ids) + .annotate(count=Count("commit")) + .order_by("-count") + ) + return ( + [u.to_v3_profile_dict("Author") for u in authors] + + [u.to_v3_profile_dict("Maintainer") for u in maintainers] + + [c.to_v3_profile_dict("Contributor") for c in contributors] + ) diff --git a/libraries/tests/test_mixins.py b/libraries/tests/test_mixins.py index 21be585d7..0dae63047 100644 --- a/libraries/tests/test_mixins.py +++ b/libraries/tests/test_mixins.py @@ -1,8 +1,11 @@ import datetime import pytest +from django.contrib.auth import get_user_model from django.test import RequestFactory from model_bakery import baker -from libraries.mixins import VersionAlertMixin +from libraries.mixins import ContributorMixin, VersionAlertMixin +from libraries.models import CommitAuthor +from libraries.utils import patch_commit_authors from libraries.views import LibraryListBase @@ -100,3 +103,78 @@ def test_version_alert_message_dev_branch(): msg = _alert_message(selected, current) assert "under active development" in msg assert "develop" in msg + + +# ── build_all_contributors ────────────────────────────────────────────────── + + +def _make_linked_author(name, email): + """User linked to a CommitAuthor via a matching CommitAuthorEmail.""" + user = baker.make(get_user_model(), display_name=name, email=email) + author = baker.make( + CommitAuthor, + name=name, + is_bot=False, + avatar_url="https://example.com/a.png", + github_profile_url="https://github.com/x", + ) + baker.make("libraries.CommitAuthorEmail", author=author, email=email) + return user, author + + +@pytest.mark.django_db +def test_build_all_contributors_linked_author_single_entry(library_version): + """A linked author is labeled Author and not double-listed as a Contributor.""" + user, author = _make_linked_author("Ada Lovelace", "ada@example.com") + baker.make("libraries.Commit", author=author, library_version=library_version) + library_version.authors.add(user) + + authors = [user] + patch_commit_authors(authors) + result = ContributorMixin().build_all_contributors(library_version, authors, []) + + ada = [c for c in result if c["name"] == "Ada Lovelace"] + assert len(ada) == 1 + assert ada[0]["role"] == "Author" + + +@pytest.mark.django_db +def test_build_all_contributors_unlinked_author_not_merged_by_name(library_version): + """An unlinked author is NOT merged with a same-named CommitAuthor.""" + user = baker.make(get_user_model(), display_name="Jane Doe", email="jane@x.com") + author = baker.make(CommitAuthor, name="Jane Doe", is_bot=False) + baker.make("libraries.Commit", author=author, library_version=library_version) + library_version.authors.add(user) + + authors = [user] + patch_commit_authors(authors) # no matching CommitAuthorEmail -> stub, no pk + result = ContributorMixin().build_all_contributors(library_version, authors, []) + + janes = [c for c in result if c["name"] == "Jane Doe"] + assert {c["role"] for c in janes} == {"Author", "Contributor"} + + +@pytest.mark.django_db +def test_build_all_contributors_bounded_by_selected_version(library, version): + """Contributors up to the selected version are included; newer ones excluded.""" + older = baker.make("versions.Version", name="boost-1.70.0", fully_imported=True) + newer = baker.make("versions.Version", name="boost-1.80.0", fully_imported=True) + older_lv = baker.make("libraries.LibraryVersion", library=library, version=older) + current_lv = baker.make( + "libraries.LibraryVersion", library=library, version=version + ) + newer_lv = baker.make("libraries.LibraryVersion", library=library, version=newer) + + past = baker.make(CommitAuthor, name="Past Dev", is_bot=False) + current = baker.make(CommitAuthor, name="Current Dev", is_bot=False) + future = baker.make(CommitAuthor, name="Future Dev", is_bot=False) + baker.make("libraries.Commit", author=past, library_version=older_lv) + baker.make("libraries.Commit", author=current, library_version=current_lv) + baker.make("libraries.Commit", author=future, library_version=newer_lv) + + names = { + c["name"] for c in ContributorMixin().build_all_contributors(current_lv, [], []) + } + assert "Past Dev" in names + assert "Current Dev" in names + assert "Future Dev" not in names diff --git a/libraries/views.py b/libraries/views.py index c6d945738..5292cf049 100644 --- a/libraries/views.py +++ b/libraries/views.py @@ -577,10 +577,16 @@ def get_v3_context_data(self, **kwargs): or SharedResources.library_release_contributors ) - all_time = [ - a.to_v3_profile_dict("Contributor") - for a in base_context.get("previous_contributors", []) - ] + library_version = base_context.get("library_version") + all_time = ( + self.build_all_contributors( + library_version, + base_context.get("authors", []), + base_context.get("maintainers", []), + ) + if library_version + else [] + ) context["all_time_contributors"] = ( apply_collective_author_overrides(all_time) or SharedResources.library_all_contributors From e3c4e7a1c50488fc1aba69225f1b90c3c750519b Mon Sep 17 00:00:00 2001 From: Julia Hoang Date: Mon, 6 Jul 2026 15:34:28 -0700 Subject: [PATCH 04/30] feat: ingest and parse optional meta/website.adoc into LibraryVersion --- config/celery.py | 6 + core/githubhelper.py | 21 + libraries/admin.py | 12 + .../import_library_version_website_adoc.py | 55 +++ .../0043_libraryversion_website_adoc.py | 22 ++ libraries/models.py | 9 + libraries/tasks.py | 44 +++ libraries/tests/test_website_adoc.py | 195 ++++++++++ libraries/website_adoc.py | 360 ++++++++++++++++++ libraries/website_adoc_template.adoc | 130 +++++++ .../admin/libraryversion_change_list.html | 1 + versions/tasks.py | 4 + 12 files changed, 859 insertions(+) create mode 100644 libraries/management/commands/import_library_version_website_adoc.py create mode 100644 libraries/migrations/0043_libraryversion_website_adoc.py create mode 100644 libraries/tests/test_website_adoc.py create mode 100644 libraries/website_adoc.py create mode 100644 libraries/website_adoc_template.adoc diff --git a/config/celery.py b/config/celery.py index f999ceb4c..05e7ccdc2 100644 --- a/config/celery.py +++ b/config/celery.py @@ -77,6 +77,12 @@ def setup_periodic_tasks(sender, **kwargs): app.signature("libraries.tasks.update_library_version_dependencies"), ) + # Refresh parsed meta/website.adoc for the current release. Daily at 8:35 AM + sender.add_periodic_task( + crontab(hour=8, minute=35), + app.signature("libraries.tasks.update_library_version_website_adoc"), + ) + # Clear the static content database cache. Executes daily at 4:05 AM. sender.add_periodic_task( crontab(hour=4, minute=5), diff --git a/core/githubhelper.py b/core/githubhelper.py index b6454ce32..f40124b12 100644 --- a/core/githubhelper.py +++ b/core/githubhelper.py @@ -331,6 +331,27 @@ def get_libraries_json(self, repo_slug: str, tag: str = "master"): else: return response.json() + def get_website_adoc(self, repo_slug: str, tag: str = "master"): + """Retrieve a library's optional 'meta/website.adoc'. + + Most libraries won't ship this file, so a 404 is expected and returns + None quietly (no traceback). See libraries/website_adoc.py for the + parser and the file's contract. + + :param repo_slug: str, the repository slug + :param tag: str, the Git tag + :return: bytes, the file content, or None if it doesn't exist + """ + url = f"https://raw.githubusercontent.com/{self.owner}/{repo_slug}/{tag}/meta/website.adoc" # noqa + + try: + response = requests.get(url) + response.raise_for_status() + except requests.exceptions.HTTPError: + self.logger.info("website_adoc_not_found", repo=repo_slug, tag=tag) + return None + return response.content + def get_file_content( self, repo_slug: str = None, diff --git a/libraries/admin.py b/libraries/admin.py index e7f54e531..fa06aca3b 100644 --- a/libraries/admin.py +++ b/libraries/admin.py @@ -54,6 +54,7 @@ update_issues, update_libraries, update_library_version_documentation_urls_all_versions, + update_library_version_website_adoc, ) from .utils import generate_release_report_filename @@ -589,6 +590,11 @@ def get_urls(self): self.admin_site.admin_view(self.update_docs_urls), name="update_docs_urls", ), + path( + "update_website_adoc/", + self.admin_site.admin_view(self.update_website_adoc), + name="update_website_adoc", + ), ] return my_urls + urls @@ -603,6 +609,12 @@ def update_docs_urls(self, request): ) return HttpResponseRedirect("../") + def update_website_adoc(self, request): + """Run the task to refresh parsed meta/website.adoc content.""" + update_library_version_website_adoc.delay() + self.message_user(request, "website.adoc content is being refreshed.") + return HttpResponseRedirect("../") + @admin.register(Issue) class IssueAdmin(admin.ModelAdmin): diff --git a/libraries/management/commands/import_library_version_website_adoc.py b/libraries/management/commands/import_library_version_website_adoc.py new file mode 100644 index 000000000..9f7312c09 --- /dev/null +++ b/libraries/management/commands/import_library_version_website_adoc.py @@ -0,0 +1,55 @@ +import djclick as click + +from django.conf import settings + +from libraries.tasks import store_library_version_website_adoc +from versions.models import Version + + +@click.command() +@click.option( + "--release", + help="Boost version number (example: 1.81.0). A partial value processes all " + "matching versions (example: '--release=1.7' processes 1.70.0, 1.71.0, ...).", +) +@click.option( + "--new", + default=True, + type=click.BOOL, + help="True (default): process the most recent version only. False: process all " + "versions >= --min-version. Overridden by --release.", +) +@click.option( + "--min-version", + default=settings.MINIMUM_BOOST_VERSION, + help="Minimum Boost version to process (default: settings.MINIMUM_BOOST_VERSION).", +) +def command(release: str, new: bool, min_version: str): + """Fetch, parse, and store each library's meta/website.adoc for the targeted + Boost versions. + + The most recent version is fetched from `master` (freshest maintainer content, + matching the daily task); older versions are fetched from their release tag (the + frozen snapshot, matching release import). A repo without the file is left + untouched. + """ + version_qs = ( + Version.objects.with_partials() + .active() + .filter(name__gte=f"boost-{min_version}") + ) + most_recent = version_qs.most_recent() + + if release: + versions = list(version_qs.filter(name__icontains=release).order_by("-name")) + elif new: + versions = [most_recent] if most_recent else [] + else: + versions = list(version_qs.order_by("-name")) + + for version in versions: + ref = "master" if version == most_recent else version.name + click.secho(f"Processing {version.name} (ref={ref})...", fg="green") + store_library_version_website_adoc(version, ref=ref) + + click.secho("Finished importing website.adoc content.", fg="green") diff --git a/libraries/migrations/0043_libraryversion_website_adoc.py b/libraries/migrations/0043_libraryversion_website_adoc.py new file mode 100644 index 000000000..0f34870fb --- /dev/null +++ b/libraries/migrations/0043_libraryversion_website_adoc.py @@ -0,0 +1,22 @@ +# Generated by Django 6.0.2 on 2026-07-06 22:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("libraries", "0042_library_slack_url"), + ] + + operations = [ + migrations.AddField( + model_name="libraryversion", + name="website_adoc", + field=models.JSONField( + blank=True, + help_text="Parsed content of the library's optional meta/website.adoc (About, Playground, Designed for, Links, Install, Benchmarks, Freeform). Null when the repo has no website.adoc for this version.", + null=True, + ), + ), + ] diff --git a/libraries/models.py b/libraries/models.py index 31006949d..71895c7d4 100644 --- a/libraries/models.py +++ b/libraries/models.py @@ -537,6 +537,15 @@ class LibraryVersion(models.Model): data = models.JSONField( default=dict, help_text="Contains the libraries.json for this library-version" ) + website_adoc = models.JSONField( + null=True, + blank=True, + help_text=( + "Parsed content of the library's optional meta/website.adoc " + "(About, Playground, Designed for, Links, Install, Benchmarks, " + "Freeform). Null when the repo has no website.adoc for this version." + ), + ) # stats from git stored between x.x.0 versions insertions = models.IntegerField(default=0) deletions = models.IntegerField(default=0) diff --git a/libraries/tasks.py b/libraries/tasks.py index d740e8b51..73fcdf0e9 100644 --- a/libraries/tasks.py +++ b/libraries/tasks.py @@ -11,6 +11,7 @@ from django.db.models import Q, Count, Sum, OuterRef from core.boostrenderer import get_content_from_s3 from core.htmlhelper import get_library_documentation_urls +from core.githubhelper import GithubAPIClient from libraries.github import LibraryUpdater from libraries.models import ( Library, @@ -19,6 +20,7 @@ CommitAuthor, ReleaseReport, ) +from libraries.website_adoc import build_website_adoc from mailing_list.models import EmailData, PostingData from reports.generation import ( generate_algolia_words, @@ -51,6 +53,48 @@ def update_library_version_documentation_urls_all_versions(): get_and_store_library_version_documentation_urls_for_version(version.pk) +@app.task +def update_library_version_website_adoc(): + """Refresh parsed meta/website.adoc for the current release. + + Scoped to the most recent version and fetched from `master` (mirroring + update_libraries) so maintainer edits between releases are picked up. + Historical versions keep the snapshot captured at their release import — a + tagged release's meta/website.adoc is immutable, so re-fetching every + version daily would be thousands of pointless requests. + """ + version = Version.objects.most_recent() + if version is None: + return + store_library_version_website_adoc(version, ref="master") + + +def store_library_version_website_adoc(version, ref): + """Fetch + parse each library's meta/website.adoc at `ref` and store it. + + A repo without the file (or an unreachable fetch) leaves the existing value + untouched, so a transient failure never wipes previously-parsed content. + """ + client = GithubAPIClient() + library_versions = LibraryVersion.objects.filter(version=version).select_related( + "library" + ) + for library_version in library_versions: + repo_slug = library_version.library.github_repo + if not repo_slug: + continue + content = client.get_website_adoc(repo_slug=repo_slug, tag=ref) + if content is None: + # Missing file or unreachable fetch — keep any existing value. + continue + try: + parsed = build_website_adoc(content) + except Exception: + logger.exception("website_adoc_parse_failed", repo=repo_slug, ref=ref) + continue + LibraryVersion.objects.filter(pk=library_version.pk).update(website_adoc=parsed) + + @app.task def get_and_store_library_version_documentation_urls_for_version(version_pk): """ diff --git a/libraries/tests/test_website_adoc.py b/libraries/tests/test_website_adoc.py new file mode 100644 index 000000000..5b0f7291f --- /dev/null +++ b/libraries/tests/test_website_adoc.py @@ -0,0 +1,195 @@ +from pathlib import Path + +import pytest + +from libraries.website_adoc import build_website_adoc, parse_website_adoc + +FILLED = """\ += Boost.Example — Website Content +:library-key: example + +[#about] +== About +Boost.Example makes X fast and safe. + +[source,cpp] +---- +#include +int main() { return 0; } +---- + +[#playground] +== Playground + +[source,cpp] +---- +#include +int main() { boost::example::run(); } +---- + +[#designed-for] +== Designed for + +=== High throughput +Handles millions of ops per second. + +=== Zero allocations +No heap use on the hot path. + +[#links] +== Links +:common-use-case-url: https://www.boost.org/doc/libs/1_90_0/libs/example/use.html +:code-example-url: https://www.boost.org/doc/libs/1_90_0/libs/example/ex.html + +[#install] +== Install + +Header-only — just add Boost to your include path. + +[source,bash] +---- +./b2 --with-example install +---- + +[#benchmarks] +== Benchmarks + +[#benchmarks-throughput] +=== Throughput +:unit: req/s +:caption: higher is better + +[cols="2,1",options="header"] +|=== +| Label | Value +| Boost.Example | 1,200 +| Other | 800 +|=== + +[#freeform] +== Notes from the maintainer + +Some free-form content with a link. +""" + + +def test_parse_returns_none_for_empty(): + assert parse_website_adoc(None) is None + assert parse_website_adoc("") is None + assert parse_website_adoc(b" ") is None + + +def test_parse_filled_document(): + parsed = parse_website_adoc(FILLED) + + assert parsed["library_key"] == "example" + + assert "fast and safe" in parsed["about"]["blurb"] + assert "#include " in parsed["about"]["code"]["code"] + assert parsed["about"]["code"]["language"] == "cpp" + + assert "boost::example::run()" in parsed["playground"]["code"] + + assert parsed["designed_for"] == [ + { + "heading": "High throughput", + "description": "Handles millions of ops per second.", + }, + {"heading": "Zero allocations", "description": "No heap use on the hot path."}, + ] + + assert parsed["links"] == { + "common_use_case_url": "https://www.boost.org/doc/libs/1_90_0/libs/example/use.html", + "code_example_url": "https://www.boost.org/doc/libs/1_90_0/libs/example/ex.html", + } + + assert ( + parsed["install"]["blurb"] + == "Header-only — just add Boost to your include path." + ) + assert parsed["install"]["code"]["language"] == "bash" + assert "./b2 --with-example install" in parsed["install"]["code"]["code"] + + [chart] = parsed["benchmarks"] + assert chart["id"] == "benchmarks-throughput" + assert chart["title"] == "Throughput" + assert chart["unit"] == "req/s" + assert chart["caption"] == "higher is better" + assert chart["data"] == [ + {"label": "Boost.Example", "value": 1200.0}, + {"label": "Other", "value": 800.0}, + ] + + assert parsed["freeform"]["heading"] == "Notes from the maintainer" + assert "free-form content" in parsed["freeform"]["content"] + + +def test_unfilled_template_drops_placeholder_sections(): + """The raw template (angle-bracket placeholders) yields no placeholder data.""" + template = ( + Path(__file__).resolve().parent.parent / "website_adoc_template.adoc" + ).read_text() + parsed = parse_website_adoc(template) + + # Placeholder values are never surfaced. + assert "library_key" not in parsed + assert "designed_for" not in parsed + assert "links" not in parsed + assert "benchmarks" not in parsed + assert "freeform" not in parsed + assert parsed.get("about", {}).get("blurb") is None + assert parsed.get("install", {}).get("blurb") is None + + +def test_omitted_sections_absent(): + parsed = parse_website_adoc( + "= Title\n:library-key: foo\n\n[#install]\n== Install\n\n" + "[source,bash]\n----\nmake\n----\n" + ) + assert set(parsed) == {"library_key", "install"} + assert parsed["install"]["code"]["code"] == "make" + + +def test_broken_section_dropped_others_kept(monkeypatch): + """A section parser that raises drops only that section, not the document.""" + import libraries.website_adoc as mod + + def boom(_lines): + raise ValueError("broken benchmark table") + + # Rebuild the builder table with benchmarks forced to raise. + monkeypatch.setattr( + mod, + "_SECTION_BUILDERS", + tuple( + (out, sid, boom if sid == "benchmarks" else fn) + for out, sid, fn in mod._SECTION_BUILDERS + ), + ) + + parsed = parse_website_adoc(FILLED) + assert "benchmarks" not in parsed # the broken section is gone + assert parsed["about"]["blurb"] # the rest survived + assert parsed["install"]["code"] + assert parsed["library_key"] == "example" + + +def test_freeform_render_failure_keeps_other_sections(monkeypatch): + """If the asciidoctor gem errors on freeform, only freeform is dropped.""" + import libraries.website_adoc as mod + + def boom(_content): + raise RuntimeError("asciidoctor failed") + + monkeypatch.setattr(mod, "convert_adoc_to_html", boom) + + parsed = build_website_adoc(FILLED) + assert "freeform" not in parsed + assert "about" in parsed and "install" in parsed + + +@pytest.mark.asciidoctor +def test_build_renders_freeform_html(): + parsed = build_website_adoc(FILLED) + assert "<" in parsed["freeform"]["html"] + assert "free-form content" in parsed["freeform"]["html"] diff --git a/libraries/website_adoc.py b/libraries/website_adoc.py new file mode 100644 index 000000000..d74bc964e --- /dev/null +++ b/libraries/website_adoc.py @@ -0,0 +1,360 @@ +"""Parse a library's optional ``meta/website.adoc`` into structured data. + +The file is a maintainer-authored AsciiDoc file whose contract is defined in +``libraries/website_adoc_template.adoc`` — the website keys off stable section +IDs (``[#about]`` …) and attribute names (``:library-key:`` …), not the prose. +Every section is optional; an absent or empty file parses to ``None``. + +``parse_website_adoc`` is pure (no external process). ``build_website_adoc`` +additionally renders the free-form section to HTML via the asciidoctor gem and +is what the ingestion pipeline stores on ``LibraryVersion.website_adoc``. +""" + +import re + +import structlog + +from core.asciidoc import convert_adoc_to_html + +logger = structlog.get_logger() + +# Top-level section IDs that anchor a block. Nested benchmark anchors +# (``[#benchmarks-*]``) are parsed within the benchmarks section, not here. +SECTION_IDS = frozenset( + [ + "about", + "playground", + "designed-for", + "links", + "install", + "benchmarks", + "freeform", + ] +) + +_ANCHOR_RE = re.compile(r"^\[#([a-z0-9-]+)\]\s*$") +_ATTR_RE = re.compile(r"^:([a-z0-9-]+):\s*(.*)$") +_SOURCE_RE = re.compile(r"^\[source(?:%[^\],]*)?(?:,\s*([a-z0-9+#-]+))?.*\]\s*$") +_HEADING_RE = re.compile(r"^=+\s+(.*\S)\s*$") + + +def _is_placeholder(value): + """A copied-but-unfilled template value, e.g. ```` or empty.""" + v = (value or "").strip() + return not v or (v.startswith("<") and v.endswith(">")) + + +def _clean(value): + return None if _is_placeholder(value) else value.strip() + + +def _first_source_block(lines): + """Return ``{"language", "code"}`` for the first ``[source]`` block, or None.""" + language = None + code = [] + in_code = False + for line in lines: + stripped = line.strip() + if not in_code: + match = _SOURCE_RE.match(stripped) + if match: + language = match.group(1) + continue + if stripped == "----": + in_code = True + continue + if stripped == "----": + code_text = "\n".join(code).strip("\n") + return {"language": language, "code": code_text} if code_text else None + code.append(line) + return None + + +def _blurb(lines): + """Leading prose of a section (before its first source/listing block).""" + text = [] + for line in lines: + stripped = line.strip() + if stripped.startswith("[source") or stripped == "----": + break + if _HEADING_RE.match(stripped) or stripped.startswith("//") or not stripped: + continue + text.append(stripped) + joined = " ".join(text).strip() + return joined or None + + +def _subsections(lines): + """Parse ``=== heading`` + description pairs (used by ``[#designed-for]``).""" + items = [] + current = None + for line in lines: + stripped = line.strip() + if stripped.startswith("//"): + continue + heading = re.match(r"^===\s+(.*\S)\s*$", stripped) + if heading: + if current and current["description"]: + items.append(current) + current = {"heading": heading.group(1).strip(), "description": ""} + elif current is not None and stripped: + current["description"] = (current["description"] + " " + stripped).strip() + if current and current["description"]: + items.append(current) + return [ + item + for item in items + if not _is_placeholder(item["heading"]) + and not _is_placeholder(item["description"]) + ] + + +def _table_rows(lines): + """Parse a two-column ``Label | Value`` table body, skipping the header.""" + rows = [] + in_table = False + for line in lines: + stripped = line.strip() + if stripped.startswith("|==="): + if in_table: + break + in_table = True + continue + if not in_table or not stripped.startswith("|"): + continue + cells = [c.strip() for c in stripped.split("|")[1:]] + if len(cells) < 2: + continue + rows.append(cells[:2]) + data = [] + for label, value in rows[1:]: # drop the header row + if _is_placeholder(label): + continue + try: + data.append({"label": label, "value": float(value.replace(",", ""))}) + except ValueError: + continue + return data + + +def _benchmarks(lines): + """Split the benchmarks body on ``[#benchmarks-*]`` anchors into charts.""" + charts = [] + current = None + for line in lines: + stripped = line.strip() + anchor = _ANCHOR_RE.match(stripped) + if anchor and anchor.group(1).startswith("benchmarks-"): + if current: + charts.append(current) + current = {"id": anchor.group(1), "lines": []} + continue + if current is not None: + current["lines"].append(line) + if current: + charts.append(current) + + result = [] + for chart in charts: + body = chart["lines"] + title = next( + (m.group(1) for m in map(_HEADING_RE.match, map(str.strip, body)) if m), + None, + ) + attrs = {} + for line in body: + attr = _ATTR_RE.match(line.strip()) + if attr: + attrs[attr.group(1)] = attr.group(2) + data = _table_rows(body) + if _is_placeholder(title) or not data: + continue + result.append( + { + "id": chart["id"], + "title": title, + "unit": _clean(attrs.get("unit")), + "caption": _clean(attrs.get("caption")), + "source": _clean(attrs.get("source")), + "data": data, + } + ) + return result + + +def _freeform(lines): + """Return ``{"heading", "content"}`` — the maintainer heading + raw AsciiDoc.""" + heading = None + body = [] + for line in lines: + stripped = line.strip() + if heading is None: + match = _HEADING_RE.match(stripped) + if match: + heading = match.group(1).strip() + continue + if stripped.startswith("//"): + continue + body.append(line) + content = "\n".join(body).strip("\n") + if _is_placeholder(heading) or not content.strip(): + return None + return {"heading": heading, "content": content} + + +def _build_blurb_and_code(lines): + """Return ``{"blurb"?, "code"?}`` — an optional blurb plus one source block. + + Used by both ``[#about]`` and ``[#install]``. + """ + blurb = _blurb(lines) + code = _first_source_block(lines) + section = {} + if blurb and not _is_placeholder(blurb): + section["blurb"] = blurb + if code: + section["code"] = code + return section or None + + +def _build_links(lines): + """Return ``{"common_use_case_url"?, "code_example_url"?}`` or None.""" + attrs = {} + for line in lines: + attr = _ATTR_RE.match(line.strip()) + if attr: + attrs[attr.group(1)] = attr.group(2) + links = {} + if url := _clean(attrs.get("common-use-case-url")): + links["common_use_case_url"] = url + if url := _clean(attrs.get("code-example-url")): + links["code_example_url"] = url + return links or None + + +# Output key -> (section ID, builder). Each builder takes the section's body +# lines and returns a truthy value or a falsy/None value when it has nothing +# usable. Builders are run in isolation so one broken section is dropped +# without affecting the others. +_SECTION_BUILDERS = ( + ("about", "about", _build_blurb_and_code), + ("playground", "playground", _first_source_block), + ("designed_for", "designed-for", _subsections), + ("links", "links", _build_links), + ("install", "install", _build_blurb_and_code), + ("benchmarks", "benchmarks", _benchmarks), + ("freeform", "freeform", _freeform), +) + + +def _segment(content): + """Split the document into ``{section_id: [body lines]}`` + document attrs. + + Anchors and comments inside ``----`` source blocks are treated as verbatim. + """ + sections = {} + doc_attrs = {} + current_id = None + body = [] + in_source = False + in_comment = False + for line in content.splitlines(): + stripped = line.strip() + if not in_source and stripped == "////": + in_comment = not in_comment + continue + if in_comment: + continue + if stripped == "----": + in_source = not in_source + if current_id is not None: + body.append(line) + continue + if not in_source: + anchor = _ANCHOR_RE.match(stripped) + if anchor and anchor.group(1) in SECTION_IDS: + if current_id is not None: + sections[current_id] = body + current_id, body = anchor.group(1), [] + continue + if stripped.startswith("//"): + continue + if current_id is None: + attr = _ATTR_RE.match(stripped) + if attr: + doc_attrs[attr.group(1)] = attr.group(2) + continue + if current_id is not None: + body.append(line) + if current_id is not None: + sections[current_id] = body + return sections, doc_attrs + + +def parse_website_adoc(content): + """Parse ``meta/website.adoc`` bytes/str into a dict, or ``None`` if empty. + + Returned keys are only present when the corresponding section has usable + content. The ``freeform`` section carries raw AsciiDoc under ``content``; + call ``build_website_adoc`` to also render it to HTML. + """ + if content is None: + return None + if isinstance(content, bytes): + content = content.decode("utf-8", errors="replace") + if not content.strip(): + return None + + sections, doc_attrs = _segment(content) + parsed = {} + + if library_key := _clean(doc_attrs.get("library-key")): + parsed["library_key"] = library_key + + for out_key, section_id, builder in _SECTION_BUILDERS: + if section_id not in sections: + continue + try: + value = builder(sections[section_id]) + except Exception: + # A broken section is dropped; the rest of the document is unaffected. + logger.exception("website_adoc_section_failed", section=section_id) + continue + if value: + parsed[out_key] = value + + return parsed or None + + +def build_website_adoc(content): + """Parse and render for storage on ``LibraryVersion.website_adoc``. + + Same as ``parse_website_adoc`` but also renders the free-form section's + AsciiDoc to HTML (``freeform["html"]``) via the asciidoctor gem. + """ + parsed = parse_website_adoc(content) + if parsed and "freeform" in parsed: + try: + parsed["freeform"]["html"] = convert_adoc_to_html( + parsed["freeform"]["content"] + ) + except Exception: + # If the gem can't render the free-form AsciiDoc, drop only that + # section — every other section is still returned. + logger.exception("website_adoc_freeform_render_failed") + parsed.pop("freeform", None) + return parsed or None + + +def fetch_website_adoc(client, repo_slug, tag): + """Fetch and parse ``meta/website.adoc`` for a repo/tag. + + Never raises — returns ``None`` on a missing file or any parse/render + error, so it is safe to call inline in the ingestion pipeline. + """ + try: + content = client.get_website_adoc(repo_slug=repo_slug, tag=tag) + return build_website_adoc(content) + except Exception: + logger.exception("website_adoc_parse_failed", repo=repo_slug, tag=tag) + return None diff --git a/libraries/website_adoc_template.adoc b/libraries/website_adoc_template.adoc new file mode 100644 index 000000000..87f4fb618 --- /dev/null +++ b/libraries/website_adoc_template.adoc @@ -0,0 +1,130 @@ +//// +Boost library website content — TEMPLATE + +Copy this file to meta/website.adoc in your library repo and fill it in. It powers +the extra cards on your library's page on boost.org. + +Everything here is OPTIONAL: delete any section you don't use. An omitted section +just renders nothing on the page — it is never an error. + +THE CONTRACT — the website keys off the section IDs (e.g. [#about]) and attribute +names (e.g. :library-key:) below, NOT the prose. You may freely change the +headings and body copy; do NOT rename the IDs or attributes. + + Document attributes + :library-key: your key in meta/libraries.json (sanity check) + + Sections (shown on the page in this order) + [#about] a blurb + one code sample + [#playground] one runnable code sample (feeds the "Edit in Compiler Explorer" button) + [#designed-for] a few headings, each with a short description + [#links] two links into your library's documentation + [#install] build/link steps + [#benchmarks] one bar chart per subsection + [#freeform] your own heading + free-form content, shown in a card +//// += Boost. — Website Content +:library-key: + + +[#about] +== About + +// One or two sentences for the "About" card, followed by exactly one code sample. + + + +[source,cpp] +---- +// A short, representative snippet. +---- + + +[#playground] +== Playground + +// Exactly one code sample. Its code is URL-encoded into the +// "Edit in Compiler Explorer" (https://godbolt.org/) button — keep it +// self-contained so it compiles and runs on its own. + +[source,cpp] +---- +// A self-contained, runnable example. +---- + + +[#designed-for] +== Designed for + +// A set of subsections — each is a heading + a short description (no intro paragraph). +// Add or remove subsections as needed. + +=== + + +=== + + + +[#links] +== Links + +// Two links into your library's own Boost documentation. Use full URLs, +// pointing at the relevant page under your library's docs, e.g. +// https://www.boost.org/doc/libs/latest/libs/beast/doc/html/beast/examples.html +// :common-use-case-url: a page showing your library's most common use case +// :code-example-url: a page with a representative code example + +:common-use-case-url: +:code-example-url: + + +[#install] +== Install + +// An optional one-sentence blurb, followed by the build/link steps. All libraries +// must provide the code needed to build and link against them; the blurb is +// optional — delete the line below if you don't want one. + + + +[source,bash] +---- +# build it with b2: +./b2 --with- install +# then link against it: +g++ app.cpp -lboost_ +---- + + +[#benchmarks] +== Benchmarks + +// Optional. One subsection per bar chart. +// :unit: value-axis label (e.g. req/s, µs) +// :caption: optional sub-label +// :source: optional citation URL +// Table columns: Label | Value (plain number) + +[#benchmarks-] +=== +:unit: +:caption: + +[cols="2,1",options="header"] +|=== +| Label | Value +| Boost. | +| | +|=== + + +[#freeform] +== + +// A free-form card: write your own heading above and any content below — +// paragraphs, lists, links, inline code, etc. Whatever you put here is +// rendered as-is into a single card on your library's page. +// Keep the [#freeform] ID; change the heading text to whatever you like. + + diff --git a/templates/admin/libraryversion_change_list.html b/templates/admin/libraryversion_change_list.html index e5379ec38..d4bc61be7 100644 --- a/templates/admin/libraryversion_change_list.html +++ b/templates/admin/libraryversion_change_list.html @@ -6,6 +6,7 @@ {% block object-tools-items %} {{ block.super }}
  • {% trans "Refresh Documentation Links" %}
  • +
  • {% trans "Refresh website.adoc" %}
  • {% endblock %} {% endblock %} diff --git a/versions/tasks.py b/versions/tasks.py index c1d89dd0e..e645b3307 100644 --- a/versions/tasks.py +++ b/versions/tasks.py @@ -28,6 +28,7 @@ from libraries.constants import SKIP_LIBRARY_VERSIONS from libraries.github import LibraryUpdater from libraries.models import Library, LibraryVersion +from libraries.website_adoc import fetch_website_adoc from libraries.tasks import get_and_store_library_version_documentation_urls_for_version from libraries.utils import version_within_range from versions.exceptions import BoostImportedDataException @@ -473,6 +474,9 @@ def import_library_versions(version_name, token=None, version_type="tag"): "cpp_standard_maximum": lib_data.get("cxxstd_max"), "cpp20_module_support": lib_data.get("cpp20_module_support"), "description": lib_data.get("description"), + "website_adoc": fetch_website_adoc( + client, library_name, version_name + ), }, ) if not library.github_url: From 58210c6c42fb55e92482b9542244d1703d0259a6 Mon Sep 17 00:00:00 2001 From: Julia Hoang Date: Mon, 6 Jul 2026 16:27:37 -0700 Subject: [PATCH 05/30] feat: render website.adoc About/Install/Quick Start/Freeform with empty states --- libraries/tests/test_views.py | 42 +++++++++++++++ libraries/views.py | 57 +++++++++++++++------ static/css/v3/library-subpage.css | 1 + templates/v3/libraries/library-subpage.html | 14 ++++- 4 files changed, 95 insertions(+), 19 deletions(-) diff --git a/libraries/tests/test_views.py b/libraries/tests/test_views.py index fa1f3dadd..9db2fb396 100644 --- a/libraries/tests/test_views.py +++ b/libraries/tests/test_views.py @@ -7,9 +7,51 @@ from ..constants import README_MISSING from ..models import Library +from ..views import _build_quick_start_links, _is_boost_url from versions.models import Version +@pytest.mark.parametrize( + "url,expected", + [ + ("https://www.boost.org/doc/libs/1_90_0/libs/x/use.html", True), + ("https://boost.org/anything", True), + ("https://github.com/boostorg/beast/blob/x.cpp", True), + ("https://github.com/someone/beast", False), + ("https://evil.example.com/boost.org", False), + ("ftp://boost.org/x", False), + ("", False), + (None, False), + ], +) +def test_is_boost_url(url, expected): + assert _is_boost_url(url) is expected + + +def test_build_quick_start_links_validates_and_falls_back(): + docs = "/doc/libs/1_90_0/libs/x/index.html" + links = { + "common_use_case_url": "https://www.boost.org/doc/libs/x/use.html", + "code_example_url": "https://evil.example.com/x", # rejected -> docs + } + result = _build_quick_start_links(docs, links) + assert result == [ + { + "label": "Common use cases", + "url": "https://www.boost.org/doc/libs/x/use.html", + }, + {"label": "Code examples", "url": docs}, + ] + + +def test_build_quick_start_links_no_adoc_links_uses_docs(): + docs = "/doc/libs/1_90_0/libs/x/index.html" + assert _build_quick_start_links(docs, None) == [ + {"label": "Common use cases", "url": docs}, + {"label": "Code examples", "url": docs}, + ] + + def test_library_list(library_version, tp, url_name="libraries", request_kwargs=None): """GET /libraries/""" # Create a version with a library diff --git a/libraries/views.py b/libraries/views.py index 5292cf049..0abd2592e 100644 --- a/libraries/views.py +++ b/libraries/views.py @@ -1,4 +1,6 @@ import datetime +from urllib.parse import urlparse + import structlog from django.contrib import messages @@ -15,7 +17,6 @@ from core.constants import SLACK_JOIN_URL from core.githubhelper import GithubAPIClient -from core.install_commands import INSTALL_PKG_MANAGERS, INSTALL_SYSTEM_INSTALL from core.mixins import V3Mixin from mailing_list.mixins import MailingListCardMixin from core.mock_data import SharedResources @@ -56,16 +57,41 @@ # ── V3 context helpers ───────────────────────────────────────────────────── -def _build_quick_start_links(documentation_url, github_url, github_issues_url): - """Build the quick-start links list for the V3 library hero card.""" - links = [] - if documentation_url: - links.append({"label": "Documentation", "url": documentation_url}) - if github_url: - links.append({"label": "Source Code", "url": github_url}) - if github_issues_url: - links.append({"label": "GitHub Issues", "url": github_issues_url}) - return links +def _is_boost_url(url): + """True when `url` points at a Boost-owned location (boost.org or + github.com/boostorg). Guards the maintainer-supplied Quick Start links so a + stray/off-site URL falls back to the documentation link instead.""" + if not url: + return False + parsed = urlparse(url) + if parsed.scheme not in ("http", "https"): + return False + host = (parsed.hostname or "").lower() + if host == "boost.org" or host.endswith(".boost.org"): + return True + if host in ("github.com", "www.github.com"): + return parsed.path.lower().lstrip("/").startswith("boostorg/") + return False + + +def _build_quick_start_links(documentation_url, links): + """Build the Quick Start card links from website.adoc's [#links] section. + + "Common use cases" and "Code examples" come from the maintainer's + :common-use-case-url: / :code-example-url:. Each is used only if it is a + valid Boost URL; otherwise it falls back to the documentation link. + """ + links = links or {} + result = [] + common = links.get("common_use_case_url") + example = links.get("code_example_url") + common_url = common if _is_boost_url(common) else documentation_url + example_url = example if _is_boost_url(example) else documentation_url + if common_url: + result.append({"label": "Common use cases", "url": common_url}) + if example_url: + result.append({"label": "Code examples", "url": example_url}) + return result def _build_dependencies_list(current_dependencies, version_str): @@ -518,10 +544,8 @@ def get_v3_context_data(self, **kwargs): version_str = base_context.get("version_str") or LATEST_RELEASE_URL_PATH_STR - context["install_card_pkg_managers"] = INSTALL_PKG_MANAGERS - context["install_card_system_install"] = INSTALL_SYSTEM_INSTALL - context["library_about_code"] = SharedResources.library_about_code - context["library_install_code"] = SharedResources.library_install_code + library_version = base_context.get("library_version") + context["website_adoc"] = getattr(library_version, "website_adoc", None) or {} context["slack_url"] = self.object.slack_url or SLACK_JOIN_URL context["category_tags_v3"] = [ @@ -545,8 +569,7 @@ def get_v3_context_data(self, **kwargs): context["quick_start_links"] = _build_quick_start_links( base_context.get("documentation_url"), - base_context.get("github_url") or self.object.github_url, - getattr(self.object, "github_issues_url", None), + context["website_adoc"].get("links"), ) dep_diff = base_context.get("dependency_diff", {}) diff --git a/static/css/v3/library-subpage.css b/static/css/v3/library-subpage.css index 791ef969a..d6caa5d05 100644 --- a/static/css/v3/library-subpage.css +++ b/static/css/v3/library-subpage.css @@ -101,6 +101,7 @@ body:has(.is_flagship) .library-subpage-container { .card-masonry-bottom > div > .card-group, .card-masonry-bottom > div > .post-card, .library-subpage__documentation > .markdown-card, +.library-subpage__freeform > .markdown-card, .library-subpage__all-contributors > .contributors-list { max-width: 100%; width: 100%; diff --git a/templates/v3/libraries/library-subpage.html b/templates/v3/libraries/library-subpage.html index 42dd2ccfd..569e3324a 100644 --- a/templates/v3/libraries/library-subpage.html +++ b/templates/v3/libraries/library-subpage.html @@ -22,18 +22,22 @@
    + {% if website_adoc.about %}
    - {% include "v3/includes/_code_block_card.html" with heading="About "|add:object.display_name description=library_version.description|default:object.description code=library_about_code language="cpp" card_variant="neutral" button_text="Explore examples" button_url=documentation_url|default:"#" button_aria_label="Explore examples for "|add:object.display_name %} + {% include "v3/includes/_code_block_card.html" with heading="About "|add:object.display_name description=website_adoc.about.blurb code=website_adoc.about.code.code language=website_adoc.about.code.language card_variant="neutral" button_text="Explore examples" button_url=documentation_url|default:"#" button_aria_label="Explore examples for "|add:object.display_name %}
    + {% endif %}
    {% include "v3/includes/_quick_start_card.html" with heading="Quick Start" links=quick_start_links %}
    + {% if website_adoc.install %}
    - {% include "v3/includes/_code_block_card.html" with heading="Install" description="Get started with header-only libraries" code=library_install_code language="bash" card_variant="teal" %} + {% include "v3/includes/_code_block_card.html" with heading="Install" description=website_adoc.install.blurb code=website_adoc.install.code.code language=website_adoc.install.code.language card_variant="teal" %}
    + {% endif %}
    {% include "v3/includes/_dependencies_card.html" with dependencies=dependencies_list %}
    @@ -51,6 +55,12 @@
    {% endif %} + {% if website_adoc.freeform %} +
    + {% include "v3/includes/_markdown_card.html" with title=website_adoc.freeform.heading html=website_adoc.freeform.html %} +
    + {% endif %} +
    {% include "v3/includes/_contributors_list.html" with title="Contributors: This Release" variant="all" contributors=this_release_contributors heading_level="2" %} From ea05c292f5fc7765f63fe873d73eb8088567351e Mon Sep 17 00:00:00 2001 From: Julia Hoang Date: Mon, 6 Jul 2026 16:44:33 -0700 Subject: [PATCH 06/30] feat: render Designed For as a markdown card with V3 demo --- core/views.py | 19 +++++++++++++++++ libraries/tests/test_views.py | 21 +++++++++++++++++++ libraries/utils.py | 18 ++++++++++++++++ libraries/views.py | 4 ++++ .../v3/examples/_v3_example_section.html | 11 ++++++++++ templates/v3/libraries/library-subpage.html | 9 +++++++- 6 files changed, 81 insertions(+), 1 deletion(-) diff --git a/core/views.py b/core/views.py index 0729fd92b..763d76922 100644 --- a/core/views.py +++ b/core/views.py @@ -44,6 +44,7 @@ get_prioritized_version, set_selected_boost_version, modernize_boost_slug, + designed_for_html, ) from mailing_list import constants from versions.models import Version, docs_path_to_boost_name @@ -1941,6 +1942,24 @@ def get_context_data(self, **kwargs): "action_url": "#", }, ] + context["designed_for_demo_html"] = designed_for_html( + [ + { + "heading": "High-throughput parsing", + "description": "Handles millions of messages per second with " + "near-zero allocations on the hot path.", + }, + { + "heading": "Header-only", + "description": "Add Boost to your include path — no separate " + "build step required.", + }, + { + "heading": "Standards-tracking", + "description": "APIs mirror the C++ standard where applicable.", + }, + ] + ) context["markdown_data"] = { "title": "Markdown Block", "markdown": dedent(""" diff --git a/libraries/tests/test_views.py b/libraries/tests/test_views.py index 9db2fb396..f23013b88 100644 --- a/libraries/tests/test_views.py +++ b/libraries/tests/test_views.py @@ -7,6 +7,7 @@ from ..constants import README_MISSING from ..models import Library +from ..utils import designed_for_html from ..views import _build_quick_start_links, _is_boost_url from versions.models import Version @@ -44,6 +45,26 @@ def test_build_quick_start_links_validates_and_falls_back(): ] +def test_designed_for_html_empty(): + assert designed_for_html(None) == "" + assert designed_for_html([]) == "" + + +def test_designed_for_html_renders_and_escapes(): + html = designed_for_html( + [ + {"heading": "Fast ", "description": "Handles 1M ops."}, + {"heading": "Header-only", "description": ""}, + ] + ) + # headings + descriptions become h3/p, dynamic text is escaped + assert "

    Fast <parsing>

    " in html + assert "

    Handles 1M ops.

    " in html + assert "

    Header-only

    " in html + # no

    emitted for the empty description + assert html.count("

    ") == 1 + + def test_build_quick_start_links_no_adoc_links_uses_docs(): docs = "/doc/libs/1_90_0/libs/x/index.html" assert _build_quick_start_links(docs, None) == [ diff --git a/libraries/utils.py b/libraries/utils.py index 0b45fa3fb..3b6015224 100644 --- a/libraries/utils.py +++ b/libraries/utils.py @@ -18,6 +18,8 @@ from django.db.models import Count, F, QuerySet from django.db.models.functions import Lower from django.urls import reverse +from django.utils.html import format_html +from django.utils.safestring import mark_safe from django.utils.text import slugify from libraries.constants import ( @@ -489,6 +491,22 @@ def apply_collective_author_overrides(author_dicts): return author_dicts +def designed_for_html(items): + """Render website.adoc [#designed-for] items as an HTML fragment. + + Each item becomes an

    heading + optional

    description, for display + in the shared markdown card. Dynamic text is escaped via format_html. + """ + if not items: + return "" + parts = [] + for item in items: + parts.append(format_html("

    {}

    ", item.get("heading") or "")) + if item.get("description"): + parts.append(format_html("

    {}

    ", item["description"])) + return mark_safe("".join(parts)) + + def build_library_intro_context( library_version, *, max_authors=None, include_contributors=False ): diff --git a/libraries/views.py b/libraries/views.py index 0abd2592e..6c05449c5 100644 --- a/libraries/views.py +++ b/libraries/views.py @@ -48,6 +48,7 @@ get_commit_data_by_release_for_library, commit_data_to_stats_bars, group_libraries_by_tier, + designed_for_html, ) from .constants import LATEST_RELEASE_URL_PATH_STR @@ -546,6 +547,9 @@ def get_v3_context_data(self, **kwargs): library_version = base_context.get("library_version") context["website_adoc"] = getattr(library_version, "website_adoc", None) or {} + context["designed_for_html"] = designed_for_html( + context["website_adoc"].get("designed_for") + ) context["slack_url"] = self.object.slack_url or SLACK_JOIN_URL context["category_tags_v3"] = [ diff --git a/templates/v3/examples/_v3_example_section.html b/templates/v3/examples/_v3_example_section.html index 06d00bb9a..a78c32cff 100644 --- a/templates/v3/examples/_v3_example_section.html +++ b/templates/v3/examples/_v3_example_section.html @@ -630,6 +630,17 @@

    {{ section_title }}

    {% endwith %} + {% comment %} + "This library is designed for" — the library subpage renders website.adoc's + [#designed-for] items (heading + description) into the shared markdown card. + {% endcomment %} + {% with section_title="Designed For Card" %} +
    +

    {{ section_title }}

    +
    {% include "v3/includes/_markdown_card.html" with title="This library is designed for" html=designed_for_demo_html %}
    +
    + {% endwith %} + {% with section_title="Why Boost" %}

    {{ section_title }}

    diff --git a/templates/v3/libraries/library-subpage.html b/templates/v3/libraries/library-subpage.html index 569e3324a..f628b2c3a 100644 --- a/templates/v3/libraries/library-subpage.html +++ b/templates/v3/libraries/library-subpage.html @@ -22,11 +22,18 @@
    - {% if website_adoc.about %} + {% if website_adoc.about or website_adoc.designed_for %}
    + {% if website_adoc.about %}
    {% include "v3/includes/_code_block_card.html" with heading="About "|add:object.display_name description=website_adoc.about.blurb code=website_adoc.about.code.code language=website_adoc.about.code.language card_variant="neutral" button_text="Explore examples" button_url=documentation_url|default:"#" button_aria_label="Explore examples for "|add:object.display_name %}
    + {% endif %} + {% if website_adoc.designed_for %} +
    + {% include "v3/includes/_markdown_card.html" with title="This library is designed for" html=designed_for_html %} +
    + {% endif %}
    {% endif %}
    From 3377d806b63b13d95e22403bd28c3f70300e617e Mon Sep 17 00:00:00 2001 From: Julia Hoang Date: Tue, 7 Jul 2026 16:50:31 -0700 Subject: [PATCH 07/30] feat: wire up benchmark card --- core/views.py | 22 +++++++++++++ libraries/tests/test_views.py | 32 ++++++++++++++++++- libraries/utils.py | 25 +++++++++++++++ libraries/views.py | 4 +++ static/css/v3/stats.css | 7 ++-- .../v3/examples/_v3_example_section.html | 11 +++++++ templates/v3/includes/_stats_benchmarks.html | 1 - templates/v3/libraries/library-subpage.html | 5 +++ 8 files changed, 103 insertions(+), 4 deletions(-) diff --git a/core/views.py b/core/views.py index 763d76922..64af58518 100644 --- a/core/views.py +++ b/core/views.py @@ -45,6 +45,7 @@ set_selected_boost_version, modernize_boost_slug, designed_for_html, + benchmark_sets, ) from mailing_list import constants from versions.models import Version, docs_path_to_boost_name @@ -1960,6 +1961,27 @@ def get_context_data(self, **kwargs): }, ] ) + context["benchmark_demo_sets"] = benchmark_sets( + [ + { + "title": "Throughput", + "unit": "req/s", + "data": [ + {"label": "Boost.Example", "value": 1200}, + {"label": "Alternative A", "value": 800}, + {"label": "Alternative B", "value": 450}, + ], + }, + { + "title": "Latency", + "unit": "µs", + "data": [ + {"label": "Boost.Example", "value": 12}, + {"label": "Alternative A", "value": 30}, + ], + }, + ] + ) context["markdown_data"] = { "title": "Markdown Block", "markdown": dedent(""" diff --git a/libraries/tests/test_views.py b/libraries/tests/test_views.py index f23013b88..95668ed48 100644 --- a/libraries/tests/test_views.py +++ b/libraries/tests/test_views.py @@ -7,7 +7,7 @@ from ..constants import README_MISSING from ..models import Library -from ..utils import designed_for_html +from ..utils import benchmark_sets, designed_for_html from ..views import _build_quick_start_links, _is_boost_url from versions.models import Version @@ -65,6 +65,36 @@ def test_designed_for_html_renders_and_escapes(): assert html.count("

    ") == 1 +def test_benchmark_sets_normalizes_widths(): + assert benchmark_sets(None) == [] + sets = benchmark_sets( + [ + { + "title": "Throughput", + "unit": "req/s", + "data": [ + {"label": "Boost", "value": 1200}, + {"label": "Other", "value": 600}, + ], + } + ] + ) + assert sets == [ + { + "title": "Throughput (req/s)", # unit folded into title + "rows": [ + {"label": "Boost", "value": 1200, "width_pct": 100.0}, + {"label": "Other", "value": 600, "width_pct": 50.0}, + ], + } + ] + + +def test_benchmark_sets_all_zero_values(): + sets = benchmark_sets([{"title": "T", "data": [{"label": "a", "value": 0}]}]) + assert sets[0]["rows"][0]["width_pct"] == 0 + + def test_build_quick_start_links_no_adoc_links_uses_docs(): docs = "/doc/libs/1_90_0/libs/x/index.html" assert _build_quick_start_links(docs, None) == [ diff --git a/libraries/utils.py b/libraries/utils.py index 3b6015224..c2e07f9db 100644 --- a/libraries/utils.py +++ b/libraries/utils.py @@ -507,6 +507,31 @@ def designed_for_html(items): return mark_safe("".join(parts)) +def benchmark_sets(benchmarks): + """Map website.adoc [#benchmarks] charts to _stats_benchmarks `sets`. + + Each chart becomes a set; bar widths (`width_pct`, 0-100) are normalized to + that chart's largest value. The chart's unit is folded into the set title + since the component has no separate unit/caption slot. + """ + sets = [] + for chart in benchmarks or []: + rows_data = chart.get("data") or [] + max_value = max((row.get("value") or 0 for row in rows_data), default=0) + rows = [] + for row in rows_data: + value = row.get("value") or 0 + width_pct = round(value / max_value * 100, 2) if max_value else 0 + rows.append( + {"label": row.get("label"), "value": value, "width_pct": width_pct} + ) + title = chart.get("title") or "" + if chart.get("unit"): + title = f"{title} ({chart['unit']})" + sets.append({"title": title, "rows": rows}) + return sets + + def build_library_intro_context( library_version, *, max_authors=None, include_contributors=False ): diff --git a/libraries/views.py b/libraries/views.py index 6c05449c5..fb7d20761 100644 --- a/libraries/views.py +++ b/libraries/views.py @@ -49,6 +49,7 @@ commit_data_to_stats_bars, group_libraries_by_tier, designed_for_html, + benchmark_sets, ) from .constants import LATEST_RELEASE_URL_PATH_STR @@ -550,6 +551,9 @@ def get_v3_context_data(self, **kwargs): context["designed_for_html"] = designed_for_html( context["website_adoc"].get("designed_for") ) + context["benchmark_sets"] = benchmark_sets( + context["website_adoc"].get("benchmarks") + ) context["slack_url"] = self.object.slack_url or SLACK_JOIN_URL context["category_tags_v3"] = [ diff --git a/static/css/v3/stats.css b/static/css/v3/stats.css index 4f2b7c60a..2a7773acd 100644 --- a/static/css/v3/stats.css +++ b/static/css/v3/stats.css @@ -1,5 +1,5 @@ .stats { - --stats-space: 24px; + --stats-space: var(--space-large); font-family: var(--font-sans); color: var(--color-text-primary); background: var(--color-bg-secondary); @@ -159,7 +159,8 @@ .stats--benchmarks { width: 460px; - height: 420px; + max-height: 420px; + overflow-y: auto; } .stats__set { @@ -172,6 +173,7 @@ .stats__set-title { margin: 0 0 var(--space-s) 0; + padding: 0; font-size: var(--font-size-small); font-weight: var(--font-weight-medium); line-height: var(--line-height-relaxed); @@ -195,6 +197,7 @@ .stats__set-label { flex: 0 0 82px; margin: 0; + padding: 0; font-size: var(--font-size-xs); font-weight: var(--font-weight-regular); line-height: var(--line-height-default); diff --git a/templates/v3/examples/_v3_example_section.html b/templates/v3/examples/_v3_example_section.html index a78c32cff..ef06c0b81 100644 --- a/templates/v3/examples/_v3_example_section.html +++ b/templates/v3/examples/_v3_example_section.html @@ -641,6 +641,17 @@

    {{ section_title }}

    {% endwith %} + {% comment %} + Benchmarks — the library subpage maps website.adoc's [#benchmarks] charts to + the _stats_benchmarks `sets` shape (bar width_pct normalized per chart). + {% endcomment %} + {% with section_title="Benchmarks Card" %} +
    +

    {{ section_title }}

    +
    {% include "v3/includes/_stats_benchmarks.html" with heading="Benchmarks" sets=benchmark_demo_sets theme="green" %}
    +
    + {% endwith %} + {% with section_title="Why Boost" %}

    {{ section_title }}

    diff --git a/templates/v3/includes/_stats_benchmarks.html b/templates/v3/includes/_stats_benchmarks.html index 25533c184..cdb6f2598 100644 --- a/templates/v3/includes/_stats_benchmarks.html +++ b/templates/v3/includes/_stats_benchmarks.html @@ -13,7 +13,6 @@ {% endcomment %} {% if sized_by_parent %}
    {% endif %}
    -

    {{ heading }}


    {% for set in sets %} diff --git a/templates/v3/libraries/library-subpage.html b/templates/v3/libraries/library-subpage.html index f628b2c3a..092b25dbf 100644 --- a/templates/v3/libraries/library-subpage.html +++ b/templates/v3/libraries/library-subpage.html @@ -48,6 +48,11 @@
    {% include "v3/includes/_dependencies_card.html" with dependencies=dependencies_list %}
    + {% if benchmark_sets %} +
    + {% include "v3/includes/_stats_benchmarks.html" with heading="Benchmarks" sets=benchmark_sets sized_by_parent=True %} +
    + {% endif %}
    From 7192424a717b50bf94b21c6d2a256cc3922f3678 Mon Sep 17 00:00:00 2001 From: Julia Hoang Date: Tue, 7 Jul 2026 17:05:24 -0700 Subject: [PATCH 08/30] feat: wire up compiler explorer URL --- libraries/godbolt.py | 123 ++++++++++++++++++++++++++++++++ libraries/tests/test_godbolt.py | 58 +++++++++++++++ libraries/views.py | 18 +++++ 3 files changed, 199 insertions(+) create mode 100644 libraries/godbolt.py create mode 100644 libraries/tests/test_godbolt.py diff --git a/libraries/godbolt.py b/libraries/godbolt.py new file mode 100644 index 000000000..8383bca98 --- /dev/null +++ b/libraries/godbolt.py @@ -0,0 +1,123 @@ +"""Build Compiler Explorer (godbolt.org) "Edit in Compiler Explorer" links. + +A library's website.adoc [#playground] code is encoded into a godbolt.org +``/clientstate/`` URL with Boost (matching the viewed release, or the +newest Compiler Explorer offers) pre-selected. Building the URL is a pure, +offline operation; only the Boost version→id lookup needs the CE libraries API, +which is cached. + +Verified against Compiler Explorer: + - ClientState: {sessions:[{id,language,source,compilers:[{id,options,libs}]}]} + - libs entry: {"id": "boost", "version": ""} (e.g. "190") + - version ids come from /api/libraries/c++ -> boost.versions[].id + - the base64 is URL-safe (decoded server-side via Buffer.from(x, "base64")) +""" + +import base64 +import json + +import requests +import structlog +from django.core.cache import caches + +logger = structlog.get_logger() + +GODBOLT_BASE_URL = "https://godbolt.org" +GODBOLT_LIBRARIES_URL = f"{GODBOLT_BASE_URL}/api/libraries/c++" +GODBOLT_CLIENTSTATE_URL = f"{GODBOLT_BASE_URL}/clientstate/" + +# A current, widely-available gcc on Compiler Explorer. If it is ever retired +# the playground still opens with the code pre-filled — only the preselected +# compiler is lost — so this is safe to bump when convenient. +DEFAULT_COMPILER_ID = "g142" # gcc 14.2 + +_CACHE_ALIAS = "static_content" +_BOOST_VERSIONS_CACHE_KEY = "godbolt_boost_version_map" +_BOOST_VERSIONS_TTL = 60 * 60 * 24 # one day + + +def _fetch_boost_version_map(): + """Fetch {version_string: version_id} for Boost from the CE libraries API. + + e.g. {"1.90.0": "190", ...}. Returns {} on any failure. + """ + try: + response = requests.get( + GODBOLT_LIBRARIES_URL, + headers={"Accept": "application/json"}, + timeout=5, + ) + response.raise_for_status() + libraries = response.json() + except (requests.RequestException, ValueError): + logger.exception("godbolt_libraries_fetch_failed") + return {} + + for library in libraries: + if library.get("id") == "boost": + return { + version["version"]: version["id"] + for version in library.get("versions", []) + if version.get("version") and version.get("id") + } + return {} + + +def get_boost_version_map(): + """Return the cached Boost version→id map, fetching + caching on a miss.""" + cache = caches[_CACHE_ALIAS] + version_map = cache.get(_BOOST_VERSIONS_CACHE_KEY) + if version_map is None: + version_map = _fetch_boost_version_map() + cache.set(_BOOST_VERSIONS_CACHE_KEY, version_map, _BOOST_VERSIONS_TTL) + return version_map + + +def _version_sort_key(version_string): + return [int(part) if part.isdigit() else 0 for part in version_string.split(".")] + + +def resolve_boost_version_id(boost_version, version_map=None): + """Map a Boost release (e.g. "1.90.0") to a CE version id (e.g. "190"). + + Falls back to the newest version Compiler Explorer offers when there is no + exact match (CE lags new Boost releases). Returns None when CE exposes no + Boost versions. + """ + if version_map is None: + version_map = get_boost_version_map() + if not version_map: + return None + if boost_version in version_map: + return version_map[boost_version] + newest = max(version_map, key=_version_sort_key) + return version_map[newest] + + +def build_compiler_explorer_url( + code, boost_version, version_map=None, compiler_id=DEFAULT_COMPILER_ID +): + """Build a godbolt.org /clientstate/ URL with `code` pre-filled and Boost + selected (matching `boost_version`, else the newest CE offers). + + Returns None when there is no code to load. Boost is omitted from the + session (rather than blocking the link) if CE's version list is unavailable. + """ + if not code: + return None + + version_id = resolve_boost_version_id(boost_version, version_map=version_map) + libs = [{"id": "boost", "version": version_id}] if version_id else [] + client_state = { + "sessions": [ + { + "id": 1, + "language": "c++", + "source": code, + "compilers": [{"id": compiler_id, "options": "", "libs": libs}], + } + ] + } + payload = json.dumps(client_state, separators=(",", ":")).encode("utf-8") + encoded = base64.urlsafe_b64encode(payload).decode("ascii").rstrip("=") + return f"{GODBOLT_CLIENTSTATE_URL}{encoded}" diff --git a/libraries/tests/test_godbolt.py b/libraries/tests/test_godbolt.py new file mode 100644 index 000000000..c3857a0ce --- /dev/null +++ b/libraries/tests/test_godbolt.py @@ -0,0 +1,58 @@ +import base64 +import json + +from libraries.godbolt import ( + GODBOLT_CLIENTSTATE_URL, + build_compiler_explorer_url, + resolve_boost_version_id, +) + +VERSION_MAP = {"1.88.0": "188", "1.89.0": "189", "1.90.0": "190"} + + +def _decode(url): + assert url.startswith(GODBOLT_CLIENTSTATE_URL) + encoded = url[len(GODBOLT_CLIENTSTATE_URL) :] + padded = encoded + "=" * (-len(encoded) % 4) + return json.loads(base64.urlsafe_b64decode(padded)) + + +def test_no_code_returns_none(): + assert build_compiler_explorer_url("", "1.90.0", version_map=VERSION_MAP) is None + assert build_compiler_explorer_url(None, "1.90.0", version_map=VERSION_MAP) is None + + +def test_builds_clientstate_with_matching_boost_version(): + url = build_compiler_explorer_url( + "int main() { return 0; }", "1.89.0", version_map=VERSION_MAP + ) + state = _decode(url) + session = state["sessions"][0] + assert session["language"] == "c++" + assert session["source"] == "int main() { return 0; }" + compiler = session["compilers"][0] + assert compiler["libs"] == [{"id": "boost", "version": "189"}] + + +def test_unknown_version_falls_back_to_newest(): + # CE lags: request a newer release than CE has -> newest available (190). + assert resolve_boost_version_id("1.99.0", version_map=VERSION_MAP) == "190" + url = build_compiler_explorer_url("x;", "1.99.0", version_map=VERSION_MAP) + assert _decode(url)["sessions"][0]["compilers"][0]["libs"] == [ + {"id": "boost", "version": "190"} + ] + + +def test_no_boost_versions_omits_lib_but_still_builds(): + url = build_compiler_explorer_url("x;", "1.90.0", version_map={}) + state = _decode(url) + assert state["sessions"][0]["compilers"][0]["libs"] == [] + assert state["sessions"][0]["source"] == "x;" + + +def test_urlsafe_base64_has_no_path_breaking_chars(): + url = build_compiler_explorer_url( + "auto x = 1 << 20; /* padding?? */", "1.90.0", version_map=VERSION_MAP + ) + encoded = url[len(GODBOLT_CLIENTSTATE_URL) :] + assert "+" not in encoded and "/" not in encoded and "=" not in encoded diff --git a/libraries/views.py b/libraries/views.py index fb7d20761..84f4c0ef7 100644 --- a/libraries/views.py +++ b/libraries/views.py @@ -26,6 +26,7 @@ from .constants import README_MISSING from .forms import CommitAuthorEmailForm +from .godbolt import build_compiler_explorer_url from .mixins import VersionAlertMixin, BoostVersionMixin, ContributorMixin from .models import ( Category, @@ -580,6 +581,23 @@ def get_v3_context_data(self, **kwargs): context["website_adoc"].get("links"), ) + playground = context["website_adoc"].get("playground") + if playground and playground.get("code"): + selected_version = base_context.get("selected_version") + boost_version = ( + selected_version.name.replace("boost-", "") if selected_version else "" + ) + compiler_explorer_url = build_compiler_explorer_url( + playground["code"], boost_version + ) + if compiler_explorer_url: + context["quick_start_links"].append( + { + "label": "Edit in Compiler Explorer", + "url": compiler_explorer_url, + } + ) + dep_diff = base_context.get("dependency_diff", {}) context["dependencies_list"] = _build_dependencies_list( dep_diff.get("current_dependencies") or [], From f39e78581d9eee77470b0b6781b23747454f14fb Mon Sep 17 00:00:00 2001 From: Julia Hoang Date: Wed, 8 Jul 2026 16:05:16 -0700 Subject: [PATCH 09/30] feat: store raw website.adoc source with admin section-status editing --- libraries/admin.py | 36 +++++ .../commands/load_website_adoc_demo.py | 93 +++++++++++++ .../0043_libraryversion_website_adoc.py | 22 --- ...43_libraryversion_website_adoc_and_more.py | 31 +++++ libraries/models.py | 11 +- libraries/tasks.py | 6 +- libraries/tests/test_website_adoc.py | 99 +++++++++++++- libraries/website_adoc.py | 115 +++++++++++----- libraries/website_adoc_demo.adoc | 127 ++++++++++++++++++ versions/tasks.py | 6 +- 10 files changed, 479 insertions(+), 67 deletions(-) create mode 100644 libraries/management/commands/load_website_adoc_demo.py delete mode 100644 libraries/migrations/0043_libraryversion_website_adoc.py create mode 100644 libraries/migrations/0043_libraryversion_website_adoc_and_more.py create mode 100644 libraries/website_adoc_demo.adoc diff --git a/libraries/admin.py b/libraries/admin.py index fa06aca3b..b22e2c351 100644 --- a/libraries/admin.py +++ b/libraries/admin.py @@ -11,6 +11,7 @@ from django.template.loader import render_to_string from django.template.response import TemplateResponse from django.urls import path, reverse +from django.utils.html import format_html, format_html_join from django.utils.safestring import mark_safe from django.shortcuts import redirect from django.views.generic import TemplateView @@ -21,6 +22,7 @@ from core.admin_filters import StaffUserCreatedByFilter from config.celery import app from libraries.forms import CreateReportForm, CreateReportFullForm +from libraries.website_adoc import build_website_adoc, website_adoc_section_statuses from reports.generation import determine_versions from versions.models import Version from versions.tasks import import_all_library_versions @@ -581,6 +583,40 @@ class LibraryVersionAdmin(admin.ModelAdmin): search_fields = ["library__name", "version__name"] change_list_template = "admin/libraryversion_change_list.html" autocomplete_fields = ["authors", "maintainers", "dependencies"] + # website_adoc is derived from website_adoc_source on save (see save_model), + # so hide the parsed JSON; the section-status view shows what will render and + # admins can open the library page to see the result. + exclude = ["website_adoc"] + readonly_fields = ["website_adoc_sections"] + + _SECTION_STATUS_DISPLAY = { + "rendered": ("✅", "#1a7f37", "shows on the page"), + "omitted": ("⚠️", "#b32d2d", "authored but won't show"), + "absent": ("⚪", "#888", "not in the file"), + } + + @admin.display(description="website.adoc sections") + def website_adoc_sections(self, obj): + """Per-section render status — rendered / authored-but-omitted / absent.""" + rows = [] + for section in website_adoc_section_statuses(obj.website_adoc): + icon, color, note = self._SECTION_STATUS_DISPLAY[section["status"]] + detail = f"{note} ({section['reason']})" if section["reason"] else note + rows.append((icon, color, section["label"], detail)) + return format_html( + '
      {}
    ', + format_html_join( + "", + "
  • {} {} " + '— {}
  • ', + ((icon, label, color, detail) for icon, color, label, detail in rows), + ), + ) + + def save_model(self, request, obj, form, change): + """Re-derive the parsed website_adoc from the edited raw source.""" + obj.website_adoc = build_website_adoc(obj.website_adoc_source) + super().save_model(request, obj, form, change) def get_urls(self): urls = super().get_urls() diff --git a/libraries/management/commands/load_website_adoc_demo.py b/libraries/management/commands/load_website_adoc_demo.py new file mode 100644 index 000000000..71e56b6a5 --- /dev/null +++ b/libraries/management/commands/load_website_adoc_demo.py @@ -0,0 +1,93 @@ +from pathlib import Path + +import djclick as click +from django.urls import reverse + +from libraries.models import Library, LibraryVersion +from libraries.website_adoc import website_adoc_fields +from versions.models import Version + +DEFAULT_DEMO_FILE = Path(__file__).resolve().parents[2] / "website_adoc_demo.adoc" + + +@click.command() +@click.option( + "--library", + "library_slug", + required=True, + help="Library slug to load the demo onto (e.g. 'beast').", +) +@click.option( + "--version", + "version_slug", + default=None, + help="Boost version slug (e.g. '1.90.0'). Defaults to the most recent.", +) +@click.option( + "--file", + "file_path", + default=str(DEFAULT_DEMO_FILE), + help="Path to a website.adoc file (defaults to the bundled demo).", +) +@click.option( + "--clear", + is_flag=True, + default=False, + help="Clear website_adoc on the target instead of loading.", +) +def command(library_slug, version_slug, file_path, clear): + """Load a demo meta/website.adoc onto a library version so the subpage can be + inspected in a browser. + + Dev/QA helper: parses through the exact same build_website_adoc() path as + ingestion and stores the result on LibraryVersion.website_adoc, so the real + view + template render it. Use --clear to restore the empty state. + """ + try: + library = Library.objects.get(slug=library_slug) + except Library.DoesNotExist: + raise click.ClickException(f"No library with slug '{library_slug}'.") + + version = ( + Version.objects.filter(slug=version_slug).first() + if version_slug + else Version.objects.most_recent() + ) + if not version: + raise click.ClickException(f"No version matching '{version_slug}'.") + + try: + library_version = LibraryVersion.objects.get(library=library, version=version) + except LibraryVersion.DoesNotExist: + raise click.ClickException( + f"'{library_slug}' has no LibraryVersion for {version.slug}." + ) + + if clear: + LibraryVersion.objects.filter(pk=library_version.pk).update( + website_adoc_source=None, website_adoc=None + ) + click.secho( + f"Cleared website_adoc on {library_slug} ({version.slug}).", fg="yellow" + ) + return + + path = Path(file_path) + if not path.exists(): + raise click.ClickException(f"File not found: {file_path}") + + fields = website_adoc_fields(path.read_bytes()) + parsed = fields["website_adoc"] + if not parsed: + raise click.ClickException( + f"{file_path} parsed to nothing (empty or placeholder-only)." + ) + + LibraryVersion.objects.filter(pk=library_version.pk).update(**fields) + click.secho(f"Loaded {path.name} onto {library_slug} ({version.slug}).", fg="green") + click.echo("Sections: " + ", ".join(sorted(parsed.keys()))) + url = reverse( + "library-detail", + kwargs={"version_slug": version.slug, "library_slug": library_slug}, + ) + click.echo(f"View it at: {url}") diff --git a/libraries/migrations/0043_libraryversion_website_adoc.py b/libraries/migrations/0043_libraryversion_website_adoc.py deleted file mode 100644 index 0f34870fb..000000000 --- a/libraries/migrations/0043_libraryversion_website_adoc.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 6.0.2 on 2026-07-06 22:30 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("libraries", "0042_library_slack_url"), - ] - - operations = [ - migrations.AddField( - model_name="libraryversion", - name="website_adoc", - field=models.JSONField( - blank=True, - help_text="Parsed content of the library's optional meta/website.adoc (About, Playground, Designed for, Links, Install, Benchmarks, Freeform). Null when the repo has no website.adoc for this version.", - null=True, - ), - ), - ] diff --git a/libraries/migrations/0043_libraryversion_website_adoc_and_more.py b/libraries/migrations/0043_libraryversion_website_adoc_and_more.py new file mode 100644 index 000000000..ac5bc2d93 --- /dev/null +++ b/libraries/migrations/0043_libraryversion_website_adoc_and_more.py @@ -0,0 +1,31 @@ +# Generated by Django 6.0.2 on 2026-07-08 00:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("libraries", "0042_library_slack_url"), + ] + + operations = [ + migrations.AddField( + model_name="libraryversion", + name="website_adoc", + field=models.JSONField( + blank=True, + help_text="Parsed content of the library's optional meta/website.adoc (About, Playground, Designed for, Links, Install, Benchmarks, Freeform), derived from website_adoc_source. Null when the repo has no website.adoc for this version.", + null=True, + ), + ), + migrations.AddField( + model_name="libraryversion", + name="website_adoc_source", + field=models.TextField( + blank=True, + help_text="Raw meta/website.adoc for this version (the source of truth). Editing this and saving re-derives website_adoc.", + null=True, + ), + ), + ] diff --git a/libraries/models.py b/libraries/models.py index 71895c7d4..9163d5747 100644 --- a/libraries/models.py +++ b/libraries/models.py @@ -537,13 +537,22 @@ class LibraryVersion(models.Model): data = models.JSONField( default=dict, help_text="Contains the libraries.json for this library-version" ) + website_adoc_source = models.TextField( + null=True, + blank=True, + help_text=( + "Raw meta/website.adoc for this version (the source of truth). " + "Editing this and saving re-derives website_adoc." + ), + ) website_adoc = models.JSONField( null=True, blank=True, help_text=( "Parsed content of the library's optional meta/website.adoc " "(About, Playground, Designed for, Links, Install, Benchmarks, " - "Freeform). Null when the repo has no website.adoc for this version." + "Freeform), derived from website_adoc_source. Null when the repo " + "has no website.adoc for this version." ), ) # stats from git stored between x.x.0 versions diff --git a/libraries/tasks.py b/libraries/tasks.py index 73fcdf0e9..11b55a975 100644 --- a/libraries/tasks.py +++ b/libraries/tasks.py @@ -20,7 +20,7 @@ CommitAuthor, ReleaseReport, ) -from libraries.website_adoc import build_website_adoc +from libraries.website_adoc import website_adoc_fields from mailing_list.models import EmailData, PostingData from reports.generation import ( generate_algolia_words, @@ -88,11 +88,11 @@ def store_library_version_website_adoc(version, ref): # Missing file or unreachable fetch — keep any existing value. continue try: - parsed = build_website_adoc(content) + fields = website_adoc_fields(content) except Exception: logger.exception("website_adoc_parse_failed", repo=repo_slug, ref=ref) continue - LibraryVersion.objects.filter(pk=library_version.pk).update(website_adoc=parsed) + LibraryVersion.objects.filter(pk=library_version.pk).update(**fields) @app.task diff --git a/libraries/tests/test_website_adoc.py b/libraries/tests/test_website_adoc.py index 5b0f7291f..31ff75034 100644 --- a/libraries/tests/test_website_adoc.py +++ b/libraries/tests/test_website_adoc.py @@ -2,7 +2,12 @@ import pytest -from libraries.website_adoc import build_website_adoc, parse_website_adoc +from libraries.website_adoc import ( + build_website_adoc, + parse_website_adoc, + website_adoc_fields, + website_adoc_section_statuses, +) FILLED = """\ = Boost.Example — Website Content @@ -141,6 +146,23 @@ def test_unfilled_template_drops_placeholder_sections(): assert parsed.get("install", {}).get("blurb") is None +def test_website_adoc_fields_keeps_raw_source_and_parsed(): + fields = website_adoc_fields(FILLED.encode()) + assert fields["website_adoc_source"] == FILLED # raw retained as source of truth + assert fields["website_adoc"]["library_key"] == "example" # derived parse + + +def test_website_adoc_fields_none_when_no_source(): + assert website_adoc_fields(None) == { + "website_adoc_source": None, + "website_adoc": None, + } + assert website_adoc_fields("") == { + "website_adoc_source": None, + "website_adoc": None, + } + + def test_omitted_sections_absent(): parsed = parse_website_adoc( "= Title\n:library-key: foo\n\n[#install]\n== Install\n\n" @@ -150,6 +172,77 @@ def test_omitted_sections_absent(): assert parsed["install"]["code"]["code"] == "make" +def test_warns_on_authored_but_empty_section(): + # [#benchmarks] anchor present but its content is all placeholders -> omitted + # from output AND flagged, while a never-authored section is not flagged. + parsed = parse_website_adoc( + "= T\n:library-key: k\n\n" + "[#install]\n== Install\n\n[source,bash]\n----\nmake\n----\n\n" + "[#benchmarks]\n== Benchmarks\n\n[#benchmarks-x]\n=== \n" + ) + assert "benchmarks" not in parsed # authored but empty -> omitted + assert parsed["_warnings"] == [ + {"section": "benchmarks", "reason": "empty_or_placeholder"} + ] + assert "install" in parsed # unaffected + + +def test_section_metadata_derives_from_single_source(): + # SECTION_IDS and the admin status list both come from _SECTION_BUILDERS, so + # adding/removing a section can't leave them out of sync. + import libraries.website_adoc as mod + + assert mod.SECTION_IDS == {sid for _, sid, _, _ in mod._SECTION_BUILDERS} + statuses = website_adoc_section_statuses({}) + assert [s["label"] for s in statuses] == [ + label for _, _, label, _ in mod._SECTION_BUILDERS + ] + + +def test_section_statuses_covers_rendered_omitted_and_absent(): + # install rendered; benchmarks authored-but-empty; links never authored. + parsed = parse_website_adoc( + "= T\n:library-key: k\n\n" + "[#install]\n== Install\n\n[source,bash]\n----\nmake\n----\n\n" + "[#benchmarks]\n== Benchmarks\n\n[#benchmarks-x]\n=== \n" + ) + by_label = {s["label"]: s for s in website_adoc_section_statuses(parsed)} + assert len(by_label) == 7 # every known section is represented + assert by_label["Install"]["status"] == "rendered" + assert by_label["Benchmarks"] == { + "label": "Benchmarks", + "status": "omitted", + "reason": "empty_or_placeholder", + } + assert by_label["Links"]["status"] == "absent" # missing -> visible, neutral + + +def test_no_warnings_key_when_all_authored_sections_render(): + parsed = parse_website_adoc( + "= T\n:library-key: k\n\n[#install]\n== Install\n\n" + "[source,bash]\n----\nmake\n----\n" + ) + assert "_warnings" not in parsed + + +def test_warns_on_parse_error(monkeypatch): + import libraries.website_adoc as mod + + def boom(_lines): + raise ValueError("broken") + + monkeypatch.setattr( + mod, + "_SECTION_BUILDERS", + tuple( + (out, sid, label, boom if sid == "benchmarks" else fn) + for out, sid, label, fn in mod._SECTION_BUILDERS + ), + ) + parsed = parse_website_adoc(FILLED) + assert {"section": "benchmarks", "reason": "parse_error"} in parsed["_warnings"] + + def test_broken_section_dropped_others_kept(monkeypatch): """A section parser that raises drops only that section, not the document.""" import libraries.website_adoc as mod @@ -162,8 +255,8 @@ def boom(_lines): mod, "_SECTION_BUILDERS", tuple( - (out, sid, boom if sid == "benchmarks" else fn) - for out, sid, fn in mod._SECTION_BUILDERS + (out, sid, label, boom if sid == "benchmarks" else fn) + for out, sid, label, fn in mod._SECTION_BUILDERS ), ) diff --git a/libraries/website_adoc.py b/libraries/website_adoc.py index d74bc964e..1e911503c 100644 --- a/libraries/website_adoc.py +++ b/libraries/website_adoc.py @@ -18,20 +18,6 @@ logger = structlog.get_logger() -# Top-level section IDs that anchor a block. Nested benchmark anchors -# (``[#benchmarks-*]``) are parsed within the benchmarks section, not here. -SECTION_IDS = frozenset( - [ - "about", - "playground", - "designed-for", - "links", - "install", - "benchmarks", - "freeform", - ] -) - _ANCHOR_RE = re.compile(r"^\[#([a-z0-9-]+)\]\s*$") _ATTR_RE = re.compile(r"^:([a-z0-9-]+):\s*(.*)$") _SOURCE_RE = re.compile(r"^\[source(?:%[^\],]*)?(?:,\s*([a-z0-9+#-]+))?.*\]\s*$") @@ -232,20 +218,51 @@ def _build_links(lines): return links or None -# Output key -> (section ID, builder). Each builder takes the section's body -# lines and returns a truthy value or a falsy/None value when it has nothing -# usable. Builders are run in isolation so one broken section is dropped -# without affecting the others. +# The single source of truth for website.adoc sections. Each entry is +# (output key, section anchor id, human label, builder). Adding or removing a +# section here automatically updates the segmenter (SECTION_IDS), the parser, +# and the admin section-status view — nothing else to keep in sync. +# The builder takes the section's body lines and returns a truthy value, or a +# falsy value when it has nothing usable; builders run in isolation so one +# broken section is dropped without affecting the others. _SECTION_BUILDERS = ( - ("about", "about", _build_blurb_and_code), - ("playground", "playground", _first_source_block), - ("designed_for", "designed-for", _subsections), - ("links", "links", _build_links), - ("install", "install", _build_blurb_and_code), - ("benchmarks", "benchmarks", _benchmarks), - ("freeform", "freeform", _freeform), + ("about", "about", "About", _build_blurb_and_code), + ("playground", "playground", "Playground", _first_source_block), + ("designed_for", "designed-for", "Designed for", _subsections), + ("links", "links", "Links", _build_links), + ("install", "install", "Install", _build_blurb_and_code), + ("benchmarks", "benchmarks", "Benchmarks", _benchmarks), + ("freeform", "freeform", "Freeform", _freeform), ) +# Top-level anchor ids, derived so the segmenter can't drift from the parser. +# Nested benchmark anchors (``[#benchmarks-*]``) are handled within the +# benchmarks section, not here. +SECTION_IDS = frozenset(section_id for _, section_id, _, _ in _SECTION_BUILDERS) + + +def website_adoc_section_statuses(parsed): + """Per-section render status for the admin, covering every known section. + + Returns an ordered list of ``{label, status, reason}``: + - ``rendered`` — present in the parsed output (will show on the page) + - ``omitted`` — authored but dropped; ``reason`` is parse_error / + empty_or_placeholder / render_error + - ``absent`` — no ``[#id]`` anchor in the source (optional, not written) + """ + parsed = parsed or {} + warned = {w["section"]: w.get("reason") for w in parsed.get("_warnings", [])} + statuses = [] + for out_key, section_id, label, _builder in _SECTION_BUILDERS: + if out_key in parsed: + status, reason = "rendered", None + elif section_id in warned: + status, reason = "omitted", warned[section_id] + else: + status, reason = "absent", None + statuses.append({"label": label, "status": status, "reason": reason}) + return statuses + def _segment(content): """Split the document into ``{section_id: [body lines]}`` + document attrs. @@ -297,6 +314,12 @@ def parse_website_adoc(content): Returned keys are only present when the corresponding section has usable content. The ``freeform`` section carries raw AsciiDoc under ``content``; call ``build_website_adoc`` to also render it to HTML. + + A reserved ``_warnings`` key (list of ``{"section", "reason"}``) is added + when a section's ``[#id]`` anchor is present but produced nothing — so QA + can tell "authored but omitted from the UI" from "never authored". Reasons: + ``parse_error``, ``empty_or_placeholder``, ``render_error`` (freeform gem). + The template ignores this key (it reads named sections only). """ if content is None: return None @@ -307,21 +330,29 @@ def parse_website_adoc(content): sections, doc_attrs = _segment(content) parsed = {} + warnings = [] if library_key := _clean(doc_attrs.get("library-key")): parsed["library_key"] = library_key - for out_key, section_id, builder in _SECTION_BUILDERS: + for out_key, section_id, _label, builder in _SECTION_BUILDERS: if section_id not in sections: - continue + continue # not authored — nothing to warn about try: value = builder(sections[section_id]) except Exception: # A broken section is dropped; the rest of the document is unaffected. logger.exception("website_adoc_section_failed", section=section_id) + warnings.append({"section": section_id, "reason": "parse_error"}) continue if value: parsed[out_key] = value + else: + # Authored (the [#id] anchor exists) but yielded nothing usable. + warnings.append({"section": section_id, "reason": "empty_or_placeholder"}) + + if warnings: + parsed["_warnings"] = warnings return parsed or None @@ -343,18 +374,34 @@ def build_website_adoc(content): # section — every other section is still returned. logger.exception("website_adoc_freeform_render_failed") parsed.pop("freeform", None) + parsed.setdefault("_warnings", []).append( + {"section": "freeform", "reason": "render_error"} + ) return parsed or None -def fetch_website_adoc(client, repo_slug, tag): - """Fetch and parse ``meta/website.adoc`` for a repo/tag. +def website_adoc_fields(raw): + """Map raw website.adoc (bytes/str/None) to LibraryVersion field values. + + Returns ``{"website_adoc_source", "website_adoc"}`` — the raw text (source + of truth) and its derived parse — for use in update()/update_or_create(). + """ + source = raw.decode("utf-8", "replace") if isinstance(raw, bytes) else raw + return { + "website_adoc_source": source or None, + "website_adoc": build_website_adoc(raw), + } + - Never raises — returns ``None`` on a missing file or any parse/render - error, so it is safe to call inline in the ingestion pipeline. +def fetch_website_adoc_fields(client, repo_slug, tag): + """Fetch meta/website.adoc for a repo/tag and return LibraryVersion fields. + + Never raises — returns both fields as ``None`` on a missing file or any + fetch/parse error, so it is safe to call inline in the ingestion pipeline. """ try: - content = client.get_website_adoc(repo_slug=repo_slug, tag=tag) - return build_website_adoc(content) + raw = client.get_website_adoc(repo_slug=repo_slug, tag=tag) + return website_adoc_fields(raw) except Exception: logger.exception("website_adoc_parse_failed", repo=repo_slug, tag=tag) - return None + return {"website_adoc_source": None, "website_adoc": None} diff --git a/libraries/website_adoc_demo.adoc b/libraries/website_adoc_demo.adoc new file mode 100644 index 000000000..6491cc490 --- /dev/null +++ b/libraries/website_adoc_demo.adoc @@ -0,0 +1,127 @@ += Boost.Demo — Website Content +:library-key: demo + +// A fully-filled example that exercises every website.adoc section, for +// previewing the library subpage UI via `manage.py load_website_adoc_demo`. + +[#about] +== About + +Boost.Demo is a header-only library for high-throughput message parsing with a +modern, allocation-free API. + +[source,cpp] +---- +#include +#include +#include + +namespace beast = boost::beast; +namespace http = beast::http; + +int main() +{ + // Build an HTTP GET request + http::request req{http::verb::get, "/index.html", 11}; + req.set(http::field::host, "www.example.com"); + req.set(http::field::user_agent, "Boost.Beast"); + + std::cout << req << std::endl; + + // Build an HTTP response + http::response res{http::status::ok, 11}; + res.set(http::field::server, "Boost.Beast"); + res.set(http::field::content_type, "text/plain"); + res.body() = "Hello from Boost.Beast!"; + res.prepare_payload(); + + std::cout << res << std::endl; + + return 0; +} +---- + +[#playground] +== Playground + +[source,cpp] +---- +#include +#include + +int main() { + boost::demo::parser p; + std::cout << p.run("hello world") << "\n"; +} +---- + +[#designed-for] +== Designed for + +=== High-throughput parsing +Handles millions of messages per second with near-zero allocations on the hot path. + +=== Header-only +Add Boost to your include path — no separate build step required. + +=== Standards-tracking +APIs mirror the C++ standard where applicable, easing future migration. + +[#links] +== Links + +:common-use-case-url: https://www.boost.org/doc/libs/1_90_0/libs/demo/doc/html/use-cases.html +:code-example-url: https://www.boost.org/doc/libs/1_90_0/libs/demo/doc/html/examples.html + +[#install] +== Install + +Header-only — add Boost to your include path, or build with b2. + +[source,bash] +---- +# build it with b2: +./b2 --with-demo install +# then link against it: +g++ app.cpp -lboost_demo +---- + +[#benchmarks] +== Benchmarks + +[#benchmarks-throughput] +=== Throughput +:unit: req/s +:caption: higher is better + +[cols="2,1",options="header"] +|=== +| Label | Value +| Boost.Demo | 1200000 +| Alternative A | 800000 +| Alternative B | 450000 +|=== + +[#benchmarks-latency] +=== Latency +:unit: microseconds +:caption: lower is better + +[cols="2,1",options="header"] +|=== +| Label | Value +| Boost.Demo | 12 +| Alternative A | 30 +| Alternative B | 55 +|=== + +[#freeform] +== Notes from the maintainer + +Boost.Demo is maintained by the demo team. Highlights: + +* Zero-allocation fast path +* Extensive fuzz testing +* Works on all major compilers + +See the https://www.boost.org/[Boost website] for more. diff --git a/versions/tasks.py b/versions/tasks.py index e645b3307..44b2d0e0b 100644 --- a/versions/tasks.py +++ b/versions/tasks.py @@ -28,7 +28,7 @@ from libraries.constants import SKIP_LIBRARY_VERSIONS from libraries.github import LibraryUpdater from libraries.models import Library, LibraryVersion -from libraries.website_adoc import fetch_website_adoc +from libraries.website_adoc import fetch_website_adoc_fields from libraries.tasks import get_and_store_library_version_documentation_urls_for_version from libraries.utils import version_within_range from versions.exceptions import BoostImportedDataException @@ -474,9 +474,7 @@ def import_library_versions(version_name, token=None, version_type="tag"): "cpp_standard_maximum": lib_data.get("cxxstd_max"), "cpp20_module_support": lib_data.get("cpp20_module_support"), "description": lib_data.get("description"), - "website_adoc": fetch_website_adoc( - client, library_name, version_name - ), + **fetch_website_adoc_fields(client, library_name, version_name), }, ) if not library.github_url: From 98cc5805038d75889f84c795eb25d9a6384b059c Mon Sep 17 00:00:00 2001 From: Julia Hoang Date: Wed, 8 Jul 2026 16:07:35 -0700 Subject: [PATCH 10/30] chore: remove caption and source from the benchmark section --- libraries/tests/test_website_adoc.py | 2 -- libraries/website_adoc.py | 2 -- libraries/website_adoc_demo.adoc | 2 -- libraries/website_adoc_template.adoc | 3 --- 4 files changed, 9 deletions(-) diff --git a/libraries/tests/test_website_adoc.py b/libraries/tests/test_website_adoc.py index 31ff75034..4fbdc09f6 100644 --- a/libraries/tests/test_website_adoc.py +++ b/libraries/tests/test_website_adoc.py @@ -62,7 +62,6 @@ [#benchmarks-throughput] === Throughput :unit: req/s -:caption: higher is better [cols="2,1",options="header"] |=== @@ -119,7 +118,6 @@ def test_parse_filled_document(): assert chart["id"] == "benchmarks-throughput" assert chart["title"] == "Throughput" assert chart["unit"] == "req/s" - assert chart["caption"] == "higher is better" assert chart["data"] == [ {"label": "Boost.Example", "value": 1200.0}, {"label": "Other", "value": 800.0}, diff --git a/libraries/website_adoc.py b/libraries/website_adoc.py index 1e911503c..104aa0d09 100644 --- a/libraries/website_adoc.py +++ b/libraries/website_adoc.py @@ -160,8 +160,6 @@ def _benchmarks(lines): "id": chart["id"], "title": title, "unit": _clean(attrs.get("unit")), - "caption": _clean(attrs.get("caption")), - "source": _clean(attrs.get("source")), "data": data, } ) diff --git a/libraries/website_adoc_demo.adoc b/libraries/website_adoc_demo.adoc index 6491cc490..00291cdd1 100644 --- a/libraries/website_adoc_demo.adoc +++ b/libraries/website_adoc_demo.adoc @@ -92,7 +92,6 @@ g++ app.cpp -lboost_demo [#benchmarks-throughput] === Throughput :unit: req/s -:caption: higher is better [cols="2,1",options="header"] |=== @@ -105,7 +104,6 @@ g++ app.cpp -lboost_demo [#benchmarks-latency] === Latency :unit: microseconds -:caption: lower is better [cols="2,1",options="header"] |=== diff --git a/libraries/website_adoc_template.adoc b/libraries/website_adoc_template.adoc index 87f4fb618..a869df0db 100644 --- a/libraries/website_adoc_template.adoc +++ b/libraries/website_adoc_template.adoc @@ -102,14 +102,11 @@ g++ app.cpp -lboost_ // Optional. One subsection per bar chart. // :unit: value-axis label (e.g. req/s, µs) -// :caption: optional sub-label -// :source: optional citation URL // Table columns: Label | Value (plain number) [#benchmarks-] === :unit: -:caption: [cols="2,1",options="header"] |=== From 47eaa0ce4316756914fd6fe2a245e9709bc7a6cc Mon Sep 17 00:00:00 2001 From: Julia Hoang Date: Wed, 8 Jul 2026 16:34:40 -0700 Subject: [PATCH 11/30] fix: fix cropped benchmark card --- static/css/v3/library-subpage.css | 8 ++++++++ templates/v3/libraries/library-subpage.html | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/static/css/v3/library-subpage.css b/static/css/v3/library-subpage.css index d6caa5d05..350d40562 100644 --- a/static/css/v3/library-subpage.css +++ b/static/css/v3/library-subpage.css @@ -96,6 +96,7 @@ body:has(.is_flagship) .library-subpage-container { .card-masonry-top .contributors-list, .card-masonry-top .markdown-card, .card-masonry-top .quick-start-card, +.card-masonry-top .stats--benchmarks, .card-masonry-bottom > div > .card, .card-masonry-bottom > div > .basic-card, .card-masonry-bottom > div > .card-group, @@ -107,6 +108,13 @@ body:has(.is_flagship) .library-subpage-container { width: 100%; } +/* Benchmark card sizes like the other cards: fill the column width, grow to + fit its content (override the component's fixed 460px / 420px cap). */ +.card-masonry-top .stats--benchmarks { + height: auto; + max-height: none; +} + .library-subpage__documentation > .markdown-card { max-height: 572px; display: flex; diff --git a/templates/v3/libraries/library-subpage.html b/templates/v3/libraries/library-subpage.html index 092b25dbf..2a1a050fc 100644 --- a/templates/v3/libraries/library-subpage.html +++ b/templates/v3/libraries/library-subpage.html @@ -50,7 +50,7 @@
    {% if benchmark_sets %}
    - {% include "v3/includes/_stats_benchmarks.html" with heading="Benchmarks" sets=benchmark_sets sized_by_parent=True %} + {% include "v3/includes/_stats_benchmarks.html" with heading="Benchmarks" sets=benchmark_sets %}
    {% endif %}
    From 3e1f442fdae078fed8e4cfc991d2b0e60f8357d3 Mon Sep 17 00:00:00 2001 From: Julia Hoang Date: Thu, 9 Jul 2026 16:11:44 -0700 Subject: [PATCH 12/30] feat: skip website.adoc refresh during beta cycle --- libraries/tasks.py | 12 ++++++++ libraries/tests/test_tasks.py | 55 +++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/libraries/tasks.py b/libraries/tasks.py index 11b55a975..73da58234 100644 --- a/libraries/tasks.py +++ b/libraries/tasks.py @@ -62,10 +62,22 @@ def update_library_version_website_adoc(): Historical versions keep the snapshot captured at their release import — a tagged release's meta/website.adoc is immutable, so re-fetching every version daily would be thousands of pointless requests. + + Skipped while a newer release is in beta: `master` has already drifted toward + that release, so refreshing the current stable from it would surface + pre-release content on the stable page. The stable keeps its release-tag + import snapshot until the beta becomes the full release. """ version = Version.objects.most_recent() if version is None: return + # During a beta cycle for the NEXT release, each library's `master` has + # already drifted toward that release, so refreshing the current stable from + # master would show it pre-release content. Hold until the beta is promoted + # to a full release (matches the newer-beta check used for the version dropdown). + beta = Version.objects.most_recent_beta() + if beta and beta.cleaned_version_parts > version.cleaned_version_parts: + return store_library_version_website_adoc(version, ref="master") diff --git a/libraries/tests/test_tasks.py b/libraries/tests/test_tasks.py index 3da27f85b..f33330131 100644 --- a/libraries/tests/test_tasks.py +++ b/libraries/tests/test_tasks.py @@ -1,9 +1,12 @@ import pytest from unittest.mock import MagicMock, patch +from model_bakery import baker + from libraries.tasks import ( get_and_store_library_version_documentation_urls_for_version, library_version_missing_docs, + update_library_version_website_adoc, version_missing_docs, ) @@ -109,3 +112,55 @@ def test_version_missing_docs(version, version_name, expected): version.save() result = version_missing_docs(version) assert result == expected + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "beta_name, expect_refresh", + [ + ("boost-1.91.0.beta1", False), # newer release in beta -> hold + ("boost-1.90.0.beta1", True), # beta for the current stable -> refresh + ("boost-1.89.0.beta1", True), # stale older beta -> refresh + (None, True), # no beta -> refresh + ], +) +def test_update_library_version_website_adoc_beta_guard(beta_name, expect_refresh): + """The daily website.adoc refresh holds only while a NEWER release is in beta. + + A newer beta means each library's `master` has drifted toward the next + release, so refreshing the current stable from `master` would surface + pre-release content on the stable page. + """ + stable = baker.make( + "versions.Version", + name="boost-1.90.0", + beta=False, + full_release=True, + active=True, + fully_imported=True, + ) + if beta_name: + baker.make( + "versions.Version", + name=beta_name, + beta=True, + full_release=False, + active=True, + fully_imported=True, + ) + + with patch("libraries.tasks.store_library_version_website_adoc") as mock_store: + update_library_version_website_adoc() + + if expect_refresh: + mock_store.assert_called_once_with(stable, ref="master") + else: + mock_store.assert_not_called() + + +@pytest.mark.django_db +def test_update_library_version_website_adoc_no_stable_release(): + """No most-recent full release -> nothing to refresh.""" + with patch("libraries.tasks.store_library_version_website_adoc") as mock_store: + update_library_version_website_adoc() + mock_store.assert_not_called() From bd50f7c3f5ae2d6b88c59c0e47dc188481e76cb9 Mon Sep 17 00:00:00 2001 From: Julia Hoang Date: Thu, 9 Jul 2026 16:56:20 -0700 Subject: [PATCH 13/30] refactor: build category tag links via Category.get_filter_url --- core/views.py | 9 +-------- libraries/models.py | 19 +++++++++++++++++-- libraries/tests/test_models.py | 16 +++++++++++++++- libraries/views.py | 16 +--------------- 4 files changed, 34 insertions(+), 26 deletions(-) diff --git a/core/views.py b/core/views.py index 64af58518..29b3e40c3 100644 --- a/core/views.py +++ b/core/views.py @@ -636,14 +636,7 @@ def _build_category_cards(): "description": category.short_description, "badge_count": category.library_count, "cta_label": "Start here", - "cta_href": reverse( - "libraries-list", - kwargs={ - "version_slug": LATEST_RELEASE_URL_PATH_STR, - "library_view_str": "list", - "category_slug": category.slug, - }, - ), + "cta_href": category.get_filter_url(), } ) return cards diff --git a/libraries/models.py b/libraries/models.py index 9163d5747..d7aecb794 100644 --- a/libraries/models.py +++ b/libraries/models.py @@ -3,7 +3,7 @@ import uuid from datetime import timedelta from typing import Self -from urllib.parse import urlparse +from urllib.parse import urlencode, urlparse from django.core.cache import caches from django.db import models, transaction @@ -31,7 +31,7 @@ ) from mailing_list.models import EmailData from versions.models import ReportConfiguration -from .constants import LIBRARY_GITHUB_URL_OVERRIDES +from .constants import LATEST_RELEASE_URL_PATH_STR, LIBRARY_GITHUB_URL_OVERRIDES from .utils import ( generate_random_string, @@ -67,6 +67,21 @@ def save(self, *args, **kwargs): self.slug = slugify(self.name) return super(Category, self).save(*args, **kwargs) + def get_filter_url(self, version_slug=LATEST_RELEASE_URL_PATH_STR): + """URL to the libraries list filtered by this category. + + e.g. /libraries/1.90.0/list/?category=asynchronous — the query-param + form the list/grid category filter reads. Single source of truth for + category-tag links; returns "#" when the category has no slug. + """ + if not self.slug: + return "#" + base = reverse( + "libraries-list", + kwargs={"version_slug": version_slug, "library_view_str": "list"}, + ) + return f"{base}?{urlencode({'category': self.slug})}" + class CommitAuthor(models.Model): name = models.CharField(max_length=100) diff --git a/libraries/tests/test_models.py b/libraries/tests/test_models.py index 35229da56..4ea4e585a 100644 --- a/libraries/tests/test_models.py +++ b/libraries/tests/test_models.py @@ -2,7 +2,7 @@ from django.db.models import Sum from model_bakery import baker -from libraries.models import CommitAuthor +from libraries.models import Category, CommitAuthor from mailing_list.models import EmailData @@ -61,6 +61,20 @@ def test_category_creation(category): assert category.name is not None +def test_category_get_filter_url(category): + """Builds the list-view URL with a ?category= query param.""" + assert category.get_filter_url("1.90.0") == "/libraries/1.90.0/list/?category=math" + + +def test_category_get_filter_url_defaults_to_latest(category): + assert category.get_filter_url() == "/libraries/latest/list/?category=math" + + +def test_category_get_filter_url_no_slug(): + """A category without a slug has no filter target.""" + assert Category(name="X", slug="").get_filter_url() == "#" + + def test_library_creation(library): assert library.versions.count() == 0 diff --git a/libraries/views.py b/libraries/views.py index 84f4c0ef7..5e4b49c42 100644 --- a/libraries/views.py +++ b/libraries/views.py @@ -558,21 +558,7 @@ def get_v3_context_data(self, **kwargs): context["slack_url"] = self.object.slack_url or SLACK_JOIN_URL context["category_tags_v3"] = [ - { - "label": cat.name, - "url": ( - reverse( - "libraries-list", - kwargs={ - "version_slug": version_str, - "library_view_str": "grid", - "category_slug": cat.slug, - }, - ) - if cat.slug - else "#" - ), - } + {"label": cat.name, "url": cat.get_filter_url(version_str)} for cat in self.object.categories.all().order_by("name") ] From 52541b1f26318cf0d14e0bc48768da1df22e7fc9 Mon Sep 17 00:00:00 2001 From: Julia Hoang Date: Thu, 9 Jul 2026 17:07:40 -0700 Subject: [PATCH 14/30] refactor: extract LibraryDetail v3 context helpers --- core/githubhelper.py | 7 ++-- libraries/tasks.py | 11 +++--- libraries/views.py | 84 +++++++++++++++++++++++--------------------- 3 files changed, 52 insertions(+), 50 deletions(-) diff --git a/core/githubhelper.py b/core/githubhelper.py index f40124b12..9dffedd22 100644 --- a/core/githubhelper.py +++ b/core/githubhelper.py @@ -334,11 +334,10 @@ def get_libraries_json(self, repo_slug: str, tag: str = "master"): def get_website_adoc(self, repo_slug: str, tag: str = "master"): """Retrieve a library's optional 'meta/website.adoc'. - Most libraries won't ship this file, so a 404 is expected and returns - None quietly (no traceback). See libraries/website_adoc.py for the - parser and the file's contract. + Many libraries might not ship this file, so it will returns None quietly if not found. + See libraries/website_adoc.py for the parser and the file's contract. - :param repo_slug: str, the repository slug + :param repo_slug: str, the repository slugs :param tag: str, the Git tag :return: bytes, the file content, or None if it doesn't exist """ diff --git a/libraries/tasks.py b/libraries/tasks.py index 73da58234..f5ed0e44e 100644 --- a/libraries/tasks.py +++ b/libraries/tasks.py @@ -57,11 +57,10 @@ def update_library_version_documentation_urls_all_versions(): def update_library_version_website_adoc(): """Refresh parsed meta/website.adoc for the current release. - Scoped to the most recent version and fetched from `master` (mirroring - update_libraries) so maintainer edits between releases are picked up. - Historical versions keep the snapshot captured at their release import — a - tagged release's meta/website.adoc is immutable, so re-fetching every - version daily would be thousands of pointless requests. + Scoped to the most recent version and fetched from `master` so maintainer + edits between releases are picked up. Historical versions keep the snapshot + captured at their release import — a tagged release's meta/website.adoc is + immutable, so re-fetching every version daily would be redundant. Skipped while a newer release is in beta: `master` has already drifted toward that release, so refreshing the current stable from it would surface @@ -74,7 +73,7 @@ def update_library_version_website_adoc(): # During a beta cycle for the NEXT release, each library's `master` has # already drifted toward that release, so refreshing the current stable from # master would show it pre-release content. Hold until the beta is promoted - # to a full release (matches the newer-beta check used for the version dropdown). + # to a full release. beta = Version.objects.most_recent_beta() if beta and beta.cleaned_version_parts > version.cleaned_version_parts: return diff --git a/libraries/views.py b/libraries/views.py index 5e4b49c42..db7c8b946 100644 --- a/libraries/views.py +++ b/libraries/views.py @@ -112,6 +112,38 @@ def _build_dependencies_list(current_dependencies, version_str): return result +def _build_release_contributors(context): + """Build the "Contributors: This Release" profile list from the context + populated by ContributorMixin: authors + maintainers + new and returning + commit contributors, each tagged with its display role.""" + return ( + [u.to_v3_profile_dict("Author") for u in context.get("authors", [])] + + [u.to_v3_profile_dict("Maintainer") for u in context.get("maintainers", [])] + + [ + a.to_v3_profile_dict("New Contributor") + for a in context.get("top_contributors_release_new", []) + ] + + [ + a.to_v3_profile_dict("Contributor") + for a in context.get("top_contributors_release_old", []) + ] + ) + + +def _build_compiler_explorer_link(website_adoc, selected_version): + """Build the "Edit in Compiler Explorer" Quick Start link from website.adoc's + [#playground] code. Returns None when there's no playground code or godbolt + can't produce a URL.""" + playground = website_adoc.get("playground") + if not (playground and playground.get("code")): + return None + boost_version = selected_version.display_name if selected_version else "" + url = build_compiler_explorer_url(playground["code"], boost_version) + if not url: + return None + return {"label": "Edit in Compiler Explorer", "url": url} + + class LibraryListDispatcher(View): def dispatch(self, request, *args, **kwargs): if view_str := request.GET.get("view", None): @@ -543,11 +575,10 @@ def get_context_data(self, **kwargs): def get_v3_context_data(self, **kwargs): context = {**kwargs} - base_context = context - version_str = base_context.get("version_str") or LATEST_RELEASE_URL_PATH_STR + version_str = context.get("version_str") or LATEST_RELEASE_URL_PATH_STR - library_version = base_context.get("library_version") + library_version = context.get("library_version") context["website_adoc"] = getattr(library_version, "website_adoc", None) or {} context["designed_for_html"] = designed_for_html( context["website_adoc"].get("designed_for") @@ -563,28 +594,16 @@ def get_v3_context_data(self, **kwargs): ] context["quick_start_links"] = _build_quick_start_links( - base_context.get("documentation_url"), + context.get("documentation_url"), context["website_adoc"].get("links"), ) + compiler_explorer_link = _build_compiler_explorer_link( + context["website_adoc"], context.get("selected_version") + ) + if compiler_explorer_link: + context["quick_start_links"].append(compiler_explorer_link) - playground = context["website_adoc"].get("playground") - if playground and playground.get("code"): - selected_version = base_context.get("selected_version") - boost_version = ( - selected_version.name.replace("boost-", "") if selected_version else "" - ) - compiler_explorer_url = build_compiler_explorer_url( - playground["code"], boost_version - ) - if compiler_explorer_url: - context["quick_start_links"].append( - { - "label": "Edit in Compiler Explorer", - "url": compiler_explorer_url, - } - ) - - dep_diff = base_context.get("dependency_diff", {}) + dep_diff = context.get("dependency_diff", {}) context["dependencies_list"] = _build_dependencies_list( dep_diff.get("current_dependencies") or [], version_str, @@ -592,32 +611,17 @@ def get_v3_context_data(self, **kwargs): context["library_posts"] = get_latest_post_cards(limit=3) - this_release = ( - [u.to_v3_profile_dict("Author") for u in base_context.get("authors", [])] - + [ - u.to_v3_profile_dict("Maintainer") - for u in base_context.get("maintainers", []) - ] - + [ - a.to_v3_profile_dict("New Contributor") - for a in base_context.get("top_contributors_release_new", []) - ] - + [ - a.to_v3_profile_dict("Contributor") - for a in base_context.get("top_contributors_release_old", []) - ] - ) + this_release = _build_release_contributors(context) context["this_release_contributors"] = ( apply_collective_author_overrides(this_release) or SharedResources.library_release_contributors ) - library_version = base_context.get("library_version") all_time = ( self.build_all_contributors( library_version, - base_context.get("authors", []), - base_context.get("maintainers", []), + context.get("authors", []), + context.get("maintainers", []), ) if library_version else [] From 679e0d89a2e95342465798e8876d8ea4fd4fa176 Mon Sep 17 00:00:00 2001 From: Julia Hoang Date: Fri, 10 Jul 2026 09:52:12 -0700 Subject: [PATCH 15/30] fix: harden website.adoc fetch against timeouts and connection errors --- core/githubhelper.py | 4 ++-- libraries/tasks.py | 6 +++++- templates/v3/includes/_hero_library.html | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/core/githubhelper.py b/core/githubhelper.py index 9dffedd22..81d399769 100644 --- a/core/githubhelper.py +++ b/core/githubhelper.py @@ -344,9 +344,9 @@ def get_website_adoc(self, repo_slug: str, tag: str = "master"): url = f"https://raw.githubusercontent.com/{self.owner}/{repo_slug}/{tag}/meta/website.adoc" # noqa try: - response = requests.get(url) + response = requests.get(url, timeout=10) response.raise_for_status() - except requests.exceptions.HTTPError: + except requests.exceptions.RequestException: self.logger.info("website_adoc_not_found", repo=repo_slug, tag=tag) return None return response.content diff --git a/libraries/tasks.py b/libraries/tasks.py index f5ed0e44e..f556fe7cc 100644 --- a/libraries/tasks.py +++ b/libraries/tasks.py @@ -94,7 +94,11 @@ def store_library_version_website_adoc(version, ref): repo_slug = library_version.library.github_repo if not repo_slug: continue - content = client.get_website_adoc(repo_slug=repo_slug, tag=ref) + try: + content = client.get_website_adoc(repo_slug=repo_slug, tag=ref) + except Exception: + logger.exception("website_adoc_fetch_failed", repo=repo_slug, ref=ref) + continue if content is None: # Missing file or unreachable fetch — keep any existing value. continue diff --git a/templates/v3/includes/_hero_library.html b/templates/v3/includes/_hero_library.html index 366e3be51..23469cb28 100644 --- a/templates/v3/includes/_hero_library.html +++ b/templates/v3/includes/_hero_library.html @@ -47,7 +47,7 @@ {% endif %} {% if added_text %}
    - {{ added_text }} + Added in {{ added_text }}
    {% endif %}
    From fa09761a2cd17c3a8f2f22e3e430011c34af7c9c Mon Sep 17 00:00:00 2001 From: Julia Hoang Date: Fri, 10 Jul 2026 09:55:33 -0700 Subject: [PATCH 16/30] fix: guard godbolt version map against malformed API payload --- libraries/godbolt.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/libraries/godbolt.py b/libraries/godbolt.py index 8383bca98..b367174d5 100644 --- a/libraries/godbolt.py +++ b/libraries/godbolt.py @@ -49,17 +49,16 @@ def _fetch_boost_version_map(): ) response.raise_for_status() libraries = response.json() - except (requests.RequestException, ValueError): + for library in libraries: + if library.get("id") == "boost": + return { + version["version"]: version["id"] + for version in library.get("versions", []) + if version.get("version") and version.get("id") + } + except (requests.RequestException, ValueError, AttributeError, TypeError, KeyError): logger.exception("godbolt_libraries_fetch_failed") return {} - - for library in libraries: - if library.get("id") == "boost": - return { - version["version"]: version["id"] - for version in library.get("versions", []) - if version.get("version") and version.get("id") - } return {} From 5ac217790883bce4178663c3a1dcbb669aba769d Mon Sep 17 00:00:00 2001 From: Julia Hoang Date: Fri, 10 Jul 2026 09:58:09 -0700 Subject: [PATCH 17/30] fix: add default to library slack_url field --- libraries/migrations/0042_library_slack_url.py | 1 + libraries/models.py | 1 + 2 files changed, 2 insertions(+) diff --git a/libraries/migrations/0042_library_slack_url.py b/libraries/migrations/0042_library_slack_url.py index 199da8233..c36201349 100644 --- a/libraries/migrations/0042_library_slack_url.py +++ b/libraries/migrations/0042_library_slack_url.py @@ -15,6 +15,7 @@ class Migration(migrations.Migration): name="slack_url", field=models.URLField( blank=True, + default="", help_text="URL of the dedicated Slack channel for this library. Falls back to the general Boost Slack when blank.", max_length=500, ), diff --git a/libraries/models.py b/libraries/models.py index d7aecb794..222c1c676 100644 --- a/libraries/models.py +++ b/libraries/models.py @@ -291,6 +291,7 @@ class Library(models.Model): slack_url = models.URLField( max_length=500, blank=True, + default="", help_text=( "URL of the dedicated Slack channel for this library. " "Falls back to the general Boost Slack when blank." From bc581c28315ea2959f87dded1b7c2025a3b82883 Mon Sep 17 00:00:00 2001 From: Julia Hoang Date: Fri, 10 Jul 2026 10:42:50 -0700 Subject: [PATCH 18/30] fix: count all releases in All Contributors for master/develop views --- libraries/mixins.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/libraries/mixins.py b/libraries/mixins.py index e392c2492..f7d2e5748 100644 --- a/libraries/mixins.py +++ b/libraries/mixins.py @@ -2,7 +2,7 @@ import structlog -from django.db.models import Count, Exists, OuterRef +from django.db.models import Count, Exists, OuterRef, Q from django.shortcuts import get_object_or_404 from django.urls import reverse from django.utils.html import format_html @@ -360,11 +360,19 @@ def build_all_contributors(self, library_version, authors, maintainers): for u in maintainers if getattr(getattr(u, "commitauthor", None), "id", None) ] + # Minor releases up to the selected version, plus the selected version + # itself so patch/beta builds keep their own contributors. master/develop + # have no numeric version, so they sit ahead of every release — count all + # minor releases (an unbounded version_array__lte would match none). + applicable_versions = Version.objects.minor_versions() + selected_parts = library_version.version.cleaned_version_parts_int + if selected_parts: + applicable_versions = applicable_versions.filter( + version_array__lte=selected_parts + ) library_versions = LibraryVersion.objects.filter( - library=library_version.library, - version__in=Version.objects.minor_versions().filter( - version_array__lte=library_version.version.cleaned_version_parts_int - ), + Q(library=library_version.library, version__in=applicable_versions) + | Q(pk=library_version.pk) ) contributors = ( CommitAuthor.humans.filter(commit__library_version__in=library_versions) From 0289ac9d20cf4a6a2f9208189cfe7475d6766acd Mon Sep 17 00:00:00 2001 From: Julia Hoang Date: Fri, 10 Jul 2026 10:44:46 -0700 Subject: [PATCH 19/30] fix: preserve // lines inside code fences in freeform parser --- libraries/tests/test_website_adoc.py | 24 ++++++++++++++++++++++++ libraries/website_adoc.py | 7 ++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/libraries/tests/test_website_adoc.py b/libraries/tests/test_website_adoc.py index 4fbdc09f6..8bf2bb8ca 100644 --- a/libraries/tests/test_website_adoc.py +++ b/libraries/tests/test_website_adoc.py @@ -127,6 +127,30 @@ def test_parse_filled_document(): assert "free-form content" in parsed["freeform"]["content"] +def test_freeform_keeps_comments_inside_code_fence(): + """`//` lines inside a `----` block are verbatim; `//` comments outside drop.""" + doc = """\ += Boost.Example — Website Content +:library-key: example + +[#freeform] +== Notes from the maintainer + +// this AsciiDoc comment is stripped +Example usage: + +---- +int main() { + // keep this C++ comment + return 0; +} +---- +""" + content = parse_website_adoc(doc)["freeform"]["content"] + assert "// keep this C++ comment" in content + assert "this AsciiDoc comment is stripped" not in content + + def test_unfilled_template_drops_placeholder_sections(): """The raw template (angle-bracket placeholders) yields no placeholder data.""" template = ( diff --git a/libraries/website_adoc.py b/libraries/website_adoc.py index 104aa0d09..e0041ae42 100644 --- a/libraries/website_adoc.py +++ b/libraries/website_adoc.py @@ -170,14 +170,19 @@ def _freeform(lines): """Return ``{"heading", "content"}`` — the maintainer heading + raw AsciiDoc.""" heading = None body = [] + in_source = False for line in lines: stripped = line.strip() + if stripped == "----": + in_source = not in_source + body.append(line) + continue if heading is None: match = _HEADING_RE.match(stripped) if match: heading = match.group(1).strip() continue - if stripped.startswith("//"): + if not in_source and stripped.startswith("//"): continue body.append(line) content = "\n".join(body).strip("\n") From e8c117dd89646ea6096dd99c4484cc70c19eb990 Mon Sep 17 00:00:00 2001 From: Julia Hoang Date: Fri, 10 Jul 2026 10:49:44 -0700 Subject: [PATCH 20/30] chore: fix template wording and exception chaining nitpicks --- libraries/management/commands/load_website_adoc_demo.py | 8 ++++---- libraries/website_adoc_template.adoc | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/libraries/management/commands/load_website_adoc_demo.py b/libraries/management/commands/load_website_adoc_demo.py index 71e56b6a5..d5bd8a803 100644 --- a/libraries/management/commands/load_website_adoc_demo.py +++ b/libraries/management/commands/load_website_adoc_demo.py @@ -45,8 +45,8 @@ def command(library_slug, version_slug, file_path, clear): """ try: library = Library.objects.get(slug=library_slug) - except Library.DoesNotExist: - raise click.ClickException(f"No library with slug '{library_slug}'.") + except Library.DoesNotExist as err: + raise click.ClickException(f"No library with slug '{library_slug}'.") from err version = ( Version.objects.filter(slug=version_slug).first() @@ -58,10 +58,10 @@ def command(library_slug, version_slug, file_path, clear): try: library_version = LibraryVersion.objects.get(library=library, version=version) - except LibraryVersion.DoesNotExist: + except LibraryVersion.DoesNotExist as err: raise click.ClickException( f"'{library_slug}' has no LibraryVersion for {version.slug}." - ) + ) from err if clear: LibraryVersion.objects.filter(pk=library_version.pk).update( diff --git a/libraries/website_adoc_template.adoc b/libraries/website_adoc_template.adoc index a869df0db..0427dba86 100644 --- a/libraries/website_adoc_template.adoc +++ b/libraries/website_adoc_template.adoc @@ -124,4 +124,4 @@ g++ app.cpp -lboost_ // rendered as-is into a single card on your library's page. // Keep the [#freeform] ID; change the heading text to whatever you like. - + From 2187471c0d3c5bc99463e715df8cddf7c59872cc Mon Sep 17 00:00:00 2001 From: Julia Hoang Date: Fri, 10 Jul 2026 11:13:59 -0700 Subject: [PATCH 21/30] chore: improve website demo file content --- libraries/website_adoc_demo.adoc | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/libraries/website_adoc_demo.adoc b/libraries/website_adoc_demo.adoc index 00291cdd1..2302a98c6 100644 --- a/libraries/website_adoc_demo.adoc +++ b/libraries/website_adoc_demo.adoc @@ -46,12 +46,21 @@ int main() [source,cpp] ---- -#include +#include #include +#include +#include int main() { - boost::demo::parser p; - std::cout << p.run("hello world") << "\n"; + std::string text = "hello,world,from,boost"; + + std::vector parts; + boost::algorithm::split(parts, text, boost::is_any_of(",")); + + for (auto& part : parts) { + boost::algorithm::to_upper(part); + std::cout << part << '\n'; + } } ---- From 3d86f8836446b181eee658ea41be35d07ca475af Mon Sep 17 00:00:00 2001 From: Julia Hoang Date: Fri, 10 Jul 2026 11:37:55 -0700 Subject: [PATCH 22/30] feat: implement autoflow masonry layout --- static/css/v3/library-subpage.css | 76 +++++--------------- templates/v3/libraries/library-subpage.html | 79 ++++++++++----------- 2 files changed, 55 insertions(+), 100 deletions(-) diff --git a/static/css/v3/library-subpage.css b/static/css/v3/library-subpage.css index 350d40562..52d1e311b 100644 --- a/static/css/v3/library-subpage.css +++ b/static/css/v3/library-subpage.css @@ -1,21 +1,12 @@ /* - Library Subpage layout — nested flex columns (independent column heights). + Library Subpage layout. - Three breakpoints: - Mobile < 768px — single stacked column - Tablet 768–1279px — two columns (col-1 + col-2 visible; col-3 hidden) - Desktop ≥ 1280px — three columns (col-3 visible; tablet duplicates hidden) + .card-masonry-top: CSS multi-column masonry. Cards flow and pack into as many + columns as fit (column-width var(--subpage-col-min)), so the layout auto- + rearranges 3 → 2 → 1 columns by available width — no strict breakpoints, no + grid row-stretching gaps. Reading order flows down each column, then across. - Masonry-top columns: - col-1 : About (+ Contributors at tablet only) - col-2 : Quick Start, Install (+ Dependencies at tablet only) - col-3 : Contributors, Dependencies (desktop only) - - Contributors and Dependencies are intentionally duplicated in the DOM so each - breakpoint can render them in the correct column while keeping each column's - height independent (avoids grid row-stretching gaps when About is very tall). - - Masonry-bottom: posts 2/3, mailing 1/3 at tablet and desktop. + .card-masonry-bottom: posts 2/3 + mailing list 1/3 (grid), stacking on mobile. All widths, gaps, and spacing derive from design tokens. */ @@ -53,21 +44,19 @@ body:has(.is_flagship) .library-subpage-container { /* ── Top card masonry ────────────────────────────────────────────────────── */ -/* Outer row: flex so each column gets independent height (no row stretching). */ +/* Multi-column masonry: cards pack into as many columns as fit at this width. + --subpage-col-min is the target column width — with the 1440px container and + 16px gaps it yields 3 columns on desktop, 2 on tablet, 1 on mobile. */ .card-masonry-top { - display: flex; - flex-direction: row; - align-items: flex-start; - gap: var(--space-card); + --subpage-col-min: 22rem; /* Hand-picked value that accommodates 3->2->1 column flow at desktop->tablet->mobile breakpoints. */ + columns: var(--subpage-col-min); + column-gap: var(--space-card); } -/* Each column: flex-column stack of cards, equal width, cards pack to top. */ -.card-masonry-top > .masonry-col { - flex: 1 1 0; - min-width: 0; - display: flex; - flex-direction: column; - gap: var(--space-card); +/* Keep each card whole within a column and space stacked cards vertically. */ +.card-masonry-top > * { + break-inside: avoid; + margin-bottom: var(--space-card); } /* ── Bottom card row (desktop/tablet: posts 2/3, mailing 1/3) ────────────── */ @@ -75,16 +64,13 @@ body:has(.is_flagship) .library-subpage-container { .card-masonry-bottom { display: grid; grid-template-columns: repeat(3, 1fr); - grid-template-areas: - "d d d" - "a a b"; + grid-template-areas: "a a b"; row-gap: var(--space-card); column-gap: var(--space-card); } .card-masonry-bottom .item-a { grid-area: a; } .card-masonry-bottom .item-b { grid-area: b; } -.card-masonry-bottom .item-d { grid-area: d; } /* ── Card max-width overrides ────────────────────────────────────────────── */ @@ -126,23 +112,7 @@ body:has(.is_flagship) .library-subpage-container { overflow-y: auto; } -/* ── Desktop (≥ 1280px): show col-3, hide tablet-only duplicates ─────────── */ - -@media (min-width: 1280px) { - .item-d--tablet { - display: none; - } -} - -/* ── Tablet (768px – 1279px): hide col-3 and its desktop-only duplicates ─── */ - -@media (min-width: 768px) and (max-width: 1279px) { - .card-masonry-top > .masonry-col--3 { - display: none; - } -} - -/* ── Mobile (< 768px): stack columns vertically, hide col-3 duplicates ───── */ +/* ── Mobile (< 768px): tighter padding, stack the bottom row ─────────────── */ @media (max-width: 767px) { .library-subpage-container { @@ -154,19 +124,9 @@ body:has(.is_flagship) .library-subpage-container { padding: var(--space-large) var(--space-default); } - .card-masonry-top { - flex-direction: column; - align-items: stretch; - } - - .card-masonry-top > .masonry-col--3 { - display: none; - } - .card-masonry-bottom { grid-template-columns: 1fr; grid-template-areas: - "d" "a" "b"; } diff --git a/templates/v3/libraries/library-subpage.html b/templates/v3/libraries/library-subpage.html index 2a1a050fc..a0cf5d3f7 100644 --- a/templates/v3/libraries/library-subpage.html +++ b/templates/v3/libraries/library-subpage.html @@ -22,43 +22,47 @@
    - {% if website_adoc.about or website_adoc.designed_for %} -
    - {% if website_adoc.about %} -
    - {% include "v3/includes/_code_block_card.html" with heading="About "|add:object.display_name description=website_adoc.about.blurb code=website_adoc.about.code.code language=website_adoc.about.code.language card_variant="neutral" button_text="Explore examples" button_url=documentation_url|default:"#" button_aria_label="Explore examples for "|add:object.display_name %} -
    - {% endif %} - {% if website_adoc.designed_for %} -
    - {% include "v3/includes/_markdown_card.html" with title="This library is designed for" html=designed_for_html %} -
    - {% endif %} + {% if website_adoc.about %} +
    + {% include "v3/includes/_code_block_card.html" with heading="About "|add:object.display_name description=website_adoc.about.blurb code=website_adoc.about.code.code language=website_adoc.about.code.language card_variant="neutral" button_text="Explore examples" button_url=documentation_url|default:"#" button_aria_label="Explore examples for "|add:object.display_name %}
    {% endif %} -
    -
    - {% include "v3/includes/_quick_start_card.html" with heading="Quick Start" links=quick_start_links %} -
    - {% if website_adoc.install %} -
    - {% include "v3/includes/_code_block_card.html" with heading="Install" description=website_adoc.install.blurb code=website_adoc.install.code.code language=website_adoc.install.code.language card_variant="teal" %} -
    - {% endif %} -
    - {% include "v3/includes/_dependencies_card.html" with dependencies=dependencies_list %} -
    - {% if benchmark_sets %} -
    - {% include "v3/includes/_stats_benchmarks.html" with heading="Benchmarks" sets=benchmark_sets %} -
    - {% endif %} + + {% if benchmark_sets %} +
    + {% include "v3/includes/_stats_benchmarks.html" with heading="Benchmarks" sets=benchmark_sets %}
    -
    -
    - {% include "v3/includes/_contributors_list.html" with title="Contributors: This Release" variant="release" contributors=this_release_contributors heading_level="2" %} -
    + {% endif %} + +
    + {% include "v3/includes/_quick_start_card.html" with heading="Quick Start" links=quick_start_links %}
    + +
    + {% include "v3/includes/_dependencies_card.html" with dependencies=dependencies_list %} +
    + + {% if website_adoc.designed_for %} +
    + {% include "v3/includes/_markdown_card.html" with title="This library is designed for" html=designed_for_html %} +
    + {% endif %} + + {% if website_adoc.install %} +
    + {% include "v3/includes/_code_block_card.html" with heading="Install" description=website_adoc.install.blurb code=website_adoc.install.code.code language=website_adoc.install.code.language card_variant="teal" %} +
    + {% endif %} + +
    + {% include "v3/includes/_contributors_list.html" with title="Contributors: This Release" variant="release" contributors=this_release_contributors heading_level="2" %} +
    + + {% if website_adoc.freeform %} +
    + {% include "v3/includes/_markdown_card.html" with title=website_adoc.freeform.heading html=website_adoc.freeform.html %} +
    + {% endif %}
    {% if description %} @@ -67,16 +71,7 @@
    {% endif %} - {% if website_adoc.freeform %} -
    - {% include "v3/includes/_markdown_card.html" with title=website_adoc.freeform.heading html=website_adoc.freeform.html %} -
    - {% endif %} -
    -
    - {% include "v3/includes/_contributors_list.html" with title="Contributors: This Release" variant="all" contributors=this_release_contributors heading_level="2" %} -
    {% include "v3/includes/_post_card.html" with heading="Latest "|add:object.display_name|add:" posts" items=library_posts variant="card" theme="teal" layout="horizontal" primary_cta_label="View all posts" primary_cta_url="/news/" %}
    From 26ececbfda42f9824a7d895a6d9cf6778698ee6d Mon Sep 17 00:00:00 2001 From: Julia Hoang Date: Fri, 10 Jul 2026 11:44:11 -0700 Subject: [PATCH 23/30] chore: renamed added_text template param to first_boost_version --- templates/v3/examples/_v3_example_section.html | 6 +++--- templates/v3/includes/_hero_library.html | 10 +++++----- templates/v3/libraries/library-subpage.html | 6 ++---- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/templates/v3/examples/_v3_example_section.html b/templates/v3/examples/_v3_example_section.html index ef06c0b81..12baf65df 100644 --- a/templates/v3/examples/_v3_example_section.html +++ b/templates/v3/examples/_v3_example_section.html @@ -89,10 +89,10 @@

    {{ section_title }}

    {% include "v3/includes/_hero_home.html" with install_card_id="hero-home-2" hero_image_url=hero_image_url %} {% include "v3/includes/_hero_home.html" with install_card_id="hero-home-3" hero_image_url="" hero_image_url_light="" hero_image_url_dark="" %}
    - {% include "v3/includes/_hero_library.html" with hero_background_image_url="" title="Boost.Beast" description="Portable HTTP, WebSocket, and network operations using only C++11 and Boost.Asio" doc_url="#" source_url="#" slack_url="#" github_url="#" version_tag="C++ 03" added_text="Added in 1.66.0" hero_image_url_light=hero_legacy_image_url_light hero_image_url_dark=hero_legacy_image_url_dark is_flagship_lib=True %} + {% include "v3/includes/_hero_library.html" with hero_background_image_url="" title="Boost.Beast" description="Portable HTTP, WebSocket, and network operations using only C++11 and Boost.Asio" doc_url="#" source_url="#" slack_url="#" github_url="#" version_tag="C++ 03" first_boost_version="1.66.0" hero_image_url_light=hero_legacy_image_url_light hero_image_url_dark=hero_legacy_image_url_dark is_flagship_lib=True %}
    - {% include "v3/includes/_hero_library.html" with title="Boost.Beast" description="Portable HTTP, WebSocket, and network operations using only C++11 and Boost.Asio" doc_url="#" source_url="#" slack_url="#" github_url="#" version_tag="C++ 03" added_text="Added in 1.66.0" hero_image_url=hero_image_url is_flagship_lib=True %} - {% include "v3/includes/_hero_library.html" with title="Boost.Beast" description="Portable HTTP, WebSocket, and network operations using only C++11 and Boost.Asio" doc_url="#" source_url="#" slack_url="#" github_url="#" version_tag="C++ 03" added_text="Added in 1.66.0" hero_image_url="" hero_image_url_light="" hero_image_url_dark="" %} + {% include "v3/includes/_hero_library.html" with title="Boost.Beast" description="Portable HTTP, WebSocket, and network operations using only C++11 and Boost.Asio" doc_url="#" source_url="#" slack_url="#" github_url="#" version_tag="C++ 03" first_boost_version="1.66.0" hero_image_url=hero_image_url is_flagship_lib=True %} + {% include "v3/includes/_hero_library.html" with title="Boost.Beast" description="Portable HTTP, WebSocket, and network operations using only C++11 and Boost.Asio" doc_url="#" source_url="#" slack_url="#" github_url="#" version_tag="C++ 03" first_boost_version="1.66.0" hero_image_url="" hero_image_url_light="" hero_image_url_dark="" %}
    {% endwith %} diff --git a/templates/v3/includes/_hero_library.html b/templates/v3/includes/_hero_library.html index 23469cb28..e0bb4e1e5 100644 --- a/templates/v3/includes/_hero_library.html +++ b/templates/v3/includes/_hero_library.html @@ -8,7 +8,7 @@ description (optional) — Short description. category_tags (optional) — List of dicts with "label" and "url". Omit to hide tag chips. version_tag (optional) — e.g. "C++ 03". Omit to hide. - added_text (optional) — e.g. "Added in 1.66.0". Omit to hide. + first_boost_version (optional) — Version display name only, e.g. "1.66.0" (rendered as "Added in {version}"). Omit to hide. doc_url (optional) — Documentation link URL. Omit to hide. source_url (optional) — Source code link URL. Omit to hide. slack_url (optional) — Discuss in Slack URL. Omit to hide. @@ -18,7 +18,7 @@ hero_background_image_url (optional) — Background image URL for the hero container. is_flagship_lib (optional) — Boolean value stating whether or not this is the hero for a flagship library. If it isn't, styles are a bit different. - If none of category_tags/version_tag/added_text are passed, the tags row is + If none of category_tags/version_tag/first_boost_version are passed, the tags row is hidden. If none of doc_url/source_url/slack_url/github_url/rss_url are passed, the links row is hidden. @@ -37,7 +37,7 @@
    - {% if category_tags or version_tag or added_text %} + {% if category_tags or version_tag or first_boost_version %}
    {% for tag in category_tags %} {% include "v3/includes/_category_tag.html" with tag_label=tag.label url=tag.url|default:"#" variant="neutral" size="default" %} @@ -45,9 +45,9 @@ {% if version_tag %} {{ version_tag }} {% endif %} - {% if added_text %} + {% if first_boost_version %}
    - Added in {{ added_text }} + Added in {{ first_boost_version }}
    {% endif %}
    diff --git a/templates/v3/libraries/library-subpage.html b/templates/v3/libraries/library-subpage.html index a0cf5d3f7..d355bcf97 100644 --- a/templates/v3/libraries/library-subpage.html +++ b/templates/v3/libraries/library-subpage.html @@ -11,13 +11,11 @@ {% block content %} {% with cpp_ver=library_version.get_cpp_standard_minimum_display|default:"C++03" %} - {% with added_text=object.first_boost_version.display_name %} {% if is_flagship_lib %} - {% include "v3/includes/_hero_library.html" with title=object.display_name description=library_version.description|default:object.description category_tags=category_tags_v3 version_tag=cpp_ver added_text=added_text doc_url=documentation_url source_url=github_url slack_url=slack_url github_url=object.github_issues_url is_flagship_lib=True hero_image_url_light=library_hero_image_url_light hero_image_url_dark=library_hero_image_url_dark %} + {% include "v3/includes/_hero_library.html" with title=object.display_name description=library_version.description|default:object.description category_tags=category_tags_v3 version_tag=cpp_ver first_boost_version=object.first_boost_version.display_name doc_url=documentation_url source_url=github_url slack_url=slack_url github_url=object.github_issues_url is_flagship_lib=True hero_image_url_light=library_hero_image_url_light hero_image_url_dark=library_hero_image_url_dark %} {% else %} - {% include "v3/includes/_hero_library.html" with title=object.display_name description=library_version.description|default:object.description category_tags=category_tags_v3 version_tag=cpp_ver added_text=added_text doc_url=documentation_url source_url=github_url slack_url=slack_url github_url=object.github_issues_url %} + {% include "v3/includes/_hero_library.html" with title=object.display_name description=library_version.description|default:object.description category_tags=category_tags_v3 version_tag=cpp_ver first_boost_version=object.first_boost_version.display_name doc_url=documentation_url source_url=github_url slack_url=slack_url github_url=object.github_issues_url %} {% endif %} - {% endwith %} {% endwith %}
    From 47814344c8fbda1f9a230a6ded0cf502b11b22ae Mon Sep 17 00:00:00 2001 From: Julia Hoang Date: Fri, 10 Jul 2026 11:49:28 -0700 Subject: [PATCH 24/30] style: improve card ordering on mobile view --- static/css/v3/library-subpage.css | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/static/css/v3/library-subpage.css b/static/css/v3/library-subpage.css index 52d1e311b..68ff70ea7 100644 --- a/static/css/v3/library-subpage.css +++ b/static/css/v3/library-subpage.css @@ -124,6 +124,22 @@ body:has(.is_flagship) .library-subpage-container { padding: var(--space-large) var(--space-default); } + /* Single column on mobile — use flex so `order` controls card sequence + (multi-column children don't respond to `order`). */ + .card-masonry-top { + display: flex; + flex-direction: column; + } + + .card-masonry-top .item-a { order: 1; } /* About */ + .card-masonry-top .item-b { order: 2; } /* Quick Start */ + .card-masonry-top .item-c { order: 3; } /* Install */ + .card-masonry-top .item-g { order: 4; } /* Benchmarks */ + .card-masonry-top .item-e { order: 5; } /* Dependencies */ + .card-masonry-top .item-d { order: 6; } /* Contributors: This Release */ + .card-masonry-top .item-f { order: 7; } /* Designed for */ + .card-masonry-top .library-subpage__freeform { order: 8; } /* Freeform */ + .card-masonry-bottom { grid-template-columns: 1fr; grid-template-areas: From e576330f83177209407deb94551a97aa59574263 Mon Sep 17 00:00:00 2001 From: Julia Hoang Date: Fri, 10 Jul 2026 12:10:14 -0700 Subject: [PATCH 25/30] style: add missing green theme to the benchmark card --- templates/v3/libraries/library-subpage.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/v3/libraries/library-subpage.html b/templates/v3/libraries/library-subpage.html index d355bcf97..4153df5b6 100644 --- a/templates/v3/libraries/library-subpage.html +++ b/templates/v3/libraries/library-subpage.html @@ -28,7 +28,7 @@ {% if benchmark_sets %}
    - {% include "v3/includes/_stats_benchmarks.html" with heading="Benchmarks" sets=benchmark_sets %} + {% include "v3/includes/_stats_benchmarks.html" with heading="Benchmarks" sets=benchmark_sets theme="green"%}
    {% endif %} From 1cdb5c6a4c48d41d360b623746f5f86e99a533c2 Mon Sep 17 00:00:00 2001 From: Julia Hoang Date: Fri, 10 Jul 2026 12:25:27 -0700 Subject: [PATCH 26/30] feat: add placeholder for library subpage empty state --- libraries/views.py | 5 +++++ templates/v3/libraries/library-subpage.html | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/libraries/views.py b/libraries/views.py index db7c8b946..5f6b05e3a 100644 --- a/libraries/views.py +++ b/libraries/views.py @@ -540,6 +540,11 @@ def get_context_data(self, **kwargs): library=self.object, version=context["selected_version"] ) except LibraryVersion.DoesNotExist: + # No LibraryVersion for the selected release (e.g. viewing a version + # older than the library's first release). Flag it so the v3 template + # renders a placeholder instead of an empty subpage. + # TODO: replace with a designed empty-state (separate ticket). + context["library_version_missing"] = True return context context["library_version"] = library_version diff --git a/templates/v3/libraries/library-subpage.html b/templates/v3/libraries/library-subpage.html index 4153df5b6..6969e1eb7 100644 --- a/templates/v3/libraries/library-subpage.html +++ b/templates/v3/libraries/library-subpage.html @@ -10,6 +10,13 @@ {% block content %} +{% if library_version_missing %} + {% comment %} TODO: Extract this to a separate template & implement UI for empty state {% endcomment %} +
    +

    No library records available for this version.

    +

    There is no version of the {{ object.display_name }} library for Boost {{ selected_version.display_name }}. The first release of {{ object.display_name }} library was version {{ object.first_boost_version.display_name }}.

    +
    +{% else %} {% with cpp_ver=library_version.get_cpp_standard_minimum_display|default:"C++03" %} {% if is_flagship_lib %} {% include "v3/includes/_hero_library.html" with title=object.display_name description=library_version.description|default:object.description category_tags=category_tags_v3 version_tag=cpp_ver first_boost_version=object.first_boost_version.display_name doc_url=documentation_url source_url=github_url slack_url=slack_url github_url=object.github_issues_url is_flagship_lib=True hero_image_url_light=library_hero_image_url_light hero_image_url_dark=library_hero_image_url_dark %} @@ -84,4 +91,5 @@
    {% endif %}
    +{% endif %} {% endblock %} From 10d70d1e751b41c5d049af408ee4c0d3743c991a Mon Sep 17 00:00:00 2001 From: Julia Hoang Date: Mon, 13 Jul 2026 09:44:54 -0700 Subject: [PATCH 27/30] style: nit adjustment to card order --- templates/v3/libraries/library-subpage.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/templates/v3/libraries/library-subpage.html b/templates/v3/libraries/library-subpage.html index 6969e1eb7..5656fe6ce 100644 --- a/templates/v3/libraries/library-subpage.html +++ b/templates/v3/libraries/library-subpage.html @@ -53,6 +53,12 @@

    No library records available for this vers

    {% endif %} + {% if website_adoc.freeform %} +
    + {% include "v3/includes/_markdown_card.html" with title=website_adoc.freeform.heading html=website_adoc.freeform.html %} +
    + {% endif %} + {% if website_adoc.install %}
    {% include "v3/includes/_code_block_card.html" with heading="Install" description=website_adoc.install.blurb code=website_adoc.install.code.code language=website_adoc.install.code.language card_variant="teal" %} @@ -62,12 +68,6 @@

    No library records available for this vers
    {% include "v3/includes/_contributors_list.html" with title="Contributors: This Release" variant="release" contributors=this_release_contributors heading_level="2" %}
    - - {% if website_adoc.freeform %} -
    - {% include "v3/includes/_markdown_card.html" with title=website_adoc.freeform.heading html=website_adoc.freeform.html %} -
    - {% endif %}

    {% if description %} From 28804edb5e63c52cc7287b62bbcb6dc97a649c54 Mon Sep 17 00:00:00 2001 From: Julia Hoang Date: Mon, 13 Jul 2026 09:53:46 -0700 Subject: [PATCH 28/30] chore: update fetch source ref from master to develop branch --- core/githubhelper.py | 2 +- .../commands/import_library_version_website_adoc.py | 4 ++-- libraries/tasks.py | 10 +++++----- libraries/tests/test_tasks.py | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/core/githubhelper.py b/core/githubhelper.py index 81d399769..4fe1f3fcf 100644 --- a/core/githubhelper.py +++ b/core/githubhelper.py @@ -331,7 +331,7 @@ def get_libraries_json(self, repo_slug: str, tag: str = "master"): else: return response.json() - def get_website_adoc(self, repo_slug: str, tag: str = "master"): + def get_website_adoc(self, repo_slug: str, tag: str = "develop"): """Retrieve a library's optional 'meta/website.adoc'. Many libraries might not ship this file, so it will returns None quietly if not found. diff --git a/libraries/management/commands/import_library_version_website_adoc.py b/libraries/management/commands/import_library_version_website_adoc.py index 9f7312c09..d9c337cf7 100644 --- a/libraries/management/commands/import_library_version_website_adoc.py +++ b/libraries/management/commands/import_library_version_website_adoc.py @@ -28,7 +28,7 @@ def command(release: str, new: bool, min_version: str): """Fetch, parse, and store each library's meta/website.adoc for the targeted Boost versions. - The most recent version is fetched from `master` (freshest maintainer content, + The most recent version is fetched from `develop` (freshest maintainer content, matching the daily task); older versions are fetched from their release tag (the frozen snapshot, matching release import). A repo without the file is left untouched. @@ -48,7 +48,7 @@ def command(release: str, new: bool, min_version: str): versions = list(version_qs.order_by("-name")) for version in versions: - ref = "master" if version == most_recent else version.name + ref = "develop" if version == most_recent else version.name click.secho(f"Processing {version.name} (ref={ref})...", fg="green") store_library_version_website_adoc(version, ref=ref) diff --git a/libraries/tasks.py b/libraries/tasks.py index f556fe7cc..e6bb0026b 100644 --- a/libraries/tasks.py +++ b/libraries/tasks.py @@ -57,12 +57,12 @@ def update_library_version_documentation_urls_all_versions(): def update_library_version_website_adoc(): """Refresh parsed meta/website.adoc for the current release. - Scoped to the most recent version and fetched from `master` so maintainer + Scoped to the most recent version and fetched from `develop` so maintainer edits between releases are picked up. Historical versions keep the snapshot captured at their release import — a tagged release's meta/website.adoc is immutable, so re-fetching every version daily would be redundant. - Skipped while a newer release is in beta: `master` has already drifted toward + Skipped while a newer release is in beta: `develop` has already drifted toward that release, so refreshing the current stable from it would surface pre-release content on the stable page. The stable keeps its release-tag import snapshot until the beta becomes the full release. @@ -70,14 +70,14 @@ def update_library_version_website_adoc(): version = Version.objects.most_recent() if version is None: return - # During a beta cycle for the NEXT release, each library's `master` has + # During a beta cycle for the NEXT release, each library's `develop` has # already drifted toward that release, so refreshing the current stable from - # master would show it pre-release content. Hold until the beta is promoted + # develop would show it pre-release content. Hold until the beta is promoted # to a full release. beta = Version.objects.most_recent_beta() if beta and beta.cleaned_version_parts > version.cleaned_version_parts: return - store_library_version_website_adoc(version, ref="master") + store_library_version_website_adoc(version, ref="develop") def store_library_version_website_adoc(version, ref): diff --git a/libraries/tests/test_tasks.py b/libraries/tests/test_tasks.py index f33330131..cffa59341 100644 --- a/libraries/tests/test_tasks.py +++ b/libraries/tests/test_tasks.py @@ -127,8 +127,8 @@ def test_version_missing_docs(version, version_name, expected): def test_update_library_version_website_adoc_beta_guard(beta_name, expect_refresh): """The daily website.adoc refresh holds only while a NEWER release is in beta. - A newer beta means each library's `master` has drifted toward the next - release, so refreshing the current stable from `master` would surface + A newer beta means each library's `develop` has drifted toward the next + release, so refreshing the current stable from `develop` would surface pre-release content on the stable page. """ stable = baker.make( @@ -153,7 +153,7 @@ def test_update_library_version_website_adoc_beta_guard(beta_name, expect_refres update_library_version_website_adoc() if expect_refresh: - mock_store.assert_called_once_with(stable, ref="master") + mock_store.assert_called_once_with(stable, ref="develop") else: mock_store.assert_not_called() From c6eaf617b82017f21749f4626c8eaeb20a1a773d Mon Sep 17 00:00:00 2001 From: Julia Hoang Date: Tue, 21 Jul 2026 14:58:37 -0700 Subject: [PATCH 29/30] doc: add regex pattern examples --- libraries/website_adoc.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libraries/website_adoc.py b/libraries/website_adoc.py index e0041ae42..90427bf5a 100644 --- a/libraries/website_adoc.py +++ b/libraries/website_adoc.py @@ -18,9 +18,13 @@ logger = structlog.get_logger() +# Section anchor, e.g. "[#about]" -> "about" _ANCHOR_RE = re.compile(r"^\[#([a-z0-9-]+)\]\s*$") +# Attribute line, e.g. ":library-key: beast" -> ("library-key", "beast") _ATTR_RE = re.compile(r"^:([a-z0-9-]+):\s*(.*)$") +# Source block header, e.g. "[source,cpp]" or "[source%linenums,c++]" -> "cpp" / "c++" _SOURCE_RE = re.compile(r"^\[source(?:%[^\],]*)?(?:,\s*([a-z0-9+#-]+))?.*\]\s*$") +# Section heading, e.g. "== Description" -> "Description" _HEADING_RE = re.compile(r"^=+\s+(.*\S)\s*$") From 3b9143aa3514d847c158b6610175918b0bb69802 Mon Sep 17 00:00:00 2001 From: Julia Hoang Date: Tue, 21 Jul 2026 14:59:20 -0700 Subject: [PATCH 30/30] fix: ensure versions are sorted by numerical value instead of name --- .../import_library_version_website_adoc.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/libraries/management/commands/import_library_version_website_adoc.py b/libraries/management/commands/import_library_version_website_adoc.py index d9c337cf7..8fedde977 100644 --- a/libraries/management/commands/import_library_version_website_adoc.py +++ b/libraries/management/commands/import_library_version_website_adoc.py @@ -33,19 +33,29 @@ def command(release: str, new: bool, min_version: str): frozen snapshot, matching release import). A repo without the file is left untouched. """ + # Order/compare on the numeric version_array so boost-1.100.0 > boost-1.71.0 + # (plain name ordering is lexicographic and breaks once minor/patch hits 100). + min_version_parts = [int(part) for part in min_version.split(".")] version_qs = ( Version.objects.with_partials() .active() - .filter(name__gte=f"boost-{min_version}") + .with_version_split() + .filter(version_array__gte=min_version_parts) + ) + most_recent = ( + version_qs.filter(beta=False, full_release=True) + .order_by("-version_array") + .first() ) - most_recent = version_qs.most_recent() if release: - versions = list(version_qs.filter(name__icontains=release).order_by("-name")) + versions = list( + version_qs.filter(name__icontains=release).order_by("-version_array") + ) elif new: versions = [most_recent] if most_recent else [] else: - versions = list(version_qs.order_by("-name")) + versions = list(version_qs.order_by("-version_array")) for version in versions: ref = "develop" if version == most_recent else version.name