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:
Niels Lohmann committed 2026-10-26 10:42:00 +01:00
1 parent 44f1bec789
commit 444dba381f
96 files changed
+6737 -69

No files matched your search

+280
View File
@@ -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&#8203;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 = "&#8722;"
JSON = "J&#8203;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] + "&#8203;" + m.group(1)[1:], text)
def esc(s):
return html.escape(s, quote=True).replace("J&amp;#8203;SON", JSON).replace("&amp;#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">&#8592; faster</text>' % (L, axis_y + 34))
b.append('<text class="nj-charts-tick nj-charts-tick--r" x="%g" y="%g">slower &#8594;</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"> &#183; %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">&#8592; 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"> &#183; %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"> &#183; 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(",", "&#8201;")))
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&#8203;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&amp;#8203;SON", "J&#8203;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&#8203;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] + "&#8203;" + 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("[", "&#91;").replace("]", "&#93;")
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()
+116
View File
@@ -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&#8203;SON and A&#8203;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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
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&#8203;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&#8203;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. &#8220;removed&#8221;: 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&#8203;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&#8203;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&#8203;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", "&#8203;")),
'<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&#8203;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] + "&#8203;" + 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()
+196
View File
@@ -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&#8203;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] + "&#8203;" + 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")