mirror of
https://github.com/nlohmann/json.git
synced 2026-10-28 16:55:41 +00:00
docs: new design for the documentation site (#5934)
* docs: new design for the documentation site Apply the brand to the Material for MkDocs site: Inter and JetBrains Mono served by the site (upright and real italic), brand colours, blue header band with the syntax pattern, wordmark in the header, new home page (hero, cards, animated feature tour), 404 page, polish for tabs, search, footer, focus rings, tables and code blocks, and theme features (code copy, previous/next footer, TOC follow, search suggestions). Draw the number railroad diagram, the parser callback events and the iterator illustrations as inline SVGs that follow the light and dark scheme. The generator scripts are in docs/mkdocs/scripts/figures; the feature slideshow (json-light.svg, json-dark.svg, replacing json.gif on the home page) is built by docs/mkdocs/scripts/slideshow. Decision nodes in five flowcharts become hexagons, which need far less room. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * docs: add figures, table captions and a macro chooser - Figures with numbered captions: SAX stepper, json_view layout and edit stepper, exception map, JSON pointer and patch walkers, QA pipeline, scaling and binary size charts, roadmap lanes, release timeline, integration decision tree and namespace builder - Table captions (nj-table-caption) on 9 pages - Compile times: modules and PCH charts, filesystem section, re-measured Figure 1, links between features/modules.md and compile_times.md - Interactive macro chooser for features/arbitrary_types.md - Link check: exclude URLs that answer the plugin with 400/401/419 or are dead (customers.md) Signed-off-by: Niels Lohmann <mail@nlohmann.me> * docs: macro chooser generates adl_serializer for non-default-constructible types Signed-off-by: Niels Lohmann <mail@nlohmann.me> * docs: cross-link concept pages and API pages in See also sections Add See also links in both directions between the concept pages (features, home, integration) and the API pages, write API links in code font, give every entry a comment after a dash, and show the callback event figure on the parser callbacks page. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * docs: address review comments on the new design - Home: add a "Latest release" button - Sponsors: update the list and group it by tier - Features: list all feature pages on the index page - Releases: add highlights per release, link the 4.0 roadmap, add the first commit to the timeline - Exceptions: prefix all example messages with the exception name - API: no code font for format names in the See also links of the to_/from_ pages - Merge Patch: show the document before, the patch, and the result - Replace "--" with typographic dashes, mark external links with an icon, center Mermaid diagrams Signed-off-by: Niels Lohmann <mail@nlohmann.me> --------- Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
1 parent
2bb2feeef4
commit
a47170346a
417 files changed
+11100
-3041
No files matched your search
@@ -0,0 +1,280 @@
|
||||
"""Draws the measurement charts as inline SVGs into docs/images/.
|
||||
|
||||
Generated files:
|
||||
|
||||
- compile_times.svg: percent change in compile time per option at -O0 (integration/compile_times.md)
|
||||
- compile_times_pch.svg, compile_times_modules.svg: percent change per translation unit with a precompiled header or
|
||||
`import nlohmann.json;` at -O0, plus the one-off build cost (integration/compile_times.md)
|
||||
- binary_sizes_<file>.svg: encoded size per binary format relative to minified JSON, one panel per test file
|
||||
(features/binary_formats/index.md, reused on features/performance.md)
|
||||
- ordered_json_scaling.svg: parse time of a flat object for json vs. ordered_json (features/performance.md)
|
||||
|
||||
Every number below is copied from the tables on those pages (or, where noted, computed from the milliseconds they
|
||||
report); the tables stay on the pages as the accessible fallback. Update both together. The SVGs are not images:
|
||||
the pages include their text with `--8<-- "images/<name>.svg"`, so they pick up the colours and fonts of the page
|
||||
(see the `nj-charts` rules in docs/css/fig-charts.css). Run `python3 scripts/figures/charts.py` from docs/mkdocs after
|
||||
changing data or drawing, and commit the generated files. In SVG text write the word JSON as `J​SON`,
|
||||
otherwise the abbreviation extension wraps it in an <abbr> and the text disappears.
|
||||
"""
|
||||
import html
|
||||
import math
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
OUT = Path(__file__).resolve().parents[2] / "docs" / "images"
|
||||
MINUS = "−"
|
||||
JSON = "J​SON"
|
||||
|
||||
|
||||
# words the abbreviation extension (includes/glossary.md) would wrap in <abbr> inside the SVG text
|
||||
ABBR = re.compile(r"(?<![\w;])(BJData|BON8|BSON|CBOR|GCC|UBJSON|MsgPack|JSON|DOM|SAX)(?!\w)")
|
||||
|
||||
|
||||
def protect(text):
|
||||
return ABBR.sub(lambda m: m.group(1)[0] + "​" + m.group(1)[1:], text)
|
||||
|
||||
|
||||
def esc(s):
|
||||
return html.escape(s, quote=True).replace("J&#8203;SON", JSON).replace("&#8722;", MINUS)
|
||||
|
||||
|
||||
def write(name, W, H, cls, tid, title, desc, body):
|
||||
s = ['<svg class="nj-fig nj-charts %s" viewBox="0 0 %g %g" role="img" aria-labelledby="%s %s-d" '
|
||||
'xmlns="http://www.w3.org/2000/svg">' % (cls, W, H, tid, tid),
|
||||
'<title id="%s">%s</title>' % (tid, esc(title)),
|
||||
'<desc id="%s-d">%s</desc>' % (tid, esc(desc))]
|
||||
s += body
|
||||
s.append('</svg>')
|
||||
(OUT / name).write_text(protect("\n".join(s)) + "\n")
|
||||
|
||||
|
||||
def hbar(x0, x1, y, h, r=4):
|
||||
"""Horizontal bar from the baseline x0 to x1, rounded only at the data end."""
|
||||
if abs(x1 - x0) < 0.5:
|
||||
return "M%g %g h0.5 v%g h-0.5 Z" % (x0, y, h)
|
||||
r = min(r, h / 2, abs(x1 - x0))
|
||||
s = 1 if x1 > x0 else -1
|
||||
return ("M%g %g H%g Q%g %g %g %g V%g Q%g %g %g %g H%g Z"
|
||||
% (x0, y, x1 - s * r, x1, y, x1, y + r, y + h - r, x1, y + h, x1 - s * r, y + h, x0))
|
||||
|
||||
|
||||
def pct(v):
|
||||
if v == 0:
|
||||
return "0 %"
|
||||
return ("+" if v > 0 else MINUS) + "%g %%" % abs(v)
|
||||
|
||||
|
||||
# ---------- compile times (integration/compile_times.md, -O0 columns) ----------
|
||||
# (label, translation unit, Apple clang %, GCC 16 %, computed?), ordered by saving
|
||||
COMPILE = [
|
||||
("json_fwd.hpp instead of json.hpp", "header-only TU", -62, -82, False),
|
||||
("extern template", "parsing TU", -7, -21, False),
|
||||
("JSON_NO_AUTOMATIC_UDLS", "model TU", -11, -11, False),
|
||||
("JSON_NO_AUTOMATIC_UDLS", "parsing TU", -1, -2, False),
|
||||
("JSON_HAS_FILESYSTEM=0 + JSON_NO_IO", "model TU", -8, -11, False),
|
||||
("JSON_HAS_FILESYSTEM=0", "model TU", -5, -9, False),
|
||||
# computed from the ms in "Options without effect on compile times": 1034/1051, 1756/1801, 1055/1051, 1783/1801
|
||||
("JSON_NO_IO", "model TU", round((1034 / 1051 - 1) * 100), round((1756 / 1801 - 1) * 100), True),
|
||||
("JSON_USE_GLOBAL_UDLS=0", "model TU", round((1055 / 1051 - 1) * 100), round((1783 / 1801 - 1) * 100), True),
|
||||
]
|
||||
|
||||
|
||||
def compile_times():
|
||||
W, L, R = 470, 8, 402
|
||||
lo, hi = -90, 10
|
||||
X = lambda v: L + (v - lo) / (hi - lo) * (R - L)
|
||||
top, pitch, bh = 64, 52, 12
|
||||
H = top + pitch * len(COMPILE) + 30
|
||||
b = []
|
||||
# legend
|
||||
b.append('<rect class="nj-charts-clang" x="%g" y="10" width="12" height="12" rx="3"/>' % L)
|
||||
b.append('<text class="nj-charts-leg" x="%g" y="20.5">Apple clang</text>' % (L + 18))
|
||||
b.append('<rect class="nj-charts-gcc" x="%g" y="10" width="12" height="12" rx="3"/>' % (L + 118))
|
||||
b.append('<text class="nj-charts-leg" x="%g" y="20.5">GCC 16</text>' % (L + 136))
|
||||
# grid and ticks
|
||||
axis_y = top + pitch * len(COMPILE) - 8
|
||||
for t in (-80, -60, -40, -20):
|
||||
b.append('<line class="nj-charts-grid" x1="%g" y1="%g" x2="%g" y2="%g"/>' % (X(t), top - 6, X(t), axis_y))
|
||||
b.append('<text class="nj-charts-tick" x="%g" y="%g">%s</text>' % (X(t), axis_y + 16, pct(t)))
|
||||
b.append('<text class="nj-charts-tick" x="%g" y="%g">0 %%</text>' % (X(0), axis_y + 16))
|
||||
b.append('<text class="nj-charts-tick nj-charts-tick--l" x="%g" y="%g">← faster</text>' % (L, axis_y + 34))
|
||||
b.append('<text class="nj-charts-tick nj-charts-tick--r" x="%g" y="%g">slower →</text>' % (W - 8, axis_y + 34))
|
||||
for i, (label, tu, clang, gcc, computed) in enumerate(COMPILE):
|
||||
y = top + i * pitch
|
||||
b.append('<text class="nj-charts-rowlabel" x="%g" y="%g"><tspan class="nj-charts-code">%s</tspan>'
|
||||
'<tspan class="nj-charts-sub"> · %s%s</tspan></text>'
|
||||
% (L, y - 2, esc(label), tu, "*" if computed else ""))
|
||||
for j, (v, cls, name) in enumerate(((clang, "clang", "Apple clang"), (gcc, "gcc", "GCC 16"))):
|
||||
by = y + 6 + j * (bh + 2)
|
||||
b.append('<path class="nj-charts-%s" d="%s"><title>%s, %s (%s): %s</title></path>'
|
||||
% (cls, hbar(X(0), X(v), by, bh), esc(label), tu, name, esc(pct(v))))
|
||||
b.append('<text class="nj-charts-val" x="%g" y="%g">%s</text>' % (max(X(0), X(v)) + 5, by + bh - 1.5, pct(v)))
|
||||
b.append('<line class="nj-charts-zero" x1="%g" y1="%g" x2="%g" y2="%g"/>' % (X(0), top - 6, X(0), axis_y))
|
||||
H = axis_y + 44
|
||||
desc = "; ".join("%s on a %s: Apple clang %s, GCC 16 %s" % (label, tu.replace("TU", "translation unit"),
|
||||
pct(c).replace(MINUS, "-"), pct(g).replace(MINUS, "-")) for label, tu, c, g, _ in COMPILE)
|
||||
write("compile_times.svg", W, H, "nj-charts--compile", "nj-charts-ct",
|
||||
"Change in compile time against the default configuration at -O0, Apple clang and GCC 16",
|
||||
"Grouped horizontal bars, negative is faster. " + desc + ".", b)
|
||||
|
||||
|
||||
# ---------- precompiled headers and C++20 modules (integration/compile_times.md, -O0 columns) ----------
|
||||
# Per-TU medians of the wall-clock time in ms from the tables "Use precompiled headers" (-std=c++17) and
|
||||
# "Use C++20 modules" (-std=c++20): {translation unit: {compiler: (default ms, with PCH/module ms)}}, plus the one-off
|
||||
# build cost in ms per compiler. None instead of a value means "not measured"; NOT_AVAILABLE then gives the reason.
|
||||
PCH = {
|
||||
"model TU": {"Apple clang": (1045, 594), "GCC 16": (1789, 1060)},
|
||||
"parsing TU": {"Apple clang": (1148, 714), "GCC 16": (2498, 1884)},
|
||||
}
|
||||
PCH_BUILD = {"Apple clang": 771, "GCC 16": 2254}
|
||||
MODULES = {
|
||||
"model TU": {"Apple clang": (1311, 680), "GCC 16": (2156, 1153)},
|
||||
"parsing TU": {"Apple clang": (1387, 800), "GCC 16": (2838, 1810)},
|
||||
}
|
||||
MODULES_BUILD = {"Apple clang": 1630, "GCC 16": 2538}
|
||||
NOT_AVAILABLE = {} # compiler: reason
|
||||
|
||||
|
||||
def tradeoff(name, tid, rows, build, what, title, desc_intro):
|
||||
"""Percent change per TU (bars) and the one-off build cost (text rows below) for two compilers."""
|
||||
W, L, R = 440, 8, 372
|
||||
lo, hi = -60, 0
|
||||
X = lambda v: L + (v - lo) / (hi - lo) * (R - L)
|
||||
top, pitch, bh = 64, 52, 12
|
||||
comps = (("Apple clang", "clang"), ("GCC 16", "gcc"))
|
||||
b, desc = [], []
|
||||
b.append('<rect class="nj-charts-clang" x="%g" y="10" width="12" height="12" rx="3"/>' % L)
|
||||
b.append('<text class="nj-charts-leg" x="%g" y="20.5">Apple clang</text>' % (L + 18))
|
||||
b.append('<rect class="nj-charts-gcc" x="%g" y="10" width="12" height="12" rx="3"/>' % (L + 118))
|
||||
b.append('<text class="nj-charts-leg" x="%g" y="20.5">GCC 16</text>' % (L + 136))
|
||||
axis_y = top + pitch * len(rows) - 8
|
||||
for t in (-50, -25):
|
||||
b.append('<line class="nj-charts-grid" x1="%g" y1="%g" x2="%g" y2="%g"/>' % (X(t), top - 6, X(t), axis_y))
|
||||
b.append('<text class="nj-charts-tick" x="%g" y="%g">%s</text>' % (X(t), axis_y + 16, pct(t)))
|
||||
b.append('<text class="nj-charts-tick" x="%g" y="%g">0 %%</text>' % (X(0), axis_y + 16))
|
||||
b.append('<text class="nj-charts-tick nj-charts-tick--l" x="%g" y="%g">← faster per TU</text>' % (L, axis_y + 34))
|
||||
for i, (tu, vals) in enumerate(rows.items()):
|
||||
y = top + i * pitch
|
||||
b.append('<text class="nj-charts-rowlabel" x="%g" y="%g">%s<tspan class="nj-charts-sub"> · %s</tspan></text>'
|
||||
% (L, y - 2, tu, esc(what)))
|
||||
for j, (comp, cls) in enumerate(comps):
|
||||
by = y + 6 + j * (bh + 2)
|
||||
pair = vals[comp]
|
||||
if pair is None:
|
||||
b.append('<text class="nj-charts-note" x="%g" y="%g">%s: not available: %s</text>'
|
||||
% (L, by + bh - 1.5, comp, esc(NOT_AVAILABLE.get(comp, "not measured"))))
|
||||
desc.append("%s, %s: not available" % (tu, comp))
|
||||
continue
|
||||
d, w = pair
|
||||
v = round((w / d - 1) * 100)
|
||||
b.append('<path class="nj-charts-%s" d="%s"><title>%s, %s: %d ms by default, %d ms %s (%s)</title></path>'
|
||||
% (cls, hbar(X(0), X(v), by, bh), tu, comp, d, w, esc(what), esc(pct(v))))
|
||||
b.append('<text class="nj-charts-val" x="%g" y="%g">%s</text>' % (X(0) + 5, by + bh - 1.5, pct(v)))
|
||||
desc.append("%s, %s: %s (%d ms to %d ms)" % (tu, comp, pct(v).replace(MINUS, "-"), d, w))
|
||||
b.append('<line class="nj-charts-zero" x1="%g" y1="%g" x2="%g" y2="%g"/>' % (X(0), top - 6, X(0), axis_y))
|
||||
# one-off cost
|
||||
y = axis_y + 62
|
||||
b.append('<text class="nj-charts-rowlabel" x="%g" y="%g">One-off cost<tspan class="nj-charts-sub"> · built once, '
|
||||
'reused by every TU</tspan></text>' % (L, y))
|
||||
for j, (comp, cls) in enumerate(comps):
|
||||
yy = y + 20 + j * 20
|
||||
b.append('<rect class="nj-charts-%s" x="%g" y="%g" width="10" height="10" rx="2.5"/>' % (cls, L, yy - 9.5))
|
||||
ms = build[comp]
|
||||
saved = sorted(vals[comp][0] - vals[comp][1] for vals in rows.values() if vals[comp] is not None)
|
||||
txt = ("%s: %d ms once, saves %d to %d ms per TU" % (comp, ms, saved[0], saved[-1])) if ms is not None else ("%s: not available: %s" % (comp, NOT_AVAILABLE.get(comp, "not measured")))
|
||||
b.append('<text class="nj-charts-val" x="%g" y="%g">%s</text>' % (L + 16, yy, esc(txt)))
|
||||
desc.append("one-off build, %s: %s" % (comp, ("%d ms" % ms) if ms is not None else "not available"))
|
||||
H = y + 20 + 20 * len(comps)
|
||||
write(name, W, H, "nj-charts--compile", tid, title, desc_intro + " " + "; ".join(desc) + ".", b)
|
||||
|
||||
|
||||
# ---------- binary sizes (features/binary_formats/index.md, "Sizes") ----------
|
||||
FORMATS = ["BJData", "BON8", "BSON", "CBOR", "MessagePack", "UBJSON"] # colour slots 1-6, same in every panel
|
||||
SIZES = {
|
||||
"canada.json": [53.2, 50.5, 85.8, 50.5, 50.5, 53.2],
|
||||
"twitter.json": [91.1, 83.8, 95.2, 86.3, 86.0, 91.3],
|
||||
"citm_catalog.json": [78.1, 63.5, 95.8, 68.4, 68.5, 78.2],
|
||||
"jeopardy.json": [96.6, 87.5, 106.7, 88.0, 87.9, 96.6],
|
||||
}
|
||||
|
||||
|
||||
def binary_sizes():
|
||||
W, L, R = 340, 104, 286
|
||||
X = lambda v: L + v / 110 * (R - L)
|
||||
top, pitch, bh = 46, 26, 16
|
||||
for fname, vals in SIZES.items():
|
||||
b, vl = [], [] # value labels go on top of the 100 % line
|
||||
lo = min(vals)
|
||||
b.append('<text class="nj-charts-panel" x="8" y="18">%s</text>' % fname)
|
||||
bottom = top + pitch * len(FORMATS) - 6
|
||||
for t in (0, 50):
|
||||
b.append('<line class="nj-charts-grid" x1="%g" y1="%g" x2="%g" y2="%g"/>' % (X(t), top - 6, X(t), bottom))
|
||||
b.append('<text class="nj-charts-tick" x="%g" y="%g">%d %%</text>' % (X(t), bottom + 15, t))
|
||||
for i, (fmt, v) in enumerate(zip(FORMATS, vals)):
|
||||
y = top + i * pitch
|
||||
best = v == lo
|
||||
b.append('<text class="nj-charts-rowlabel nj-charts-fmt%s" x="%g" y="%g">%s</text>'
|
||||
% (" nj-charts-best" if best else "", L - 8, y + bh - 3.5, fmt))
|
||||
b.append('<path class="nj-charts-c%d%s" d="%s"><title>%s, %s: %g %% of minified %s%s</title></path>'
|
||||
% (i + 1, "" if best else " nj-charts-dim", hbar(X(0), X(v), y, bh), fname, fmt, v, JSON,
|
||||
" (smallest)" if best else ""))
|
||||
vl.append('<text class="nj-charts-val%s" x="%g" y="%g">%.1f %%</text>'
|
||||
% (" nj-charts-best" if best else "", X(v) + 5, y + bh - 3.5, v))
|
||||
b.append('<line class="nj-charts-ref" x1="%g" y1="%g" x2="%g" y2="%g"/>' % (X(100), top - 10, X(100), bottom))
|
||||
b.append('<text class="nj-charts-tick nj-charts-tick--r" x="%g" y="%g">minified %s = 100 %%</text>'
|
||||
% (X(100) + 2, top - 14, JSON))
|
||||
b.append('<text class="nj-charts-tick" x="%g" y="%g">100 %%</text>' % (X(100), bottom + 15))
|
||||
b += vl
|
||||
H = bottom + 24
|
||||
best = [f for f, v in zip(FORMATS, vals) if v == lo]
|
||||
desc = "; ".join("%s %g %%" % (f, v) for f, v in zip(FORMATS, vals))
|
||||
stem = fname.split(".")[0]
|
||||
write("binary_sizes_%s.svg" % stem, W, H, "nj-charts--sizes", "nj-charts-bs-" + stem,
|
||||
"Size of %s in each binary format relative to minified %s" % (fname, JSON),
|
||||
"%s. Smallest: %s." % (desc, " and ".join(best)), b)
|
||||
|
||||
|
||||
# ---------- json vs. ordered_json (api/ordered_map.md, cited on features/performance.md) ----------
|
||||
SCALING = [(2000, 0.7, 3.6, 5), (4000, 0.8, 14.0, 19), (8000, 1.6, 67.8, 43), (16000, 3.3, 181.6, 54)]
|
||||
|
||||
|
||||
def scaling():
|
||||
W, L, R, T, B = 440, 66, 330, 16, 210
|
||||
xs = [n for n, *_ in SCALING]
|
||||
X = lambda n: L + math.log2(n / xs[0]) / math.log2(xs[-1] / xs[0]) * (R - L)
|
||||
Y = lambda ms: B - (math.log10(ms) - math.log10(0.3)) / (math.log10(300) - math.log10(0.3)) * (B - T)
|
||||
b = []
|
||||
for t in (1, 10, 100):
|
||||
b.append('<line class="nj-charts-grid" x1="%g" y1="%g" x2="%g" y2="%g"/>' % (L, Y(t), R, Y(t)))
|
||||
b.append('<text class="nj-charts-tick nj-charts-tick--r" x="%g" y="%g">%g ms</text>' % (L - 6, Y(t) + 4, t))
|
||||
b.append('<line class="nj-charts-axis" x1="%g" y1="%g" x2="%g" y2="%g"/>' % (L, B, R, B))
|
||||
for n, a, o, f in SCALING:
|
||||
b.append('<text class="nj-charts-tick" x="%g" y="%g">%s</text>' % (X(n), B + 16, "{:,}".format(n).replace(",", " ")))
|
||||
for key, idx, cls, name in (("json", 1, "json", "json"), ("ojson", 2, "ojson", "ordered_json")):
|
||||
pts = [(X(r[0]), Y(r[idx])) for r in SCALING]
|
||||
b.append('<polyline class="nj-charts-%s" points="%s"/>' % (cls, " ".join("%g,%g" % p for p in pts)))
|
||||
for (x, y), r in zip(pts, SCALING):
|
||||
b.append('<circle class="nj-charts-%s-dot" cx="%g" cy="%g" r="4.5"><title>%s, %d keys: %g ms</title></circle>'
|
||||
% (cls, x, y, name, r[0], r[idx]))
|
||||
last = SCALING[-1]
|
||||
b.append('<text class="nj-charts-val nj-charts-name-%s" x="%g" y="%g">%s</text>' % (cls, pts[-1][0] + 10, pts[-1][1] - 2, name))
|
||||
b.append('<text class="nj-charts-val" x="%g" y="%g">%g ms</text>' % (pts[-1][0] + 10, pts[-1][1] + 13, last[idx]))
|
||||
first = SCALING[0]
|
||||
dy = 16 if key == "json" else 20
|
||||
b.append('<text class="nj-charts-val" x="%g" y="%g">%g ms</text>' % (pts[0][0] + 6, pts[0][1] + dy, first[idx]))
|
||||
write("ordered_json_scaling.svg", W, B + 28, "nj-charts--scaling", "nj-charts-sc",
|
||||
"Time to parse a flat object of n keys, json against ordered_json, logarithmic axes",
|
||||
"2000 keys: json 0.7 ms, ordered_json 3.6 ms (5x). 4000 keys: 0.8 ms and 14.0 ms (19x). "
|
||||
"8000 keys: 1.6 ms and 67.8 ms (43x). 16000 keys: 3.3 ms and 181.6 ms (54x).", b)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
compile_times()
|
||||
tradeoff("compile_times_pch.svg", "nj-charts-pch", PCH, PCH_BUILD, "with PCH",
|
||||
"Compile time per translation unit with a precompiled json.hpp at -O0, and the one-off cost of the PCH",
|
||||
"Horizontal bars, negative is faster, medians of nine runs, -std=c++17.")
|
||||
tradeoff("compile_times_modules.svg", "nj-charts-mod", MODULES, MODULES_BUILD, "import nlohmann.json",
|
||||
"Compile time per translation unit with import nlohmann.json at -O0, and the one-off cost of the module",
|
||||
"Horizontal bars, negative is faster, medians of nine runs, -std=c++20 for both.")
|
||||
binary_sizes()
|
||||
scaling()
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Draws the chunk-buffering strip of the large-input page as an inline SVG into docs/images/.
|
||||
|
||||
The strip follows docs/examples/partial_input_buffering.cpp: five chunks are appended to a buffer, and after each one
|
||||
the whole buffer is parsed again. A parse error whose byte is one past the buffer size means the input ended early
|
||||
(wait for more); an error before the end is a real syntax error. The byte positions were checked by running the
|
||||
example (4/5, 12/13, 19/20, 30/31, complete at 35; `[1, ]` fails at byte 5).
|
||||
|
||||
The SVG is not an image: features/parsing/large_and_partial_input.md includes its text with
|
||||
`--8<-- "images/chunk_buffering.svg"`, so it picks up the colours and fonts of the page (see the `nj-fig` rules in
|
||||
docs/css/brand.css and the `nj-sax-` rules in docs/css/fig-sax.css). Run `python3 scripts/figures/chunk_buffering.py`
|
||||
from docs/mkdocs after changing the drawing, and commit the generated file. In SVG text write the word JSON as
|
||||
`J​SON`, otherwise the abbreviation extension wraps it in an <abbr> and the text disappears.
|
||||
"""
|
||||
import html
|
||||
from pathlib import Path
|
||||
|
||||
OUT = Path(__file__).resolve().parents[2] / "docs" / "images"
|
||||
|
||||
CW = 8.4 # JetBrains Mono advance at 14px
|
||||
TX = 96 # x of the buffer text
|
||||
SX = 420 # x of the result column
|
||||
ROW = 50
|
||||
CHUNKS = ['{"na', 'me": "Ma', 'ry", "l', 'ist": [1, 2', ', 3]}']
|
||||
|
||||
|
||||
def esc(s):
|
||||
return html.escape(s).replace("J&#8203;SON", "J​SON")
|
||||
|
||||
|
||||
def row(body, y, label, old, new, err_byte, status, status_cls, note):
|
||||
body.append('<text class="fg-note fg-note--l" x="16" y="%g">%s</text>' % (y + 5, esc(label)))
|
||||
if old:
|
||||
body.append('<rect class="nj-sax-old" x="%g" y="%g" width="%g" height="24" rx="4"/>' % (TX - 3, y - 12, CW * len(old) + 3))
|
||||
body.append('<rect class="nj-sax-new" x="%g" y="%g" width="%g" height="24" rx="4"/>' % (TX + CW * len(old), y - 12, CW * len(new) + 3))
|
||||
body.append('<text class="nj-sax-buf" x="%g" y="%g" xml:space="preserve">%s</text>' % (TX, y + 5, esc(old + new)))
|
||||
if err_byte is not None:
|
||||
# the byte of a parse_error is 1-based: byte n is the character at index n - 1
|
||||
cx = TX + CW * (err_byte - 1) + CW / 2
|
||||
body.append('<path class="nj-sax-caret %s" d="M%g %g l-5 8 h10 z"/>' % (status_cls, cx, y + 14))
|
||||
body.append('<text class="nj-sax-status %s" x="%d" y="%g">%s</text>' % (status_cls, SX, y - 1, esc(status)))
|
||||
body.append('<text class="fg-note fg-note--l" x="%d" y="%g">%s</text>' % (SX, y + 15, esc(note)))
|
||||
|
||||
|
||||
def main():
|
||||
W = 700
|
||||
body = []
|
||||
body.append('<text class="fg-h" x="16" y="24">chunk</text>')
|
||||
body.append('<text class="fg-h" x="%d" y="24">buffer, parsed from byte 1 each time</text>' % TX)
|
||||
body.append('<text class="fg-h" x="%d" y="24">result</text>' % SX)
|
||||
y, buf, parsed = 62, "", []
|
||||
for i, chunk in enumerate(CHUNKS):
|
||||
old, buf = buf, buf + chunk
|
||||
parsed.append(len(buf))
|
||||
if i < len(CHUNKS) - 1:
|
||||
row(body, y, "chunk %d" % (i + 1), old, chunk, len(buf) + 1,
|
||||
"error at byte %d = size + 1" % (len(buf) + 1), "nj-sax-s-wait", "incomplete: wait for more")
|
||||
else:
|
||||
row(body, y, "chunk %d" % (i + 1), old, chunk, None,
|
||||
"parsed, size %d" % len(buf), "nj-sax-s-ok", "complete: one value")
|
||||
y += ROW
|
||||
# the syntax error of the example, in a buffer of its own
|
||||
y += 8
|
||||
body.append('<path class="nj-sax-sep" d="M16 %g H%d"/>' % (y - 30, W - 16))
|
||||
row(body, y, "other input", "", "[1, ]", 5, "error at byte 5, size 5", "nj-sax-s-err", "before the end: a real error")
|
||||
y += 46
|
||||
total = " + ".join(str(n) for n in parsed)
|
||||
for k, line in enumerate(["Bytes parsed: %s = %d for a value of %d bytes." % (total, sum(parsed), parsed[-1]),
|
||||
"Every attempt starts again at byte 1, so the total work grows",
|
||||
"quadratically with the size of the value."]):
|
||||
body.append('<text class="fg-note fg-note--l" x="16" y="%g">%s</text>' % (y + 20 * k, esc(line)))
|
||||
y += 40
|
||||
H = int(y + 36)
|
||||
|
||||
tid, did = "fig-sax-buf-title", "fig-sax-buf-desc"
|
||||
title = "Buffering chunks until the value is complete: five chunks of {\"name\": \"Mary\", \"list\": [1, 2, 3]} appended to a buffer."
|
||||
desc = ("After chunks 1 to 4 the buffer has 4, 12, 19, and 30 bytes, and parsing it fails at byte 5, 13, 20, and 31: "
|
||||
"one past the end, so the input is incomplete and the program waits for more. After chunk 5 the buffer of 35 "
|
||||
"bytes parses as one complete value. For the input [1, ] of 5 bytes the error is at byte 5, before the end, "
|
||||
"so it is a real syntax error. Every attempt parses the buffer from byte 1, so 4 + 12 + 19 + 30 + 35 = 100 "
|
||||
"bytes are parsed for a 35-byte value, and the work grows quadratically.")
|
||||
s = ['<svg class="nj-fig nj-fig--sax-buf" viewBox="0 0 %d %d" role="img" aria-labelledby="%s %s" '
|
||||
'xmlns="http://www.w3.org/2000/svg">' % (W, H, tid, did),
|
||||
'<title id="%s">%s</title>' % (tid, esc(title)),
|
||||
'<desc id="%s">%s</desc>' % (did, esc(desc))]
|
||||
s += body
|
||||
s.append("</svg>")
|
||||
(OUT / "chunk_buffering.svg").write_text("\n".join(s) + "\n")
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,168 @@
|
||||
"""Draws the exception id map of home/exceptions.md as an inline SVG into docs/images/exception_map.svg.
|
||||
|
||||
The SVG is not an image: the page includes its text with `--8<-- "images/exception_map.svg"`, so it picks up the
|
||||
colours and fonts of the page (see the `nj-fig` rules in docs/css/brand.css and the `nj-exceptions-*` rules in
|
||||
docs/css/fig-exceptions.css). The file holds two drawings of the same map, a wide one and a narrow one for phones;
|
||||
the CSS shows one of them. Every id chip is a link to the `###` section of that id on the page, every family box a
|
||||
link to the family's `##` section.
|
||||
|
||||
Run `python3 scripts/figures/exception_map.py` from docs/mkdocs after changing the drawing or after adding an
|
||||
exception section to home/exceptions.md (the ids are read from the page), and commit the generated file. In SVG text
|
||||
write the word JSON as `J​SON`, otherwise the abbreviation extension wraps it in an <abbr> and the text
|
||||
disappears.
|
||||
"""
|
||||
import html
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
OUT = Path(__file__).resolve().parents[2] / "docs" / "images"
|
||||
PAGE = Path(__file__).resolve().parents[2] / "docs" / "home" / "exceptions.md"
|
||||
P = "nj-exceptions-"
|
||||
|
||||
# family name, id range label, anchor of the family section, extra member
|
||||
FAMILIES = [
|
||||
("parse_error", "1xx", "parse-errors", "byte"),
|
||||
("invalid_iterator", "2xx", "iterator-errors", None),
|
||||
("type_error", "3xx", "type-errors", None),
|
||||
("out_of_range", "4xx", "out-of-range", None),
|
||||
("other_error", "5xx", "further-exceptions", None),
|
||||
]
|
||||
|
||||
# the chips show the id only; naming just some of them was confusing
|
||||
LABELS = {}
|
||||
|
||||
|
||||
GLOSSARY = Path(__file__).resolve().parents[2] / "includes" / "glossary.md"
|
||||
ABBRS = sorted(re.findall(r"^\*\[([^\]]+)\]:", GLOSSARY.read_text(), re.M), key=len, reverse=True)
|
||||
|
||||
|
||||
def noabbr(s):
|
||||
"""Breaks every glossary abbreviation with a zero-width space, so the abbr extension leaves SVG text alone."""
|
||||
for a in ABBRS:
|
||||
s = re.sub(r"(?<![\w&#])%s(?!\w)" % re.escape(a), a[0] + "​" + a[1:], s)
|
||||
return s
|
||||
|
||||
|
||||
def ids_on_page():
|
||||
found = {}
|
||||
for m in re.finditer(r"^### json\.exception\.(\w+)\.(\d+)\s*$", PAGE.read_text(), re.M):
|
||||
found.setdefault(m.group(1), []).append(int(m.group(2)))
|
||||
return found
|
||||
|
||||
|
||||
def text_w(s, size):
|
||||
"""Rough advance width of Inter at the given size (good enough to size chips)."""
|
||||
w = 0.0
|
||||
for c in s:
|
||||
if c in "il.,:;'|![]()":
|
||||
w += 0.28
|
||||
elif c in " ft":
|
||||
w += 0.32
|
||||
elif c in "mwMW":
|
||||
w += 0.85
|
||||
elif c.isupper() or c.isdigit():
|
||||
w += 0.64
|
||||
else:
|
||||
w += 0.55
|
||||
return w * size
|
||||
|
||||
|
||||
def esc(s):
|
||||
return html.escape(s, quote=True).replace("[", "[").replace("]", "]")
|
||||
|
||||
|
||||
MONO = 7.9 # JetBrains Mono advance at 13px
|
||||
CH = 28 # chip height
|
||||
GAP = 6
|
||||
|
||||
|
||||
def chip(x, y, fam, num):
|
||||
label = LABELS.get(num)
|
||||
w = 3 * MONO + 18 + (text_w(label, 12.5) + 10 if label else 0)
|
||||
anchor = "jsonexception%s%d" % (fam, num)
|
||||
aria = "json.exception.%s.%d%s" % (fam, num, (": " + label) if label else "")
|
||||
s = ['<a class="%schip%s" xlink:href="#%s" aria-label="%s">' % (P, " %schip--named" % P if label else "", anchor, esc(aria)),
|
||||
'<rect x="%g" y="%g" width="%g" height="%d" rx="6"/>' % (x, y, w, CH),
|
||||
'<text class="%sid" x="%g" y="%g">%d</text>' % (P, x + 9, y + CH / 2 + 4.5, num)]
|
||||
if label:
|
||||
s.append('<text class="%slabel" x="%g" y="%g">%s</text>' % (P, x + 9 + 3 * MONO + 9, y + CH / 2 + 4.5, noabbr(esc(label))))
|
||||
s.append('</a>')
|
||||
return w, "".join(s)
|
||||
|
||||
|
||||
def flow(fam, nums, x0, x1, y0):
|
||||
"""Places the chips of one family left to right, wrapping at x1. Returns (svg parts, bottom y)."""
|
||||
parts, x, y = [], x0, y0
|
||||
for n in nums:
|
||||
w, _ = chip(0, 0, fam, n)
|
||||
if x > x0 and x + w > x1:
|
||||
x, y = x0, y + CH + GAP
|
||||
_, s = chip(x, y, fam, n)
|
||||
parts.append(s)
|
||||
x += w + GAP
|
||||
return parts, y + CH
|
||||
|
||||
|
||||
def box(x, y, w, h, title, sub, href=None, cls=""):
|
||||
s = []
|
||||
if href:
|
||||
s.append('<a class="%sfam" xlink:href="#%s" aria-label="%s">' % (P, href, esc(title + ", " + sub)))
|
||||
s.append('<rect class="%sbox%s" x="%g" y="%g" width="%g" height="%g" rx="7"/>' % (P, cls, x, y, w, h))
|
||||
s.append('<text class="%sname" x="%g" y="%g">%s</text>' % (P, x + 12, y + 22, noabbr(esc(title))))
|
||||
s.append('<text class="%ssub" x="%g" y="%g">%s</text>' % (P, x + 12, y + 41, noabbr(esc(sub))))
|
||||
if href:
|
||||
s.append('</a>')
|
||||
return "".join(s)
|
||||
|
||||
|
||||
def drawing(W, narrow, suffix):
|
||||
global CH
|
||||
CH = 34 if narrow else 28 # larger touch targets on phones
|
||||
ids = ids_on_page()
|
||||
mid = "nj-exc-tri-" + suffix
|
||||
body = []
|
||||
trunk = 34
|
||||
# std::exception and json::exception
|
||||
body.append(box(12, 12, 190 if narrow else 200, 52, "std::exception", "C++ standard library base"))
|
||||
body.append('<line class="%sedge" x1="%d" y1="%d" x2="%d" y2="%d" marker-end="url(#%s)"/>' % (P, trunk, 104, trunk, 66, mid))
|
||||
body.append(box(12, 104, 250 if narrow else 300, 52, "json::exception", "id, what() \u00b7 catch-all"))
|
||||
y = 196
|
||||
last_branch = y
|
||||
fam_w = W - 64 - 12 if narrow else 214
|
||||
for fam, rng, href, member in FAMILIES:
|
||||
nums = ids[fam]
|
||||
sub = "%s \u00b7 %d ids" % (rng, len(nums)) + (" \u00b7 adds member %s" % member if member else "")
|
||||
body.append('<line class="%sedge" x1="%d" y1="%d" x2="%d" y2="%d"/>' % (P, trunk, y + 26, 64, y + 26))
|
||||
last_branch = y + 26
|
||||
body.append(box(64, y, fam_w, 52, "json::" + fam, sub, href, " %sbox--fam" % P))
|
||||
if narrow:
|
||||
parts, bottom = flow(fam, nums, 64, W - 12, y + 52 + 10)
|
||||
else:
|
||||
parts, bottom = flow(fam, nums, 64 + fam_w + 16, W - 12, y + 12)
|
||||
body += parts
|
||||
y = max(bottom, y + 52) + 26
|
||||
# trunk from json::exception down to the last family, with the inheritance triangle at the top
|
||||
body.insert(0, '<line class="%sedge" x1="%d" y1="%d" x2="%d" y2="%d" marker-end="url(#%s)"/>' % (P, trunk, last_branch, trunk, 158, mid))
|
||||
H = y - 14
|
||||
defs = ('<defs><marker id="%s" viewBox="0 0 12 12" refX="11" refY="6" markerWidth="11" markerHeight="11" orient="auto">'
|
||||
'<path class="%stri" d="M1 1 L11 6 L1 11 z"/></marker></defs>' % (mid, P))
|
||||
tid, did = "nj-exc-map-title-" + suffix, "nj-exc-map-desc-" + suffix
|
||||
total = sum(len(v) for v in ids.values())
|
||||
desc = ("Class tree: std::exception is the base of json::exception, which is the base of the five exception "
|
||||
"families parse_error (ids 1xx), invalid_iterator (2xx), type_error (3xx), out_of_range (4xx) and "
|
||||
"other_error (5xx). Each family lists the ids documented on this page, %d in total; every id links to "
|
||||
"its section." % total)
|
||||
s = ['<svg class="nj-fig %smap %smap--%s" viewBox="0 0 %d %d" aria-labelledby="%s %s" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">'
|
||||
% (P, P, "narrow" if narrow else "wide", W, H, tid, did),
|
||||
'<title id="%s">Exception id map</title>' % tid,
|
||||
'<desc id="%s">%s</desc>' % (did, noabbr(esc(desc))),
|
||||
defs] + body + ['</svg>']
|
||||
return "\n".join(s)
|
||||
|
||||
|
||||
def main():
|
||||
out = drawing(760, False, "w") + "\n" + drawing(360, True, "n") + "\n"
|
||||
(OUT / "exception_map.svg").write_text(out)
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,116 @@
|
||||
"""Draws the 3.x / 4.0 lane diagram of community/roadmap.md as an inline SVG into docs/images/.
|
||||
|
||||
The rows come from the page itself: the macro table under "Trying out 4.0 today" gives each macro, its 3.x default,
|
||||
whether 4.0 removes it, and the version that added it; the `#define` block below that table gives the 4.0 value. Run
|
||||
`python3 scripts/figures/integration.py` from docs/mkdocs after changing that section, and commit the generated
|
||||
docs/images/roadmap_lanes.svg. The page includes the SVG text with `--8<-- "images/roadmap_lanes.svg"`, so it follows
|
||||
the light/dark toggle (see the `nj-fig` rules in docs/css/brand.css and the `nj-int-*` rules in
|
||||
docs/css/fig-integration.css). In SVG text write J​SON and A​PI, otherwise the abbreviation extension
|
||||
wraps the word in an <abbr> and the text disappears.
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
DOCS = Path(__file__).resolve().parents[2] / "docs"
|
||||
OUT = Path(__file__).resolve().parents[2] / "docs" / "images"
|
||||
PAGE = DOCS / "community" / "roadmap.md"
|
||||
|
||||
|
||||
def esc(text):
|
||||
return text.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
|
||||
|
||||
def vkey(version):
|
||||
return tuple(int(p) for p in version.split("."))
|
||||
|
||||
|
||||
def parse(markdown):
|
||||
section = markdown.split("### Trying out 4.0 today", 1)[1].split("\n### ", 1)[0]
|
||||
rows = []
|
||||
for line in section.splitlines():
|
||||
cells = [c.strip() for c in line.strip().strip("|").split("|")]
|
||||
m = re.match(r"\[`(JSON_[A-Z0-9_]+)`\]", cells[0]) if len(cells) == 5 else None
|
||||
if not m:
|
||||
continue
|
||||
default = re.fullmatch(r"`(\d)`", cells[1])
|
||||
added = re.fullmatch(r"(\d+\.\d+\.\d+)", cells[4])
|
||||
if not default or not added:
|
||||
sys.exit("cannot parse row: " + line)
|
||||
rows.append({"macro": m.group(1), "default": default.group(1), "added": added.group(1),
|
||||
"removed": cells[2].startswith("removed")})
|
||||
values = dict(re.findall(r"^#define (JSON_[A-Z0-9_]+) (\d)$", section, re.MULTILINE))
|
||||
for row in rows:
|
||||
if row["macro"] not in values:
|
||||
sys.exit("no #define for " + row["macro"])
|
||||
row["v4"] = values[row["macro"]]
|
||||
if not rows:
|
||||
sys.exit("no macro rows found in " + str(PAGE))
|
||||
return rows
|
||||
|
||||
|
||||
def draw(rows):
|
||||
W, ROW, TOP = 880, 34, 168
|
||||
H = TOP + ROW * len(rows) + 30
|
||||
versions = sorted({r["added"] for r in rows}, key=vkey)
|
||||
s = ['<svg class="nj-fig nj-fig--lanes" viewBox="0 0 %d %d" role="img" aria-labelledby="nj-int-lanes-title" '
|
||||
'xmlns="http://www.w3.org/2000/svg">' % (W, H)]
|
||||
s.append('<title id="nj-int-lanes-title">Two lanes: the 3.x release train, whose minor and patch releases do not '
|
||||
'break code that uses the public A​PI, followed by version 4.0, which has no release date yet. '
|
||||
'Each breaking change is a macro that is available in 3.x today with its 3.x default and becomes the '
|
||||
'default in 4.0: %s.</title>'
|
||||
% "; ".join("%s, added in %s, 3.x default %s, 4.0 value %s%s"
|
||||
% (r["macro"], r["added"], r["default"], r["v4"], ", macro removed" if r["removed"] else "")
|
||||
for r in rows))
|
||||
s.append('<defs><marker id="nj-int-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" '
|
||||
'orient="auto"><path class="fg-head" d="M0 1 L10 5 L0 9 z"/></marker></defs>')
|
||||
# lanes
|
||||
s.append('<rect class="nj-int-lane" x="16" y="16" width="600" height="56" rx="10"/>')
|
||||
s.append('<text class="nj-int-lanetext" x="32" y="41">3.x release train</text>')
|
||||
s.append('<text class="nj-int-mute" x="32" y="60">available today</text>')
|
||||
x0, x1 = 300, 590
|
||||
step = (x1 - x0) / max(1, len(versions) - 1) if len(versions) > 1 else 0
|
||||
for i, v in enumerate(versions):
|
||||
x = x0 + i * step if len(versions) > 1 else (x0 + x1) / 2
|
||||
s.append('<line class="nj-int-axis" x1="%g" y1="30" x2="%g" y2="44"/>' % (x, x))
|
||||
s.append('<text class="nj-int-mute" x="%g" y="60" text-anchor="middle">%s</text>' % (x, v))
|
||||
s.append('<line class="fg-arrow" x1="210" y1="37" x2="600" y2="37"/>')
|
||||
s.append('<line class="fg-arrow fg-arrow--dash" x1="618" y1="44" x2="660" y2="44" marker-end="url(#nj-int-arrow)"/>')
|
||||
s.append('<rect class="nj-int-lane nj-int-lane--future" x="664" y="16" width="200" height="56" rx="10"/>')
|
||||
s.append('<text class="nj-int-lanetext" x="680" y="41">4.0</text>')
|
||||
s.append('<text class="nj-int-mute" x="680" y="60">no release date yet</text>')
|
||||
# guarantee
|
||||
s.append('<text class="nj-int-text" x="16" y="98">A minor or patch release of 3.x does not break code that uses '
|
||||
'the public A​PI, unless that code opts in with a macro.</text>')
|
||||
s.append('<text class="nj-int-text" x="16" y="118">4.0 is not developed on a separate branch: it switches the '
|
||||
'defaults of these macros and removes the macros.</text>')
|
||||
# header
|
||||
hy = TOP - 18
|
||||
s.append('<text class="fg-h" x="32" y="%d">Macro</text>' % hy)
|
||||
s.append('<text class="fg-h" x="380" y="%d">added</text>' % hy)
|
||||
s.append('<text class="fg-h" x="500" y="%d" text-anchor="middle">3.x default</text>' % hy)
|
||||
s.append('<text class="nj-int-mute" x="632" y="%d" text-anchor="middle">define to try today</text>' % hy)
|
||||
s.append('<text class="fg-h" x="764" y="%d" text-anchor="middle">4.0 default</text>' % hy)
|
||||
for i, r in enumerate(rows):
|
||||
y = TOP + i * ROW + ROW / 2
|
||||
s.append('<rect class="nj-int-row" x="16" y="%g" width="848" height="%d" rx="6"/>' % (y - ROW / 2 + 2, ROW - 4))
|
||||
s.append('<text class="nj-int-macro" x="32" y="%g">%s</text>' % (y + 4.5, r["macro"]))
|
||||
s.append('<text class="nj-int-mute" x="380" y="%g">%s</text>' % (y + 4.5, r["added"]))
|
||||
s.append('<rect class="nj-int-pill" x="470" y="%g" width="60" height="22" rx="11"/>' % (y - 11))
|
||||
s.append('<text class="nj-int-val" x="500" y="%g" text-anchor="middle">%s</text>' % (y + 4.5, r["default"]))
|
||||
s.append('<line class="fg-arrow fg-arrow--dash" x1="544" y1="%g" x2="724" y2="%g" '
|
||||
'marker-end="url(#nj-int-arrow)"/>' % (y, y))
|
||||
s.append('<rect class="nj-int-pill nj-int-pill--v4" x="728" y="%g" width="72" height="22" rx="11"/>' % (y - 11))
|
||||
if r["removed"]:
|
||||
s.append('<text class="nj-int-val nj-int-val--v4" x="764" y="%g" text-anchor="middle">removed</text>' % (y + 4.5))
|
||||
else:
|
||||
s.append('<text class="nj-int-val nj-int-val--v4" x="764" y="%g" text-anchor="middle">%s</text>' % (y + 4.5, r["v4"]))
|
||||
s.append('<text class="nj-int-mute" x="16" y="%d">Versions on the 3.x lane are the ones that added a macro, '
|
||||
'not to scale. “removed”: see the 4.0 behavior column of the table.</text>' % (H - 8))
|
||||
s.append('</svg>')
|
||||
return "\n".join(s) + "\n"
|
||||
|
||||
|
||||
rows = parse(PAGE.read_text(encoding="utf-8"))
|
||||
(OUT / "roadmap_lanes.svg").write_text(draw(rows), encoding="utf-8")
|
||||
print("wrote", OUT / "roadmap_lanes.svg", "with", len(rows), "macros")
|
||||
@@ -0,0 +1,103 @@
|
||||
"""Draws the parser-callback and iterator illustrations as an inline SVG into docs/images/.
|
||||
|
||||
The SVG is not an image: the pages include its text with `--8<-- "images/callback_events.svg, range-begin-end.svg and range-rbegin-rend.svg"`, so it picks up the colours and fonts of the
|
||||
page (see the `railroad` and `nj-fig` rules in docs/css/brand.css). Run `python3 scripts/figures/iterator_callback_figures.py` from docs/mkdocs after
|
||||
changing the drawing, and commit the generated file. In SVG text write the word JSON as `J​SON`, otherwise the
|
||||
abbreviation extension wraps it in an <abbr> and the text disappears.
|
||||
"""
|
||||
import html
|
||||
from pathlib import Path
|
||||
|
||||
OUT = Path(__file__).resolve().parents[2] / "docs" / "images"
|
||||
CW = 9.6 # JetBrains Mono advance at 16px
|
||||
|
||||
def write(name, W, H, cls, tid, title, body, defs=""):
|
||||
s = ['<svg class="nj-fig %s" viewBox="0 0 %d %d" role="img" aria-labelledby="%s" xmlns="http://www.w3.org/2000/svg">' % (cls, W, H, tid),
|
||||
'<title id="%s">%s</title>' % (tid, html.escape(title).replace('J&#8203;SON', 'J​SON'))]
|
||||
if defs: s.append(defs)
|
||||
s += body
|
||||
s.append('</svg>')
|
||||
open(OUT / name, "w").write("\n".join(s) + "\n")
|
||||
|
||||
# ---------- callback events ----------
|
||||
def callback():
|
||||
body, y0, lh = [], 74, 28
|
||||
body.append('<text class="fg-h" x="16" y="28">J​SON to parse</text>')
|
||||
body.append('<text class="fg-h" x="320" y="28">callback events</text>')
|
||||
lines = [
|
||||
(0, [("{", None), (None, "os")]),
|
||||
(2, [('"name"', None), (None, "k"), (': "Berlin"', None), (None, "v"), (",", None)]),
|
||||
(2, [('"location"', None), (None, "k"), (": [", None), (None, "as")]),
|
||||
(4, [("52.519444", None), (None, "v"), (",", None)]),
|
||||
(4, [("13.406667", None), (None, "v")]),
|
||||
(2, [("]", None), (None, "ae"), (None, "v")]),
|
||||
(0, [("}", None), (None, "oe"), (None, "v")]),
|
||||
]
|
||||
for i, (indent, parts) in enumerate(lines):
|
||||
y = y0 + i * lh
|
||||
x = 16 + indent * CW
|
||||
for text, ev in parts:
|
||||
if text is not None:
|
||||
body.append('<text class="fg-code" x="%g" y="%d" xml:space="preserve">%s</text>' % (x, y, html.escape(text)))
|
||||
x += CW * len(text)
|
||||
else:
|
||||
body.append('<circle class="cb-%s" cx="%g" cy="%d" r="5.5"/>' % (ev, x + 10, y - 5))
|
||||
x += 20 if text is None else 0
|
||||
legend = [("os", "object_start"), ("k", "key"), ("oe", "object_end"), ("as", "array_start"), ("ae", "array_end"), ("v", "value")]
|
||||
for i, (ev, label) in enumerate(legend):
|
||||
y = 74 + i * 30
|
||||
body.append('<circle class="cb-%s" cx="327" cy="%d" r="6"/>' % (ev, y - 5))
|
||||
body.append('<text class="fg-code fg-leg" x="344" y="%d">%s</text>' % (y, label))
|
||||
write("callback_events.svg", 480, 262, "nj-fig--cb", "fig-cb-title",
|
||||
"Example of the parse events a callback sees for the J​SON text {\"name\": \"Berlin\", \"location\": [52.519444, 13.406667]}: object_start after the opening brace, key after each key, array_start after the opening bracket, value after each value, array_end after the closing bracket, and object_end after the closing brace. Every array and object also produces a value event when it is complete.", body)
|
||||
|
||||
# ---------- ranges ----------
|
||||
BW, GAP, X0 = 30, 4, 30
|
||||
def cell_x(i): return X0 + i * (BW + GAP)
|
||||
def cell(i, y, kind):
|
||||
c = "fg-cell" + (" fg-cell--end" if kind == "end" else "")
|
||||
return '<rect class="%s" x="%d" y="%d" width="%d" height="30" rx="5"/>' % (c, cell_x(i), y, BW)
|
||||
def pill(cx, y, label, W, label_w):
|
||||
x = max(4, min(W - 4 - label_w, cx - label_w / 2))
|
||||
return ['<rect class="fg-pill" x="%g" y="%d" width="%d" height="24" rx="6"/>' % (x, y, label_w),
|
||||
'<text class="fg-pilltext" x="%g" y="%d">%s</text>' % (x + label_w / 2, y + 16.5, label)]
|
||||
def arrow(x, ya, yb, mid, dashed=False):
|
||||
return '<line class="fg-arrow%s" x1="%g" y1="%g" x2="%g" y2="%g" marker-end="url(#%s)"/>' % (" fg-arrow--dash" if dashed else "", x, ya, x, yb, mid)
|
||||
|
||||
def ranges1():
|
||||
W, H, mid = 600, 150, "fig-be-arrow"
|
||||
defs = '<defs><marker id="%s" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path class="fg-head" d="M0 1 L10 5 L0 9 z"/></marker></defs>' % mid
|
||||
body = []
|
||||
for i in range(15): body.append(cell(i, 62, "elem"))
|
||||
body.append(cell(15, 62, "end"))
|
||||
body += pill(cell_x(0) + BW / 2, 8, "begin", W, 60); body.append(arrow(cell_x(0) + BW / 2, 32, 60, mid))
|
||||
body += pill(cell_x(15) + BW / 2, 8, "end", W, 48); body.append(arrow(cell_x(15) + BW / 2, 32, 60, mid))
|
||||
body.append('<text class="fg-note fg-note--r" x="%d" y="132">Past-the-last element</text>' % (cell_x(15) + BW / 2 - 18))
|
||||
body.append(arrow(cell_x(15) + BW / 2, 124, 96, mid))
|
||||
write("range-begin-end.svg", W, H, "nj-fig--range", "fig-be-title",
|
||||
"A row of fifteen elements followed by a dashed past-the-last position. begin() points to the first element and end() points to the past-the-last position.", body, defs)
|
||||
|
||||
def ranges2():
|
||||
W, H, mid = 600, 236, "fig-rbe-arrow"
|
||||
defs = '<defs><marker id="%s" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path class="fg-head" d="M0 1 L10 5 L0 9 z"/></marker></defs>' % mid
|
||||
body = []
|
||||
ytop, ybot = 62, 144
|
||||
body.append(cell(0, ytop, "end"))
|
||||
for i in range(1, 16): body.append(cell(i, ytop, "elem"))
|
||||
for i in range(15): body.append(cell(i, ybot, "elem"))
|
||||
body.append(cell(15, ybot, "end"))
|
||||
body += pill(cell_x(0) + BW / 2, 8, "rend", W, 52); body.append(arrow(cell_x(0) + BW / 2, 32, 60, mid))
|
||||
body += pill(cell_x(15) + BW / 2, 8, "rbegin", W, 68); body.append(arrow(cell_x(15) + BW / 2, 32, 60, mid))
|
||||
body.append('<text class="fg-note fg-note--l" x="%d" y="24">Reverse past-the-last element</text>' % (cell_x(0) + BW / 2 + 36))
|
||||
for col in (0, 15):
|
||||
x = cell_x(col) + BW / 2
|
||||
body.append('<line class="fg-arrow fg-arrow--dash" x1="%g" y1="94" x2="%g" y2="%d"/>' % (x, x, ybot - 2))
|
||||
body.append('<text class="fg-note" x="300" y="123">A reverse iterator stores an iterator to the next element</text>')
|
||||
body.append('<text class="fg-note" x="300" y="141" dy="0" style="display:none"></text>')
|
||||
body += pill(cell_x(0) + BW / 2, 204, "begin", W, 60); body.append(arrow(cell_x(0) + BW / 2, 202, 178, mid))
|
||||
body += pill(cell_x(15) + BW / 2, 204, "end", W, 48); body.append(arrow(cell_x(15) + BW / 2, 202, 178, mid))
|
||||
body.append('<text class="fg-note fg-note--r" x="%d" y="221">Past-the-last element</text>' % (cell_x(15) + BW / 2 - 40))
|
||||
write("range-rbegin-rend.svg", W, H, "nj-fig--range", "fig-rbe-title",
|
||||
"Two rows of sixteen cells. The upper row shows the reversed sequence: rend() points to a dashed position before the first element and rbegin() to the last element. The lower row shows the forward sequence: begin() points to the first element and end() to a dashed position after the last element. A reverse iterator stores an iterator to the next element, so rend() corresponds to begin() and rbegin() to end().", body, defs)
|
||||
|
||||
callback(); ranges1(); ranges2()
|
||||
@@ -0,0 +1,110 @@
|
||||
"""Draws the flatten()/unflatten() illustration of features/json_pointer.md as an inline SVG into docs/images/.
|
||||
|
||||
The SVG is not an image: the page includes its text with `--8<-- "images/json_pointer_flatten.svg"`, so it picks up
|
||||
the colours and fonts of the page (see the `nj-fig` rules in docs/css/brand.css and the `nj-pointer` rules in
|
||||
docs/css/fig-pointer.css). Run `python3 scripts/figures/json_pointer_flatten.py` from docs/mkdocs after changing the
|
||||
drawing, and commit the generated file. In SVG text write the word JSON as `J​SON`, otherwise the abbreviation
|
||||
extension wraps it in an <abbr> and the text disappears.
|
||||
|
||||
Left: the example document of the page as a tree. Right: the flat object that flatten() returns, one key per leaf.
|
||||
Each leaf is joined to its flat key by a connector; leaf, connector and flat value share the colour of the value type
|
||||
(string, number, boolean) used by the code highlighting.
|
||||
"""
|
||||
import html
|
||||
from pathlib import Path
|
||||
|
||||
OUT = Path(__file__).resolve().parents[2] / "docs" / "images"
|
||||
CW = 9.6 # JetBrains Mono advance at 16px
|
||||
ROW = 28
|
||||
Y0 = 92 # baseline of the first tree row
|
||||
X0 = 20 # x of the root label
|
||||
IND = 26 # indentation per level
|
||||
|
||||
# (depth, label, value or None, value type, flat key)
|
||||
TREE = [
|
||||
(0, "root", None, None, None),
|
||||
(1, '"array"', None, None, None),
|
||||
(2, "0", '"A"', "s", "/array/0"),
|
||||
(2, "1", '"B"', "s", "/array/1"),
|
||||
(2, "2", '"C"', "s", "/array/2"),
|
||||
(1, '"nested"', None, None, None),
|
||||
(2, '"one"', "1", "n", "/nested/one"),
|
||||
(2, '"two"', "2", "n", "/nested/two"),
|
||||
(2, '"three"', None, None, None),
|
||||
(3, "0", "true", "b", "/nested/three/0"),
|
||||
(3, "1", "false", "b", "/nested/three/1"),
|
||||
]
|
||||
W = 760
|
||||
FX = 470 # x of the flat keys
|
||||
|
||||
|
||||
def esc(s):
|
||||
return html.escape(s, quote=False)
|
||||
|
||||
|
||||
def main():
|
||||
body = []
|
||||
H = Y0 + (len(TREE) - 1) * ROW + 70
|
||||
mid = "fig-flat-arrow"
|
||||
defs = ('<defs><marker id="%s" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" '
|
||||
'orient="auto"><path class="fg-head" d="M0 1 L10 5 L0 9 z"/></marker></defs>' % mid)
|
||||
|
||||
body.append('<text class="fg-h" x="%d" y="26">nested value</text>' % X0)
|
||||
body.append('<text class="fg-h" x="%d" y="26">flat object: key = J​SON Pointer</text>' % FX)
|
||||
|
||||
# flatten() arrow on top, unflatten() arrow at the bottom
|
||||
ax1, ax2 = 250, FX - 20
|
||||
body.append('<line class="fg-arrow" x1="%d" y1="52" x2="%d" y2="52" marker-end="url(#%s)"/>' % (ax1, ax2, mid))
|
||||
body.append('<text class="nj-pointer-fl-op" x="%d" y="44">flatten()</text>' % ((ax1 + ax2) // 2))
|
||||
yb = H - 22
|
||||
body.append('<line class="fg-arrow" x1="%d" y1="%d" x2="%d" y2="%d" marker-end="url(#%s)"/>' % (ax2, yb, ax1, yb, mid))
|
||||
body.append('<text class="nj-pointer-fl-op" x="%d" y="%d">unflatten()</text>' % ((ax1 + ax2) // 2, yb - 8))
|
||||
|
||||
# tree: elbow lines from each node to its parent
|
||||
rows = []
|
||||
parents = []
|
||||
for i, (d, label, val, typ, key) in enumerate(TREE):
|
||||
y = Y0 + i * ROW
|
||||
x = X0 + d * IND
|
||||
while parents and parents[-1][0] >= d:
|
||||
parents.pop()
|
||||
if parents:
|
||||
px = X0 + parents[-1][0] * IND + 5
|
||||
py = parents[-1][1] + 7
|
||||
body.append('<path class="nj-pointer-fl-edge" d="M%d %d V%d H%d"/>' % (px, py, y - 5, x - 4))
|
||||
parents.append((d, y))
|
||||
rows.append((x, y, label, val, typ, key))
|
||||
|
||||
leaves = [r for r in rows if r[3] is not None]
|
||||
# flat keys: seven consecutive rows, centred on the leaf rows (tree rows 2 to 10)
|
||||
for j, (x, y, label, val, typ, key) in enumerate(leaves):
|
||||
fy = Y0 + (3 + j) * ROW
|
||||
lx = x + CW * (len(label) + 2 + len(val)) + 10
|
||||
body.append('<path class="nj-pointer-fl-link nj-pointer-fl--%s" d="M%g %d C%g %d %g %d %d %d"/>'
|
||||
% (typ, lx, y - 5, lx + 120, y - 5, FX - 130, fy - 5, FX - 10, fy - 5))
|
||||
body.append('<circle class="nj-pointer-fl-dot nj-pointer-fl--%s" cx="%g" cy="%d" r="3.5"/>' % (typ, lx, y - 5))
|
||||
body.append('<text class="fg-code" x="%d" y="%d" xml:space="preserve"><tspan class="nj-pointer-fl-key">"%s"</tspan>: '
|
||||
'<tspan class="nj-pointer-fl-v nj-pointer-fl--%s">%s</tspan></text>' % (FX, fy, esc(key), typ, esc(val)))
|
||||
|
||||
for x, y, label, val, typ, key in rows:
|
||||
if val is None:
|
||||
cls = "fg-code nj-pointer-fl-inner" if label != "root" else "fg-h"
|
||||
body.append('<text class="%s" x="%d" y="%d">%s</text>' % (cls, x, y, esc(label)))
|
||||
else:
|
||||
body.append('<text class="fg-code" x="%d" y="%d" xml:space="preserve"><tspan class="nj-pointer-fl-k">%s</tspan>: '
|
||||
'<tspan class="nj-pointer-fl-v nj-pointer-fl--%s">%s</tspan></text>' % (x, y, esc(label), typ, esc(val)))
|
||||
|
||||
title = ("flatten() turns the nested example document into a flat object, and unflatten() turns it back. "
|
||||
"Left, the document as a tree: array with the elements A, B, C, and nested with one = 1, two = 2, and "
|
||||
"three with the elements true and false. Right, the flat object with one key per leaf: /array/0 = A, "
|
||||
"/array/1 = B, /array/2 = C, /nested/one = 1, /nested/two = 2, /nested/three/0 = true, "
|
||||
"/nested/three/1 = false. Each leaf is joined to its key by a line.")
|
||||
s = ['<svg class="nj-fig nj-pointer-fl" viewBox="0 0 %d %d" role="img" aria-labelledby="fig-flat-title" '
|
||||
'xmlns="http://www.w3.org/2000/svg">' % (W, H),
|
||||
'<title id="fig-flat-title">%s</title>' % esc(title), defs]
|
||||
s += body
|
||||
s.append('</svg>')
|
||||
(OUT / "json_pointer_flatten.svg").write_text("\n".join(s) + "\n")
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,391 @@
|
||||
"""Draws the json_view illustrations of docs/features/json_view.md as inline SVGs into docs/images/.
|
||||
|
||||
The SVGs are not images: the page includes their text with `--8<-- "images/json_view_layout.svg"` (and
|
||||
json_view_lifetime.svg, json_view_edit_0.svg ... json_view_edit_4.svg), so they pick up the colours and fonts of
|
||||
the page (see the `nj-fig` rules in docs/css/brand.css and the `nj-jsonview-*` rules in docs/css/fig-jsonview.css).
|
||||
The edit frames are the steps of a stepper (docs/js/fig-jsonview.js); without JavaScript they show one below the
|
||||
other. Run `python3 scripts/figures/json_view_layout.py` from docs/mkdocs after changing the drawing, and commit the
|
||||
generated files. In SVG text write the word JSON as `J​SON`, otherwise the abbreviation extension wraps it in
|
||||
an <abbr> and the text disappears.
|
||||
|
||||
The node fields drawn here follow `struct node` in include/nlohmann/detail/view/node.hpp and the edit storage in
|
||||
detail/view/edit.hpp and edit_storage.hpp, for the 54-byte text in TEXT (9 nodes).
|
||||
"""
|
||||
import html
|
||||
from pathlib import Path
|
||||
|
||||
OUT = Path(__file__).resolve().parents[2] / "docs" / "images"
|
||||
TEXT = '{"port": 6379, "price": 19.90, "tags": ["eu", "a\\"b"]}'
|
||||
assert len(TEXT) == 54
|
||||
|
||||
# concept -> colour class (fig-jsonview.css maps them to brand colours)
|
||||
STRUCT, KEY, STR, NUM, DEC, OWN, BAD, LINE = "struct", "key", "str", "num", "dec", "own", "bad", "line"
|
||||
|
||||
|
||||
def esc(s):
|
||||
return html.escape(s, quote=True)
|
||||
|
||||
|
||||
def write(name, W, H, tid, title, desc, body, extra_cls=""):
|
||||
s = ['<svg class="nj-fig nj-jsonview %s" viewBox="0 0 %d %d" role="img" aria-labelledby="%s %s-d" '
|
||||
'xmlns="http://www.w3.org/2000/svg">' % (extra_cls, W, H, tid, tid),
|
||||
'<title id="%s">%s</title>' % (tid, esc(title).replace("\u200b", "​")),
|
||||
'<desc id="%s-d">%s</desc>' % (tid, esc(desc))]
|
||||
s += body
|
||||
s.append('</svg>')
|
||||
text = "\n".join(s) + "\n"
|
||||
assert "\n\n" not in text # a blank line would end the raw HTML block
|
||||
(OUT / name).write_text(text)
|
||||
|
||||
|
||||
def markers(prefix, kinds=(LINE, STR, KEY, NUM, DEC, OWN, STRUCT, BAD)):
|
||||
"""one arrow head per colour; ids are unique per SVG (hidden frames must not own the markers)"""
|
||||
m = ['<defs>']
|
||||
for k in kinds:
|
||||
m.append('<marker id="%s-%s" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6.5" markerHeight="6.5" '
|
||||
'orient="auto-start-reverse"><path class="nj-jsonview-c-%s nj-jsonview-head" d="M0 1 L10 5 L0 9 z"/></marker>' % (prefix, k, k))
|
||||
m.append('</defs>')
|
||||
return m
|
||||
|
||||
|
||||
def rect(x, y, w, h, cls, c=None, rx=5):
|
||||
cc = " nj-jsonview-c-%s" % c if c else ""
|
||||
return '<rect class="%s%s" x="%g" y="%g" width="%g" height="%g" rx="%g"/>' % (cls, cc, x, y, w, h, rx)
|
||||
|
||||
|
||||
def text(x, y, s, cls="nj-jsonview-t", c=None, anchor=None):
|
||||
cc = " nj-jsonview-c-%s" % c if c else ""
|
||||
a = ' text-anchor="%s"' % anchor if anchor else ""
|
||||
return '<text class="%s%s" x="%g" y="%g"%s xml:space="preserve">%s</text>' % (cls, cc, x, y, a, esc(s))
|
||||
|
||||
|
||||
def curve(x1, y1, x2, y2, c, prefix, dashed=False, bend=None, head=True):
|
||||
"""vertical-ish cubic from (x1, y1) to (x2, y2)"""
|
||||
b = bend if bend is not None else (y2 - y1) / 2
|
||||
d = "M%g %g C%g %g %g %g %g %g" % (x1, y1, x1, y1 + b, x2, y2 - b, x2, y2)
|
||||
return '<path class="nj-jsonview-arr nj-jsonview-c-%s%s" d="%s"%s/>' % (
|
||||
c, " nj-jsonview-dash" if dashed else "", d, ' marker-end="url(#%s-%s)"' % (prefix, c) if head else "")
|
||||
|
||||
|
||||
def line(x1, y1, x2, y2, c, prefix, dashed=False, head=True):
|
||||
return '<path class="nj-jsonview-arr nj-jsonview-c-%s%s" d="M%g %g L%g %g"%s/>' % (
|
||||
c, " nj-jsonview-dash" if dashed else "", x1, y1, x2, y2, ' marker-end="url(#%s-%s)"' % (prefix, c) if head else "")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- the parsed index of TEXT
|
||||
# (index, colour, kind, field lines, source span or None, label for compact frames)
|
||||
NODES = [
|
||||
(0, STRUCT, "object", ["len 3", "next 9"], None, "{ } 3"),
|
||||
(1, KEY, "key", ["off 2", "len 4"], (2, 6), '"port"'),
|
||||
(2, NUM, "uint", ["6379", "(converted)"], (9, 13), "6379"),
|
||||
(3, KEY, "key", ["off 16", "len 5"], (16, 21), '"price"'),
|
||||
(4, NUM, "float", ["off 24", "len 5"], (24, 29), "19.90"),
|
||||
(5, KEY, "key", ["off 32", "len 4"], (32, 36), '"tags"'),
|
||||
(6, STRUCT, "array", ["len 2", "next 3"], None, "[ ] 2"),
|
||||
(7, STR, "string", ["off 41", "len 2"], (41, 43), '"eu"'),
|
||||
(8, DEC, "string", ["escaped", "len 3"], (47, 51), 'a"b'),
|
||||
]
|
||||
|
||||
|
||||
def ribbon(body, x0, y, cw, prefix, label=True):
|
||||
"""the source text as a strip of bytes, with the spans the nodes refer to tinted"""
|
||||
w = cw * len(TEXT)
|
||||
body.append(rect(x0, y, w, 26, "nj-jsonview-ribbon", rx=4))
|
||||
for _, c, kind, _, span, _ in NODES:
|
||||
if span:
|
||||
body.append(rect(x0 + span[0] * cw + 1, y + 2, (span[1] - span[0]) * cw - 2, 22, "nj-jsonview-span", c, rx=3))
|
||||
for i, ch in enumerate(TEXT):
|
||||
if ch != " ":
|
||||
body.append(text(x0 + i * cw + cw / 2, y + 17.5, ch, "nj-jsonview-mono nj-jsonview-byte", anchor="middle"))
|
||||
if label:
|
||||
for off in (0, 10, 20, 30, 40, 50):
|
||||
body.append(text(x0 + off * cw + cw / 2, y + 40, str(off), "nj-jsonview-tiny", anchor="middle"))
|
||||
return w
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- figure 1: parse() vs json_document
|
||||
def layout():
|
||||
W, H, P = 840, 692, "jvl"
|
||||
b = markers(P)
|
||||
# --- panel A: basic_json::parse()
|
||||
b.append(text(16, 26, "basic_json::parse(): a tree, every array, object, and string in its own heap allocation", "nj-jsonview-h"))
|
||||
b.append(rect(16, 98, 60, 34, "nj-jsonview-box"))
|
||||
b.append(text(46, 120, "json", "nj-jsonview-mono", anchor="middle"))
|
||||
b.append(line(76, 115, 106, 115, LINE, P))
|
||||
b.append(rect(108, 98, 84, 34, "nj-jsonview-heap"))
|
||||
b.append(text(150, 120, "std::map", "nj-jsonview-mono", anchor="middle"))
|
||||
rows = [('"port"', "6379", None), ('"price"', "19.9", "double: 19.90 is gone"), ('"tags"', "\u2022", None)]
|
||||
for i, (k, v, note) in enumerate(rows):
|
||||
y = 50 + i * 48
|
||||
b.append(curve(192, 115, 226, y + 17, LINE, P, bend=0))
|
||||
b.append(rect(228, y, 180, 34, "nj-jsonview-heap"))
|
||||
b.append(text(240, y + 22, k, "nj-jsonview-mono", KEY))
|
||||
b.append(text(396, y + 22, v, "nj-jsonview-mono", NUM if i < 2 else LINE, anchor="end"))
|
||||
if note:
|
||||
b.append(text(418, y + 22, note, "nj-jsonview-note"))
|
||||
yt = 50 + 2 * 48 + 17
|
||||
b.append(line(408, yt, 442, yt, LINE, P))
|
||||
b.append(rect(444, yt - 17, 104, 34, "nj-jsonview-heap"))
|
||||
b.append(text(496, yt + 5, "std::vector", "nj-jsonview-mono", anchor="middle"))
|
||||
b.append(line(548, yt, 576, yt, LINE, P))
|
||||
b.append(rect(578, yt - 17, 92, 34, "nj-jsonview-heap"))
|
||||
b.append('<path class="nj-jsonview-sep" d="M624 %g V%g"/>' % (yt - 17, yt + 17))
|
||||
b.append(text(601, yt + 5, "json", "nj-jsonview-mono", anchor="middle"))
|
||||
b.append(text(647, yt + 5, "json", "nj-jsonview-mono", anchor="middle"))
|
||||
b.append(curve(601, yt - 17, 706, 130, LINE, P, bend=-20))
|
||||
b.append(curve(647, yt + 17, 706, 196, LINE, P, bend=20))
|
||||
b.append(rect(708, 113, 126, 34, "nj-jsonview-heap"))
|
||||
b.append(text(771, 135, "std::string eu", "nj-jsonview-mono nj-jsonview-sm", STR, anchor="middle"))
|
||||
b.append(rect(708, 179, 126, 34, "nj-jsonview-heap"))
|
||||
b.append(text(771, 201, 'std::string a"b', "nj-jsonview-mono nj-jsonview-sm", STR, anchor="middle"))
|
||||
# legend
|
||||
b.append(rect(16, 186, 18, 14, "nj-jsonview-heap", rx=3))
|
||||
b.append(text(42, 198, "a separate heap allocation", "nj-jsonview-note"))
|
||||
# --- panel B: json_document::parse()
|
||||
y0 = 252
|
||||
b.append('<path class="nj-jsonview-sep" d="M16 %g H824"/>' % (y0 - 18))
|
||||
b.append(text(16, y0 + 10, "json_document::parse(): the source text stays where it is; one flat index of 16-byte nodes", "nj-jsonview-h"))
|
||||
b.append(text(16, y0 + 38, "source text, 54 bytes (borrowed, or owned by the document)", "nj-jsonview-note"))
|
||||
cw, rx0, ry = 14, 42, y0 + 48
|
||||
ribbon(b, rx0, ry, cw, P)
|
||||
# node strip inside the document frame
|
||||
fy = ry + 66
|
||||
b.append(rect(16, fy, 808, 254, "nj-jsonview-frame", rx=10))
|
||||
nw, gap, nx0, ny, nh = 84, 4, 36, fy + 40, 68
|
||||
b.append(text(30, fy + 236, "json_document: owns the node index and the buffer of decoded strings", "nj-jsonview-note nj-jsonview-strong"))
|
||||
b.append(text(30, ny + nh + 26, "node index: 9 nodes \u00d7 16 bytes = 144 bytes, in document order", "nj-jsonview-note"))
|
||||
for i, c, kind, fields, span, _ in NODES:
|
||||
x = nx0 + i * (nw + gap)
|
||||
b.append(rect(x, ny, nw, nh, "nj-jsonview-node", c))
|
||||
b.append(text(x + 6, ny + 14, str(i), "nj-jsonview-tiny"))
|
||||
b.append(text(x + nw / 2, ny + 28, kind, "nj-jsonview-kind", c, anchor="middle"))
|
||||
for j, f in enumerate(fields):
|
||||
b.append(text(x + nw / 2, ny + 45 + j * 14, f, "nj-jsonview-mono nj-jsonview-xs", anchor="middle"))
|
||||
if span and c in (KEY, STR, NUM) and not (c == NUM and kind == "uint"):
|
||||
sx = rx0 + (span[0] + span[1]) / 2 * cw
|
||||
b.append(curve(x + nw / 2, ny, sx, ry + 28, c, P))
|
||||
# the integer has no pointer: its value is in the node
|
||||
# decoded strings
|
||||
dx, dy = 740, ny + nh + 44
|
||||
n8x = nx0 + 8 * (nw + gap) + nw / 2
|
||||
b.append(text(dx + 33, dy + 50, "decoded strings", "nj-jsonview-note", anchor="middle"))
|
||||
b.append(rect(dx, dy, 3 * 22, 30, "nj-jsonview-node", DEC, rx=4))
|
||||
for k, ch in enumerate('a"b'):
|
||||
b.append(text(dx + 11 + k * 22, dy + 20, ch, "nj-jsonview-mono", anchor="middle"))
|
||||
b.append(curve(n8x, ny + nh, dx + 33, dy, DEC, P))
|
||||
# the view: two pointers
|
||||
vx, vy = 480, fy + 254 + 28
|
||||
b.append(text(vx - 10, vy + 22, 'json_view v = doc.root()["tags"]', "nj-jsonview-mono nj-jsonview-sm", anchor="end"))
|
||||
b.append(rect(vx, vy, 140, 34, "nj-jsonview-node", STRUCT, rx=6))
|
||||
b.append('<path class="nj-jsonview-sep" d="M%g %g V%g"/>' % (vx + 70, vy, vy + 34))
|
||||
b.append(text(vx + 35, vy + 22, "doc*", "nj-jsonview-mono", anchor="middle"))
|
||||
b.append(text(vx + 105, vy + 22, "node*", "nj-jsonview-mono", anchor="middle"))
|
||||
b.append(line(vx + 35, vy, vx + 35, fy + 254 + 2, STRUCT, P))
|
||||
n6x = nx0 + 6 * (nw + gap) + nw / 2
|
||||
b.append(curve(vx + 105, vy, n6x, ny + nh, STRUCT, P))
|
||||
b.append(text(30, ny + nh + 50, "integers are converted while parsing and kept in their node;", "nj-jsonview-note"))
|
||||
b.append(text(30, ny + nh + 68, "floats and strings point back into the text", "nj-jsonview-note"))
|
||||
write("json_view_layout.svg", W, H, "fig-jvl-title",
|
||||
"The same J\u200bSON text parsed by basic_json::parse() and by json_document::parse()",
|
||||
"The text {\"port\": 6379, \"price\": 19.90, \"tags\": [\"eu\", \"a\\\"b\"]}. Top: basic_json::parse() builds a tree: "
|
||||
"the root json value points to a std::map whose three nodes are separate heap allocations; the price is stored "
|
||||
"as the double 19.9; the tags array is a std::vector of two json values, each pointing to its own std::string. "
|
||||
"Bottom: json_document::parse() keeps the 54 bytes of source text and builds a flat index of nine 16-byte nodes in "
|
||||
"document order: object, key port, unsigned integer 6379 (the converted value is in the node), key price, float, "
|
||||
"key tags, array, string eu, and an escaped string. Keys, the float, and the plain string point back into the "
|
||||
"source text; the escaped string points into the document's buffer of decoded strings. A json_view is two "
|
||||
"pointers: one to the document and one to a node, here the array.", b)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- figure 2: lifetimes
|
||||
def lifetime():
|
||||
W, P = 840, "jvt"
|
||||
L, R = 210, 820
|
||||
b = markers(P, (BAD,))
|
||||
b.append(rect(L, 12, 34, 12, "nj-jsonview-bar", STRUCT, rx=3))
|
||||
b.append(text(L + 42, 23, "alive / valid", "nj-jsonview-note"))
|
||||
b.append(rect(L + 160, 12, 34, 12, "nj-jsonview-bad", rx=3))
|
||||
b.append(text(L + 202, 23, "dangling: using it is undefined behavior", "nj-jsonview-note"))
|
||||
b.append(line(L, 44, R, 44, LINE, P, head=False))
|
||||
b.append(text(R, 38, "time \u2192", "nj-jsonview-tiny", anchor="end"))
|
||||
scen = [
|
||||
("The borrowed text is destroyed", 470, "text destroyed", [
|
||||
("std::string text", [(L, 470, "ok"), ], "end"),
|
||||
("json_document doc", [(L + 60, 470, "ok"), (470, R, "bad")], None),
|
||||
("json_view v", [(L + 120, 470, "ok"), (470, R, "bad")], None)]),
|
||||
("The document is re-parsed", 470, "doc.read(other)", [
|
||||
("json_document doc", [(L, R, "ok")], None),
|
||||
("json_view v", [(L + 60, 470, "ok"), (470, R, "bad")], None),
|
||||
("v = doc.root()", [(530, R, "ok")], None)]),
|
||||
("The document is shrunk", 470, "doc.shrink_to_fit()", [
|
||||
("json_document doc", [(L, R, "ok")], None),
|
||||
("json_view v", [(L + 60, 470, "ok"), (470, R, "bad")], None),
|
||||
("v = doc.root()", [(530, R, "ok")], None)]),
|
||||
("The document is moved, then destroyed", 680, "moved is destroyed", [
|
||||
("json_document doc", [(L, 420, "ok")], None),
|
||||
("moved = std::move(doc)", [(420, 680, "ok")], "end"),
|
||||
("json_view v", [(L + 60, 680, "ok"), (680, R, "bad")], None)]),
|
||||
]
|
||||
y = 70
|
||||
for title, ev, evlabel, rows in scen:
|
||||
b.append(text(16, y + 4, title, "nj-jsonview-h nj-jsonview-sm2"))
|
||||
top = y + 14
|
||||
for k, (name, segs, end) in enumerate(rows):
|
||||
ry = top + k * 28
|
||||
b.append(text(L - 12, ry + 15, name, "nj-jsonview-mono nj-jsonview-sm", anchor="end"))
|
||||
for x1, x2, st in segs:
|
||||
b.append(rect(x1, ry + 3, x2 - x1, 16, "nj-jsonview-bar" if st == "ok" else "nj-jsonview-bad", STRUCT if st == "ok" else None, rx=3))
|
||||
bottom = top + len(rows) * 28
|
||||
if title.startswith("The document is moved"):
|
||||
b.append('<path class="nj-jsonview-evt nj-jsonview-evt--ok" d="M420 %g V%g"/>' % (top - 2, bottom))
|
||||
b.append(text(424, top - 4, "std::move: views stay valid", "nj-jsonview-tiny"))
|
||||
b.append('<path class="nj-jsonview-evt" d="M%g %g V%g"/>' % (ev, top - 2, bottom))
|
||||
b.append(text(ev + 4, top - 4, evlabel, "nj-jsonview-tiny nj-jsonview-c-bad"))
|
||||
if title == "The borrowed text is destroyed":
|
||||
b.append(text(476, top + 2 * 28 + 15, "every view, and the document itself", "nj-jsonview-note nj-jsonview-onbad"))
|
||||
elif title.startswith("The document is moved"):
|
||||
pass
|
||||
else:
|
||||
b.append(text(476, top + 28 + 15, "views taken before", "nj-jsonview-note nj-jsonview-onbad"))
|
||||
b.append(text(536, top + 2 * 28 + 15, "a fresh view is valid again", "nj-jsonview-note nj-jsonview-onok"))
|
||||
y = bottom + 30
|
||||
H = y - 10
|
||||
write("json_view_lifetime.svg", W, H, "fig-jvt-title",
|
||||
"When a json_view becomes invalid",
|
||||
"Four timelines. First: a document borrows a std::string; when the string is destroyed, the document and every view "
|
||||
"taken from it dangle. Second: doc.read() re-parses the document; views taken before are invalid, a fresh view from "
|
||||
"root() is valid. Third: doc.shrink_to_fit() has the same effect. Fourth: moving the document does not invalidate its "
|
||||
"views; destroying the moved-to document does.", b)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- figure 3: edit frames
|
||||
NW, NG, NX0 = 84, 4, 36 # parsed nodes
|
||||
OW, OG, OX0 = 54, 3, 16 # owned storage slots
|
||||
|
||||
|
||||
def nx(i):
|
||||
return NX0 + i * (NW + NG)
|
||||
|
||||
|
||||
def ox(i):
|
||||
return OX0 + i * (OW + OG)
|
||||
|
||||
|
||||
# owned storage in allocation order (all in the first 64-node chunk), per step
|
||||
# entries: (colour, line1, line2, link target) ; link target = ("p", i) parsed node or ("o", j) owned slot
|
||||
OWN_STEPS = {
|
||||
2: [(STRUCT, "header", "[ ] 1", None), (OWN, "link", "\u2192 8", ("p", 8)), (None, "", "", None)],
|
||||
}
|
||||
OWN_STEPS[3] = [(STRUCT, "header", "[ ] 2", None), (OWN, "link", "\u2192 8", ("p", 8)), (OWN, "link", "\u2192 3'", ("o", 3)),
|
||||
(STR, "string", '"new"', None)]
|
||||
OWN_STEPS[4] = OWN_STEPS[3] + [(NUM, "uint", "3", None), (STRUCT, "header", "{ } 4", None),
|
||||
(KEY, "key", "port", None), (OWN, "link", "\u2192 2", ("p", 2)),
|
||||
(KEY, "key", "price", None), (OWN, "link", "\u2192 4", ("p", 4)),
|
||||
(KEY, "key", "tags", None), (OWN, "link", "\u2192 6", ("p", 6)),
|
||||
(KEY, "key", "retries", None), (OWN, "link", "\u2192 4'", ("o", 4))]
|
||||
|
||||
STEPS = [
|
||||
("parsed", "After parse: nine nodes, nothing owned yet"),
|
||||
('doc.set(doc.root(), "port", 6380)', "set: the node of the integer is overwritten in place"),
|
||||
('doc.erase(doc.root()["tags"], 0)', "erase: the array moves to a block of links in owned storage"),
|
||||
('doc.push_back(doc.root()["tags"], "new")', "push_back: a new node, and a link appended to the block"),
|
||||
('doc.set(doc.root(), "retries", 3)', "set of a new member: the object moves to a block as well"),
|
||||
]
|
||||
|
||||
DUMPS = [
|
||||
'{"port":6379,"price":19.90,"tags":["eu","a\\"b"]}',
|
||||
'{"port":6380,"price":19.90,"tags":["eu","a\\"b"]}',
|
||||
'{"port":6380,"price":19.90,"tags":["a\\"b"]}',
|
||||
'{"port":6380,"price":19.90,"tags":["a\\"b","new"]}',
|
||||
'{"port":6380,"price":19.90,"tags":["a\\"b","new"],"retries":3}',
|
||||
]
|
||||
|
||||
|
||||
def edit_frame(step):
|
||||
W, H, P = 840, 372, "jve%d" % step
|
||||
b = markers(P, (LINE, OWN, STRUCT, BAD))
|
||||
b.append(text(16, 22, "parsed index: fixed, never moves", "nj-jsonview-note nj-jsonview-strong"))
|
||||
b.append(text(824, 22, "the source text is never written", "nj-jsonview-note", anchor="end"))
|
||||
ny, nh = 34, 50
|
||||
moved = {6: 0} if step >= 2 else {}
|
||||
if step >= 4:
|
||||
moved[0] = 5
|
||||
for i, c, kind, fields, span, short in NODES:
|
||||
x = nx(i)
|
||||
cls = "nj-jsonview-node"
|
||||
label = short
|
||||
changed = False
|
||||
if i == 2 and step >= 1:
|
||||
label, changed = "6380", step == 1
|
||||
if c == KEY:
|
||||
label = label.strip('"')
|
||||
gap_node = (i == 7 and step >= 2)
|
||||
if gap_node:
|
||||
cls += " nj-jsonview-gap"
|
||||
if changed:
|
||||
cls += " nj-jsonview-hot"
|
||||
b.append(rect(x, ny, NW, nh, cls, c))
|
||||
b.append(text(x + 6, ny + 13, str(i), "nj-jsonview-tiny"))
|
||||
b.append(text(x + NW / 2, ny + 27, kind, "nj-jsonview-kind nj-jsonview-xs", c, anchor="middle"))
|
||||
if i in moved:
|
||||
b.append(text(x + NW - 5, ny + 13, "moved", "nj-jsonview-tiny nj-jsonview-c-struct nj-jsonview-strong", anchor="end"))
|
||||
b.append(text(x + NW / 2, ny + 43, label, "nj-jsonview-mono nj-jsonview-sm", anchor="middle"))
|
||||
if gap_node:
|
||||
b.append(text(x + NW / 2, ny + nh + 14, "unreferenced", "nj-jsonview-tiny nj-jsonview-c-bad", anchor="middle"))
|
||||
# owned storage
|
||||
oy, oh = 196, 46
|
||||
b.append(text(16, oy + oh + 46, "owned storage: allocated on demand, never moves", "nj-jsonview-note nj-jsonview-strong", OWN))
|
||||
slots = OWN_STEPS.get(step, [])
|
||||
b.append(rect(OX0 - 6, oy - 4, 820, oh + 8, "nj-jsonview-store", rx=8))
|
||||
if not slots:
|
||||
b.append(text(OX0 + 10, oy + 28, "empty: a read-only json_document has none of this", "nj-jsonview-note"))
|
||||
for j, (c, l1, l2, tgt) in enumerate(slots):
|
||||
x = ox(j)
|
||||
if c is None:
|
||||
b.append(rect(x, oy, OW, oh, "nj-jsonview-node nj-jsonview-free", OWN))
|
||||
b.append(text(x + OW / 2, oy + 28, "free", "nj-jsonview-tiny", anchor="middle"))
|
||||
continue
|
||||
hot = (step == 2 and j < 3) or (step == 3 and j in (2, 3)) or (step == 4 and j >= 4)
|
||||
b.append(rect(x, oy, OW, oh, "nj-jsonview-node" + (" nj-jsonview-hot" if hot else ""), c))
|
||||
b.append(text(x + OW - 4, oy + 11, "%d'" % j, "nj-jsonview-tiny", anchor="end"))
|
||||
b.append(text(x + OW / 2, oy + 24, l1, "nj-jsonview-kind nj-jsonview-xs", c, anchor="middle"))
|
||||
b.append(text(x + OW / 2, oy + 39, l2, "nj-jsonview-mono nj-jsonview-xs", anchor="middle"))
|
||||
if tgt:
|
||||
cx = x + OW / 2
|
||||
if tgt[0] == "p":
|
||||
b.append(curve(cx, oy, nx(tgt[1]) + NW / 2 + (8 if tgt[1] in moved else 0), ny + nh, OWN, P))
|
||||
else:
|
||||
tx = ox(tgt[1]) + OW / 2
|
||||
d = "M%g %g C%g %g %g %g %g %g" % (cx, oy + oh, cx, oy + oh + 26, tx, oy + oh + 26, tx, oy + oh + 2)
|
||||
b.append('<path class="nj-jsonview-arr nj-jsonview-c-own" d="%s" marker-end="url(#%s-own)"/>' % (d, P))
|
||||
# moved containers point to their block header
|
||||
for i, hj in moved.items():
|
||||
b.append(curve(nx(i) + NW / 2 - 8, ny + nh, ox(hj) + OW / 2, oy, STRUCT, P, dashed=True))
|
||||
# edit text buffer
|
||||
texts = ["new", "retries"][: (1 if step == 3 else 2 if step >= 4 else 0)]
|
||||
if texts:
|
||||
tx0 = 596
|
||||
b.append(text(tx0 - 8, oy + oh + 40, "edited text", "nj-jsonview-note", anchor="end"))
|
||||
b.append(rect(tx0, oy + oh + 22, 10 * len("".join(texts)) + 12, 26, "nj-jsonview-node", STR, rx=4))
|
||||
b.append(text(tx0 + 6, oy + oh + 40, "".join(texts), "nj-jsonview-mono nj-jsonview-sm"))
|
||||
# views and iterators
|
||||
vy = 318
|
||||
b.append('<path class="nj-jsonview-sep" d="M16 %g H824"/>' % (vy - 18))
|
||||
b.append(text(16, vy, 'view v = root()["tags"] (taken after parse):', "nj-jsonview-mono nj-jsonview-xs"))
|
||||
b.append(text(320, vy, "valid, sees every edit", "nj-jsonview-note nj-jsonview-c-struct nj-jsonview-strong"))
|
||||
b.append(text(16, vy + 20, "iterator over tags (taken after parse):", "nj-jsonview-mono nj-jsonview-xs"))
|
||||
if step >= 2:
|
||||
b.append(text(320, vy + 20, "invalid: tags no longer uses the parsed layout", "nj-jsonview-note nj-jsonview-c-bad nj-jsonview-strong"))
|
||||
else:
|
||||
b.append(text(320, vy + 20, "valid", "nj-jsonview-note nj-jsonview-c-struct nj-jsonview-strong"))
|
||||
b.append(text(16, vy + 42, "dump(): " + DUMPS[step], "nj-jsonview-mono nj-jsonview-xs nj-jsonview-muted"))
|
||||
code, cap = STEPS[step]
|
||||
write("json_view_edit_%d.svg" % step, W, H, "fig-jve%d-title" % step,
|
||||
"Step %d of %d: %s" % (step + 1, len(STEPS), code),
|
||||
cap + ". The dump with number_format::source is " + DUMPS[step] + ".", b, "nj-jsonview-editfig")
|
||||
|
||||
|
||||
layout()
|
||||
lifetime()
|
||||
for s in range(len(STEPS)):
|
||||
edit_frame(s)
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Draws the illustrative memory-over-time chart of the large-input page as an inline SVG into docs/images/.
|
||||
|
||||
The chart shows the qualitative shape of the memory use while a large array of records is parsed in four ways: parse()
|
||||
builds the whole tree (DOM), a parser callback discards every record, a SAX handler keeps only what it needs, and a
|
||||
json_document keeps the text plus its index. It is not a measurement: there are no numbers on the axes, and the
|
||||
relative heights depend on the data.
|
||||
|
||||
The SVG is not an image: features/parsing/large_and_partial_input.md includes its text with
|
||||
`--8<-- "images/memory_over_time.svg"`, so it picks up the colours and fonts of the page (see the `nj-fig` rules in
|
||||
docs/css/brand.css and the `nj-sax-` rules in docs/css/fig-sax.css). Run `python3 scripts/figures/memory_over_time.py`
|
||||
from docs/mkdocs after changing the drawing, and commit the generated file. In SVG text write the word JSON as
|
||||
`J​SON` (likewise DOM and SAX, which esc() does), otherwise the abbreviation extension wraps it in an <abbr> and
|
||||
the text disappears.
|
||||
"""
|
||||
import html
|
||||
from pathlib import Path
|
||||
|
||||
OUT = Path(__file__).resolve().parents[2] / "docs" / "images"
|
||||
|
||||
W, H = 660, 312
|
||||
X0, X1 = 56, 440 # plot area
|
||||
Y0, Y1 = 48, 272 # top, baseline
|
||||
|
||||
|
||||
def esc(s):
|
||||
# break the words of the glossary (includes/glossary.md) so the abbreviation extension leaves them alone
|
||||
s = html.escape(s)
|
||||
for word in ("DOM", "SAX", "JSON"):
|
||||
s = s.replace(word, word[0] + "​" + word[1:])
|
||||
return s
|
||||
|
||||
|
||||
def main():
|
||||
body = []
|
||||
tid, did = "fig-sax-mem-title", "fig-sax-mem-desc"
|
||||
# axes
|
||||
body.append('<path class="nj-sax-axis" d="M%d %d V%d H%d"/>' % (X0, Y0 - 12, Y1, X1 + 8))
|
||||
body.append('<text class="fg-h" x="%d" y="24">memory</text>' % (X0 - 40))
|
||||
body.append('<text class="fg-note fg-note--l" x="%d" y="%d">time: records parsed</text>' % (X0, Y1 + 22))
|
||||
body.append('<text class="fg-note fg-note--r nj-sax-illus" x="%d" y="24">illustrative, not a measurement</text>' % (W - 12))
|
||||
|
||||
# DOM: grows with every record until the whole tree is built
|
||||
body.append('<path class="nj-sax-curve nj-sax-c-dom" d="M%d %d L%d %d"/>' % (X0, Y1 - 4, X1, Y0 + 4))
|
||||
# json_document: the text is in memory from the start; the index grows while parsing
|
||||
yt, yi = 168, 146
|
||||
body.append('<path class="nj-sax-curve nj-sax-c-doc" d="M%d %d L%d %d"/>' % (X0, yt, X1, yi))
|
||||
# callback: one record at a time, released when it is discarded
|
||||
hi, lo, step = Y1 - 34, Y1 - 22, 32
|
||||
d, x = "M%d %d" % (X0, lo), X0
|
||||
while x + step <= X1:
|
||||
d += " L%d %d L%d %d" % (x + step, hi, x + step, lo)
|
||||
x += step
|
||||
body.append('<path class="nj-sax-curve nj-sax-c-cb" d="%s"/>' % d)
|
||||
# SAX: no value is built, only what the handler keeps
|
||||
body.append('<path class="nj-sax-curve nj-sax-c-sax" d="M%d %d H%d"/>' % (X0, Y1 - 8, X1))
|
||||
|
||||
labels = [
|
||||
(Y0 + 9, "nj-sax-c-dom", "DOM: parse()", "the whole tree"),
|
||||
(yi + 5, "nj-sax-c-doc", "json_document", "the text plus its index"),
|
||||
(hi - 2, "nj-sax-c-cb", "callback, discard", "one record at a time"),
|
||||
(Y1 + 6, "nj-sax-c-sax", "SAX", "only what the handler keeps"),
|
||||
]
|
||||
for y, cls, name, note in labels:
|
||||
body.append('<text class="nj-sax-lab %s" x="%d" y="%d">%s</text>' % (cls, X1 + 16, y - 9, esc(name)))
|
||||
body.append('<text class="fg-note fg-note--l" x="%d" y="%d">%s</text>' % (X1 + 16, y + 8, esc(note)))
|
||||
|
||||
title = "Illustrative memory use over time while a large array of records is parsed, not a measurement."
|
||||
desc = ("Four curves. DOM, parse(): rises steadily until the whole tree is in memory. json_document: starts at about "
|
||||
"the size of the text, which it keeps, and grows a little while its index is built. Parser callback that "
|
||||
"discards every record: stays flat at the size of one record. SAX: stays flat and lowest, because no value "
|
||||
"is built and only what the handler keeps is in memory.")
|
||||
s = ['<svg class="nj-fig nj-fig--sax-mem" viewBox="0 0 %d %d" role="img" aria-labelledby="%s %s" '
|
||||
'xmlns="http://www.w3.org/2000/svg">' % (W, H, tid, did),
|
||||
'<title id="%s">%s</title>' % (tid, esc(title)),
|
||||
'<desc id="%s">%s</desc>' % (did, esc(desc))]
|
||||
s += body
|
||||
s.append("</svg>")
|
||||
(OUT / "memory_over_time.svg").write_text("\n".join(s) + "\n")
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,196 @@
|
||||
"""Draws the quality-assurance pipeline overview of community/quality_assurance.md as an inline SVG into
|
||||
docs/images/qa_pipeline.svg.
|
||||
|
||||
The SVG is not an image: the page includes its text with `--8<-- "images/qa_pipeline.svg"`, so it picks up the colours
|
||||
and fonts of the page (see the `nj-fig` rules in docs/css/brand.css and the `nj-exceptions-*` rules in
|
||||
docs/css/fig-exceptions.css). The file holds two drawings, a three-column one and a one-column one for phones; the CSS
|
||||
shows one of them.
|
||||
|
||||
The lanes, tools and counts are taken from the text of community/quality_assurance.md, the trigger badges from the
|
||||
`on:` sections of .github/workflows/*.yml. Update LANES when the page changes, run
|
||||
`python3 scripts/figures/qa_pipeline.py` from docs/mkdocs, and commit the generated file. In SVG text write the word
|
||||
JSON as `J​SON`, otherwise the abbreviation extension wraps it in an <abbr> and the text disappears.
|
||||
"""
|
||||
import html
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
OUT = Path(__file__).resolve().parents[2] / "docs" / "images"
|
||||
P = "nj-exceptions-"
|
||||
|
||||
# trigger badges: key -> (label, css modifier)
|
||||
TRIGGERS = {
|
||||
"push": ("every push / PR", "push"),
|
||||
"pr": ("every PR", "push"),
|
||||
"sched": ("scheduled", "sched"),
|
||||
"cont": ("continuous", "cont"),
|
||||
}
|
||||
|
||||
# title, triggers, lines (at most 31 characters, so they fit a card of the three-column drawing)
|
||||
LANES = [
|
||||
("Build matrix", ["push"], [
|
||||
"50+ compilers: GCC, Clang, MSVC,",
|
||||
"AppleClang, Intel, NVIDIA, ...",
|
||||
"C++11, 14, 17, 20, 23, and 26",
|
||||
"libc++ and libstdc++",
|
||||
"warnings: Clang -Weverything,",
|
||||
"GCC 300+ warnings",
|
||||
]),
|
||||
("Unit tests (doctest)", ["push"], [
|
||||
"every public API function",
|
||||
"every exception id and message",
|
||||
"all Unicode code points",
|
||||
"single header and split headers",
|
||||
"custom number, string, array,",
|
||||
"and object types",
|
||||
]),
|
||||
("Coverage", ["push"], [
|
||||
"unit tests cover all lines",
|
||||
"of the code base",
|
||||
]),
|
||||
("Static analysis", ["push", "sched"], [
|
||||
"Clang-Tidy, Cppcheck, Infer",
|
||||
"Clang Static Analyzer (52",
|
||||
"checkers), IWYU, Codacy",
|
||||
"CodeQL, Semgrep, Flawfinder",
|
||||
"(also scheduled)",
|
||||
]),
|
||||
("Dynamic analysis", ["push"], [
|
||||
"runtime assertions",
|
||||
"Valgrind (Memcheck)",
|
||||
"address and undefined behavior",
|
||||
"sanitizers, integer overflow,",
|
||||
"nullability violations",
|
||||
]),
|
||||
("Fuzzing", ["pr", "cont"], [
|
||||
"CIFuzz on every pull request",
|
||||
"OSS-Fuzz: billions of inputs",
|
||||
"every crash fixed with a",
|
||||
"reproducing unit test",
|
||||
]),
|
||||
("Style", ["push"], [
|
||||
"Artistic Style (astyle),",
|
||||
"enforced in the CI",
|
||||
"cpplint with 61 rules",
|
||||
]),
|
||||
("Docs and examples", ["push", "sched"], [
|
||||
"every documentation example is",
|
||||
"tested; changed output fails",
|
||||
"site built with warnings",
|
||||
"as errors; links checked",
|
||||
"regularly (scheduled)",
|
||||
]),
|
||||
("Integration, supply chain", ["push", "sched"], [
|
||||
"CMake 3.5, 3.31.6, and 4.0.0",
|
||||
"Meson, Swift Package Manager",
|
||||
"dependency review on every PR",
|
||||
"Dependabot daily, OpenSSF",
|
||||
"Scorecard, REUSE",
|
||||
]),
|
||||
]
|
||||
|
||||
|
||||
GLOSSARY = Path(__file__).resolve().parents[2] / "includes" / "glossary.md"
|
||||
ABBRS = sorted(re.findall(r"^\*\[([^\]]+)\]:", GLOSSARY.read_text(), re.M), key=len, reverse=True)
|
||||
|
||||
|
||||
def noabbr(s):
|
||||
"""Breaks every glossary abbreviation with a zero-width space, so the abbr extension leaves SVG text alone."""
|
||||
for a in ABBRS:
|
||||
s = re.sub(r"(?<![\w&#])%s(?!\w)" % re.escape(a), a[0] + "​" + a[1:], s)
|
||||
return s
|
||||
|
||||
|
||||
def esc(s):
|
||||
return html.escape(s, quote=True)
|
||||
|
||||
|
||||
def txt(s):
|
||||
return noabbr(esc(s))
|
||||
|
||||
|
||||
def badge_w(label):
|
||||
return len(label) * 6.3 + 16
|
||||
|
||||
|
||||
def card(x, y, w, h, title, triggers, lines):
|
||||
s = ['<g class="%scard">' % P,
|
||||
'<rect class="%sbox" x="%g" y="%g" width="%g" height="%g" rx="8"/>' % (P, x, y, w, h),
|
||||
'<text class="%sctitle" x="%g" y="%g">%s</text>' % (P, x + 14, y + 25, txt(title))]
|
||||
bx = x + 14
|
||||
for t in triggers:
|
||||
label, mod = TRIGGERS[t]
|
||||
bw = badge_w(label)
|
||||
s.append('<rect class="%sbadge %sbadge--%s" x="%g" y="%g" width="%g" height="20" rx="10"/>' % (P, P, mod, bx, y + 36, bw))
|
||||
s.append('<text class="%sbadgetext %sbadgetext--%s" x="%g" y="%g">%s</text>' % (P, P, mod, bx + bw / 2, y + 50, txt(label)))
|
||||
bx += bw + 6
|
||||
for i, line in enumerate(lines):
|
||||
s.append('<text class="%sline" x="%g" y="%g">%s</text>' % (P, x + 14, y + 78 + i * 18, txt(line)))
|
||||
s.append('</g>')
|
||||
return "\n".join(s)
|
||||
|
||||
|
||||
def pill(cx, y, w, label, sub=None, cls=""):
|
||||
h = 46 if sub else 34
|
||||
s = ['<rect class="%sstage%s" x="%g" y="%g" width="%g" height="%d" rx="%g"/>' % (P, cls, cx - w / 2, y, w, h, h / 2),
|
||||
'<text class="%sstagetext" x="%g" y="%g">%s</text>' % (P, cx, y + 22, txt(label))]
|
||||
if sub:
|
||||
s.append('<text class="%sstagesub" x="%g" y="%g">%s</text>' % (P, cx, y + 38, txt(sub)))
|
||||
return "\n".join(s), h
|
||||
|
||||
|
||||
def arrow(x, y1, y2, mid):
|
||||
return '<line class="%sedge" x1="%g" y1="%g" x2="%g" y2="%g" marker-end="url(#%s)"/>' % (P, x, y1, x, y2, mid)
|
||||
|
||||
|
||||
def drawing(W, cols, suffix):
|
||||
mid = "nj-qa-arrow-" + suffix
|
||||
body = []
|
||||
cx = W / 2
|
||||
s, h = pill(cx, 8, 230, "push or pull request")
|
||||
body.append(s)
|
||||
y = 8 + h
|
||||
body.append(arrow(cx, y, y + 26, mid))
|
||||
# container with the lanes
|
||||
top = y + 28
|
||||
pad, gap = 14, 12
|
||||
cw = (W - 24 - 2 * pad - (cols - 1) * gap) / cols
|
||||
body.append('<text class="%sgroup" x="%g" y="%g">checks, run in parallel on GitHub Actions and AppVeyor</text>' % (P, 12 + pad, top + 24) if cols > 1 else
|
||||
'<text class="%sgroup" x="%g" y="%g">checks, run in parallel</text>' % (P, 12 + pad, top + 24))
|
||||
gy = top + 40
|
||||
rows = [LANES[i:i + cols] for i in range(0, len(LANES), cols)]
|
||||
for r in rows:
|
||||
ch = 78 + 18 * (max(len(l[2]) for l in r) - 1) + 16
|
||||
for j, (title, trig, lines) in enumerate(r):
|
||||
body.append(card(12 + pad + j * (cw + gap), gy, cw, ch, title, trig, lines))
|
||||
gy += ch + gap
|
||||
bottom = gy - gap + pad
|
||||
body.insert(0, '<rect class="%sgroupbox" x="12" y="%g" width="%g" height="%g" rx="12"/>' % (P, top, W - 24, bottom - top))
|
||||
# after the checks
|
||||
body.append(arrow(cx, bottom, bottom + 26, mid))
|
||||
s, h = pill(cx, bottom + 28, 230, "merge into develop", cls=" %sstage--merge" % P)
|
||||
body.append(s)
|
||||
y = bottom + 28 + h
|
||||
body.append(arrow(cx, y, y + 26, mid))
|
||||
s, h = pill(cx, y + 28, 300, "release", "no open vulnerability alert", cls=" %sstage--merge" % P)
|
||||
body.append(s)
|
||||
H = y + 28 + h + 8
|
||||
defs = ('<defs><marker id="%s" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">'
|
||||
'<path class="fg-head" d="M0 1 L10 5 L0 9 z"/></marker></defs>' % mid)
|
||||
tid, did = "nj-qa-title-" + suffix, "nj-qa-desc-" + suffix
|
||||
desc = ("Every push or pull request starts these checks in parallel: " +
|
||||
"; ".join("%s (%s): %s" % (t, ", ".join(TRIGGERS[k][0] for k in trig), " ".join(lines)) for t, trig, lines in LANES) +
|
||||
". When they pass, the change is merged into develop; a release is made only when no vulnerability alert is open.")
|
||||
out = ['<svg class="nj-fig %sqa %sqa--%s" viewBox="0 0 %d %d" role="img" aria-labelledby="%s %s" xmlns="http://www.w3.org/2000/svg">'
|
||||
% (P, P, "narrow" if cols == 1 else "wide", W, H, tid, did),
|
||||
'<title id="%s">Quality assurance pipeline</title>' % tid,
|
||||
'<desc id="%s">%s</desc>' % (did, txt(desc)),
|
||||
defs] + body + ['</svg>']
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def main():
|
||||
(OUT / "qa_pipeline.svg").write_text(drawing(760, 3, "w") + "\n" + drawing(360, 1, "n") + "\n")
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,76 @@
|
||||
"""Draws the JSON number railroad diagram as an inline SVG into docs/images/.
|
||||
|
||||
The SVG is not an image: the pages include its text with `--8<-- "images/json_syntax_number.svg"`, so it picks up the colours and fonts of the
|
||||
page (see the `railroad` and `nj-fig` rules in docs/css/brand.css). Run `python3 scripts/figures/railroad_number.py` from docs/mkdocs after
|
||||
changing the drawing, and commit the generated file. In SVG text write the word JSON as `J​SON`, otherwise the
|
||||
abbreviation extension wraps it in an <abbr> and the text disappears.
|
||||
"""
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
OUT = Path(__file__).resolve().parents[2] / "docs" / "images"
|
||||
Y0, Y1, Y2, Y3 = 44, 92, 136, 180
|
||||
R = 10
|
||||
paths, shapes, texts = [], [], []
|
||||
|
||||
def rounded(pts, r=R):
|
||||
d = "M%g %g" % pts[0]
|
||||
for i in range(1, len(pts) - 1):
|
||||
(x0, y0), (x1, y1), (x2, y2) = pts[i-1], pts[i], pts[i+1]
|
||||
l1 = math.hypot(x1-x0, y1-y0); l2 = math.hypot(x2-x1, y2-y1)
|
||||
rr = min(r, l1/2, l2/2)
|
||||
ax, ay = x1 - (x1-x0)/l1*rr, y1 - (y1-y0)/l1*rr
|
||||
bx, by = x1 + (x2-x1)/l2*rr, y1 + (y2-y1)/l2*rr
|
||||
d += " L%g %g Q%g %g %g %g" % (ax, ay, x1, y1, bx, by)
|
||||
d += " L%g %g" % pts[-1]
|
||||
return d
|
||||
|
||||
def path(*pts): paths.append(rounded(list(pts)))
|
||||
def circle(cx, cy, ch):
|
||||
shapes.append('<circle class="rr-bg" cx="%g" cy="%g" r="13"/>' % (cx, cy))
|
||||
shapes.append('<circle class="rr-t" cx="%g" cy="%g" r="13"/>' % (cx, cy))
|
||||
texts.append('<text class="rr-c" x="%g" y="%g">%s</text>' % (cx, cy+5, ch))
|
||||
def box(x0, x1, cy, label):
|
||||
shapes.append('<rect class="rr-bg" x="%g" y="%g" width="%g" height="28" rx="6"/>' % (x0, cy-14, x1-x0))
|
||||
shapes.append('<rect class="rr-n" x="%g" y="%g" width="%g" height="28" rx="6"/>' % (x0, cy-14, x1-x0))
|
||||
texts.append('<text class="rr-b" x="%g" y="%g">%s</text>' % ((x0+x1)/2, cy+5, label))
|
||||
def loop(xr, xl, cy): # return path below a box row
|
||||
path((xr, cy), (xr, cy+34), (xl, cy+34), (xl, cy))
|
||||
|
||||
# start / main
|
||||
path((8, Y0), (850, Y0))
|
||||
# A: optional minus
|
||||
path((24, Y0), (38, Y0), (38, Y1), (59, Y1)); circle(72, Y1, "−"); path((85, Y1), (106, Y1), (106, Y0), (120, Y0))
|
||||
# B: 0 | 1-9 digit*
|
||||
circle(257, Y0, "0")
|
||||
path((120, Y0), (134, Y0), (134, Y1), (160, Y1)); box(160, 242, Y1, "digit 1–9")
|
||||
path((242, Y1), (380, Y1), (380, Y0), (396, Y0))
|
||||
path((242, Y1), (258, Y1), (258, Y2), (278, Y2)); box(278, 332, Y2, "digit")
|
||||
path((332, Y2), (348, Y2), (348, Y1)); loop(340, 270, Y2)
|
||||
# C: fraction
|
||||
path((396, Y0), (410, Y0), (410, Y1), (425, Y1)); circle(438, Y1, ".")
|
||||
path((451, Y1), (462, Y1)); box(462, 516, Y1, "digit"); loop(524, 456, Y1)
|
||||
path((516, Y1), (530, Y1), (530, Y0), (546, Y0))
|
||||
# D: exponent
|
||||
path((546, Y0), (560, Y0), (560, Y1), (577, Y1)); circle(590, Y1, "e")
|
||||
path((546, Y0), (560, Y0), (560, Y2), (577, Y2)); circle(590, Y2, "E")
|
||||
path((603, Y1), (728, Y1)); path((603, Y2), (620, Y2), (620, Y1))
|
||||
path((640, Y1), (654, Y1), (654, Y2), (670, Y2)); circle(683, Y2, "−")
|
||||
path((640, Y1), (654, Y1), (654, Y3), (670, Y3)); circle(683, Y3, "+")
|
||||
path((696, Y2), (712, Y2), (712, Y1)); path((696, Y3), (712, Y3), (712, Y1))
|
||||
path((728, Y1), (746, Y1)); box(746, 800, Y1, "digit"); loop(808, 738, Y1)
|
||||
path((800, Y1), (816, Y1), (816, Y0), (832, Y0))
|
||||
|
||||
svg = ['<svg class="railroad" viewBox="0 0 860 214" role="img" aria-labelledby="rr-title" xmlns="http://www.w3.org/2000/svg">',
|
||||
'<title id="rr-title">Railroad diagram of the J​SON number syntax: optional minus, then 0 or a digit from 1 to 9 followed by digits, then an optional fraction (a dot and digits) and an optional exponent (e or E, an optional sign, and digits).</title>',
|
||||
'<g class="rr-track" fill="none" stroke="currentColor" stroke-opacity=".6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">']
|
||||
for d in paths: svg.append('<path d="%s"/>' % d)
|
||||
svg.append('</g>')
|
||||
svg.append('<polygon class="rr-arrow" points="2,%d 12,%d 2,%d"/>' % (Y0-6, Y0, Y0+6))
|
||||
svg.append('<polygon class="rr-arrow" points="846,%d 858,%d 846,%d"/>' % (Y0-6, Y0, Y0+6))
|
||||
svg += shapes + texts
|
||||
svg.append('<text class="rr-l rr-name" x="24" y="%d">number</text>' % (Y0-12))
|
||||
svg.append('<text class="rr-l" x="420" y="%d">fraction</text>' % (Y0-12))
|
||||
svg.append('<text class="rr-l" x="580" y="%d">exponent</text>' % (Y0-12))
|
||||
svg.append('</svg>')
|
||||
open(OUT / "json_syntax_number.svg", "w").write("\n".join(svg) + "\n")
|
||||
@@ -0,0 +1,127 @@
|
||||
"""Draws the release timeline of home/releases/index.md as an inline SVG into docs/images/.
|
||||
|
||||
The releases and their dates come from the tables of that page (rows like `| [v3.11.2](v3.11.2.md) | 2022-08-12 | ...`),
|
||||
so after adding a release to the page, run `python3 scripts/figures/release_timeline.py` from docs/mkdocs and commit
|
||||
the regenerated docs/images/release_timeline.svg. The page includes the SVG text with
|
||||
`--8<-- "images/release_timeline.svg"`, so it follows the light/dark toggle (see the `nj-fig` rules in
|
||||
docs/css/brand.css and the `nj-int-*` rules in docs/css/fig-integration.css). Every marker has a <title> tooltip.
|
||||
The timeline also marks the first commit of the repository (FIRST_COMMIT) as a diamond. Rows without a date (the "Unreleased" row) are skipped; any other release row that cannot be parsed stops the script.
|
||||
"""
|
||||
import datetime
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
DOCS = Path(__file__).resolve().parents[2] / "docs"
|
||||
OUT = Path(__file__).resolve().parents[2] / "docs" / "images"
|
||||
PAGE = DOCS / "home" / "releases" / "index.md"
|
||||
|
||||
ROW = re.compile(r"^\|\s*\[v(\d+)\.(\d+)\.(\d+)\]\([^)]*\)\s*\|\s*([^|]*?)\s*\|")
|
||||
W, LEFT, RIGHT = 880, 24, 856
|
||||
LABEL_ROW, CHAR_W = 17, 7.0
|
||||
FIRST_COMMIT = datetime.date(2013, 7, 4) # "- initial commit" of the repository
|
||||
|
||||
|
||||
def parse(markdown):
|
||||
releases = []
|
||||
for line in markdown.splitlines():
|
||||
if not line.startswith("| [v"):
|
||||
continue
|
||||
m = ROW.match(line)
|
||||
if not m:
|
||||
sys.exit("cannot parse release row: " + line)
|
||||
try:
|
||||
date = datetime.date.fromisoformat(m.group(4))
|
||||
except ValueError:
|
||||
sys.exit("cannot parse date in row: " + line)
|
||||
releases.append(((int(m.group(1)), int(m.group(2)), int(m.group(3))), date))
|
||||
if len(releases) < 2:
|
||||
sys.exit("found fewer than two releases in " + str(PAGE))
|
||||
if len({v for v, _ in releases}) != len(releases):
|
||||
sys.exit("duplicate release in " + str(PAGE))
|
||||
return sorted(releases, key=lambda r: (r[1], r[0]))
|
||||
|
||||
|
||||
def name(version):
|
||||
return "v%d.%d.%d" % version
|
||||
|
||||
|
||||
def draw(releases):
|
||||
first, last = FIRST_COMMIT, releases[-1][1]
|
||||
start, end = datetime.date(first.year, 1, 1), datetime.date(last.year + 1, 1, 1)
|
||||
span = (end - start).days
|
||||
|
||||
def xof(date):
|
||||
return LEFT + (RIGHT - LEFT) * (date - start).days / span
|
||||
|
||||
# minor labels, placed greedily into the lowest free row above the axis
|
||||
rows_end, labels = [], []
|
||||
for version, date in [(None, FIRST_COMMIT)] + releases:
|
||||
if version is not None and version[2] != 0:
|
||||
continue
|
||||
text = "first commit" if version is None else "%d.%d" % version[:2]
|
||||
x, half = xof(date), len(text) * CHAR_W / 2 + 2
|
||||
row = next((i for i, e in enumerate(rows_end) if e < x - half), None)
|
||||
if row is None:
|
||||
rows_end.append(0)
|
||||
row = len(rows_end) - 1
|
||||
rows_end[row] = x + half
|
||||
labels.append((x, row, text))
|
||||
top = 14 + LABEL_ROW * len(rows_end)
|
||||
axis = top + 12
|
||||
height = axis + 70
|
||||
|
||||
s = ['<svg class="nj-fig nj-fig--timeline" viewBox="0 0 %d %d" role="img" aria-labelledby="nj-int-tl-title" '
|
||||
'xmlns="http://www.w3.org/2000/svg">' % (W, height)]
|
||||
minors = sum(1 for v, _ in releases if v[2] == 0)
|
||||
s.append('<title id="nj-int-tl-title">Timeline from the first commit (%s) and the %d releases from %s (%s) to %s '
|
||||
'(%s): %d minor or major releases, drawn as filled dots with their version above the axis, and %d patch '
|
||||
'releases, drawn as rings. The first commit is drawn as a diamond.</title>'
|
||||
% (first.isoformat(), len(releases), name(releases[0][0]), releases[0][1].isoformat(),
|
||||
name(releases[-1][0]), last.isoformat(), minors, len(releases) - minors))
|
||||
# year grid
|
||||
for year in range(start.year, end.year + 1):
|
||||
x = xof(datetime.date(year, 1, 1))
|
||||
s.append('<line class="nj-int-grid" x1="%.1f" y1="%d" x2="%.1f" y2="%d"/>' % (x, top - 4, x, axis + 8))
|
||||
if year < end.year:
|
||||
mid = xof(datetime.date(year, 7, 1))
|
||||
s.append('<text class="nj-int-year" x="%.1f" y="%d">%d</text>' % (mid, axis + 24, year))
|
||||
s.append('<line class="nj-int-axis" x1="%d" y1="%d" x2="%d" y2="%d"/>' % (LEFT, axis, RIGHT, axis))
|
||||
# major version spans
|
||||
majors = {}
|
||||
for version, date in releases:
|
||||
majors.setdefault(version[0], []).append(date)
|
||||
for major, dates in sorted(majors.items()):
|
||||
x0, x1 = xof(min(dates)), xof(max(dates))
|
||||
y = axis + 42
|
||||
s.append('<path class="nj-int-axis" fill="none" d="M%.1f %d V%d H%.1f V%d"/>' % (x0, y - 5, y, x1, y - 5))
|
||||
s.append('<text class="nj-int-major" x="%.1f" y="%d" text-anchor="%s">%d.x</text>'
|
||||
% ((x0 + x1) / 2 if x1 - x0 > 30 else x0, y + 17, "middle" if x1 - x0 > 30 else "start", major))
|
||||
# leaders and labels
|
||||
for x, row, text in labels:
|
||||
y = top - row * LABEL_ROW
|
||||
s.append('<line class="nj-int-leader" x1="%.1f" y1="%d" x2="%.1f" y2="%d"/>' % (x, y + 3, x, axis - 7))
|
||||
s.append('<text class="nj-int-label" x="%.1f" y="%d">%s</text>' % (x, y, text))
|
||||
# markers: the first commit, then one per date, patch rings first so that minor dots stay on top
|
||||
x = xof(FIRST_COMMIT)
|
||||
s.append('<g><title>First commit %s</title><circle class="nj-int-hit" cx="%.1f" cy="%d" r="9"/>'
|
||||
'<path class="nj-int-minor" d="M%.1f %d l7 -7 l7 7 l-7 7z" transform="translate(-7 0)"/></g>'
|
||||
% (FIRST_COMMIT.isoformat(), x, axis, x, axis))
|
||||
by_date = {}
|
||||
for version, date in releases:
|
||||
by_date.setdefault(date, []).append(version)
|
||||
groups = sorted(by_date.items(), key=lambda g: any(v[2] == 0 for v in g[1]))
|
||||
for date, versions in groups:
|
||||
minor = any(v[2] == 0 for v in versions)
|
||||
x = xof(date)
|
||||
tip = ", ".join(name(v) for v in sorted(versions)) + " released " + date.isoformat()
|
||||
s.append('<g><title>%s</title><circle class="nj-int-hit" cx="%.1f" cy="%d" r="9"/>'
|
||||
'<circle class="%s" cx="%.1f" cy="%d" r="%g"/></g>'
|
||||
% (tip, x, axis, "nj-int-minor" if minor else "nj-int-patch", x, axis, 6 if minor else 3.5))
|
||||
s.append('</svg>')
|
||||
return "\n".join(s) + "\n"
|
||||
|
||||
|
||||
releases = parse(PAGE.read_text(encoding="utf-8"))
|
||||
(OUT / "release_timeline.svg").write_text(draw(releases), encoding="utf-8")
|
||||
print("wrote", OUT / "release_timeline.svg", "with", len(releases), "releases")
|
||||
Reference in new issue
Block a user