mirror of
https://github.com/nlohmann/json.git
synced 2026-10-27 16:28:57 +00:00
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>
This commit is contained in:
96 files changed
+6737
-69
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,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,121 @@
|
||||
"""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.
|
||||
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
|
||||
|
||||
|
||||
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 = releases[0][1], 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 releases:
|
||||
if version[2] != 0:
|
||||
continue
|
||||
text = "%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 of 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.</title>' % (len(releases), name(releases[0][0]), first.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: one per date, patch rings first so that minor dots stay on top
|
||||
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