", "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_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) == [
+ {"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/tests/test_website_adoc.py b/libraries/tests/test_website_adoc.py
new file mode 100644
index 000000000..8bf2bb8ca
--- /dev/null
+++ b/libraries/tests/test_website_adoc.py
@@ -0,0 +1,310 @@
+from pathlib import Path
+
+import pytest
+
+from libraries.website_adoc import (
+ build_website_adoc,
+ parse_website_adoc,
+ website_adoc_fields,
+ website_adoc_section_statuses,
+)
+
+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
+
+[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["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_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 = (
+ 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_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"
+ "[source,bash]\n----\nmake\n----\n"
+ )
+ assert set(parsed) == {"library_key", "install"}
+ 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
+
+ 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, label, boom if sid == "benchmarks" else fn)
+ for out, sid, label, 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/utils.py b/libraries/utils.py
index 0b45fa3fb..c2e07f9db 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,47 @@ 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 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 d1561543c..5f6b05e3a 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
@@ -13,9 +15,8 @@
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
from mailing_list.mixins import MailingListCardMixin
from core.mock_data import SharedResources
@@ -25,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,
@@ -47,6 +49,8 @@
get_commit_data_by_release_for_library,
commit_data_to_stats_bars,
group_libraries_by_tier,
+ designed_for_html,
+ benchmark_sets,
)
from .constants import LATEST_RELEASE_URL_PATH_STR
@@ -56,16 +60,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):
@@ -83,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):
@@ -479,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
@@ -514,42 +580,35 @@ 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
- 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
- context["slack_url"] = SLACK_URL
+ 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")
+ )
+ 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"] = [
- {
- "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")
]
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.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)
- 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,
@@ -557,30 +616,21 @@ 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
)
- all_time = [
- a.to_v3_profile_dict("Contributor")
- for a in base_context.get("previous_contributors", [])
- ]
+ all_time = (
+ self.build_all_contributors(
+ library_version,
+ context.get("authors", []),
+ context.get("maintainers", []),
+ )
+ if library_version
+ else []
+ )
context["all_time_contributors"] = (
apply_collective_author_overrides(all_time)
or SharedResources.library_all_contributors
diff --git a/libraries/website_adoc.py b/libraries/website_adoc.py
new file mode 100644
index 000000000..90427bf5a
--- /dev/null
+++ b/libraries/website_adoc.py
@@ -0,0 +1,414 @@
+"""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()
+
+# 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*$")
+
+
+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")),
+ "data": data,
+ }
+ )
+ return result
+
+
+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 not in_source and 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
+
+
+# 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", "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.
+
+ 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.
+
+ 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
+ if isinstance(content, bytes):
+ content = content.decode("utf-8", errors="replace")
+ if not content.strip():
+ return None
+
+ 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, _label, builder in _SECTION_BUILDERS:
+ if section_id not in sections:
+ 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
+
+
+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)
+ parsed.setdefault("_warnings", []).append(
+ {"section": "freeform", "reason": "render_error"}
+ )
+ return parsed or None
+
+
+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),
+ }
+
+
+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:
+ 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 {"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..2302a98c6
--- /dev/null
+++ b/libraries/website_adoc_demo.adoc
@@ -0,0 +1,134 @@
+= 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
+#include
+#include
+
+int main() {
+ 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';
+ }
+}
+----
+
+[#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
+
+[cols="2,1",options="header"]
+|===
+| Label | Value
+| Boost.Demo | 1200000
+| Alternative A | 800000
+| Alternative B | 450000
+|===
+
+[#benchmarks-latency]
+=== Latency
+:unit: microseconds
+
+[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/libraries/website_adoc_template.adoc b/libraries/website_adoc_template.adoc
new file mode 100644
index 000000000..0427dba86
--- /dev/null
+++ b/libraries/website_adoc_template.adoc
@@ -0,0 +1,127 @@
+////
+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)
+// Table columns: Label | Value (plain number)
+
+[#benchmarks-]
+===
+:unit:
+
+[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/static/css/v3/library-subpage.css b/static/css/v3/library-subpage.css
index ec6800494..68ff70ea7 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 ────────────────────────────────────────────── */
@@ -96,32 +82,37 @@ 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,
.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%;
}
-/* ── Desktop (≥ 1280px): show col-3, hide tablet-only duplicates ─────────── */
-
-@media (min-width: 1280px) {
- .item-d--tablet {
- display: none;
- }
+/* 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;
}
-/* ── Tablet (768px – 1279px): hide col-3 and its desktop-only duplicates ─── */
+.library-subpage__documentation > .markdown-card {
+ max-height: 572px;
+ display: flex;
+ flex-direction: column;
+}
-@media (min-width: 768px) and (max-width: 1279px) {
- .card-masonry-top > .masonry-col--3 {
- display: none;
- }
+.library-subpage__documentation > .markdown-card .markdown-content {
+ min-height: 0;
+ overflow-y: auto;
}
-/* ── Mobile (< 768px): stack columns vertically, hide col-3 duplicates ───── */
+/* ── Mobile (< 768px): tighter padding, stack the bottom row ─────────────── */
@media (max-width: 767px) {
.library-subpage-container {
@@ -133,19 +124,25 @@ 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;
- align-items: stretch;
}
- .card-masonry-top > .masonry-col--3 {
- display: none;
- }
+ .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:
- "d"
"a"
"b";
}
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/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/templates/v3/examples/_v3_example_section.html b/templates/v3/examples/_v3_example_section.html
index 06d00bb9a..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 %}
@@ -630,6 +630,28 @@ {{ 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 %}
+
+ {% 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/_hero_library.html b/templates/v3/includes/_hero_library.html
index 366e3be51..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 @@