Improve documentation: byte layouts, release pages, See also, dark-mode images (#5906)

- Make the iterator range illustrations legible in dark mode (prefers-color-scheme
  rules inside the SVGs).
- Add a "Byte layout" section with a color-coded byte table to each binary format
  page (BJData, BON8, BSON, CBOR, MessagePack, UBJSON), and highlight the smallest
  size per file in the comparison table.
- Add "See also" sections to the overview pages that lacked one.
- Split the release notes into one page per release, grouped by minor version on
  the overview page, with an "API changes" section per release generated from the
  API pages' version histories (hooks/api_changes.py).
- Fact-check the release notes and version histories against the release tags and
  correct wrong or misleading entries (e.g., json_serializer 2.1.0, merge_patch
  3.1.0, items, count, operator[], to_bjdata, std::string_view key support).

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
Niels Lohmann authored and GitHub committed 2026-10-24 17:57:44 +02:00
1 parent 01139dc192
commit 52b72b5684
115 files changed
+1462 -546

No files matched your search

+163
View File
@@ -0,0 +1,163 @@
"""Generate the "API changes" section of the release pages from the version histories of the API pages."""
# Every page below docs/api ends in a "Version history" section ("Added in version 3.8.0.", "... since version 3.13.0.").
# This hook collects these entries and replaces the `<!-- api-changes -->` marker of each page in home/releases with
# the pages that were added in that release and the changes made to existing pages. The page of a release is named
# after its version (v3.12.0.md); unreleased.md collects all versions newer than the latest release. The result is an
# overview of what changed in the API, not a change log.
import logging
import os
import posixpath
import re
log = logging.getLogger("mkdocs.hooks.api_changes")
_DIR = "home/releases"
_PAGE_VERSION = re.compile(r"v(\d+)\.(\d+)\.(\d+)\.md")
_MARKER = "<!-- api-changes -->"
_HISTORY = re.compile(r"^## Version history\n(.*?)(?=^## |\Z)", re.DOTALL | re.MULTILINE)
_TITLE = re.compile(r"^# (.+)$", re.MULTILINE)
_ITEM = re.compile(r"^(?:[-*]|\d+\.)\s+")
_SENTENCE = re.compile(r"(?<=\.)\s+(?=[A-Z`\[])")
_VERSION = re.compile(r"\b(?:version|since|in)\s+(\d+)\.(\d+)\.(\d+)\b", re.IGNORECASE)
_FUTURE = re.compile(r"\bwill\b|\bplanned\b", re.IGNORECASE) # statements about versions that do not exist yet
_NEXT_MAJOR = (4, 0, 0) # versions from here on are announcements, not changes
_ADDED = re.compile(r"^(?:Added|Introduced)\b.*\bversion \d|^Since version \d+\.\d+\.\d+\.?$", re.IGNORECASE)
_LINK = re.compile(r"(\]\()([^)\s]+)(\))")
def on_page_markdown(markdown, *, page, config, files):
uri = page.file.src_uri
if posixpath.dirname(uri) != _DIR or _MARKER not in markdown:
return markdown
docs_dir = config["docs_dir"]
entries = _collect(docs_dir)
released = sorted(tuple(map(int, m.groups())) for m in
(_PAGE_VERSION.fullmatch(name) for name in os.listdir(os.path.join(docs_dir, _DIR))) if m)
match = _PAGE_VERSION.fullmatch(posixpath.basename(uri))
if match:
versions = [tuple(map(int, match.groups()))]
else: # unreleased.md
versions = sorted((v for v in entries if not released or v > released[-1]), reverse=True)
return markdown.replace(_MARKER, _render(uri, [(v, entries[v]) for v in versions if v in entries]))
def _title(text, path):
match = _TITLE.search(text)
title = match.group(1) if match else os.path.splitext(os.path.basename(path))[0]
title = re.sub(r"</?small>", "", title)
title = title.replace("`", "").replace("\\", "").strip()
return title[len("nlohmann::"):] if title.startswith("nlohmann::") else title
def _collect(docs_dir):
"""Return {version: {"added": [(title, uri)], "changed": [(title, uri, text)]}}."""
result = {}
api = os.path.join(docs_dir, "api")
for root, _, names in sorted(os.walk(api)):
for name in sorted(names):
if not name.endswith(".md"):
continue
path = os.path.join(root, name)
uri = posixpath.relpath(path.replace(os.sep, "/"), docs_dir.replace(os.sep, "/"))
with open(path, encoding="utf-8") as source:
text = source.read()
history = _HISTORY.search(text)
if not history:
continue
title = _title(text, path)
found = _sentences(history.group(1))
# a page is added in the earliest version named by an "Added in" sentence that opens an entry (an entry of
# a numbered list describes one overload; later unnumbered entries extend an existing page)
starts = [v for i, n, first, v, sentence in found if first and _ADDED.match(sentence) and (i == 0 or n)]
added = min(starts) if starts else None
page_added = False
for index, number, first, version, sentence in found:
if version >= _NEXT_MAJOR:
continue
bucket = result.setdefault(version, {"added": [], "changed": []})
opens = first and _ADDED.match(sentence) and (index == 0 or number)
if opens and version == added:
if not page_added:
bucket["added"].append((title, uri))
page_added = True
continue
if number and re.fullmatch(r"Added in version [\d.]+\.?", sentence):
sentence = f"Overload {number} added."
elif number:
sentence = f"Overload {number}: {sentence}"
if (title, uri, sentence) not in bucket["changed"]:
bucket["changed"].append((title, uri, sentence))
return result
def _sentences(section):
"""Yield (entry index, overload number, opens entry, version, sentence) for every sentence naming a version."""
found = []
for index, (number, entry) in enumerate(_entries(section)):
for position, sentence in enumerate(_SENTENCE.split(entry)):
version = _VERSION.search(sentence)
if not version or _FUTURE.search(sentence):
continue
found.append((index, number, position == 0, tuple(int(part) for part in version.groups()), sentence.strip()))
return found
def _entries(section):
entries = []
for line in section.split("\n"):
item = _ITEM.match(line)
if item:
number = re.match(r"\d+", line)
entries.append([number.group(0) if number else None, _ITEM.sub("", line).strip()])
elif line.strip() and entries and line[0].isspace():
entries[-1][1] += " " + line.strip() # wrapped list item
elif line.strip():
break # text after the list, such as an admonition
return [tuple(entry) for entry in entries]
def _relink(text, uri, page):
"""Rewrite links relative to the API page `uri` so they work from the generated page."""
def fix(match):
target = match.group(2)
if re.match(r"[a-z]+:|#|/", target):
return match.group(0)
resolved = posixpath.normpath(posixpath.join(posixpath.dirname(uri), target))
return match.group(1) + posixpath.relpath(resolved, posixpath.dirname(page)) + match.group(3)
return _LINK.sub(fix, text)
def _link(title, uri, page):
return f"[`{title}`]({posixpath.relpath(uri, posixpath.dirname(page))})"
def _added(pages, page):
"""Group the added pages by class: `basic_json`: [`get`](..), ...; pages without a class come first."""
groups, single = {}, []
for title, uri in pages:
directory = posixpath.dirname(uri)
if directory == "api":
single.append(_link(title, uri, page))
elif posixpath.basename(uri) == "index.md":
groups.setdefault(directory, []).insert(0, f"[(overview)]({posixpath.relpath(uri, posixpath.dirname(page))})")
else:
groups.setdefault(directory, []).append(_link(title.split("::")[-1], uri, page))
lines = [f"- {item}" for item in single]
for directory, items in groups.items():
lines.append(f"- `{posixpath.basename(directory)}`: " + ", ".join(items))
return lines
def _render(page, entries):
lines = []
for version, bucket in entries:
if len(entries) > 1:
lines += [f"### Version {'.'.join(map(str, version))}", ""]
if bucket["added"]:
lines += [f"**Added ({len(bucket['added'])} pages):**", ""] + _added(bucket["added"], page) + [""]
if bucket["changed"]:
lines += ["**Changed:**", ""]
lines += [f"- {_link(t, u, page)}: {_relink(x, u, page)}" for t, u, x in bucket["changed"]] + [""]
return "\n".join(lines) if lines else "No documented API changes."