mirror of
https://github.com/nlohmann/json.git
synced 2026-10-28 08:45:43 +00:00
docs: new design for the documentation site (#5934)
* docs: new design for the documentation site Apply the brand to the Material for MkDocs site: Inter and JetBrains Mono served by the site (upright and real italic), brand colours, blue header band with the syntax pattern, wordmark in the header, new home page (hero, cards, animated feature tour), 404 page, polish for tabs, search, footer, focus rings, tables and code blocks, and theme features (code copy, previous/next footer, TOC follow, search suggestions). Draw the number railroad diagram, the parser callback events and the iterator illustrations as inline SVGs that follow the light and dark scheme. The generator scripts are in docs/mkdocs/scripts/figures; the feature slideshow (json-light.svg, json-dark.svg, replacing json.gif on the home page) is built by docs/mkdocs/scripts/slideshow. Decision nodes in five flowcharts become hexagons, which need far less room. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * docs: add figures, table captions and a macro chooser - Figures with numbered captions: SAX stepper, json_view layout and edit stepper, exception map, JSON pointer and patch walkers, QA pipeline, scaling and binary size charts, roadmap lanes, release timeline, integration decision tree and namespace builder - Table captions (nj-table-caption) on 9 pages - Compile times: modules and PCH charts, filesystem section, re-measured Figure 1, links between features/modules.md and compile_times.md - Interactive macro chooser for features/arbitrary_types.md - Link check: exclude URLs that answer the plugin with 400/401/419 or are dead (customers.md) Signed-off-by: Niels Lohmann <mail@nlohmann.me> * docs: macro chooser generates adl_serializer for non-default-constructible types Signed-off-by: Niels Lohmann <mail@nlohmann.me> * docs: cross-link concept pages and API pages in See also sections Add See also links in both directions between the concept pages (features, home, integration) and the API pages, write API links in code font, give every entry a comment after a dash, and show the callback event figure on the parser callbacks page. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * docs: address review comments on the new design - Home: add a "Latest release" button - Sponsors: update the list and group it by tier - Features: list all feature pages on the index page - Releases: add highlights per release, link the 4.0 roadmap, add the first commit to the timeline - Exceptions: prefix all example messages with the exception name - API: no code font for format names in the See also links of the to_/from_ pages - Merge Patch: show the document before, the patch, and the result - Replace "--" with typographic dashes, mark external links with an icon, center Mermaid diagrams Signed-off-by: Niels Lohmann <mail@nlohmann.me> --------- Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
1 parent
2bb2feeef4
commit
a47170346a
417 files changed
+11100
-3041
No files matched your search
@@ -0,0 +1,280 @@
|
||||
"""Draws the measurement charts as inline SVGs into docs/images/.
|
||||
|
||||
Generated files:
|
||||
|
||||
- compile_times.svg: percent change in compile time per option at -O0 (integration/compile_times.md)
|
||||
- compile_times_pch.svg, compile_times_modules.svg: percent change per translation unit with a precompiled header or
|
||||
`import nlohmann.json;` at -O0, plus the one-off build cost (integration/compile_times.md)
|
||||
- binary_sizes_<file>.svg: encoded size per binary format relative to minified JSON, one panel per test file
|
||||
(features/binary_formats/index.md, reused on features/performance.md)
|
||||
- ordered_json_scaling.svg: parse time of a flat object for json vs. ordered_json (features/performance.md)
|
||||
|
||||
Every number below is copied from the tables on those pages (or, where noted, computed from the milliseconds they
|
||||
report); the tables stay on the pages as the accessible fallback. Update both together. The SVGs are not images:
|
||||
the pages include their text with `--8<-- "images/<name>.svg"`, so they pick up the colours and fonts of the page
|
||||
(see the `nj-charts` rules in docs/css/fig-charts.css). Run `python3 scripts/figures/charts.py` from docs/mkdocs after
|
||||
changing data or drawing, and commit the generated files. In SVG text write the word JSON as `J​SON`,
|
||||
otherwise the abbreviation extension wraps it in an <abbr> and the text disappears.
|
||||
"""
|
||||
import html
|
||||
import math
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
OUT = Path(__file__).resolve().parents[2] / "docs" / "images"
|
||||
MINUS = "−"
|
||||
JSON = "J​SON"
|
||||
|
||||
|
||||
# words the abbreviation extension (includes/glossary.md) would wrap in <abbr> inside the SVG text
|
||||
ABBR = re.compile(r"(?<![\w;])(BJData|BON8|BSON|CBOR|GCC|UBJSON|MsgPack|JSON|DOM|SAX)(?!\w)")
|
||||
|
||||
|
||||
def protect(text):
|
||||
return ABBR.sub(lambda m: m.group(1)[0] + "​" + m.group(1)[1:], text)
|
||||
|
||||
|
||||
def esc(s):
|
||||
return html.escape(s, quote=True).replace("J&#8203;SON", JSON).replace("&#8722;", MINUS)
|
||||
|
||||
|
||||
def write(name, W, H, cls, tid, title, desc, body):
|
||||
s = ['<svg class="nj-fig nj-charts %s" viewBox="0 0 %g %g" role="img" aria-labelledby="%s %s-d" '
|
||||
'xmlns="http://www.w3.org/2000/svg">' % (cls, W, H, tid, tid),
|
||||
'<title id="%s">%s</title>' % (tid, esc(title)),
|
||||
'<desc id="%s-d">%s</desc>' % (tid, esc(desc))]
|
||||
s += body
|
||||
s.append('</svg>')
|
||||
(OUT / name).write_text(protect("\n".join(s)) + "\n")
|
||||
|
||||
|
||||
def hbar(x0, x1, y, h, r=4):
|
||||
"""Horizontal bar from the baseline x0 to x1, rounded only at the data end."""
|
||||
if abs(x1 - x0) < 0.5:
|
||||
return "M%g %g h0.5 v%g h-0.5 Z" % (x0, y, h)
|
||||
r = min(r, h / 2, abs(x1 - x0))
|
||||
s = 1 if x1 > x0 else -1
|
||||
return ("M%g %g H%g Q%g %g %g %g V%g Q%g %g %g %g H%g Z"
|
||||
% (x0, y, x1 - s * r, x1, y, x1, y + r, y + h - r, x1, y + h, x1 - s * r, y + h, x0))
|
||||
|
||||
|
||||
def pct(v):
|
||||
if v == 0:
|
||||
return "0 %"
|
||||
return ("+" if v > 0 else MINUS) + "%g %%" % abs(v)
|
||||
|
||||
|
||||
# ---------- compile times (integration/compile_times.md, -O0 columns) ----------
|
||||
# (label, translation unit, Apple clang %, GCC 16 %, computed?), ordered by saving
|
||||
COMPILE = [
|
||||
("json_fwd.hpp instead of json.hpp", "header-only TU", -62, -82, False),
|
||||
("extern template", "parsing TU", -7, -21, False),
|
||||
("JSON_NO_AUTOMATIC_UDLS", "model TU", -11, -11, False),
|
||||
("JSON_NO_AUTOMATIC_UDLS", "parsing TU", -1, -2, False),
|
||||
("JSON_HAS_FILESYSTEM=0 + JSON_NO_IO", "model TU", -8, -11, False),
|
||||
("JSON_HAS_FILESYSTEM=0", "model TU", -5, -9, False),
|
||||
# computed from the ms in "Options without effect on compile times": 1034/1051, 1756/1801, 1055/1051, 1783/1801
|
||||
("JSON_NO_IO", "model TU", round((1034 / 1051 - 1) * 100), round((1756 / 1801 - 1) * 100), True),
|
||||
("JSON_USE_GLOBAL_UDLS=0", "model TU", round((1055 / 1051 - 1) * 100), round((1783 / 1801 - 1) * 100), True),
|
||||
]
|
||||
|
||||
|
||||
def compile_times():
|
||||
W, L, R = 470, 8, 402
|
||||
lo, hi = -90, 10
|
||||
X = lambda v: L + (v - lo) / (hi - lo) * (R - L)
|
||||
top, pitch, bh = 64, 52, 12
|
||||
H = top + pitch * len(COMPILE) + 30
|
||||
b = []
|
||||
# legend
|
||||
b.append('<rect class="nj-charts-clang" x="%g" y="10" width="12" height="12" rx="3"/>' % L)
|
||||
b.append('<text class="nj-charts-leg" x="%g" y="20.5">Apple clang</text>' % (L + 18))
|
||||
b.append('<rect class="nj-charts-gcc" x="%g" y="10" width="12" height="12" rx="3"/>' % (L + 118))
|
||||
b.append('<text class="nj-charts-leg" x="%g" y="20.5">GCC 16</text>' % (L + 136))
|
||||
# grid and ticks
|
||||
axis_y = top + pitch * len(COMPILE) - 8
|
||||
for t in (-80, -60, -40, -20):
|
||||
b.append('<line class="nj-charts-grid" x1="%g" y1="%g" x2="%g" y2="%g"/>' % (X(t), top - 6, X(t), axis_y))
|
||||
b.append('<text class="nj-charts-tick" x="%g" y="%g">%s</text>' % (X(t), axis_y + 16, pct(t)))
|
||||
b.append('<text class="nj-charts-tick" x="%g" y="%g">0 %%</text>' % (X(0), axis_y + 16))
|
||||
b.append('<text class="nj-charts-tick nj-charts-tick--l" x="%g" y="%g">← faster</text>' % (L, axis_y + 34))
|
||||
b.append('<text class="nj-charts-tick nj-charts-tick--r" x="%g" y="%g">slower →</text>' % (W - 8, axis_y + 34))
|
||||
for i, (label, tu, clang, gcc, computed) in enumerate(COMPILE):
|
||||
y = top + i * pitch
|
||||
b.append('<text class="nj-charts-rowlabel" x="%g" y="%g"><tspan class="nj-charts-code">%s</tspan>'
|
||||
'<tspan class="nj-charts-sub"> · %s%s</tspan></text>'
|
||||
% (L, y - 2, esc(label), tu, "*" if computed else ""))
|
||||
for j, (v, cls, name) in enumerate(((clang, "clang", "Apple clang"), (gcc, "gcc", "GCC 16"))):
|
||||
by = y + 6 + j * (bh + 2)
|
||||
b.append('<path class="nj-charts-%s" d="%s"><title>%s, %s (%s): %s</title></path>'
|
||||
% (cls, hbar(X(0), X(v), by, bh), esc(label), tu, name, esc(pct(v))))
|
||||
b.append('<text class="nj-charts-val" x="%g" y="%g">%s</text>' % (max(X(0), X(v)) + 5, by + bh - 1.5, pct(v)))
|
||||
b.append('<line class="nj-charts-zero" x1="%g" y1="%g" x2="%g" y2="%g"/>' % (X(0), top - 6, X(0), axis_y))
|
||||
H = axis_y + 44
|
||||
desc = "; ".join("%s on a %s: Apple clang %s, GCC 16 %s" % (label, tu.replace("TU", "translation unit"),
|
||||
pct(c).replace(MINUS, "-"), pct(g).replace(MINUS, "-")) for label, tu, c, g, _ in COMPILE)
|
||||
write("compile_times.svg", W, H, "nj-charts--compile", "nj-charts-ct",
|
||||
"Change in compile time against the default configuration at -O0, Apple clang and GCC 16",
|
||||
"Grouped horizontal bars, negative is faster. " + desc + ".", b)
|
||||
|
||||
|
||||
# ---------- precompiled headers and C++20 modules (integration/compile_times.md, -O0 columns) ----------
|
||||
# Per-TU medians of the wall-clock time in ms from the tables "Use precompiled headers" (-std=c++17) and
|
||||
# "Use C++20 modules" (-std=c++20): {translation unit: {compiler: (default ms, with PCH/module ms)}}, plus the one-off
|
||||
# build cost in ms per compiler. None instead of a value means "not measured"; NOT_AVAILABLE then gives the reason.
|
||||
PCH = {
|
||||
"model TU": {"Apple clang": (1045, 594), "GCC 16": (1789, 1060)},
|
||||
"parsing TU": {"Apple clang": (1148, 714), "GCC 16": (2498, 1884)},
|
||||
}
|
||||
PCH_BUILD = {"Apple clang": 771, "GCC 16": 2254}
|
||||
MODULES = {
|
||||
"model TU": {"Apple clang": (1311, 680), "GCC 16": (2156, 1153)},
|
||||
"parsing TU": {"Apple clang": (1387, 800), "GCC 16": (2838, 1810)},
|
||||
}
|
||||
MODULES_BUILD = {"Apple clang": 1630, "GCC 16": 2538}
|
||||
NOT_AVAILABLE = {} # compiler: reason
|
||||
|
||||
|
||||
def tradeoff(name, tid, rows, build, what, title, desc_intro):
|
||||
"""Percent change per TU (bars) and the one-off build cost (text rows below) for two compilers."""
|
||||
W, L, R = 440, 8, 372
|
||||
lo, hi = -60, 0
|
||||
X = lambda v: L + (v - lo) / (hi - lo) * (R - L)
|
||||
top, pitch, bh = 64, 52, 12
|
||||
comps = (("Apple clang", "clang"), ("GCC 16", "gcc"))
|
||||
b, desc = [], []
|
||||
b.append('<rect class="nj-charts-clang" x="%g" y="10" width="12" height="12" rx="3"/>' % L)
|
||||
b.append('<text class="nj-charts-leg" x="%g" y="20.5">Apple clang</text>' % (L + 18))
|
||||
b.append('<rect class="nj-charts-gcc" x="%g" y="10" width="12" height="12" rx="3"/>' % (L + 118))
|
||||
b.append('<text class="nj-charts-leg" x="%g" y="20.5">GCC 16</text>' % (L + 136))
|
||||
axis_y = top + pitch * len(rows) - 8
|
||||
for t in (-50, -25):
|
||||
b.append('<line class="nj-charts-grid" x1="%g" y1="%g" x2="%g" y2="%g"/>' % (X(t), top - 6, X(t), axis_y))
|
||||
b.append('<text class="nj-charts-tick" x="%g" y="%g">%s</text>' % (X(t), axis_y + 16, pct(t)))
|
||||
b.append('<text class="nj-charts-tick" x="%g" y="%g">0 %%</text>' % (X(0), axis_y + 16))
|
||||
b.append('<text class="nj-charts-tick nj-charts-tick--l" x="%g" y="%g">← faster per TU</text>' % (L, axis_y + 34))
|
||||
for i, (tu, vals) in enumerate(rows.items()):
|
||||
y = top + i * pitch
|
||||
b.append('<text class="nj-charts-rowlabel" x="%g" y="%g">%s<tspan class="nj-charts-sub"> · %s</tspan></text>'
|
||||
% (L, y - 2, tu, esc(what)))
|
||||
for j, (comp, cls) in enumerate(comps):
|
||||
by = y + 6 + j * (bh + 2)
|
||||
pair = vals[comp]
|
||||
if pair is None:
|
||||
b.append('<text class="nj-charts-note" x="%g" y="%g">%s: not available: %s</text>'
|
||||
% (L, by + bh - 1.5, comp, esc(NOT_AVAILABLE.get(comp, "not measured"))))
|
||||
desc.append("%s, %s: not available" % (tu, comp))
|
||||
continue
|
||||
d, w = pair
|
||||
v = round((w / d - 1) * 100)
|
||||
b.append('<path class="nj-charts-%s" d="%s"><title>%s, %s: %d ms by default, %d ms %s (%s)</title></path>'
|
||||
% (cls, hbar(X(0), X(v), by, bh), tu, comp, d, w, esc(what), esc(pct(v))))
|
||||
b.append('<text class="nj-charts-val" x="%g" y="%g">%s</text>' % (X(0) + 5, by + bh - 1.5, pct(v)))
|
||||
desc.append("%s, %s: %s (%d ms to %d ms)" % (tu, comp, pct(v).replace(MINUS, "-"), d, w))
|
||||
b.append('<line class="nj-charts-zero" x1="%g" y1="%g" x2="%g" y2="%g"/>' % (X(0), top - 6, X(0), axis_y))
|
||||
# one-off cost
|
||||
y = axis_y + 62
|
||||
b.append('<text class="nj-charts-rowlabel" x="%g" y="%g">One-off cost<tspan class="nj-charts-sub"> · built once, '
|
||||
'reused by every TU</tspan></text>' % (L, y))
|
||||
for j, (comp, cls) in enumerate(comps):
|
||||
yy = y + 20 + j * 20
|
||||
b.append('<rect class="nj-charts-%s" x="%g" y="%g" width="10" height="10" rx="2.5"/>' % (cls, L, yy - 9.5))
|
||||
ms = build[comp]
|
||||
saved = sorted(vals[comp][0] - vals[comp][1] for vals in rows.values() if vals[comp] is not None)
|
||||
txt = ("%s: %d ms once, saves %d to %d ms per TU" % (comp, ms, saved[0], saved[-1])) if ms is not None else ("%s: not available: %s" % (comp, NOT_AVAILABLE.get(comp, "not measured")))
|
||||
b.append('<text class="nj-charts-val" x="%g" y="%g">%s</text>' % (L + 16, yy, esc(txt)))
|
||||
desc.append("one-off build, %s: %s" % (comp, ("%d ms" % ms) if ms is not None else "not available"))
|
||||
H = y + 20 + 20 * len(comps)
|
||||
write(name, W, H, "nj-charts--compile", tid, title, desc_intro + " " + "; ".join(desc) + ".", b)
|
||||
|
||||
|
||||
# ---------- binary sizes (features/binary_formats/index.md, "Sizes") ----------
|
||||
FORMATS = ["BJData", "BON8", "BSON", "CBOR", "MessagePack", "UBJSON"] # colour slots 1-6, same in every panel
|
||||
SIZES = {
|
||||
"canada.json": [53.2, 50.5, 85.8, 50.5, 50.5, 53.2],
|
||||
"twitter.json": [91.1, 83.8, 95.2, 86.3, 86.0, 91.3],
|
||||
"citm_catalog.json": [78.1, 63.5, 95.8, 68.4, 68.5, 78.2],
|
||||
"jeopardy.json": [96.6, 87.5, 106.7, 88.0, 87.9, 96.6],
|
||||
}
|
||||
|
||||
|
||||
def binary_sizes():
|
||||
W, L, R = 340, 104, 286
|
||||
X = lambda v: L + v / 110 * (R - L)
|
||||
top, pitch, bh = 46, 26, 16
|
||||
for fname, vals in SIZES.items():
|
||||
b, vl = [], [] # value labels go on top of the 100 % line
|
||||
lo = min(vals)
|
||||
b.append('<text class="nj-charts-panel" x="8" y="18">%s</text>' % fname)
|
||||
bottom = top + pitch * len(FORMATS) - 6
|
||||
for t in (0, 50):
|
||||
b.append('<line class="nj-charts-grid" x1="%g" y1="%g" x2="%g" y2="%g"/>' % (X(t), top - 6, X(t), bottom))
|
||||
b.append('<text class="nj-charts-tick" x="%g" y="%g">%d %%</text>' % (X(t), bottom + 15, t))
|
||||
for i, (fmt, v) in enumerate(zip(FORMATS, vals)):
|
||||
y = top + i * pitch
|
||||
best = v == lo
|
||||
b.append('<text class="nj-charts-rowlabel nj-charts-fmt%s" x="%g" y="%g">%s</text>'
|
||||
% (" nj-charts-best" if best else "", L - 8, y + bh - 3.5, fmt))
|
||||
b.append('<path class="nj-charts-c%d%s" d="%s"><title>%s, %s: %g %% of minified %s%s</title></path>'
|
||||
% (i + 1, "" if best else " nj-charts-dim", hbar(X(0), X(v), y, bh), fname, fmt, v, JSON,
|
||||
" (smallest)" if best else ""))
|
||||
vl.append('<text class="nj-charts-val%s" x="%g" y="%g">%.1f %%</text>'
|
||||
% (" nj-charts-best" if best else "", X(v) + 5, y + bh - 3.5, v))
|
||||
b.append('<line class="nj-charts-ref" x1="%g" y1="%g" x2="%g" y2="%g"/>' % (X(100), top - 10, X(100), bottom))
|
||||
b.append('<text class="nj-charts-tick nj-charts-tick--r" x="%g" y="%g">minified %s = 100 %%</text>'
|
||||
% (X(100) + 2, top - 14, JSON))
|
||||
b.append('<text class="nj-charts-tick" x="%g" y="%g">100 %%</text>' % (X(100), bottom + 15))
|
||||
b += vl
|
||||
H = bottom + 24
|
||||
best = [f for f, v in zip(FORMATS, vals) if v == lo]
|
||||
desc = "; ".join("%s %g %%" % (f, v) for f, v in zip(FORMATS, vals))
|
||||
stem = fname.split(".")[0]
|
||||
write("binary_sizes_%s.svg" % stem, W, H, "nj-charts--sizes", "nj-charts-bs-" + stem,
|
||||
"Size of %s in each binary format relative to minified %s" % (fname, JSON),
|
||||
"%s. Smallest: %s." % (desc, " and ".join(best)), b)
|
||||
|
||||
|
||||
# ---------- json vs. ordered_json (api/ordered_map.md, cited on features/performance.md) ----------
|
||||
SCALING = [(2000, 0.7, 3.6, 5), (4000, 0.8, 14.0, 19), (8000, 1.6, 67.8, 43), (16000, 3.3, 181.6, 54)]
|
||||
|
||||
|
||||
def scaling():
|
||||
W, L, R, T, B = 440, 66, 330, 16, 210
|
||||
xs = [n for n, *_ in SCALING]
|
||||
X = lambda n: L + math.log2(n / xs[0]) / math.log2(xs[-1] / xs[0]) * (R - L)
|
||||
Y = lambda ms: B - (math.log10(ms) - math.log10(0.3)) / (math.log10(300) - math.log10(0.3)) * (B - T)
|
||||
b = []
|
||||
for t in (1, 10, 100):
|
||||
b.append('<line class="nj-charts-grid" x1="%g" y1="%g" x2="%g" y2="%g"/>' % (L, Y(t), R, Y(t)))
|
||||
b.append('<text class="nj-charts-tick nj-charts-tick--r" x="%g" y="%g">%g ms</text>' % (L - 6, Y(t) + 4, t))
|
||||
b.append('<line class="nj-charts-axis" x1="%g" y1="%g" x2="%g" y2="%g"/>' % (L, B, R, B))
|
||||
for n, a, o, f in SCALING:
|
||||
b.append('<text class="nj-charts-tick" x="%g" y="%g">%s</text>' % (X(n), B + 16, "{:,}".format(n).replace(",", " ")))
|
||||
for key, idx, cls, name in (("json", 1, "json", "json"), ("ojson", 2, "ojson", "ordered_json")):
|
||||
pts = [(X(r[0]), Y(r[idx])) for r in SCALING]
|
||||
b.append('<polyline class="nj-charts-%s" points="%s"/>' % (cls, " ".join("%g,%g" % p for p in pts)))
|
||||
for (x, y), r in zip(pts, SCALING):
|
||||
b.append('<circle class="nj-charts-%s-dot" cx="%g" cy="%g" r="4.5"><title>%s, %d keys: %g ms</title></circle>'
|
||||
% (cls, x, y, name, r[0], r[idx]))
|
||||
last = SCALING[-1]
|
||||
b.append('<text class="nj-charts-val nj-charts-name-%s" x="%g" y="%g">%s</text>' % (cls, pts[-1][0] + 10, pts[-1][1] - 2, name))
|
||||
b.append('<text class="nj-charts-val" x="%g" y="%g">%g ms</text>' % (pts[-1][0] + 10, pts[-1][1] + 13, last[idx]))
|
||||
first = SCALING[0]
|
||||
dy = 16 if key == "json" else 20
|
||||
b.append('<text class="nj-charts-val" x="%g" y="%g">%g ms</text>' % (pts[0][0] + 6, pts[0][1] + dy, first[idx]))
|
||||
write("ordered_json_scaling.svg", W, B + 28, "nj-charts--scaling", "nj-charts-sc",
|
||||
"Time to parse a flat object of n keys, json against ordered_json, logarithmic axes",
|
||||
"2000 keys: json 0.7 ms, ordered_json 3.6 ms (5x). 4000 keys: 0.8 ms and 14.0 ms (19x). "
|
||||
"8000 keys: 1.6 ms and 67.8 ms (43x). 16000 keys: 3.3 ms and 181.6 ms (54x).", b)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
compile_times()
|
||||
tradeoff("compile_times_pch.svg", "nj-charts-pch", PCH, PCH_BUILD, "with PCH",
|
||||
"Compile time per translation unit with a precompiled json.hpp at -O0, and the one-off cost of the PCH",
|
||||
"Horizontal bars, negative is faster, medians of nine runs, -std=c++17.")
|
||||
tradeoff("compile_times_modules.svg", "nj-charts-mod", MODULES, MODULES_BUILD, "import nlohmann.json",
|
||||
"Compile time per translation unit with import nlohmann.json at -O0, and the one-off cost of the module",
|
||||
"Horizontal bars, negative is faster, medians of nine runs, -std=c++20 for both.")
|
||||
binary_sizes()
|
||||
scaling()
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Draws the chunk-buffering strip of the large-input page as an inline SVG into docs/images/.
|
||||
|
||||
The strip follows docs/examples/partial_input_buffering.cpp: five chunks are appended to a buffer, and after each one
|
||||
the whole buffer is parsed again. A parse error whose byte is one past the buffer size means the input ended early
|
||||
(wait for more); an error before the end is a real syntax error. The byte positions were checked by running the
|
||||
example (4/5, 12/13, 19/20, 30/31, complete at 35; `[1, ]` fails at byte 5).
|
||||
|
||||
The SVG is not an image: features/parsing/large_and_partial_input.md includes its text with
|
||||
`--8<-- "images/chunk_buffering.svg"`, so it picks up the colours and fonts of the page (see the `nj-fig` rules in
|
||||
docs/css/brand.css and the `nj-sax-` rules in docs/css/fig-sax.css). Run `python3 scripts/figures/chunk_buffering.py`
|
||||
from docs/mkdocs after changing the drawing, and commit the generated file. In SVG text write the word JSON as
|
||||
`J​SON`, otherwise the abbreviation extension wraps it in an <abbr> and the text disappears.
|
||||
"""
|
||||
import html
|
||||
from pathlib import Path
|
||||
|
||||
OUT = Path(__file__).resolve().parents[2] / "docs" / "images"
|
||||
|
||||
CW = 8.4 # JetBrains Mono advance at 14px
|
||||
TX = 96 # x of the buffer text
|
||||
SX = 420 # x of the result column
|
||||
ROW = 50
|
||||
CHUNKS = ['{"na', 'me": "Ma', 'ry", "l', 'ist": [1, 2', ', 3]}']
|
||||
|
||||
|
||||
def esc(s):
|
||||
return html.escape(s).replace("J&#8203;SON", "J​SON")
|
||||
|
||||
|
||||
def row(body, y, label, old, new, err_byte, status, status_cls, note):
|
||||
body.append('<text class="fg-note fg-note--l" x="16" y="%g">%s</text>' % (y + 5, esc(label)))
|
||||
if old:
|
||||
body.append('<rect class="nj-sax-old" x="%g" y="%g" width="%g" height="24" rx="4"/>' % (TX - 3, y - 12, CW * len(old) + 3))
|
||||
body.append('<rect class="nj-sax-new" x="%g" y="%g" width="%g" height="24" rx="4"/>' % (TX + CW * len(old), y - 12, CW * len(new) + 3))
|
||||
body.append('<text class="nj-sax-buf" x="%g" y="%g" xml:space="preserve">%s</text>' % (TX, y + 5, esc(old + new)))
|
||||
if err_byte is not None:
|
||||
# the byte of a parse_error is 1-based: byte n is the character at index n - 1
|
||||
cx = TX + CW * (err_byte - 1) + CW / 2
|
||||
body.append('<path class="nj-sax-caret %s" d="M%g %g l-5 8 h10 z"/>' % (status_cls, cx, y + 14))
|
||||
body.append('<text class="nj-sax-status %s" x="%d" y="%g">%s</text>' % (status_cls, SX, y - 1, esc(status)))
|
||||
body.append('<text class="fg-note fg-note--l" x="%d" y="%g">%s</text>' % (SX, y + 15, esc(note)))
|
||||
|
||||
|
||||
def main():
|
||||
W = 700
|
||||
body = []
|
||||
body.append('<text class="fg-h" x="16" y="24">chunk</text>')
|
||||
body.append('<text class="fg-h" x="%d" y="24">buffer, parsed from byte 1 each time</text>' % TX)
|
||||
body.append('<text class="fg-h" x="%d" y="24">result</text>' % SX)
|
||||
y, buf, parsed = 62, "", []
|
||||
for i, chunk in enumerate(CHUNKS):
|
||||
old, buf = buf, buf + chunk
|
||||
parsed.append(len(buf))
|
||||
if i < len(CHUNKS) - 1:
|
||||
row(body, y, "chunk %d" % (i + 1), old, chunk, len(buf) + 1,
|
||||
"error at byte %d = size + 1" % (len(buf) + 1), "nj-sax-s-wait", "incomplete: wait for more")
|
||||
else:
|
||||
row(body, y, "chunk %d" % (i + 1), old, chunk, None,
|
||||
"parsed, size %d" % len(buf), "nj-sax-s-ok", "complete: one value")
|
||||
y += ROW
|
||||
# the syntax error of the example, in a buffer of its own
|
||||
y += 8
|
||||
body.append('<path class="nj-sax-sep" d="M16 %g H%d"/>' % (y - 30, W - 16))
|
||||
row(body, y, "other input", "", "[1, ]", 5, "error at byte 5, size 5", "nj-sax-s-err", "before the end: a real error")
|
||||
y += 46
|
||||
total = " + ".join(str(n) for n in parsed)
|
||||
for k, line in enumerate(["Bytes parsed: %s = %d for a value of %d bytes." % (total, sum(parsed), parsed[-1]),
|
||||
"Every attempt starts again at byte 1, so the total work grows",
|
||||
"quadratically with the size of the value."]):
|
||||
body.append('<text class="fg-note fg-note--l" x="16" y="%g">%s</text>' % (y + 20 * k, esc(line)))
|
||||
y += 40
|
||||
H = int(y + 36)
|
||||
|
||||
tid, did = "fig-sax-buf-title", "fig-sax-buf-desc"
|
||||
title = "Buffering chunks until the value is complete: five chunks of {\"name\": \"Mary\", \"list\": [1, 2, 3]} appended to a buffer."
|
||||
desc = ("After chunks 1 to 4 the buffer has 4, 12, 19, and 30 bytes, and parsing it fails at byte 5, 13, 20, and 31: "
|
||||
"one past the end, so the input is incomplete and the program waits for more. After chunk 5 the buffer of 35 "
|
||||
"bytes parses as one complete value. For the input [1, ] of 5 bytes the error is at byte 5, before the end, "
|
||||
"so it is a real syntax error. Every attempt parses the buffer from byte 1, so 4 + 12 + 19 + 30 + 35 = 100 "
|
||||
"bytes are parsed for a 35-byte value, and the work grows quadratically.")
|
||||
s = ['<svg class="nj-fig nj-fig--sax-buf" viewBox="0 0 %d %d" role="img" aria-labelledby="%s %s" '
|
||||
'xmlns="http://www.w3.org/2000/svg">' % (W, H, tid, did),
|
||||
'<title id="%s">%s</title>' % (tid, esc(title)),
|
||||
'<desc id="%s">%s</desc>' % (did, esc(desc))]
|
||||
s += body
|
||||
s.append("</svg>")
|
||||
(OUT / "chunk_buffering.svg").write_text("\n".join(s) + "\n")
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,168 @@
|
||||
"""Draws the exception id map of home/exceptions.md as an inline SVG into docs/images/exception_map.svg.
|
||||
|
||||
The SVG is not an image: the page includes its text with `--8<-- "images/exception_map.svg"`, so it picks up the
|
||||
colours and fonts of the page (see the `nj-fig` rules in docs/css/brand.css and the `nj-exceptions-*` rules in
|
||||
docs/css/fig-exceptions.css). The file holds two drawings of the same map, a wide one and a narrow one for phones;
|
||||
the CSS shows one of them. Every id chip is a link to the `###` section of that id on the page, every family box a
|
||||
link to the family's `##` section.
|
||||
|
||||
Run `python3 scripts/figures/exception_map.py` from docs/mkdocs after changing the drawing or after adding an
|
||||
exception section to home/exceptions.md (the ids are read from the page), and commit the generated file. In SVG text
|
||||
write the word JSON as `J​SON`, otherwise the abbreviation extension wraps it in an <abbr> and the text
|
||||
disappears.
|
||||
"""
|
||||
import html
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
OUT = Path(__file__).resolve().parents[2] / "docs" / "images"
|
||||
PAGE = Path(__file__).resolve().parents[2] / "docs" / "home" / "exceptions.md"
|
||||
P = "nj-exceptions-"
|
||||
|
||||
# family name, id range label, anchor of the family section, extra member
|
||||
FAMILIES = [
|
||||
("parse_error", "1xx", "parse-errors", "byte"),
|
||||
("invalid_iterator", "2xx", "iterator-errors", None),
|
||||
("type_error", "3xx", "type-errors", None),
|
||||
("out_of_range", "4xx", "out-of-range", None),
|
||||
("other_error", "5xx", "further-exceptions", None),
|
||||
]
|
||||
|
||||
# the chips show the id only; naming just some of them was confusing
|
||||
LABELS = {}
|
||||
|
||||
|
||||
GLOSSARY = Path(__file__).resolve().parents[2] / "includes" / "glossary.md"
|
||||
ABBRS = sorted(re.findall(r"^\*\[([^\]]+)\]:", GLOSSARY.read_text(), re.M), key=len, reverse=True)
|
||||
|
||||
|
||||
def noabbr(s):
|
||||
"""Breaks every glossary abbreviation with a zero-width space, so the abbr extension leaves SVG text alone."""
|
||||
for a in ABBRS:
|
||||
s = re.sub(r"(?<![\w&#])%s(?!\w)" % re.escape(a), a[0] + "​" + a[1:], s)
|
||||
return s
|
||||
|
||||
|
||||
def ids_on_page():
|
||||
found = {}
|
||||
for m in re.finditer(r"^### json\.exception\.(\w+)\.(\d+)\s*$", PAGE.read_text(), re.M):
|
||||
found.setdefault(m.group(1), []).append(int(m.group(2)))
|
||||
return found
|
||||
|
||||
|
||||
def text_w(s, size):
|
||||
"""Rough advance width of Inter at the given size (good enough to size chips)."""
|
||||
w = 0.0
|
||||
for c in s:
|
||||
if c in "il.,:;'|![]()":
|
||||
w += 0.28
|
||||
elif c in " ft":
|
||||
w += 0.32
|
||||
elif c in "mwMW":
|
||||
w += 0.85
|
||||
elif c.isupper() or c.isdigit():
|
||||
w += 0.64
|
||||
else:
|
||||
w += 0.55
|
||||
return w * size
|
||||
|
||||
|
||||
def esc(s):
|
||||
return html.escape(s, quote=True).replace("[", "[").replace("]", "]")
|
||||
|
||||
|
||||
MONO = 7.9 # JetBrains Mono advance at 13px
|
||||
CH = 28 # chip height
|
||||
GAP = 6
|
||||
|
||||
|
||||
def chip(x, y, fam, num):
|
||||
label = LABELS.get(num)
|
||||
w = 3 * MONO + 18 + (text_w(label, 12.5) + 10 if label else 0)
|
||||
anchor = "jsonexception%s%d" % (fam, num)
|
||||
aria = "json.exception.%s.%d%s" % (fam, num, (": " + label) if label else "")
|
||||
s = ['<a class="%schip%s" xlink:href="#%s" aria-label="%s">' % (P, " %schip--named" % P if label else "", anchor, esc(aria)),
|
||||
'<rect x="%g" y="%g" width="%g" height="%d" rx="6"/>' % (x, y, w, CH),
|
||||
'<text class="%sid" x="%g" y="%g">%d</text>' % (P, x + 9, y + CH / 2 + 4.5, num)]
|
||||
if label:
|
||||
s.append('<text class="%slabel" x="%g" y="%g">%s</text>' % (P, x + 9 + 3 * MONO + 9, y + CH / 2 + 4.5, noabbr(esc(label))))
|
||||
s.append('</a>')
|
||||
return w, "".join(s)
|
||||
|
||||
|
||||
def flow(fam, nums, x0, x1, y0):
|
||||
"""Places the chips of one family left to right, wrapping at x1. Returns (svg parts, bottom y)."""
|
||||
parts, x, y = [], x0, y0
|
||||
for n in nums:
|
||||
w, _ = chip(0, 0, fam, n)
|
||||
if x > x0 and x + w > x1:
|
||||
x, y = x0, y + CH + GAP
|
||||
_, s = chip(x, y, fam, n)
|
||||
parts.append(s)
|
||||
x += w + GAP
|
||||
return parts, y + CH
|
||||
|
||||
|
||||
def box(x, y, w, h, title, sub, href=None, cls=""):
|
||||
s = []
|
||||
if href:
|
||||
s.append('<a class="%sfam" xlink:href="#%s" aria-label="%s">' % (P, href, esc(title + ", " + sub)))
|
||||
s.append('<rect class="%sbox%s" x="%g" y="%g" width="%g" height="%g" rx="7"/>' % (P, cls, x, y, w, h))
|
||||
s.append('<text class="%sname" x="%g" y="%g">%s</text>' % (P, x + 12, y + 22, noabbr(esc(title))))
|
||||
s.append('<text class="%ssub" x="%g" y="%g">%s</text>' % (P, x + 12, y + 41, noabbr(esc(sub))))
|
||||
if href:
|
||||
s.append('</a>')
|
||||
return "".join(s)
|
||||
|
||||
|
||||
def drawing(W, narrow, suffix):
|
||||
global CH
|
||||
CH = 34 if narrow else 28 # larger touch targets on phones
|
||||
ids = ids_on_page()
|
||||
mid = "nj-exc-tri-" + suffix
|
||||
body = []
|
||||
trunk = 34
|
||||
# std::exception and json::exception
|
||||
body.append(box(12, 12, 190 if narrow else 200, 52, "std::exception", "C++ standard library base"))
|
||||
body.append('<line class="%sedge" x1="%d" y1="%d" x2="%d" y2="%d" marker-end="url(#%s)"/>' % (P, trunk, 104, trunk, 66, mid))
|
||||
body.append(box(12, 104, 250 if narrow else 300, 52, "json::exception", "id, what() \u00b7 catch-all"))
|
||||
y = 196
|
||||
last_branch = y
|
||||
fam_w = W - 64 - 12 if narrow else 214
|
||||
for fam, rng, href, member in FAMILIES:
|
||||
nums = ids[fam]
|
||||
sub = "%s \u00b7 %d ids" % (rng, len(nums)) + (" \u00b7 adds member %s" % member if member else "")
|
||||
body.append('<line class="%sedge" x1="%d" y1="%d" x2="%d" y2="%d"/>' % (P, trunk, y + 26, 64, y + 26))
|
||||
last_branch = y + 26
|
||||
body.append(box(64, y, fam_w, 52, "json::" + fam, sub, href, " %sbox--fam" % P))
|
||||
if narrow:
|
||||
parts, bottom = flow(fam, nums, 64, W - 12, y + 52 + 10)
|
||||
else:
|
||||
parts, bottom = flow(fam, nums, 64 + fam_w + 16, W - 12, y + 12)
|
||||
body += parts
|
||||
y = max(bottom, y + 52) + 26
|
||||
# trunk from json::exception down to the last family, with the inheritance triangle at the top
|
||||
body.insert(0, '<line class="%sedge" x1="%d" y1="%d" x2="%d" y2="%d" marker-end="url(#%s)"/>' % (P, trunk, last_branch, trunk, 158, mid))
|
||||
H = y - 14
|
||||
defs = ('<defs><marker id="%s" viewBox="0 0 12 12" refX="11" refY="6" markerWidth="11" markerHeight="11" orient="auto">'
|
||||
'<path class="%stri" d="M1 1 L11 6 L1 11 z"/></marker></defs>' % (mid, P))
|
||||
tid, did = "nj-exc-map-title-" + suffix, "nj-exc-map-desc-" + suffix
|
||||
total = sum(len(v) for v in ids.values())
|
||||
desc = ("Class tree: std::exception is the base of json::exception, which is the base of the five exception "
|
||||
"families parse_error (ids 1xx), invalid_iterator (2xx), type_error (3xx), out_of_range (4xx) and "
|
||||
"other_error (5xx). Each family lists the ids documented on this page, %d in total; every id links to "
|
||||
"its section." % total)
|
||||
s = ['<svg class="nj-fig %smap %smap--%s" viewBox="0 0 %d %d" aria-labelledby="%s %s" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">'
|
||||
% (P, P, "narrow" if narrow else "wide", W, H, tid, did),
|
||||
'<title id="%s">Exception id map</title>' % tid,
|
||||
'<desc id="%s">%s</desc>' % (did, noabbr(esc(desc))),
|
||||
defs] + body + ['</svg>']
|
||||
return "\n".join(s)
|
||||
|
||||
|
||||
def main():
|
||||
out = drawing(760, False, "w") + "\n" + drawing(360, True, "n") + "\n"
|
||||
(OUT / "exception_map.svg").write_text(out)
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,116 @@
|
||||
"""Draws the 3.x / 4.0 lane diagram of community/roadmap.md as an inline SVG into docs/images/.
|
||||
|
||||
The rows come from the page itself: the macro table under "Trying out 4.0 today" gives each macro, its 3.x default,
|
||||
whether 4.0 removes it, and the version that added it; the `#define` block below that table gives the 4.0 value. Run
|
||||
`python3 scripts/figures/integration.py` from docs/mkdocs after changing that section, and commit the generated
|
||||
docs/images/roadmap_lanes.svg. The page includes the SVG text with `--8<-- "images/roadmap_lanes.svg"`, so it follows
|
||||
the light/dark toggle (see the `nj-fig` rules in docs/css/brand.css and the `nj-int-*` rules in
|
||||
docs/css/fig-integration.css). In SVG text write J​SON and A​PI, otherwise the abbreviation extension
|
||||
wraps the word in an <abbr> and the text disappears.
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
DOCS = Path(__file__).resolve().parents[2] / "docs"
|
||||
OUT = Path(__file__).resolve().parents[2] / "docs" / "images"
|
||||
PAGE = DOCS / "community" / "roadmap.md"
|
||||
|
||||
|
||||
def esc(text):
|
||||
return text.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
|
||||
|
||||
def vkey(version):
|
||||
return tuple(int(p) for p in version.split("."))
|
||||
|
||||
|
||||
def parse(markdown):
|
||||
section = markdown.split("### Trying out 4.0 today", 1)[1].split("\n### ", 1)[0]
|
||||
rows = []
|
||||
for line in section.splitlines():
|
||||
cells = [c.strip() for c in line.strip().strip("|").split("|")]
|
||||
m = re.match(r"\[`(JSON_[A-Z0-9_]+)`\]", cells[0]) if len(cells) == 5 else None
|
||||
if not m:
|
||||
continue
|
||||
default = re.fullmatch(r"`(\d)`", cells[1])
|
||||
added = re.fullmatch(r"(\d+\.\d+\.\d+)", cells[4])
|
||||
if not default or not added:
|
||||
sys.exit("cannot parse row: " + line)
|
||||
rows.append({"macro": m.group(1), "default": default.group(1), "added": added.group(1),
|
||||
"removed": cells[2].startswith("removed")})
|
||||
values = dict(re.findall(r"^#define (JSON_[A-Z0-9_]+) (\d)$", section, re.MULTILINE))
|
||||
for row in rows:
|
||||
if row["macro"] not in values:
|
||||
sys.exit("no #define for " + row["macro"])
|
||||
row["v4"] = values[row["macro"]]
|
||||
if not rows:
|
||||
sys.exit("no macro rows found in " + str(PAGE))
|
||||
return rows
|
||||
|
||||
|
||||
def draw(rows):
|
||||
W, ROW, TOP = 880, 34, 168
|
||||
H = TOP + ROW * len(rows) + 30
|
||||
versions = sorted({r["added"] for r in rows}, key=vkey)
|
||||
s = ['<svg class="nj-fig nj-fig--lanes" viewBox="0 0 %d %d" role="img" aria-labelledby="nj-int-lanes-title" '
|
||||
'xmlns="http://www.w3.org/2000/svg">' % (W, H)]
|
||||
s.append('<title id="nj-int-lanes-title">Two lanes: the 3.x release train, whose minor and patch releases do not '
|
||||
'break code that uses the public A​PI, followed by version 4.0, which has no release date yet. '
|
||||
'Each breaking change is a macro that is available in 3.x today with its 3.x default and becomes the '
|
||||
'default in 4.0: %s.</title>'
|
||||
% "; ".join("%s, added in %s, 3.x default %s, 4.0 value %s%s"
|
||||
% (r["macro"], r["added"], r["default"], r["v4"], ", macro removed" if r["removed"] else "")
|
||||
for r in rows))
|
||||
s.append('<defs><marker id="nj-int-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" '
|
||||
'orient="auto"><path class="fg-head" d="M0 1 L10 5 L0 9 z"/></marker></defs>')
|
||||
# lanes
|
||||
s.append('<rect class="nj-int-lane" x="16" y="16" width="600" height="56" rx="10"/>')
|
||||
s.append('<text class="nj-int-lanetext" x="32" y="41">3.x release train</text>')
|
||||
s.append('<text class="nj-int-mute" x="32" y="60">available today</text>')
|
||||
x0, x1 = 300, 590
|
||||
step = (x1 - x0) / max(1, len(versions) - 1) if len(versions) > 1 else 0
|
||||
for i, v in enumerate(versions):
|
||||
x = x0 + i * step if len(versions) > 1 else (x0 + x1) / 2
|
||||
s.append('<line class="nj-int-axis" x1="%g" y1="30" x2="%g" y2="44"/>' % (x, x))
|
||||
s.append('<text class="nj-int-mute" x="%g" y="60" text-anchor="middle">%s</text>' % (x, v))
|
||||
s.append('<line class="fg-arrow" x1="210" y1="37" x2="600" y2="37"/>')
|
||||
s.append('<line class="fg-arrow fg-arrow--dash" x1="618" y1="44" x2="660" y2="44" marker-end="url(#nj-int-arrow)"/>')
|
||||
s.append('<rect class="nj-int-lane nj-int-lane--future" x="664" y="16" width="200" height="56" rx="10"/>')
|
||||
s.append('<text class="nj-int-lanetext" x="680" y="41">4.0</text>')
|
||||
s.append('<text class="nj-int-mute" x="680" y="60">no release date yet</text>')
|
||||
# guarantee
|
||||
s.append('<text class="nj-int-text" x="16" y="98">A minor or patch release of 3.x does not break code that uses '
|
||||
'the public A​PI, unless that code opts in with a macro.</text>')
|
||||
s.append('<text class="nj-int-text" x="16" y="118">4.0 is not developed on a separate branch: it switches the '
|
||||
'defaults of these macros and removes the macros.</text>')
|
||||
# header
|
||||
hy = TOP - 18
|
||||
s.append('<text class="fg-h" x="32" y="%d">Macro</text>' % hy)
|
||||
s.append('<text class="fg-h" x="380" y="%d">added</text>' % hy)
|
||||
s.append('<text class="fg-h" x="500" y="%d" text-anchor="middle">3.x default</text>' % hy)
|
||||
s.append('<text class="nj-int-mute" x="632" y="%d" text-anchor="middle">define to try today</text>' % hy)
|
||||
s.append('<text class="fg-h" x="764" y="%d" text-anchor="middle">4.0 default</text>' % hy)
|
||||
for i, r in enumerate(rows):
|
||||
y = TOP + i * ROW + ROW / 2
|
||||
s.append('<rect class="nj-int-row" x="16" y="%g" width="848" height="%d" rx="6"/>' % (y - ROW / 2 + 2, ROW - 4))
|
||||
s.append('<text class="nj-int-macro" x="32" y="%g">%s</text>' % (y + 4.5, r["macro"]))
|
||||
s.append('<text class="nj-int-mute" x="380" y="%g">%s</text>' % (y + 4.5, r["added"]))
|
||||
s.append('<rect class="nj-int-pill" x="470" y="%g" width="60" height="22" rx="11"/>' % (y - 11))
|
||||
s.append('<text class="nj-int-val" x="500" y="%g" text-anchor="middle">%s</text>' % (y + 4.5, r["default"]))
|
||||
s.append('<line class="fg-arrow fg-arrow--dash" x1="544" y1="%g" x2="724" y2="%g" '
|
||||
'marker-end="url(#nj-int-arrow)"/>' % (y, y))
|
||||
s.append('<rect class="nj-int-pill nj-int-pill--v4" x="728" y="%g" width="72" height="22" rx="11"/>' % (y - 11))
|
||||
if r["removed"]:
|
||||
s.append('<text class="nj-int-val nj-int-val--v4" x="764" y="%g" text-anchor="middle">removed</text>' % (y + 4.5))
|
||||
else:
|
||||
s.append('<text class="nj-int-val nj-int-val--v4" x="764" y="%g" text-anchor="middle">%s</text>' % (y + 4.5, r["v4"]))
|
||||
s.append('<text class="nj-int-mute" x="16" y="%d">Versions on the 3.x lane are the ones that added a macro, '
|
||||
'not to scale. “removed”: see the 4.0 behavior column of the table.</text>' % (H - 8))
|
||||
s.append('</svg>')
|
||||
return "\n".join(s) + "\n"
|
||||
|
||||
|
||||
rows = parse(PAGE.read_text(encoding="utf-8"))
|
||||
(OUT / "roadmap_lanes.svg").write_text(draw(rows), encoding="utf-8")
|
||||
print("wrote", OUT / "roadmap_lanes.svg", "with", len(rows), "macros")
|
||||
@@ -0,0 +1,103 @@
|
||||
"""Draws the parser-callback and iterator illustrations as an inline SVG into docs/images/.
|
||||
|
||||
The SVG is not an image: the pages include its text with `--8<-- "images/callback_events.svg, range-begin-end.svg and range-rbegin-rend.svg"`, so it picks up the colours and fonts of the
|
||||
page (see the `railroad` and `nj-fig` rules in docs/css/brand.css). Run `python3 scripts/figures/iterator_callback_figures.py` from docs/mkdocs after
|
||||
changing the drawing, and commit the generated file. In SVG text write the word JSON as `J​SON`, otherwise the
|
||||
abbreviation extension wraps it in an <abbr> and the text disappears.
|
||||
"""
|
||||
import html
|
||||
from pathlib import Path
|
||||
|
||||
OUT = Path(__file__).resolve().parents[2] / "docs" / "images"
|
||||
CW = 9.6 # JetBrains Mono advance at 16px
|
||||
|
||||
def write(name, W, H, cls, tid, title, body, defs=""):
|
||||
s = ['<svg class="nj-fig %s" viewBox="0 0 %d %d" role="img" aria-labelledby="%s" xmlns="http://www.w3.org/2000/svg">' % (cls, W, H, tid),
|
||||
'<title id="%s">%s</title>' % (tid, html.escape(title).replace('J&#8203;SON', 'J​SON'))]
|
||||
if defs: s.append(defs)
|
||||
s += body
|
||||
s.append('</svg>')
|
||||
open(OUT / name, "w").write("\n".join(s) + "\n")
|
||||
|
||||
# ---------- callback events ----------
|
||||
def callback():
|
||||
body, y0, lh = [], 74, 28
|
||||
body.append('<text class="fg-h" x="16" y="28">J​SON to parse</text>')
|
||||
body.append('<text class="fg-h" x="320" y="28">callback events</text>')
|
||||
lines = [
|
||||
(0, [("{", None), (None, "os")]),
|
||||
(2, [('"name"', None), (None, "k"), (': "Berlin"', None), (None, "v"), (",", None)]),
|
||||
(2, [('"location"', None), (None, "k"), (": [", None), (None, "as")]),
|
||||
(4, [("52.519444", None), (None, "v"), (",", None)]),
|
||||
(4, [("13.406667", None), (None, "v")]),
|
||||
(2, [("]", None), (None, "ae"), (None, "v")]),
|
||||
(0, [("}", None), (None, "oe"), (None, "v")]),
|
||||
]
|
||||
for i, (indent, parts) in enumerate(lines):
|
||||
y = y0 + i * lh
|
||||
x = 16 + indent * CW
|
||||
for text, ev in parts:
|
||||
if text is not None:
|
||||
body.append('<text class="fg-code" x="%g" y="%d" xml:space="preserve">%s</text>' % (x, y, html.escape(text)))
|
||||
x += CW * len(text)
|
||||
else:
|
||||
body.append('<circle class="cb-%s" cx="%g" cy="%d" r="5.5"/>' % (ev, x + 10, y - 5))
|
||||
x += 20 if text is None else 0
|
||||
legend = [("os", "object_start"), ("k", "key"), ("oe", "object_end"), ("as", "array_start"), ("ae", "array_end"), ("v", "value")]
|
||||
for i, (ev, label) in enumerate(legend):
|
||||
y = 74 + i * 30
|
||||
body.append('<circle class="cb-%s" cx="327" cy="%d" r="6"/>' % (ev, y - 5))
|
||||
body.append('<text class="fg-code fg-leg" x="344" y="%d">%s</text>' % (y, label))
|
||||
write("callback_events.svg", 480, 262, "nj-fig--cb", "fig-cb-title",
|
||||
"Example of the parse events a callback sees for the J​SON text {\"name\": \"Berlin\", \"location\": [52.519444, 13.406667]}: object_start after the opening brace, key after each key, array_start after the opening bracket, value after each value, array_end after the closing bracket, and object_end after the closing brace. Every array and object also produces a value event when it is complete.", body)
|
||||
|
||||
# ---------- ranges ----------
|
||||
BW, GAP, X0 = 30, 4, 30
|
||||
def cell_x(i): return X0 + i * (BW + GAP)
|
||||
def cell(i, y, kind):
|
||||
c = "fg-cell" + (" fg-cell--end" if kind == "end" else "")
|
||||
return '<rect class="%s" x="%d" y="%d" width="%d" height="30" rx="5"/>' % (c, cell_x(i), y, BW)
|
||||
def pill(cx, y, label, W, label_w):
|
||||
x = max(4, min(W - 4 - label_w, cx - label_w / 2))
|
||||
return ['<rect class="fg-pill" x="%g" y="%d" width="%d" height="24" rx="6"/>' % (x, y, label_w),
|
||||
'<text class="fg-pilltext" x="%g" y="%d">%s</text>' % (x + label_w / 2, y + 16.5, label)]
|
||||
def arrow(x, ya, yb, mid, dashed=False):
|
||||
return '<line class="fg-arrow%s" x1="%g" y1="%g" x2="%g" y2="%g" marker-end="url(#%s)"/>' % (" fg-arrow--dash" if dashed else "", x, ya, x, yb, mid)
|
||||
|
||||
def ranges1():
|
||||
W, H, mid = 600, 150, "fig-be-arrow"
|
||||
defs = '<defs><marker id="%s" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path class="fg-head" d="M0 1 L10 5 L0 9 z"/></marker></defs>' % mid
|
||||
body = []
|
||||
for i in range(15): body.append(cell(i, 62, "elem"))
|
||||
body.append(cell(15, 62, "end"))
|
||||
body += pill(cell_x(0) + BW / 2, 8, "begin", W, 60); body.append(arrow(cell_x(0) + BW / 2, 32, 60, mid))
|
||||
body += pill(cell_x(15) + BW / 2, 8, "end", W, 48); body.append(arrow(cell_x(15) + BW / 2, 32, 60, mid))
|
||||
body.append('<text class="fg-note fg-note--r" x="%d" y="132">Past-the-last element</text>' % (cell_x(15) + BW / 2 - 18))
|
||||
body.append(arrow(cell_x(15) + BW / 2, 124, 96, mid))
|
||||
write("range-begin-end.svg", W, H, "nj-fig--range", "fig-be-title",
|
||||
"A row of fifteen elements followed by a dashed past-the-last position. begin() points to the first element and end() points to the past-the-last position.", body, defs)
|
||||
|
||||
def ranges2():
|
||||
W, H, mid = 600, 236, "fig-rbe-arrow"
|
||||
defs = '<defs><marker id="%s" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path class="fg-head" d="M0 1 L10 5 L0 9 z"/></marker></defs>' % mid
|
||||
body = []
|
||||
ytop, ybot = 62, 144
|
||||
body.append(cell(0, ytop, "end"))
|
||||
for i in range(1, 16): body.append(cell(i, ytop, "elem"))
|
||||
for i in range(15): body.append(cell(i, ybot, "elem"))
|
||||
body.append(cell(15, ybot, "end"))
|
||||
body += pill(cell_x(0) + BW / 2, 8, "rend", W, 52); body.append(arrow(cell_x(0) + BW / 2, 32, 60, mid))
|
||||
body += pill(cell_x(15) + BW / 2, 8, "rbegin", W, 68); body.append(arrow(cell_x(15) + BW / 2, 32, 60, mid))
|
||||
body.append('<text class="fg-note fg-note--l" x="%d" y="24">Reverse past-the-last element</text>' % (cell_x(0) + BW / 2 + 36))
|
||||
for col in (0, 15):
|
||||
x = cell_x(col) + BW / 2
|
||||
body.append('<line class="fg-arrow fg-arrow--dash" x1="%g" y1="94" x2="%g" y2="%d"/>' % (x, x, ybot - 2))
|
||||
body.append('<text class="fg-note" x="300" y="123">A reverse iterator stores an iterator to the next element</text>')
|
||||
body.append('<text class="fg-note" x="300" y="141" dy="0" style="display:none"></text>')
|
||||
body += pill(cell_x(0) + BW / 2, 204, "begin", W, 60); body.append(arrow(cell_x(0) + BW / 2, 202, 178, mid))
|
||||
body += pill(cell_x(15) + BW / 2, 204, "end", W, 48); body.append(arrow(cell_x(15) + BW / 2, 202, 178, mid))
|
||||
body.append('<text class="fg-note fg-note--r" x="%d" y="221">Past-the-last element</text>' % (cell_x(15) + BW / 2 - 40))
|
||||
write("range-rbegin-rend.svg", W, H, "nj-fig--range", "fig-rbe-title",
|
||||
"Two rows of sixteen cells. The upper row shows the reversed sequence: rend() points to a dashed position before the first element and rbegin() to the last element. The lower row shows the forward sequence: begin() points to the first element and end() to a dashed position after the last element. A reverse iterator stores an iterator to the next element, so rend() corresponds to begin() and rbegin() to end().", body, defs)
|
||||
|
||||
callback(); ranges1(); ranges2()
|
||||
@@ -0,0 +1,110 @@
|
||||
"""Draws the flatten()/unflatten() illustration of features/json_pointer.md as an inline SVG into docs/images/.
|
||||
|
||||
The SVG is not an image: the page includes its text with `--8<-- "images/json_pointer_flatten.svg"`, so it picks up
|
||||
the colours and fonts of the page (see the `nj-fig` rules in docs/css/brand.css and the `nj-pointer` rules in
|
||||
docs/css/fig-pointer.css). Run `python3 scripts/figures/json_pointer_flatten.py` from docs/mkdocs after changing the
|
||||
drawing, and commit the generated file. In SVG text write the word JSON as `J​SON`, otherwise the abbreviation
|
||||
extension wraps it in an <abbr> and the text disappears.
|
||||
|
||||
Left: the example document of the page as a tree. Right: the flat object that flatten() returns, one key per leaf.
|
||||
Each leaf is joined to its flat key by a connector; leaf, connector and flat value share the colour of the value type
|
||||
(string, number, boolean) used by the code highlighting.
|
||||
"""
|
||||
import html
|
||||
from pathlib import Path
|
||||
|
||||
OUT = Path(__file__).resolve().parents[2] / "docs" / "images"
|
||||
CW = 9.6 # JetBrains Mono advance at 16px
|
||||
ROW = 28
|
||||
Y0 = 92 # baseline of the first tree row
|
||||
X0 = 20 # x of the root label
|
||||
IND = 26 # indentation per level
|
||||
|
||||
# (depth, label, value or None, value type, flat key)
|
||||
TREE = [
|
||||
(0, "root", None, None, None),
|
||||
(1, '"array"', None, None, None),
|
||||
(2, "0", '"A"', "s", "/array/0"),
|
||||
(2, "1", '"B"', "s", "/array/1"),
|
||||
(2, "2", '"C"', "s", "/array/2"),
|
||||
(1, '"nested"', None, None, None),
|
||||
(2, '"one"', "1", "n", "/nested/one"),
|
||||
(2, '"two"', "2", "n", "/nested/two"),
|
||||
(2, '"three"', None, None, None),
|
||||
(3, "0", "true", "b", "/nested/three/0"),
|
||||
(3, "1", "false", "b", "/nested/three/1"),
|
||||
]
|
||||
W = 760
|
||||
FX = 470 # x of the flat keys
|
||||
|
||||
|
||||
def esc(s):
|
||||
return html.escape(s, quote=False)
|
||||
|
||||
|
||||
def main():
|
||||
body = []
|
||||
H = Y0 + (len(TREE) - 1) * ROW + 70
|
||||
mid = "fig-flat-arrow"
|
||||
defs = ('<defs><marker id="%s" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" '
|
||||
'orient="auto"><path class="fg-head" d="M0 1 L10 5 L0 9 z"/></marker></defs>' % mid)
|
||||
|
||||
body.append('<text class="fg-h" x="%d" y="26">nested value</text>' % X0)
|
||||
body.append('<text class="fg-h" x="%d" y="26">flat object: key = J​SON Pointer</text>' % FX)
|
||||
|
||||
# flatten() arrow on top, unflatten() arrow at the bottom
|
||||
ax1, ax2 = 250, FX - 20
|
||||
body.append('<line class="fg-arrow" x1="%d" y1="52" x2="%d" y2="52" marker-end="url(#%s)"/>' % (ax1, ax2, mid))
|
||||
body.append('<text class="nj-pointer-fl-op" x="%d" y="44">flatten()</text>' % ((ax1 + ax2) // 2))
|
||||
yb = H - 22
|
||||
body.append('<line class="fg-arrow" x1="%d" y1="%d" x2="%d" y2="%d" marker-end="url(#%s)"/>' % (ax2, yb, ax1, yb, mid))
|
||||
body.append('<text class="nj-pointer-fl-op" x="%d" y="%d">unflatten()</text>' % ((ax1 + ax2) // 2, yb - 8))
|
||||
|
||||
# tree: elbow lines from each node to its parent
|
||||
rows = []
|
||||
parents = []
|
||||
for i, (d, label, val, typ, key) in enumerate(TREE):
|
||||
y = Y0 + i * ROW
|
||||
x = X0 + d * IND
|
||||
while parents and parents[-1][0] >= d:
|
||||
parents.pop()
|
||||
if parents:
|
||||
px = X0 + parents[-1][0] * IND + 5
|
||||
py = parents[-1][1] + 7
|
||||
body.append('<path class="nj-pointer-fl-edge" d="M%d %d V%d H%d"/>' % (px, py, y - 5, x - 4))
|
||||
parents.append((d, y))
|
||||
rows.append((x, y, label, val, typ, key))
|
||||
|
||||
leaves = [r for r in rows if r[3] is not None]
|
||||
# flat keys: seven consecutive rows, centred on the leaf rows (tree rows 2 to 10)
|
||||
for j, (x, y, label, val, typ, key) in enumerate(leaves):
|
||||
fy = Y0 + (3 + j) * ROW
|
||||
lx = x + CW * (len(label) + 2 + len(val)) + 10
|
||||
body.append('<path class="nj-pointer-fl-link nj-pointer-fl--%s" d="M%g %d C%g %d %g %d %d %d"/>'
|
||||
% (typ, lx, y - 5, lx + 120, y - 5, FX - 130, fy - 5, FX - 10, fy - 5))
|
||||
body.append('<circle class="nj-pointer-fl-dot nj-pointer-fl--%s" cx="%g" cy="%d" r="3.5"/>' % (typ, lx, y - 5))
|
||||
body.append('<text class="fg-code" x="%d" y="%d" xml:space="preserve"><tspan class="nj-pointer-fl-key">"%s"</tspan>: '
|
||||
'<tspan class="nj-pointer-fl-v nj-pointer-fl--%s">%s</tspan></text>' % (FX, fy, esc(key), typ, esc(val)))
|
||||
|
||||
for x, y, label, val, typ, key in rows:
|
||||
if val is None:
|
||||
cls = "fg-code nj-pointer-fl-inner" if label != "root" else "fg-h"
|
||||
body.append('<text class="%s" x="%d" y="%d">%s</text>' % (cls, x, y, esc(label)))
|
||||
else:
|
||||
body.append('<text class="fg-code" x="%d" y="%d" xml:space="preserve"><tspan class="nj-pointer-fl-k">%s</tspan>: '
|
||||
'<tspan class="nj-pointer-fl-v nj-pointer-fl--%s">%s</tspan></text>' % (x, y, esc(label), typ, esc(val)))
|
||||
|
||||
title = ("flatten() turns the nested example document into a flat object, and unflatten() turns it back. "
|
||||
"Left, the document as a tree: array with the elements A, B, C, and nested with one = 1, two = 2, and "
|
||||
"three with the elements true and false. Right, the flat object with one key per leaf: /array/0 = A, "
|
||||
"/array/1 = B, /array/2 = C, /nested/one = 1, /nested/two = 2, /nested/three/0 = true, "
|
||||
"/nested/three/1 = false. Each leaf is joined to its key by a line.")
|
||||
s = ['<svg class="nj-fig nj-pointer-fl" viewBox="0 0 %d %d" role="img" aria-labelledby="fig-flat-title" '
|
||||
'xmlns="http://www.w3.org/2000/svg">' % (W, H),
|
||||
'<title id="fig-flat-title">%s</title>' % esc(title), defs]
|
||||
s += body
|
||||
s.append('</svg>')
|
||||
(OUT / "json_pointer_flatten.svg").write_text("\n".join(s) + "\n")
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,391 @@
|
||||
"""Draws the json_view illustrations of docs/features/json_view.md as inline SVGs into docs/images/.
|
||||
|
||||
The SVGs are not images: the page includes their text with `--8<-- "images/json_view_layout.svg"` (and
|
||||
json_view_lifetime.svg, json_view_edit_0.svg ... json_view_edit_4.svg), so they pick up the colours and fonts of
|
||||
the page (see the `nj-fig` rules in docs/css/brand.css and the `nj-jsonview-*` rules in docs/css/fig-jsonview.css).
|
||||
The edit frames are the steps of a stepper (docs/js/fig-jsonview.js); without JavaScript they show one below the
|
||||
other. Run `python3 scripts/figures/json_view_layout.py` from docs/mkdocs after changing the drawing, and commit the
|
||||
generated files. In SVG text write the word JSON as `J​SON`, otherwise the abbreviation extension wraps it in
|
||||
an <abbr> and the text disappears.
|
||||
|
||||
The node fields drawn here follow `struct node` in include/nlohmann/detail/view/node.hpp and the edit storage in
|
||||
detail/view/edit.hpp and edit_storage.hpp, for the 54-byte text in TEXT (9 nodes).
|
||||
"""
|
||||
import html
|
||||
from pathlib import Path
|
||||
|
||||
OUT = Path(__file__).resolve().parents[2] / "docs" / "images"
|
||||
TEXT = '{"port": 6379, "price": 19.90, "tags": ["eu", "a\\"b"]}'
|
||||
assert len(TEXT) == 54
|
||||
|
||||
# concept -> colour class (fig-jsonview.css maps them to brand colours)
|
||||
STRUCT, KEY, STR, NUM, DEC, OWN, BAD, LINE = "struct", "key", "str", "num", "dec", "own", "bad", "line"
|
||||
|
||||
|
||||
def esc(s):
|
||||
return html.escape(s, quote=True)
|
||||
|
||||
|
||||
def write(name, W, H, tid, title, desc, body, extra_cls=""):
|
||||
s = ['<svg class="nj-fig nj-jsonview %s" viewBox="0 0 %d %d" role="img" aria-labelledby="%s %s-d" '
|
||||
'xmlns="http://www.w3.org/2000/svg">' % (extra_cls, W, H, tid, tid),
|
||||
'<title id="%s">%s</title>' % (tid, esc(title).replace("\u200b", "​")),
|
||||
'<desc id="%s-d">%s</desc>' % (tid, esc(desc))]
|
||||
s += body
|
||||
s.append('</svg>')
|
||||
text = "\n".join(s) + "\n"
|
||||
assert "\n\n" not in text # a blank line would end the raw HTML block
|
||||
(OUT / name).write_text(text)
|
||||
|
||||
|
||||
def markers(prefix, kinds=(LINE, STR, KEY, NUM, DEC, OWN, STRUCT, BAD)):
|
||||
"""one arrow head per colour; ids are unique per SVG (hidden frames must not own the markers)"""
|
||||
m = ['<defs>']
|
||||
for k in kinds:
|
||||
m.append('<marker id="%s-%s" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6.5" markerHeight="6.5" '
|
||||
'orient="auto-start-reverse"><path class="nj-jsonview-c-%s nj-jsonview-head" d="M0 1 L10 5 L0 9 z"/></marker>' % (prefix, k, k))
|
||||
m.append('</defs>')
|
||||
return m
|
||||
|
||||
|
||||
def rect(x, y, w, h, cls, c=None, rx=5):
|
||||
cc = " nj-jsonview-c-%s" % c if c else ""
|
||||
return '<rect class="%s%s" x="%g" y="%g" width="%g" height="%g" rx="%g"/>' % (cls, cc, x, y, w, h, rx)
|
||||
|
||||
|
||||
def text(x, y, s, cls="nj-jsonview-t", c=None, anchor=None):
|
||||
cc = " nj-jsonview-c-%s" % c if c else ""
|
||||
a = ' text-anchor="%s"' % anchor if anchor else ""
|
||||
return '<text class="%s%s" x="%g" y="%g"%s xml:space="preserve">%s</text>' % (cls, cc, x, y, a, esc(s))
|
||||
|
||||
|
||||
def curve(x1, y1, x2, y2, c, prefix, dashed=False, bend=None, head=True):
|
||||
"""vertical-ish cubic from (x1, y1) to (x2, y2)"""
|
||||
b = bend if bend is not None else (y2 - y1) / 2
|
||||
d = "M%g %g C%g %g %g %g %g %g" % (x1, y1, x1, y1 + b, x2, y2 - b, x2, y2)
|
||||
return '<path class="nj-jsonview-arr nj-jsonview-c-%s%s" d="%s"%s/>' % (
|
||||
c, " nj-jsonview-dash" if dashed else "", d, ' marker-end="url(#%s-%s)"' % (prefix, c) if head else "")
|
||||
|
||||
|
||||
def line(x1, y1, x2, y2, c, prefix, dashed=False, head=True):
|
||||
return '<path class="nj-jsonview-arr nj-jsonview-c-%s%s" d="M%g %g L%g %g"%s/>' % (
|
||||
c, " nj-jsonview-dash" if dashed else "", x1, y1, x2, y2, ' marker-end="url(#%s-%s)"' % (prefix, c) if head else "")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- the parsed index of TEXT
|
||||
# (index, colour, kind, field lines, source span or None, label for compact frames)
|
||||
NODES = [
|
||||
(0, STRUCT, "object", ["len 3", "next 9"], None, "{ } 3"),
|
||||
(1, KEY, "key", ["off 2", "len 4"], (2, 6), '"port"'),
|
||||
(2, NUM, "uint", ["6379", "(converted)"], (9, 13), "6379"),
|
||||
(3, KEY, "key", ["off 16", "len 5"], (16, 21), '"price"'),
|
||||
(4, NUM, "float", ["off 24", "len 5"], (24, 29), "19.90"),
|
||||
(5, KEY, "key", ["off 32", "len 4"], (32, 36), '"tags"'),
|
||||
(6, STRUCT, "array", ["len 2", "next 3"], None, "[ ] 2"),
|
||||
(7, STR, "string", ["off 41", "len 2"], (41, 43), '"eu"'),
|
||||
(8, DEC, "string", ["escaped", "len 3"], (47, 51), 'a"b'),
|
||||
]
|
||||
|
||||
|
||||
def ribbon(body, x0, y, cw, prefix, label=True):
|
||||
"""the source text as a strip of bytes, with the spans the nodes refer to tinted"""
|
||||
w = cw * len(TEXT)
|
||||
body.append(rect(x0, y, w, 26, "nj-jsonview-ribbon", rx=4))
|
||||
for _, c, kind, _, span, _ in NODES:
|
||||
if span:
|
||||
body.append(rect(x0 + span[0] * cw + 1, y + 2, (span[1] - span[0]) * cw - 2, 22, "nj-jsonview-span", c, rx=3))
|
||||
for i, ch in enumerate(TEXT):
|
||||
if ch != " ":
|
||||
body.append(text(x0 + i * cw + cw / 2, y + 17.5, ch, "nj-jsonview-mono nj-jsonview-byte", anchor="middle"))
|
||||
if label:
|
||||
for off in (0, 10, 20, 30, 40, 50):
|
||||
body.append(text(x0 + off * cw + cw / 2, y + 40, str(off), "nj-jsonview-tiny", anchor="middle"))
|
||||
return w
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- figure 1: parse() vs json_document
|
||||
def layout():
|
||||
W, H, P = 840, 692, "jvl"
|
||||
b = markers(P)
|
||||
# --- panel A: basic_json::parse()
|
||||
b.append(text(16, 26, "basic_json::parse(): a tree, every array, object, and string in its own heap allocation", "nj-jsonview-h"))
|
||||
b.append(rect(16, 98, 60, 34, "nj-jsonview-box"))
|
||||
b.append(text(46, 120, "json", "nj-jsonview-mono", anchor="middle"))
|
||||
b.append(line(76, 115, 106, 115, LINE, P))
|
||||
b.append(rect(108, 98, 84, 34, "nj-jsonview-heap"))
|
||||
b.append(text(150, 120, "std::map", "nj-jsonview-mono", anchor="middle"))
|
||||
rows = [('"port"', "6379", None), ('"price"', "19.9", "double: 19.90 is gone"), ('"tags"', "\u2022", None)]
|
||||
for i, (k, v, note) in enumerate(rows):
|
||||
y = 50 + i * 48
|
||||
b.append(curve(192, 115, 226, y + 17, LINE, P, bend=0))
|
||||
b.append(rect(228, y, 180, 34, "nj-jsonview-heap"))
|
||||
b.append(text(240, y + 22, k, "nj-jsonview-mono", KEY))
|
||||
b.append(text(396, y + 22, v, "nj-jsonview-mono", NUM if i < 2 else LINE, anchor="end"))
|
||||
if note:
|
||||
b.append(text(418, y + 22, note, "nj-jsonview-note"))
|
||||
yt = 50 + 2 * 48 + 17
|
||||
b.append(line(408, yt, 442, yt, LINE, P))
|
||||
b.append(rect(444, yt - 17, 104, 34, "nj-jsonview-heap"))
|
||||
b.append(text(496, yt + 5, "std::vector", "nj-jsonview-mono", anchor="middle"))
|
||||
b.append(line(548, yt, 576, yt, LINE, P))
|
||||
b.append(rect(578, yt - 17, 92, 34, "nj-jsonview-heap"))
|
||||
b.append('<path class="nj-jsonview-sep" d="M624 %g V%g"/>' % (yt - 17, yt + 17))
|
||||
b.append(text(601, yt + 5, "json", "nj-jsonview-mono", anchor="middle"))
|
||||
b.append(text(647, yt + 5, "json", "nj-jsonview-mono", anchor="middle"))
|
||||
b.append(curve(601, yt - 17, 706, 130, LINE, P, bend=-20))
|
||||
b.append(curve(647, yt + 17, 706, 196, LINE, P, bend=20))
|
||||
b.append(rect(708, 113, 126, 34, "nj-jsonview-heap"))
|
||||
b.append(text(771, 135, "std::string eu", "nj-jsonview-mono nj-jsonview-sm", STR, anchor="middle"))
|
||||
b.append(rect(708, 179, 126, 34, "nj-jsonview-heap"))
|
||||
b.append(text(771, 201, 'std::string a"b', "nj-jsonview-mono nj-jsonview-sm", STR, anchor="middle"))
|
||||
# legend
|
||||
b.append(rect(16, 186, 18, 14, "nj-jsonview-heap", rx=3))
|
||||
b.append(text(42, 198, "a separate heap allocation", "nj-jsonview-note"))
|
||||
# --- panel B: json_document::parse()
|
||||
y0 = 252
|
||||
b.append('<path class="nj-jsonview-sep" d="M16 %g H824"/>' % (y0 - 18))
|
||||
b.append(text(16, y0 + 10, "json_document::parse(): the source text stays where it is; one flat index of 16-byte nodes", "nj-jsonview-h"))
|
||||
b.append(text(16, y0 + 38, "source text, 54 bytes (borrowed, or owned by the document)", "nj-jsonview-note"))
|
||||
cw, rx0, ry = 14, 42, y0 + 48
|
||||
ribbon(b, rx0, ry, cw, P)
|
||||
# node strip inside the document frame
|
||||
fy = ry + 66
|
||||
b.append(rect(16, fy, 808, 254, "nj-jsonview-frame", rx=10))
|
||||
nw, gap, nx0, ny, nh = 84, 4, 36, fy + 40, 68
|
||||
b.append(text(30, fy + 236, "json_document: owns the node index and the buffer of decoded strings", "nj-jsonview-note nj-jsonview-strong"))
|
||||
b.append(text(30, ny + nh + 26, "node index: 9 nodes \u00d7 16 bytes = 144 bytes, in document order", "nj-jsonview-note"))
|
||||
for i, c, kind, fields, span, _ in NODES:
|
||||
x = nx0 + i * (nw + gap)
|
||||
b.append(rect(x, ny, nw, nh, "nj-jsonview-node", c))
|
||||
b.append(text(x + 6, ny + 14, str(i), "nj-jsonview-tiny"))
|
||||
b.append(text(x + nw / 2, ny + 28, kind, "nj-jsonview-kind", c, anchor="middle"))
|
||||
for j, f in enumerate(fields):
|
||||
b.append(text(x + nw / 2, ny + 45 + j * 14, f, "nj-jsonview-mono nj-jsonview-xs", anchor="middle"))
|
||||
if span and c in (KEY, STR, NUM) and not (c == NUM and kind == "uint"):
|
||||
sx = rx0 + (span[0] + span[1]) / 2 * cw
|
||||
b.append(curve(x + nw / 2, ny, sx, ry + 28, c, P))
|
||||
# the integer has no pointer: its value is in the node
|
||||
# decoded strings
|
||||
dx, dy = 740, ny + nh + 44
|
||||
n8x = nx0 + 8 * (nw + gap) + nw / 2
|
||||
b.append(text(dx + 33, dy + 50, "decoded strings", "nj-jsonview-note", anchor="middle"))
|
||||
b.append(rect(dx, dy, 3 * 22, 30, "nj-jsonview-node", DEC, rx=4))
|
||||
for k, ch in enumerate('a"b'):
|
||||
b.append(text(dx + 11 + k * 22, dy + 20, ch, "nj-jsonview-mono", anchor="middle"))
|
||||
b.append(curve(n8x, ny + nh, dx + 33, dy, DEC, P))
|
||||
# the view: two pointers
|
||||
vx, vy = 480, fy + 254 + 28
|
||||
b.append(text(vx - 10, vy + 22, 'json_view v = doc.root()["tags"]', "nj-jsonview-mono nj-jsonview-sm", anchor="end"))
|
||||
b.append(rect(vx, vy, 140, 34, "nj-jsonview-node", STRUCT, rx=6))
|
||||
b.append('<path class="nj-jsonview-sep" d="M%g %g V%g"/>' % (vx + 70, vy, vy + 34))
|
||||
b.append(text(vx + 35, vy + 22, "doc*", "nj-jsonview-mono", anchor="middle"))
|
||||
b.append(text(vx + 105, vy + 22, "node*", "nj-jsonview-mono", anchor="middle"))
|
||||
b.append(line(vx + 35, vy, vx + 35, fy + 254 + 2, STRUCT, P))
|
||||
n6x = nx0 + 6 * (nw + gap) + nw / 2
|
||||
b.append(curve(vx + 105, vy, n6x, ny + nh, STRUCT, P))
|
||||
b.append(text(30, ny + nh + 50, "integers are converted while parsing and kept in their node;", "nj-jsonview-note"))
|
||||
b.append(text(30, ny + nh + 68, "floats and strings point back into the text", "nj-jsonview-note"))
|
||||
write("json_view_layout.svg", W, H, "fig-jvl-title",
|
||||
"The same J\u200bSON text parsed by basic_json::parse() and by json_document::parse()",
|
||||
"The text {\"port\": 6379, \"price\": 19.90, \"tags\": [\"eu\", \"a\\\"b\"]}. Top: basic_json::parse() builds a tree: "
|
||||
"the root json value points to a std::map whose three nodes are separate heap allocations; the price is stored "
|
||||
"as the double 19.9; the tags array is a std::vector of two json values, each pointing to its own std::string. "
|
||||
"Bottom: json_document::parse() keeps the 54 bytes of source text and builds a flat index of nine 16-byte nodes in "
|
||||
"document order: object, key port, unsigned integer 6379 (the converted value is in the node), key price, float, "
|
||||
"key tags, array, string eu, and an escaped string. Keys, the float, and the plain string point back into the "
|
||||
"source text; the escaped string points into the document's buffer of decoded strings. A json_view is two "
|
||||
"pointers: one to the document and one to a node, here the array.", b)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- figure 2: lifetimes
|
||||
def lifetime():
|
||||
W, P = 840, "jvt"
|
||||
L, R = 210, 820
|
||||
b = markers(P, (BAD,))
|
||||
b.append(rect(L, 12, 34, 12, "nj-jsonview-bar", STRUCT, rx=3))
|
||||
b.append(text(L + 42, 23, "alive / valid", "nj-jsonview-note"))
|
||||
b.append(rect(L + 160, 12, 34, 12, "nj-jsonview-bad", rx=3))
|
||||
b.append(text(L + 202, 23, "dangling: using it is undefined behavior", "nj-jsonview-note"))
|
||||
b.append(line(L, 44, R, 44, LINE, P, head=False))
|
||||
b.append(text(R, 38, "time \u2192", "nj-jsonview-tiny", anchor="end"))
|
||||
scen = [
|
||||
("The borrowed text is destroyed", 470, "text destroyed", [
|
||||
("std::string text", [(L, 470, "ok"), ], "end"),
|
||||
("json_document doc", [(L + 60, 470, "ok"), (470, R, "bad")], None),
|
||||
("json_view v", [(L + 120, 470, "ok"), (470, R, "bad")], None)]),
|
||||
("The document is re-parsed", 470, "doc.read(other)", [
|
||||
("json_document doc", [(L, R, "ok")], None),
|
||||
("json_view v", [(L + 60, 470, "ok"), (470, R, "bad")], None),
|
||||
("v = doc.root()", [(530, R, "ok")], None)]),
|
||||
("The document is shrunk", 470, "doc.shrink_to_fit()", [
|
||||
("json_document doc", [(L, R, "ok")], None),
|
||||
("json_view v", [(L + 60, 470, "ok"), (470, R, "bad")], None),
|
||||
("v = doc.root()", [(530, R, "ok")], None)]),
|
||||
("The document is moved, then destroyed", 680, "moved is destroyed", [
|
||||
("json_document doc", [(L, 420, "ok")], None),
|
||||
("moved = std::move(doc)", [(420, 680, "ok")], "end"),
|
||||
("json_view v", [(L + 60, 680, "ok"), (680, R, "bad")], None)]),
|
||||
]
|
||||
y = 70
|
||||
for title, ev, evlabel, rows in scen:
|
||||
b.append(text(16, y + 4, title, "nj-jsonview-h nj-jsonview-sm2"))
|
||||
top = y + 14
|
||||
for k, (name, segs, end) in enumerate(rows):
|
||||
ry = top + k * 28
|
||||
b.append(text(L - 12, ry + 15, name, "nj-jsonview-mono nj-jsonview-sm", anchor="end"))
|
||||
for x1, x2, st in segs:
|
||||
b.append(rect(x1, ry + 3, x2 - x1, 16, "nj-jsonview-bar" if st == "ok" else "nj-jsonview-bad", STRUCT if st == "ok" else None, rx=3))
|
||||
bottom = top + len(rows) * 28
|
||||
if title.startswith("The document is moved"):
|
||||
b.append('<path class="nj-jsonview-evt nj-jsonview-evt--ok" d="M420 %g V%g"/>' % (top - 2, bottom))
|
||||
b.append(text(424, top - 4, "std::move: views stay valid", "nj-jsonview-tiny"))
|
||||
b.append('<path class="nj-jsonview-evt" d="M%g %g V%g"/>' % (ev, top - 2, bottom))
|
||||
b.append(text(ev + 4, top - 4, evlabel, "nj-jsonview-tiny nj-jsonview-c-bad"))
|
||||
if title == "The borrowed text is destroyed":
|
||||
b.append(text(476, top + 2 * 28 + 15, "every view, and the document itself", "nj-jsonview-note nj-jsonview-onbad"))
|
||||
elif title.startswith("The document is moved"):
|
||||
pass
|
||||
else:
|
||||
b.append(text(476, top + 28 + 15, "views taken before", "nj-jsonview-note nj-jsonview-onbad"))
|
||||
b.append(text(536, top + 2 * 28 + 15, "a fresh view is valid again", "nj-jsonview-note nj-jsonview-onok"))
|
||||
y = bottom + 30
|
||||
H = y - 10
|
||||
write("json_view_lifetime.svg", W, H, "fig-jvt-title",
|
||||
"When a json_view becomes invalid",
|
||||
"Four timelines. First: a document borrows a std::string; when the string is destroyed, the document and every view "
|
||||
"taken from it dangle. Second: doc.read() re-parses the document; views taken before are invalid, a fresh view from "
|
||||
"root() is valid. Third: doc.shrink_to_fit() has the same effect. Fourth: moving the document does not invalidate its "
|
||||
"views; destroying the moved-to document does.", b)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- figure 3: edit frames
|
||||
NW, NG, NX0 = 84, 4, 36 # parsed nodes
|
||||
OW, OG, OX0 = 54, 3, 16 # owned storage slots
|
||||
|
||||
|
||||
def nx(i):
|
||||
return NX0 + i * (NW + NG)
|
||||
|
||||
|
||||
def ox(i):
|
||||
return OX0 + i * (OW + OG)
|
||||
|
||||
|
||||
# owned storage in allocation order (all in the first 64-node chunk), per step
|
||||
# entries: (colour, line1, line2, link target) ; link target = ("p", i) parsed node or ("o", j) owned slot
|
||||
OWN_STEPS = {
|
||||
2: [(STRUCT, "header", "[ ] 1", None), (OWN, "link", "\u2192 8", ("p", 8)), (None, "", "", None)],
|
||||
}
|
||||
OWN_STEPS[3] = [(STRUCT, "header", "[ ] 2", None), (OWN, "link", "\u2192 8", ("p", 8)), (OWN, "link", "\u2192 3'", ("o", 3)),
|
||||
(STR, "string", '"new"', None)]
|
||||
OWN_STEPS[4] = OWN_STEPS[3] + [(NUM, "uint", "3", None), (STRUCT, "header", "{ } 4", None),
|
||||
(KEY, "key", "port", None), (OWN, "link", "\u2192 2", ("p", 2)),
|
||||
(KEY, "key", "price", None), (OWN, "link", "\u2192 4", ("p", 4)),
|
||||
(KEY, "key", "tags", None), (OWN, "link", "\u2192 6", ("p", 6)),
|
||||
(KEY, "key", "retries", None), (OWN, "link", "\u2192 4'", ("o", 4))]
|
||||
|
||||
STEPS = [
|
||||
("parsed", "After parse: nine nodes, nothing owned yet"),
|
||||
('doc.set(doc.root(), "port", 6380)', "set: the node of the integer is overwritten in place"),
|
||||
('doc.erase(doc.root()["tags"], 0)', "erase: the array moves to a block of links in owned storage"),
|
||||
('doc.push_back(doc.root()["tags"], "new")', "push_back: a new node, and a link appended to the block"),
|
||||
('doc.set(doc.root(), "retries", 3)', "set of a new member: the object moves to a block as well"),
|
||||
]
|
||||
|
||||
DUMPS = [
|
||||
'{"port":6379,"price":19.90,"tags":["eu","a\\"b"]}',
|
||||
'{"port":6380,"price":19.90,"tags":["eu","a\\"b"]}',
|
||||
'{"port":6380,"price":19.90,"tags":["a\\"b"]}',
|
||||
'{"port":6380,"price":19.90,"tags":["a\\"b","new"]}',
|
||||
'{"port":6380,"price":19.90,"tags":["a\\"b","new"],"retries":3}',
|
||||
]
|
||||
|
||||
|
||||
def edit_frame(step):
|
||||
W, H, P = 840, 372, "jve%d" % step
|
||||
b = markers(P, (LINE, OWN, STRUCT, BAD))
|
||||
b.append(text(16, 22, "parsed index: fixed, never moves", "nj-jsonview-note nj-jsonview-strong"))
|
||||
b.append(text(824, 22, "the source text is never written", "nj-jsonview-note", anchor="end"))
|
||||
ny, nh = 34, 50
|
||||
moved = {6: 0} if step >= 2 else {}
|
||||
if step >= 4:
|
||||
moved[0] = 5
|
||||
for i, c, kind, fields, span, short in NODES:
|
||||
x = nx(i)
|
||||
cls = "nj-jsonview-node"
|
||||
label = short
|
||||
changed = False
|
||||
if i == 2 and step >= 1:
|
||||
label, changed = "6380", step == 1
|
||||
if c == KEY:
|
||||
label = label.strip('"')
|
||||
gap_node = (i == 7 and step >= 2)
|
||||
if gap_node:
|
||||
cls += " nj-jsonview-gap"
|
||||
if changed:
|
||||
cls += " nj-jsonview-hot"
|
||||
b.append(rect(x, ny, NW, nh, cls, c))
|
||||
b.append(text(x + 6, ny + 13, str(i), "nj-jsonview-tiny"))
|
||||
b.append(text(x + NW / 2, ny + 27, kind, "nj-jsonview-kind nj-jsonview-xs", c, anchor="middle"))
|
||||
if i in moved:
|
||||
b.append(text(x + NW - 5, ny + 13, "moved", "nj-jsonview-tiny nj-jsonview-c-struct nj-jsonview-strong", anchor="end"))
|
||||
b.append(text(x + NW / 2, ny + 43, label, "nj-jsonview-mono nj-jsonview-sm", anchor="middle"))
|
||||
if gap_node:
|
||||
b.append(text(x + NW / 2, ny + nh + 14, "unreferenced", "nj-jsonview-tiny nj-jsonview-c-bad", anchor="middle"))
|
||||
# owned storage
|
||||
oy, oh = 196, 46
|
||||
b.append(text(16, oy + oh + 46, "owned storage: allocated on demand, never moves", "nj-jsonview-note nj-jsonview-strong", OWN))
|
||||
slots = OWN_STEPS.get(step, [])
|
||||
b.append(rect(OX0 - 6, oy - 4, 820, oh + 8, "nj-jsonview-store", rx=8))
|
||||
if not slots:
|
||||
b.append(text(OX0 + 10, oy + 28, "empty: a read-only json_document has none of this", "nj-jsonview-note"))
|
||||
for j, (c, l1, l2, tgt) in enumerate(slots):
|
||||
x = ox(j)
|
||||
if c is None:
|
||||
b.append(rect(x, oy, OW, oh, "nj-jsonview-node nj-jsonview-free", OWN))
|
||||
b.append(text(x + OW / 2, oy + 28, "free", "nj-jsonview-tiny", anchor="middle"))
|
||||
continue
|
||||
hot = (step == 2 and j < 3) or (step == 3 and j in (2, 3)) or (step == 4 and j >= 4)
|
||||
b.append(rect(x, oy, OW, oh, "nj-jsonview-node" + (" nj-jsonview-hot" if hot else ""), c))
|
||||
b.append(text(x + OW - 4, oy + 11, "%d'" % j, "nj-jsonview-tiny", anchor="end"))
|
||||
b.append(text(x + OW / 2, oy + 24, l1, "nj-jsonview-kind nj-jsonview-xs", c, anchor="middle"))
|
||||
b.append(text(x + OW / 2, oy + 39, l2, "nj-jsonview-mono nj-jsonview-xs", anchor="middle"))
|
||||
if tgt:
|
||||
cx = x + OW / 2
|
||||
if tgt[0] == "p":
|
||||
b.append(curve(cx, oy, nx(tgt[1]) + NW / 2 + (8 if tgt[1] in moved else 0), ny + nh, OWN, P))
|
||||
else:
|
||||
tx = ox(tgt[1]) + OW / 2
|
||||
d = "M%g %g C%g %g %g %g %g %g" % (cx, oy + oh, cx, oy + oh + 26, tx, oy + oh + 26, tx, oy + oh + 2)
|
||||
b.append('<path class="nj-jsonview-arr nj-jsonview-c-own" d="%s" marker-end="url(#%s-own)"/>' % (d, P))
|
||||
# moved containers point to their block header
|
||||
for i, hj in moved.items():
|
||||
b.append(curve(nx(i) + NW / 2 - 8, ny + nh, ox(hj) + OW / 2, oy, STRUCT, P, dashed=True))
|
||||
# edit text buffer
|
||||
texts = ["new", "retries"][: (1 if step == 3 else 2 if step >= 4 else 0)]
|
||||
if texts:
|
||||
tx0 = 596
|
||||
b.append(text(tx0 - 8, oy + oh + 40, "edited text", "nj-jsonview-note", anchor="end"))
|
||||
b.append(rect(tx0, oy + oh + 22, 10 * len("".join(texts)) + 12, 26, "nj-jsonview-node", STR, rx=4))
|
||||
b.append(text(tx0 + 6, oy + oh + 40, "".join(texts), "nj-jsonview-mono nj-jsonview-sm"))
|
||||
# views and iterators
|
||||
vy = 318
|
||||
b.append('<path class="nj-jsonview-sep" d="M16 %g H824"/>' % (vy - 18))
|
||||
b.append(text(16, vy, 'view v = root()["tags"] (taken after parse):', "nj-jsonview-mono nj-jsonview-xs"))
|
||||
b.append(text(320, vy, "valid, sees every edit", "nj-jsonview-note nj-jsonview-c-struct nj-jsonview-strong"))
|
||||
b.append(text(16, vy + 20, "iterator over tags (taken after parse):", "nj-jsonview-mono nj-jsonview-xs"))
|
||||
if step >= 2:
|
||||
b.append(text(320, vy + 20, "invalid: tags no longer uses the parsed layout", "nj-jsonview-note nj-jsonview-c-bad nj-jsonview-strong"))
|
||||
else:
|
||||
b.append(text(320, vy + 20, "valid", "nj-jsonview-note nj-jsonview-c-struct nj-jsonview-strong"))
|
||||
b.append(text(16, vy + 42, "dump(): " + DUMPS[step], "nj-jsonview-mono nj-jsonview-xs nj-jsonview-muted"))
|
||||
code, cap = STEPS[step]
|
||||
write("json_view_edit_%d.svg" % step, W, H, "fig-jve%d-title" % step,
|
||||
"Step %d of %d: %s" % (step + 1, len(STEPS), code),
|
||||
cap + ". The dump with number_format::source is " + DUMPS[step] + ".", b, "nj-jsonview-editfig")
|
||||
|
||||
|
||||
layout()
|
||||
lifetime()
|
||||
for s in range(len(STEPS)):
|
||||
edit_frame(s)
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Draws the illustrative memory-over-time chart of the large-input page as an inline SVG into docs/images/.
|
||||
|
||||
The chart shows the qualitative shape of the memory use while a large array of records is parsed in four ways: parse()
|
||||
builds the whole tree (DOM), a parser callback discards every record, a SAX handler keeps only what it needs, and a
|
||||
json_document keeps the text plus its index. It is not a measurement: there are no numbers on the axes, and the
|
||||
relative heights depend on the data.
|
||||
|
||||
The SVG is not an image: features/parsing/large_and_partial_input.md includes its text with
|
||||
`--8<-- "images/memory_over_time.svg"`, so it picks up the colours and fonts of the page (see the `nj-fig` rules in
|
||||
docs/css/brand.css and the `nj-sax-` rules in docs/css/fig-sax.css). Run `python3 scripts/figures/memory_over_time.py`
|
||||
from docs/mkdocs after changing the drawing, and commit the generated file. In SVG text write the word JSON as
|
||||
`J​SON` (likewise DOM and SAX, which esc() does), otherwise the abbreviation extension wraps it in an <abbr> and
|
||||
the text disappears.
|
||||
"""
|
||||
import html
|
||||
from pathlib import Path
|
||||
|
||||
OUT = Path(__file__).resolve().parents[2] / "docs" / "images"
|
||||
|
||||
W, H = 660, 312
|
||||
X0, X1 = 56, 440 # plot area
|
||||
Y0, Y1 = 48, 272 # top, baseline
|
||||
|
||||
|
||||
def esc(s):
|
||||
# break the words of the glossary (includes/glossary.md) so the abbreviation extension leaves them alone
|
||||
s = html.escape(s)
|
||||
for word in ("DOM", "SAX", "JSON"):
|
||||
s = s.replace(word, word[0] + "​" + word[1:])
|
||||
return s
|
||||
|
||||
|
||||
def main():
|
||||
body = []
|
||||
tid, did = "fig-sax-mem-title", "fig-sax-mem-desc"
|
||||
# axes
|
||||
body.append('<path class="nj-sax-axis" d="M%d %d V%d H%d"/>' % (X0, Y0 - 12, Y1, X1 + 8))
|
||||
body.append('<text class="fg-h" x="%d" y="24">memory</text>' % (X0 - 40))
|
||||
body.append('<text class="fg-note fg-note--l" x="%d" y="%d">time: records parsed</text>' % (X0, Y1 + 22))
|
||||
body.append('<text class="fg-note fg-note--r nj-sax-illus" x="%d" y="24">illustrative, not a measurement</text>' % (W - 12))
|
||||
|
||||
# DOM: grows with every record until the whole tree is built
|
||||
body.append('<path class="nj-sax-curve nj-sax-c-dom" d="M%d %d L%d %d"/>' % (X0, Y1 - 4, X1, Y0 + 4))
|
||||
# json_document: the text is in memory from the start; the index grows while parsing
|
||||
yt, yi = 168, 146
|
||||
body.append('<path class="nj-sax-curve nj-sax-c-doc" d="M%d %d L%d %d"/>' % (X0, yt, X1, yi))
|
||||
# callback: one record at a time, released when it is discarded
|
||||
hi, lo, step = Y1 - 34, Y1 - 22, 32
|
||||
d, x = "M%d %d" % (X0, lo), X0
|
||||
while x + step <= X1:
|
||||
d += " L%d %d L%d %d" % (x + step, hi, x + step, lo)
|
||||
x += step
|
||||
body.append('<path class="nj-sax-curve nj-sax-c-cb" d="%s"/>' % d)
|
||||
# SAX: no value is built, only what the handler keeps
|
||||
body.append('<path class="nj-sax-curve nj-sax-c-sax" d="M%d %d H%d"/>' % (X0, Y1 - 8, X1))
|
||||
|
||||
labels = [
|
||||
(Y0 + 9, "nj-sax-c-dom", "DOM: parse()", "the whole tree"),
|
||||
(yi + 5, "nj-sax-c-doc", "json_document", "the text plus its index"),
|
||||
(hi - 2, "nj-sax-c-cb", "callback, discard", "one record at a time"),
|
||||
(Y1 + 6, "nj-sax-c-sax", "SAX", "only what the handler keeps"),
|
||||
]
|
||||
for y, cls, name, note in labels:
|
||||
body.append('<text class="nj-sax-lab %s" x="%d" y="%d">%s</text>' % (cls, X1 + 16, y - 9, esc(name)))
|
||||
body.append('<text class="fg-note fg-note--l" x="%d" y="%d">%s</text>' % (X1 + 16, y + 8, esc(note)))
|
||||
|
||||
title = "Illustrative memory use over time while a large array of records is parsed, not a measurement."
|
||||
desc = ("Four curves. DOM, parse(): rises steadily until the whole tree is in memory. json_document: starts at about "
|
||||
"the size of the text, which it keeps, and grows a little while its index is built. Parser callback that "
|
||||
"discards every record: stays flat at the size of one record. SAX: stays flat and lowest, because no value "
|
||||
"is built and only what the handler keeps is in memory.")
|
||||
s = ['<svg class="nj-fig nj-fig--sax-mem" viewBox="0 0 %d %d" role="img" aria-labelledby="%s %s" '
|
||||
'xmlns="http://www.w3.org/2000/svg">' % (W, H, tid, did),
|
||||
'<title id="%s">%s</title>' % (tid, esc(title)),
|
||||
'<desc id="%s">%s</desc>' % (did, esc(desc))]
|
||||
s += body
|
||||
s.append("</svg>")
|
||||
(OUT / "memory_over_time.svg").write_text("\n".join(s) + "\n")
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,196 @@
|
||||
"""Draws the quality-assurance pipeline overview of community/quality_assurance.md as an inline SVG into
|
||||
docs/images/qa_pipeline.svg.
|
||||
|
||||
The SVG is not an image: the page includes its text with `--8<-- "images/qa_pipeline.svg"`, so it picks up the colours
|
||||
and fonts of the page (see the `nj-fig` rules in docs/css/brand.css and the `nj-exceptions-*` rules in
|
||||
docs/css/fig-exceptions.css). The file holds two drawings, a three-column one and a one-column one for phones; the CSS
|
||||
shows one of them.
|
||||
|
||||
The lanes, tools and counts are taken from the text of community/quality_assurance.md, the trigger badges from the
|
||||
`on:` sections of .github/workflows/*.yml. Update LANES when the page changes, run
|
||||
`python3 scripts/figures/qa_pipeline.py` from docs/mkdocs, and commit the generated file. In SVG text write the word
|
||||
JSON as `J​SON`, otherwise the abbreviation extension wraps it in an <abbr> and the text disappears.
|
||||
"""
|
||||
import html
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
OUT = Path(__file__).resolve().parents[2] / "docs" / "images"
|
||||
P = "nj-exceptions-"
|
||||
|
||||
# trigger badges: key -> (label, css modifier)
|
||||
TRIGGERS = {
|
||||
"push": ("every push / PR", "push"),
|
||||
"pr": ("every PR", "push"),
|
||||
"sched": ("scheduled", "sched"),
|
||||
"cont": ("continuous", "cont"),
|
||||
}
|
||||
|
||||
# title, triggers, lines (at most 31 characters, so they fit a card of the three-column drawing)
|
||||
LANES = [
|
||||
("Build matrix", ["push"], [
|
||||
"50+ compilers: GCC, Clang, MSVC,",
|
||||
"AppleClang, Intel, NVIDIA, ...",
|
||||
"C++11, 14, 17, 20, 23, and 26",
|
||||
"libc++ and libstdc++",
|
||||
"warnings: Clang -Weverything,",
|
||||
"GCC 300+ warnings",
|
||||
]),
|
||||
("Unit tests (doctest)", ["push"], [
|
||||
"every public API function",
|
||||
"every exception id and message",
|
||||
"all Unicode code points",
|
||||
"single header and split headers",
|
||||
"custom number, string, array,",
|
||||
"and object types",
|
||||
]),
|
||||
("Coverage", ["push"], [
|
||||
"unit tests cover all lines",
|
||||
"of the code base",
|
||||
]),
|
||||
("Static analysis", ["push", "sched"], [
|
||||
"Clang-Tidy, Cppcheck, Infer",
|
||||
"Clang Static Analyzer (52",
|
||||
"checkers), IWYU, Codacy",
|
||||
"CodeQL, Semgrep, Flawfinder",
|
||||
"(also scheduled)",
|
||||
]),
|
||||
("Dynamic analysis", ["push"], [
|
||||
"runtime assertions",
|
||||
"Valgrind (Memcheck)",
|
||||
"address and undefined behavior",
|
||||
"sanitizers, integer overflow,",
|
||||
"nullability violations",
|
||||
]),
|
||||
("Fuzzing", ["pr", "cont"], [
|
||||
"CIFuzz on every pull request",
|
||||
"OSS-Fuzz: billions of inputs",
|
||||
"every crash fixed with a",
|
||||
"reproducing unit test",
|
||||
]),
|
||||
("Style", ["push"], [
|
||||
"Artistic Style (astyle),",
|
||||
"enforced in the CI",
|
||||
"cpplint with 61 rules",
|
||||
]),
|
||||
("Docs and examples", ["push", "sched"], [
|
||||
"every documentation example is",
|
||||
"tested; changed output fails",
|
||||
"site built with warnings",
|
||||
"as errors; links checked",
|
||||
"regularly (scheduled)",
|
||||
]),
|
||||
("Integration, supply chain", ["push", "sched"], [
|
||||
"CMake 3.5, 3.31.6, and 4.0.0",
|
||||
"Meson, Swift Package Manager",
|
||||
"dependency review on every PR",
|
||||
"Dependabot daily, OpenSSF",
|
||||
"Scorecard, REUSE",
|
||||
]),
|
||||
]
|
||||
|
||||
|
||||
GLOSSARY = Path(__file__).resolve().parents[2] / "includes" / "glossary.md"
|
||||
ABBRS = sorted(re.findall(r"^\*\[([^\]]+)\]:", GLOSSARY.read_text(), re.M), key=len, reverse=True)
|
||||
|
||||
|
||||
def noabbr(s):
|
||||
"""Breaks every glossary abbreviation with a zero-width space, so the abbr extension leaves SVG text alone."""
|
||||
for a in ABBRS:
|
||||
s = re.sub(r"(?<![\w&#])%s(?!\w)" % re.escape(a), a[0] + "​" + a[1:], s)
|
||||
return s
|
||||
|
||||
|
||||
def esc(s):
|
||||
return html.escape(s, quote=True)
|
||||
|
||||
|
||||
def txt(s):
|
||||
return noabbr(esc(s))
|
||||
|
||||
|
||||
def badge_w(label):
|
||||
return len(label) * 6.3 + 16
|
||||
|
||||
|
||||
def card(x, y, w, h, title, triggers, lines):
|
||||
s = ['<g class="%scard">' % P,
|
||||
'<rect class="%sbox" x="%g" y="%g" width="%g" height="%g" rx="8"/>' % (P, x, y, w, h),
|
||||
'<text class="%sctitle" x="%g" y="%g">%s</text>' % (P, x + 14, y + 25, txt(title))]
|
||||
bx = x + 14
|
||||
for t in triggers:
|
||||
label, mod = TRIGGERS[t]
|
||||
bw = badge_w(label)
|
||||
s.append('<rect class="%sbadge %sbadge--%s" x="%g" y="%g" width="%g" height="20" rx="10"/>' % (P, P, mod, bx, y + 36, bw))
|
||||
s.append('<text class="%sbadgetext %sbadgetext--%s" x="%g" y="%g">%s</text>' % (P, P, mod, bx + bw / 2, y + 50, txt(label)))
|
||||
bx += bw + 6
|
||||
for i, line in enumerate(lines):
|
||||
s.append('<text class="%sline" x="%g" y="%g">%s</text>' % (P, x + 14, y + 78 + i * 18, txt(line)))
|
||||
s.append('</g>')
|
||||
return "\n".join(s)
|
||||
|
||||
|
||||
def pill(cx, y, w, label, sub=None, cls=""):
|
||||
h = 46 if sub else 34
|
||||
s = ['<rect class="%sstage%s" x="%g" y="%g" width="%g" height="%d" rx="%g"/>' % (P, cls, cx - w / 2, y, w, h, h / 2),
|
||||
'<text class="%sstagetext" x="%g" y="%g">%s</text>' % (P, cx, y + 22, txt(label))]
|
||||
if sub:
|
||||
s.append('<text class="%sstagesub" x="%g" y="%g">%s</text>' % (P, cx, y + 38, txt(sub)))
|
||||
return "\n".join(s), h
|
||||
|
||||
|
||||
def arrow(x, y1, y2, mid):
|
||||
return '<line class="%sedge" x1="%g" y1="%g" x2="%g" y2="%g" marker-end="url(#%s)"/>' % (P, x, y1, x, y2, mid)
|
||||
|
||||
|
||||
def drawing(W, cols, suffix):
|
||||
mid = "nj-qa-arrow-" + suffix
|
||||
body = []
|
||||
cx = W / 2
|
||||
s, h = pill(cx, 8, 230, "push or pull request")
|
||||
body.append(s)
|
||||
y = 8 + h
|
||||
body.append(arrow(cx, y, y + 26, mid))
|
||||
# container with the lanes
|
||||
top = y + 28
|
||||
pad, gap = 14, 12
|
||||
cw = (W - 24 - 2 * pad - (cols - 1) * gap) / cols
|
||||
body.append('<text class="%sgroup" x="%g" y="%g">checks, run in parallel on GitHub Actions and AppVeyor</text>' % (P, 12 + pad, top + 24) if cols > 1 else
|
||||
'<text class="%sgroup" x="%g" y="%g">checks, run in parallel</text>' % (P, 12 + pad, top + 24))
|
||||
gy = top + 40
|
||||
rows = [LANES[i:i + cols] for i in range(0, len(LANES), cols)]
|
||||
for r in rows:
|
||||
ch = 78 + 18 * (max(len(l[2]) for l in r) - 1) + 16
|
||||
for j, (title, trig, lines) in enumerate(r):
|
||||
body.append(card(12 + pad + j * (cw + gap), gy, cw, ch, title, trig, lines))
|
||||
gy += ch + gap
|
||||
bottom = gy - gap + pad
|
||||
body.insert(0, '<rect class="%sgroupbox" x="12" y="%g" width="%g" height="%g" rx="12"/>' % (P, top, W - 24, bottom - top))
|
||||
# after the checks
|
||||
body.append(arrow(cx, bottom, bottom + 26, mid))
|
||||
s, h = pill(cx, bottom + 28, 230, "merge into develop", cls=" %sstage--merge" % P)
|
||||
body.append(s)
|
||||
y = bottom + 28 + h
|
||||
body.append(arrow(cx, y, y + 26, mid))
|
||||
s, h = pill(cx, y + 28, 300, "release", "no open vulnerability alert", cls=" %sstage--merge" % P)
|
||||
body.append(s)
|
||||
H = y + 28 + h + 8
|
||||
defs = ('<defs><marker id="%s" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">'
|
||||
'<path class="fg-head" d="M0 1 L10 5 L0 9 z"/></marker></defs>' % mid)
|
||||
tid, did = "nj-qa-title-" + suffix, "nj-qa-desc-" + suffix
|
||||
desc = ("Every push or pull request starts these checks in parallel: " +
|
||||
"; ".join("%s (%s): %s" % (t, ", ".join(TRIGGERS[k][0] for k in trig), " ".join(lines)) for t, trig, lines in LANES) +
|
||||
". When they pass, the change is merged into develop; a release is made only when no vulnerability alert is open.")
|
||||
out = ['<svg class="nj-fig %sqa %sqa--%s" viewBox="0 0 %d %d" role="img" aria-labelledby="%s %s" xmlns="http://www.w3.org/2000/svg">'
|
||||
% (P, P, "narrow" if cols == 1 else "wide", W, H, tid, did),
|
||||
'<title id="%s">Quality assurance pipeline</title>' % tid,
|
||||
'<desc id="%s">%s</desc>' % (did, txt(desc)),
|
||||
defs] + body + ['</svg>']
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def main():
|
||||
(OUT / "qa_pipeline.svg").write_text(drawing(760, 3, "w") + "\n" + drawing(360, 1, "n") + "\n")
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,76 @@
|
||||
"""Draws the JSON number railroad diagram as an inline SVG into docs/images/.
|
||||
|
||||
The SVG is not an image: the pages include its text with `--8<-- "images/json_syntax_number.svg"`, so it picks up the colours and fonts of the
|
||||
page (see the `railroad` and `nj-fig` rules in docs/css/brand.css). Run `python3 scripts/figures/railroad_number.py` from docs/mkdocs after
|
||||
changing the drawing, and commit the generated file. In SVG text write the word JSON as `J​SON`, otherwise the
|
||||
abbreviation extension wraps it in an <abbr> and the text disappears.
|
||||
"""
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
OUT = Path(__file__).resolve().parents[2] / "docs" / "images"
|
||||
Y0, Y1, Y2, Y3 = 44, 92, 136, 180
|
||||
R = 10
|
||||
paths, shapes, texts = [], [], []
|
||||
|
||||
def rounded(pts, r=R):
|
||||
d = "M%g %g" % pts[0]
|
||||
for i in range(1, len(pts) - 1):
|
||||
(x0, y0), (x1, y1), (x2, y2) = pts[i-1], pts[i], pts[i+1]
|
||||
l1 = math.hypot(x1-x0, y1-y0); l2 = math.hypot(x2-x1, y2-y1)
|
||||
rr = min(r, l1/2, l2/2)
|
||||
ax, ay = x1 - (x1-x0)/l1*rr, y1 - (y1-y0)/l1*rr
|
||||
bx, by = x1 + (x2-x1)/l2*rr, y1 + (y2-y1)/l2*rr
|
||||
d += " L%g %g Q%g %g %g %g" % (ax, ay, x1, y1, bx, by)
|
||||
d += " L%g %g" % pts[-1]
|
||||
return d
|
||||
|
||||
def path(*pts): paths.append(rounded(list(pts)))
|
||||
def circle(cx, cy, ch):
|
||||
shapes.append('<circle class="rr-bg" cx="%g" cy="%g" r="13"/>' % (cx, cy))
|
||||
shapes.append('<circle class="rr-t" cx="%g" cy="%g" r="13"/>' % (cx, cy))
|
||||
texts.append('<text class="rr-c" x="%g" y="%g">%s</text>' % (cx, cy+5, ch))
|
||||
def box(x0, x1, cy, label):
|
||||
shapes.append('<rect class="rr-bg" x="%g" y="%g" width="%g" height="28" rx="6"/>' % (x0, cy-14, x1-x0))
|
||||
shapes.append('<rect class="rr-n" x="%g" y="%g" width="%g" height="28" rx="6"/>' % (x0, cy-14, x1-x0))
|
||||
texts.append('<text class="rr-b" x="%g" y="%g">%s</text>' % ((x0+x1)/2, cy+5, label))
|
||||
def loop(xr, xl, cy): # return path below a box row
|
||||
path((xr, cy), (xr, cy+34), (xl, cy+34), (xl, cy))
|
||||
|
||||
# start / main
|
||||
path((8, Y0), (850, Y0))
|
||||
# A: optional minus
|
||||
path((24, Y0), (38, Y0), (38, Y1), (59, Y1)); circle(72, Y1, "−"); path((85, Y1), (106, Y1), (106, Y0), (120, Y0))
|
||||
# B: 0 | 1-9 digit*
|
||||
circle(257, Y0, "0")
|
||||
path((120, Y0), (134, Y0), (134, Y1), (160, Y1)); box(160, 242, Y1, "digit 1–9")
|
||||
path((242, Y1), (380, Y1), (380, Y0), (396, Y0))
|
||||
path((242, Y1), (258, Y1), (258, Y2), (278, Y2)); box(278, 332, Y2, "digit")
|
||||
path((332, Y2), (348, Y2), (348, Y1)); loop(340, 270, Y2)
|
||||
# C: fraction
|
||||
path((396, Y0), (410, Y0), (410, Y1), (425, Y1)); circle(438, Y1, ".")
|
||||
path((451, Y1), (462, Y1)); box(462, 516, Y1, "digit"); loop(524, 456, Y1)
|
||||
path((516, Y1), (530, Y1), (530, Y0), (546, Y0))
|
||||
# D: exponent
|
||||
path((546, Y0), (560, Y0), (560, Y1), (577, Y1)); circle(590, Y1, "e")
|
||||
path((546, Y0), (560, Y0), (560, Y2), (577, Y2)); circle(590, Y2, "E")
|
||||
path((603, Y1), (728, Y1)); path((603, Y2), (620, Y2), (620, Y1))
|
||||
path((640, Y1), (654, Y1), (654, Y2), (670, Y2)); circle(683, Y2, "−")
|
||||
path((640, Y1), (654, Y1), (654, Y3), (670, Y3)); circle(683, Y3, "+")
|
||||
path((696, Y2), (712, Y2), (712, Y1)); path((696, Y3), (712, Y3), (712, Y1))
|
||||
path((728, Y1), (746, Y1)); box(746, 800, Y1, "digit"); loop(808, 738, Y1)
|
||||
path((800, Y1), (816, Y1), (816, Y0), (832, Y0))
|
||||
|
||||
svg = ['<svg class="railroad" viewBox="0 0 860 214" role="img" aria-labelledby="rr-title" xmlns="http://www.w3.org/2000/svg">',
|
||||
'<title id="rr-title">Railroad diagram of the J​SON number syntax: optional minus, then 0 or a digit from 1 to 9 followed by digits, then an optional fraction (a dot and digits) and an optional exponent (e or E, an optional sign, and digits).</title>',
|
||||
'<g class="rr-track" fill="none" stroke="currentColor" stroke-opacity=".6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">']
|
||||
for d in paths: svg.append('<path d="%s"/>' % d)
|
||||
svg.append('</g>')
|
||||
svg.append('<polygon class="rr-arrow" points="2,%d 12,%d 2,%d"/>' % (Y0-6, Y0, Y0+6))
|
||||
svg.append('<polygon class="rr-arrow" points="846,%d 858,%d 846,%d"/>' % (Y0-6, Y0, Y0+6))
|
||||
svg += shapes + texts
|
||||
svg.append('<text class="rr-l rr-name" x="24" y="%d">number</text>' % (Y0-12))
|
||||
svg.append('<text class="rr-l" x="420" y="%d">fraction</text>' % (Y0-12))
|
||||
svg.append('<text class="rr-l" x="580" y="%d">exponent</text>' % (Y0-12))
|
||||
svg.append('</svg>')
|
||||
open(OUT / "json_syntax_number.svg", "w").write("\n".join(svg) + "\n")
|
||||
@@ -0,0 +1,127 @@
|
||||
"""Draws the release timeline of home/releases/index.md as an inline SVG into docs/images/.
|
||||
|
||||
The releases and their dates come from the tables of that page (rows like `| [v3.11.2](v3.11.2.md) | 2022-08-12 | ...`),
|
||||
so after adding a release to the page, run `python3 scripts/figures/release_timeline.py` from docs/mkdocs and commit
|
||||
the regenerated docs/images/release_timeline.svg. The page includes the SVG text with
|
||||
`--8<-- "images/release_timeline.svg"`, so it follows the light/dark toggle (see the `nj-fig` rules in
|
||||
docs/css/brand.css and the `nj-int-*` rules in docs/css/fig-integration.css). Every marker has a <title> tooltip.
|
||||
The timeline also marks the first commit of the repository (FIRST_COMMIT) as a diamond. Rows without a date (the "Unreleased" row) are skipped; any other release row that cannot be parsed stops the script.
|
||||
"""
|
||||
import datetime
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
DOCS = Path(__file__).resolve().parents[2] / "docs"
|
||||
OUT = Path(__file__).resolve().parents[2] / "docs" / "images"
|
||||
PAGE = DOCS / "home" / "releases" / "index.md"
|
||||
|
||||
ROW = re.compile(r"^\|\s*\[v(\d+)\.(\d+)\.(\d+)\]\([^)]*\)\s*\|\s*([^|]*?)\s*\|")
|
||||
W, LEFT, RIGHT = 880, 24, 856
|
||||
LABEL_ROW, CHAR_W = 17, 7.0
|
||||
FIRST_COMMIT = datetime.date(2013, 7, 4) # "- initial commit" of the repository
|
||||
|
||||
|
||||
def parse(markdown):
|
||||
releases = []
|
||||
for line in markdown.splitlines():
|
||||
if not line.startswith("| [v"):
|
||||
continue
|
||||
m = ROW.match(line)
|
||||
if not m:
|
||||
sys.exit("cannot parse release row: " + line)
|
||||
try:
|
||||
date = datetime.date.fromisoformat(m.group(4))
|
||||
except ValueError:
|
||||
sys.exit("cannot parse date in row: " + line)
|
||||
releases.append(((int(m.group(1)), int(m.group(2)), int(m.group(3))), date))
|
||||
if len(releases) < 2:
|
||||
sys.exit("found fewer than two releases in " + str(PAGE))
|
||||
if len({v for v, _ in releases}) != len(releases):
|
||||
sys.exit("duplicate release in " + str(PAGE))
|
||||
return sorted(releases, key=lambda r: (r[1], r[0]))
|
||||
|
||||
|
||||
def name(version):
|
||||
return "v%d.%d.%d" % version
|
||||
|
||||
|
||||
def draw(releases):
|
||||
first, last = FIRST_COMMIT, releases[-1][1]
|
||||
start, end = datetime.date(first.year, 1, 1), datetime.date(last.year + 1, 1, 1)
|
||||
span = (end - start).days
|
||||
|
||||
def xof(date):
|
||||
return LEFT + (RIGHT - LEFT) * (date - start).days / span
|
||||
|
||||
# minor labels, placed greedily into the lowest free row above the axis
|
||||
rows_end, labels = [], []
|
||||
for version, date in [(None, FIRST_COMMIT)] + releases:
|
||||
if version is not None and version[2] != 0:
|
||||
continue
|
||||
text = "first commit" if version is None else "%d.%d" % version[:2]
|
||||
x, half = xof(date), len(text) * CHAR_W / 2 + 2
|
||||
row = next((i for i, e in enumerate(rows_end) if e < x - half), None)
|
||||
if row is None:
|
||||
rows_end.append(0)
|
||||
row = len(rows_end) - 1
|
||||
rows_end[row] = x + half
|
||||
labels.append((x, row, text))
|
||||
top = 14 + LABEL_ROW * len(rows_end)
|
||||
axis = top + 12
|
||||
height = axis + 70
|
||||
|
||||
s = ['<svg class="nj-fig nj-fig--timeline" viewBox="0 0 %d %d" role="img" aria-labelledby="nj-int-tl-title" '
|
||||
'xmlns="http://www.w3.org/2000/svg">' % (W, height)]
|
||||
minors = sum(1 for v, _ in releases if v[2] == 0)
|
||||
s.append('<title id="nj-int-tl-title">Timeline from the first commit (%s) and the %d releases from %s (%s) to %s '
|
||||
'(%s): %d minor or major releases, drawn as filled dots with their version above the axis, and %d patch '
|
||||
'releases, drawn as rings. The first commit is drawn as a diamond.</title>'
|
||||
% (first.isoformat(), len(releases), name(releases[0][0]), releases[0][1].isoformat(),
|
||||
name(releases[-1][0]), last.isoformat(), minors, len(releases) - minors))
|
||||
# year grid
|
||||
for year in range(start.year, end.year + 1):
|
||||
x = xof(datetime.date(year, 1, 1))
|
||||
s.append('<line class="nj-int-grid" x1="%.1f" y1="%d" x2="%.1f" y2="%d"/>' % (x, top - 4, x, axis + 8))
|
||||
if year < end.year:
|
||||
mid = xof(datetime.date(year, 7, 1))
|
||||
s.append('<text class="nj-int-year" x="%.1f" y="%d">%d</text>' % (mid, axis + 24, year))
|
||||
s.append('<line class="nj-int-axis" x1="%d" y1="%d" x2="%d" y2="%d"/>' % (LEFT, axis, RIGHT, axis))
|
||||
# major version spans
|
||||
majors = {}
|
||||
for version, date in releases:
|
||||
majors.setdefault(version[0], []).append(date)
|
||||
for major, dates in sorted(majors.items()):
|
||||
x0, x1 = xof(min(dates)), xof(max(dates))
|
||||
y = axis + 42
|
||||
s.append('<path class="nj-int-axis" fill="none" d="M%.1f %d V%d H%.1f V%d"/>' % (x0, y - 5, y, x1, y - 5))
|
||||
s.append('<text class="nj-int-major" x="%.1f" y="%d" text-anchor="%s">%d.x</text>'
|
||||
% ((x0 + x1) / 2 if x1 - x0 > 30 else x0, y + 17, "middle" if x1 - x0 > 30 else "start", major))
|
||||
# leaders and labels
|
||||
for x, row, text in labels:
|
||||
y = top - row * LABEL_ROW
|
||||
s.append('<line class="nj-int-leader" x1="%.1f" y1="%d" x2="%.1f" y2="%d"/>' % (x, y + 3, x, axis - 7))
|
||||
s.append('<text class="nj-int-label" x="%.1f" y="%d">%s</text>' % (x, y, text))
|
||||
# markers: the first commit, then one per date, patch rings first so that minor dots stay on top
|
||||
x = xof(FIRST_COMMIT)
|
||||
s.append('<g><title>First commit %s</title><circle class="nj-int-hit" cx="%.1f" cy="%d" r="9"/>'
|
||||
'<path class="nj-int-minor" d="M%.1f %d l7 -7 l7 7 l-7 7z" transform="translate(-7 0)"/></g>'
|
||||
% (FIRST_COMMIT.isoformat(), x, axis, x, axis))
|
||||
by_date = {}
|
||||
for version, date in releases:
|
||||
by_date.setdefault(date, []).append(version)
|
||||
groups = sorted(by_date.items(), key=lambda g: any(v[2] == 0 for v in g[1]))
|
||||
for date, versions in groups:
|
||||
minor = any(v[2] == 0 for v in versions)
|
||||
x = xof(date)
|
||||
tip = ", ".join(name(v) for v in sorted(versions)) + " released " + date.isoformat()
|
||||
s.append('<g><title>%s</title><circle class="nj-int-hit" cx="%.1f" cy="%d" r="9"/>'
|
||||
'<circle class="%s" cx="%.1f" cy="%d" r="%g"/></g>'
|
||||
% (tip, x, axis, "nj-int-minor" if minor else "nj-int-patch", x, axis, 6 if minor else 3.5))
|
||||
s.append('</svg>')
|
||||
return "\n".join(s) + "\n"
|
||||
|
||||
|
||||
releases = parse(PAGE.read_text(encoding="utf-8"))
|
||||
(OUT / "release_timeline.svg").write_text(draw(releases), encoding="utf-8")
|
||||
print("wrote", OUT / "release_timeline.svg", "with", len(releases), "releases")
|
||||
@@ -0,0 +1 @@
|
||||
fonts/
|
||||
@@ -0,0 +1,73 @@
|
||||
# Rebuild the animated feature slideshow of the home page (docs/images/json-light.svg and json-dark.svg) when
|
||||
# slides.md, build.py or the fonts change. Needs uv (build.py declares its dependencies inline, PEP 723), curl,
|
||||
# unzip and shasum. Run it from docs/mkdocs/scripts/slideshow.
|
||||
#
|
||||
# make download missing fonts and build both SVGs
|
||||
# make fonts download only the fonts
|
||||
# make SECONDS=12 change the time per slide
|
||||
# make clean remove the generated SVGs
|
||||
# make distclean also remove the downloaded fonts
|
||||
|
||||
SECONDS ?= 15
|
||||
|
||||
IMAGES := ../../docs/images
|
||||
LIGHT := $(IMAGES)/json-light.svg
|
||||
DARK := $(IMAGES)/json-dark.svg
|
||||
|
||||
RUN := uv run --script build.py
|
||||
FONTS := fonts/Inter.ttf fonts/JetBrainsMono.ttf
|
||||
DEPS := build.py slides.md $(FONTS)
|
||||
|
||||
# Pinned sources; the checksums make a changed upstream file fail instead of silently changing the output.
|
||||
# Inter: Google Fonts repository at a fixed commit (variable font with opsz and wght axes).
|
||||
INTER_URL := https://raw.githubusercontent.com/google/fonts/0b58fb370093f9a9f4ff785d94405710b79de67c/ofl/inter/Inter%5Bopsz%2Cwght%5D.ttf
|
||||
# JetBrains Mono: release 2.304 (the variable font in fonts/variable/ of the zip).
|
||||
JBM_URL := https://github.com/JetBrains/JetBrainsMono/releases/download/v2.304/JetBrainsMono-2.304.zip
|
||||
JBM_ZIP := fonts/JetBrainsMono-2.304.zip
|
||||
|
||||
SHA256_Inter.ttf := 29160a80ff49ddcab2c97711247e08b1fab27a484a329ce8b813d820dc559031
|
||||
SHA256_JetBrainsMono.ttf := 662a196d58f1183bf2d77428b6d5283fe3f45161ab021bea4036bc98e5cac016
|
||||
SHA256_JetBrainsMono-2.304.zip := 6f6376c6ed2960ea8a963cd7387ec9d76e3f629125bc33d1fdcd7eb7012f7bbf
|
||||
|
||||
# Check $@.tmp against its pinned checksum, then move it into place.
|
||||
define verify
|
||||
echo '$(SHA256_$(notdir $@)) $@.tmp' | shasum -a 256 -c --quiet - || { rm -f $@.tmp; exit 1; }
|
||||
mv $@.tmp $@
|
||||
endef
|
||||
|
||||
.PHONY: all fonts clean distclean
|
||||
.INTERMEDIATE: $(JBM_ZIP)
|
||||
|
||||
all: $(LIGHT) $(DARK)
|
||||
|
||||
fonts: $(FONTS)
|
||||
|
||||
# fixed colours for the docs: an <img> cannot see Material's theme toggle, so the home page includes both
|
||||
# and shows the one that matches
|
||||
$(LIGHT): $(DEPS)
|
||||
$(RUN) $@.tmp $(SECONDS) light
|
||||
mv $@.tmp $@
|
||||
|
||||
$(DARK): $(DEPS)
|
||||
$(RUN) $@.tmp $(SECONDS) dark
|
||||
mv $@.tmp $@
|
||||
|
||||
fonts/Inter.ttf:
|
||||
mkdir -p fonts
|
||||
curl -fsSL -m 300 -o $@.tmp '$(INTER_URL)'
|
||||
$(verify)
|
||||
|
||||
$(JBM_ZIP):
|
||||
mkdir -p fonts
|
||||
curl -fsSL -m 300 -o $@.tmp '$(JBM_URL)'
|
||||
$(verify)
|
||||
|
||||
fonts/JetBrainsMono.ttf: | $(JBM_ZIP)
|
||||
unzip -p $(JBM_ZIP) 'fonts/variable/JetBrainsMono[[]wght].ttf' > $@.tmp
|
||||
$(verify)
|
||||
|
||||
clean:
|
||||
rm -f $(LIGHT) $(DARK) $(IMAGES)/*.svg.tmp
|
||||
|
||||
distclean: clean
|
||||
rm -f $(FONTS) fonts/*.tmp $(JBM_ZIP)
|
||||
@@ -0,0 +1,27 @@
|
||||
# Feature slideshow of the home page
|
||||
|
||||
`make` builds `docs/images/json-light.svg` and `json-dark.svg`, the animated feature tour on the home page. They replace
|
||||
`json.gif` there. All text is outlined, so the SVGs need no fonts. Material's theme toggle cannot be seen from an
|
||||
`<img>`, so the home page includes both files (`#only-light`, `#only-dark`) and Material shows the one that matches.
|
||||
|
||||
The slides are in [`slides.md`](slides.md): one `# Title` per slide (`{badge: TEXT}` at the end adds a red chip), `- `
|
||||
bullets, and one fenced code block. `build.py` adds the title and closing slides around them, so the slide count follows
|
||||
the file. Every snippet should compile against the current `develop`. Use a non-breaking space to keep words together
|
||||
("16 bytes").
|
||||
|
||||
```bash
|
||||
make # download missing fonts, build both SVGs
|
||||
make SECONDS=12
|
||||
make clean # remove the SVGs; make distclean also removes the fonts
|
||||
```
|
||||
|
||||
Run it from this directory. It needs [uv](https://docs.astral.sh/uv/), plus curl, unzip and shasum for the fonts:
|
||||
`build.py` declares its dependencies inline (PEP 723, pinned, so the output stays byte-identical) and `uv run --script`
|
||||
provides them. `fonts/` is git-ignored: Inter (variable, Google Fonts repository) and JetBrains Mono 2.304 (variable)
|
||||
are downloaded from pinned URLs and checked against their SHA-256; a changed upstream file stops the build.
|
||||
`./build.py OUT.svg [seconds] [auto|light|dark]` works directly too. A mistake in `slides.md` stops the build with
|
||||
`slides.md:LINE` and leaves the old SVGs in place.
|
||||
|
||||
Layout: title 48 px, bullets 24 px, code 19 px; the build warns when a slide's code would drop below 18 px. Syntax colours
|
||||
are the `code-*` tokens of the brand (also used by `css/brand.css`). A CSS animation dissolves each slide over the
|
||||
previous one and fills the progress bar; with reduced motion only the first slide shows.
|
||||
Executable
+461
@@ -0,0 +1,461 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.9"
|
||||
# dependencies = ["fonttools==4.67.0", "uharfbuzz==0.57.0"]
|
||||
# ///
|
||||
"""Animated SVG slideshow prototype for nlohmann/json, all text outlined (harfbuzz shaping, glyphs as <path> + <use>).
|
||||
|
||||
Usage: uv run --script build.py OUT.svg [seconds-per-slide] [auto|light|dark]
|
||||
|
||||
auto (the default) follows prefers-color-scheme. light and dark fix the colours, for pages with their own theme
|
||||
toggle (MkDocs Material), which an <img> cannot see.
|
||||
|
||||
The pinned versions keep the output byte-identical; a different harfbuzz may shape slightly differently.
|
||||
"""
|
||||
import re, sys, os
|
||||
import uharfbuzz as hb
|
||||
from fontTools.pens.svgPathPen import SVGPathPen
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
FONTS = os.path.join(HERE, "fonts")
|
||||
W, H = 1200, 675
|
||||
TOTAL = 0 # number of slides, set once slides.md is read (for the "02 / 20" labels and the progress segments)
|
||||
|
||||
# ---------------------------------------------------------------- glyphs
|
||||
|
||||
class Face:
|
||||
def __init__(self, file, key, feats=None):
|
||||
blob = hb.Blob.from_file_path(os.path.join(FONTS, file))
|
||||
self.face = hb.Face(blob)
|
||||
self.upem = self.face.upem
|
||||
self.key = key
|
||||
self.feats = feats or {}
|
||||
|
||||
def font(self, var):
|
||||
f = hb.Font(self.face)
|
||||
f.set_variations(var)
|
||||
return f
|
||||
|
||||
|
||||
INTER = Face("Inter.ttf", "i", {"cv11": True, "ss03": True, "kern": True})
|
||||
MONO = Face("JetBrainsMono.ttf", "m", {"calt": False, "liga": False})
|
||||
|
||||
GLYPHS = {} # (face, var, gid) -> id
|
||||
DEFS = []
|
||||
|
||||
|
||||
def gid_ref(face, font, var, gid):
|
||||
k = (face.key, tuple(sorted(var.items())), gid)
|
||||
if k not in GLYPHS:
|
||||
pen = SVGPathPen(None, ntos=lambda v: str(round(v)))
|
||||
font.draw_glyph_with_pen(gid, pen)
|
||||
d = pen.getCommands()
|
||||
gidname = f"g{len(GLYPHS):x}"
|
||||
GLYPHS[k] = gidname if d else None
|
||||
if d:
|
||||
DEFS.append(f'<path id="{gidname}" d="{d}"/>')
|
||||
return GLYPHS[k]
|
||||
|
||||
|
||||
def shape(face, text, wght, size, track=0.0):
|
||||
if not text:
|
||||
return [], 0, face.upem
|
||||
var = {"wght": wght}
|
||||
if face is INTER:
|
||||
var["opsz"] = max(14, min(32, size))
|
||||
font = face.font(var)
|
||||
buf = hb.Buffer()
|
||||
buf.add_str(text)
|
||||
buf.guess_segment_properties()
|
||||
hb.shape(font, buf, face.feats)
|
||||
out, x = [], 0
|
||||
tr = track * face.upem
|
||||
for info, pos in zip(buf.glyph_infos, buf.glyph_positions):
|
||||
out.append((gid_ref(face, font, var, info.codepoint), x + pos.x_offset, pos.y_offset))
|
||||
x += pos.x_advance + tr
|
||||
return out, x - tr, face.upem
|
||||
|
||||
|
||||
def width(text, face=INTER, wght=400, size=16, track=0.0):
|
||||
_, w, upem = shape(face, text, wght, size, track)
|
||||
return w * size / upem
|
||||
|
||||
|
||||
def run(text, x, y, size, wght=400, cls="tx", face=INTER, track=0.0):
|
||||
"""One line of text, baseline at y. Returns (svg, width_px)."""
|
||||
glyphs, w, upem = shape(face, text, wght, size, track)
|
||||
s = size / upem
|
||||
uses = "".join(f'<use href="#{g}" x="{gx}"' + (f' y="{-gy}"' if gy else "") + "/>"
|
||||
for g, gx, gy in glyphs if g)
|
||||
return (f'<g class="{cls}" transform="translate({x:g} {y:g}) scale({s:.6g} {-s:.6g})">{uses}</g>', w * s)
|
||||
|
||||
|
||||
def runs(parts, x, y, size, face=INTER, track=0.0):
|
||||
"""Several (text, wght, cls) pieces on one line."""
|
||||
out = []
|
||||
for text, wght, cls in parts:
|
||||
svg, w = run(text, x, y, size, wght, cls, face, track)
|
||||
out.append(svg)
|
||||
x += w + track * size
|
||||
return "".join(out), x
|
||||
|
||||
|
||||
def wrap(text, maxw, size, wght=400):
|
||||
lines = _wrap(text, maxw, size, wght)
|
||||
# no one-word widow: pull the last word of the previous line down if it still fits
|
||||
if len(lines) > 1 and " " not in lines[-1] and " " in lines[-2]:
|
||||
head, last = lines[-2].rsplit(" ", 1)
|
||||
if width(last + " " + lines[-1], size=size, wght=wght) <= maxw:
|
||||
lines[-2:] = [head, last + " " + lines[-1]]
|
||||
return lines
|
||||
|
||||
|
||||
def _wrap(text, maxw, size, wght=400):
|
||||
lines, cur = [], ""
|
||||
for word in text.split(" "):
|
||||
t = (cur + " " + word).strip()
|
||||
if cur and width(t, size=size, wght=wght) > maxw:
|
||||
lines.append(cur)
|
||||
cur = word
|
||||
else:
|
||||
cur = t
|
||||
return lines + [cur]
|
||||
|
||||
# ---------------------------------------------------------------- pieces
|
||||
|
||||
|
||||
def wordmark(x, y, size, on="page"):
|
||||
"""The wordmark with hand kerning around the slash (see the logo in the brand kit). Below 32 px it switches to the
|
||||
small optical master (nlohmann 250 / slash 450), because Thin falls apart at footer size."""
|
||||
a, sl = ("wm-a", "wm-s") if on == "page" else ("on-a", "on-s")
|
||||
small = size < 32
|
||||
w = (250, 450, 800) if small else (100, 400, 800)
|
||||
k = (0.004, 0.024) if small else (-0.006, 0.012)
|
||||
out = []
|
||||
for i, (t, wg, c) in enumerate([("nlohmann", w[0], a), ("/", w[1], sl), ("json", w[2], a)]):
|
||||
svg, wd = run(t, x, y, size, wg, c, track=-0.02)
|
||||
out.append(svg)
|
||||
x += wd - 0.02 * size + (k[i] * size if i < 2 else 0)
|
||||
return "".join(out), x
|
||||
|
||||
|
||||
KW = {"using", "auto", "return", "const", "struct", "for", "override"}
|
||||
TYPES = {"int", "void", "bool", "double", "char", "float"}
|
||||
CONSTANTS = {"true", "false", "nullptr"}
|
||||
TOKEN = re.compile(r'(//.*$)|(R"\(.*?\)"|R"\(.*$|"(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)\')|(<[\w/.]+>(?=\s*$))|(#\w+)'
|
||||
r'|(\b\d+(?:\.\d+)?\b)|(\b[A-Za-z_]\w*\b)|(\s+)|(.)')
|
||||
|
||||
|
||||
def code_block(lines, x, y, size, lh):
|
||||
"""Lines of C++; a raw string literal may span lines."""
|
||||
out, in_raw = [], False
|
||||
for line in lines:
|
||||
if line:
|
||||
svg, in_raw = code_line(line, x, y, size, in_raw)
|
||||
out.append(svg)
|
||||
y += lh
|
||||
else:
|
||||
y += lh * BLANK
|
||||
return "".join(out)
|
||||
|
||||
|
||||
BLANK = 0.55 # an empty code line is a paragraph break, not a full line
|
||||
|
||||
|
||||
def code_height(lines, size, lh):
|
||||
n_full = sum(1 for l in lines if l)
|
||||
n_blank = len(lines) - n_full
|
||||
return lh * (n_full - 1) + lh * BLANK * n_blank + size
|
||||
|
||||
|
||||
def code_line(line, x, y, size, in_raw=False):
|
||||
if line.startswith("$ "):
|
||||
return runs([("$ ", 400, "tx3"), (line[2:], 400, "tx")], x, y, size, MONO)[0], False
|
||||
out, cx = [], x
|
||||
if in_raw:
|
||||
end = line.find(')"')
|
||||
head = line if end < 0 else line[:end + 2]
|
||||
svg, w = run(head, cx, y, size, 400, "str", MONO)
|
||||
out.append(svg)
|
||||
if end < 0:
|
||||
return "".join(out), True
|
||||
cx += w
|
||||
line = line[end + 2:]
|
||||
in_raw = False
|
||||
for m in TOKEN.finditer(line):
|
||||
com, st, inc, pp, num, ident, ws, other = m.groups()
|
||||
t = m.group(0)
|
||||
if st and st.startswith('R"(') and not st.endswith(')"'):
|
||||
in_raw = True
|
||||
face, wght, cls = MONO, 400, "tx"
|
||||
if com:
|
||||
cls = "com" # upright: JetBrains Mono Italic's cursive f reads as a florin at README size
|
||||
elif st or inc:
|
||||
cls = "str"
|
||||
elif pp:
|
||||
cls = "pp"
|
||||
elif ident in KW:
|
||||
wght, cls = 600, "kw"
|
||||
elif ident in TYPES:
|
||||
cls = "ty"
|
||||
elif num or ident in CONSTANTS:
|
||||
cls = "num"
|
||||
elif ident and line[m.end():m.end() + 1] == "(":
|
||||
cls = "fn" # a call or definition: name directly followed by "("
|
||||
svg, w = run(t, cx, y, size, wght, cls, face)
|
||||
if not ws:
|
||||
out.append(svg)
|
||||
cx += w
|
||||
return "".join(out), in_raw
|
||||
|
||||
|
||||
SEG, SEG_GAP = 20, 5
|
||||
|
||||
|
||||
def progress(idx, n_proto, y):
|
||||
"""TOTAL segments at the bottom right; earlier slides full, current one fills over the slide's time."""
|
||||
seg, gap = SEG, SEG_GAP
|
||||
x0 = W - 64 - TOTAL * seg - (TOTAL - 1) * gap
|
||||
out = []
|
||||
for k in range(TOTAL):
|
||||
x = x0 + k * (seg + gap)
|
||||
out.append(f'<rect class="seg" x="{x}" y="{y}" width="{seg}" height="3" rx="1.5"/>')
|
||||
if k < idx:
|
||||
out.append(f'<rect class="seg-on" x="{x}" y="{y}" width="{seg}" height="3" rx="1.5"/>')
|
||||
elif k == idx:
|
||||
out.append(f'<rect class="seg-on fill" x="{x}" y="{y}" width="{seg}" height="3" rx="1.5"/>')
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def pattern_def():
|
||||
tile = [("{", 14, 34), ("]", 92, 22), (":", 136, 60), ("[", 52, 82), (",", 118, 112),
|
||||
("}", 20, 140), (":", 78, 150), ("{", 140, 158)]
|
||||
glyphs = "".join(run(c, x, y, 26, 600, "pat", MONO)[0] for c, x, y in tile)
|
||||
return f'<pattern id="pat" width="160" height="160" patternUnits="userSpaceOnUse">{glyphs}</pattern>'
|
||||
|
||||
# ---------------------------------------------------------------- slides
|
||||
|
||||
|
||||
CODE_SIZES = []
|
||||
SLOGAN = "What if JSON was part of modern C++?"
|
||||
|
||||
|
||||
def band_bg():
|
||||
"""Hero gradient plus the syntax pattern, faded out towards the text side (left)."""
|
||||
return ('<rect width="1200" height="675" fill="url(#hero)"/>'
|
||||
'<rect width="1200" height="675" fill="url(#pat)" mask="url(#patfade)"/>')
|
||||
|
||||
|
||||
def footer_band(idx, n, left=None):
|
||||
out = []
|
||||
if left:
|
||||
out.append(run(left, 64, FOOT_Y, 20, 600, "on-tx")[0])
|
||||
out.append(progress(idx, n, FOOT_Y - 7.5).replace('class="seg"', 'class="seg on-band"').replace('class="seg-on', 'class="seg-on on-band'))
|
||||
return "".join(out)
|
||||
|
||||
|
||||
FOOT_Y = 640 # one footer baseline on every slide: wordmark or URL left, progress right
|
||||
|
||||
|
||||
def slide_title(idx, n):
|
||||
out = [band_bg()]
|
||||
x = 96
|
||||
label = "VERSION 3.13.0"
|
||||
lw = width(label, MONO, 500, 15, 0.04)
|
||||
out.append(f'<rect class="chip" x="{x}" y="186" width="{lw + 28:.1f}" height="34" rx="4"/>')
|
||||
out.append(run(label, x + 14, 208.5, 15, 500, "on-tx", MONO, 0.04)[0])
|
||||
out.append(wordmark(x - 6, 336, 112, on="band")[0])
|
||||
out.append(runs([("// ", 400, "w70"), (SLOGAN, 400, "on-s")], x, 404, 26, MONO)[0])
|
||||
out.append(footer_band(idx, n, "github.com/nlohmann/json"))
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def slide_feature(idx, n, title, bullets, code, badge=None):
|
||||
out = ['<rect class="bg" width="1200" height="675"/>']
|
||||
x, top, bottom = 64, 136, 596
|
||||
# title row: 48/800, -3 %
|
||||
svg, tw = run(title, x - 2, 98, 48, 800, "tx", track=-0.03)
|
||||
out.append(svg)
|
||||
if badge:
|
||||
cx = x + tw + 16
|
||||
cw = width(badge, MONO, 600, 13, 0.06) + 18
|
||||
out.append(f'<rect class="red" x="{cx:.1f}" y="68.5" width="{cw:.1f}" height="26" rx="4"/>')
|
||||
out.append(run(badge, cx + 9, 86.2, 13, 600, "on-tx", MONO, 0.06)[0])
|
||||
# code panel: fixed left edge, fixed width, fixed top on every slide; only its height follows the code
|
||||
pad, lf = 28, 1.45
|
||||
px, pw = 488, 1136 - 488
|
||||
em = max(width(l, MONO, 400, 1) for l in code)
|
||||
cs = 19.0
|
||||
while cs > 14 and (em * cs > pw - 2 * pad or code_height(code, cs, lf * cs) > bottom - top - 2 * pad):
|
||||
cs -= 0.5
|
||||
CODE_SIZES.append((title, cs))
|
||||
if cs < 18:
|
||||
print(f" warning: {title!r} code at {cs} px ({em * 19:.0f} px wide at 19 px)", file=sys.stderr)
|
||||
lh = lf * cs
|
||||
ph = max(2 * pad + code_height(code, cs, lh), 240)
|
||||
out.append(f'<rect class="panel" x="{px}" y="{top}" width="{pw}" height="{ph:.0f}" rx="6"/>')
|
||||
first = top + pad + cs * 0.78
|
||||
out.append(code_block(code, px + pad, first, cs, lh))
|
||||
# bullets: 24/400 on 34, first baseline level with the first code baseline
|
||||
colw = px - x - 40
|
||||
by = first
|
||||
for b in bullets:
|
||||
out.append(f'<rect class="blk" x="{x}" y="{by - 10.6:.1f}" width="8" height="8" rx="1.5"/>')
|
||||
for line in wrap(b, colw - 24, 24):
|
||||
out.append(run(line, x + 24, by, 24, 400, "tx2")[0])
|
||||
by += 34
|
||||
by += 16
|
||||
# footer: small-master wordmark left, progress right, no rule, no numeral
|
||||
out.append(wordmark(64, FOOT_Y, 22)[0])
|
||||
out.append(progress(idx, n, FOOT_Y - 7.5))
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def slide_closing(idx, n):
|
||||
out = [band_bg()]
|
||||
x = 96
|
||||
out.append(wordmark(x - 4, 146, 80, on="band")[0])
|
||||
out.append(runs([("// ", 400, "w70"), (SLOGAN, 400, "on-s")], x, 200, 22, MONO)[0])
|
||||
info = [
|
||||
("{", None),
|
||||
(' "copyright": ', "(c) 2013-2026 Niels Lohmann"),
|
||||
(' "license": ', "MIT"),
|
||||
(' "docs": ', "json.nlohmann.me"),
|
||||
(' "github": ', "github.com/nlohmann/json"),
|
||||
(' "sponsor": ', "github.com/sponsors/nlohmann"),
|
||||
(' "mail": ', "mail@nlohmann.me"),
|
||||
("}", None),
|
||||
]
|
||||
cs, lh, pad = 21, 33, 28
|
||||
py = 244
|
||||
pw = max(width(k + '"' + (v or "") + '",', MONO, 400, cs) for k, v in info) + 2 * pad
|
||||
out.append(f'<rect class="on-panel" x="{x}" y="{py}" width="{pw:.0f}" height="{2 * pad + lh * (len(info) - 1) + cs:.0f}" rx="6"/>')
|
||||
y = py + pad + cs * 0.8
|
||||
for k, (key, val) in enumerate(info):
|
||||
if val is None:
|
||||
out.append(run(key, x + pad, y, cs, 400, "on-tx", MONO)[0])
|
||||
else:
|
||||
comma = "," if k < len(info) - 2 else ""
|
||||
out.append(runs([(key, 400, "on-tx"), (f'"{val}"', 400, "on-s"), (comma, 400, "on-tx")], x + pad, y, cs, MONO)[0])
|
||||
y += lh
|
||||
out.append(footer_band(idx, n))
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def load_slides(path):
|
||||
"""Feature slides from Markdown: '# Title {badge: TEXT}', '- bullet' lines, one fenced code block."""
|
||||
text = re.sub(r"<!--.*?-->", "", open(path).read(), flags=re.S)
|
||||
slides, cur, in_code = [], None, False
|
||||
for no, line in enumerate(text.split("\n"), 1):
|
||||
if in_code:
|
||||
if line.startswith("```"):
|
||||
in_code = False
|
||||
else:
|
||||
cur["code"].append(line)
|
||||
elif line.startswith("```"):
|
||||
in_code = True
|
||||
elif line.startswith("# "):
|
||||
title, badge = line[2:].strip(), None
|
||||
m = re.search(r"\s*\{badge:\s*(.*?)\}$", title)
|
||||
if m:
|
||||
title, badge = title[:m.start()], m.group(1)
|
||||
cur = {"title": title, "bullets": [], "code": [], "badge": badge}
|
||||
slides.append(cur)
|
||||
elif line.startswith("- ") and cur:
|
||||
cur["bullets"].append(line[2:].strip())
|
||||
elif line[:1] in " \t" and line.strip() and cur and cur["bullets"]:
|
||||
cur["bullets"][-1] += " " + line.strip()
|
||||
elif line.strip():
|
||||
raise SystemExit(f"{path}:{no}: unexpected line: {line!r}")
|
||||
for sl in slides:
|
||||
while sl["code"] and not sl["code"][-1].strip():
|
||||
sl["code"].pop()
|
||||
if not sl["code"]:
|
||||
raise SystemExit(f"{path}: slide {sl['title']!r} has no code block")
|
||||
return slides
|
||||
|
||||
|
||||
FEATURES = load_slides(os.path.join(HERE, "slides.md"))
|
||||
|
||||
|
||||
def _feature(f):
|
||||
return lambda i, n: slide_feature(i, n, f["title"], f["bullets"], f["code"], f["badge"])
|
||||
|
||||
|
||||
SLIDES = [slide_title] + [_feature(f) for f in FEATURES] + [slide_closing]
|
||||
TOTAL = len(SLIDES)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- assemble
|
||||
|
||||
|
||||
def build(sec):
|
||||
n = len(SLIDES)
|
||||
T = n * sec
|
||||
fade = 0.6
|
||||
a, b, c = fade / T * 100, sec / T * 100, (sec + fade) / T * 100
|
||||
def cls(i):
|
||||
return "s s0" if i == 0 else "s sl" if i == n - 1 else "s"
|
||||
bodies = [f'<g class="{cls(i)}" style="animation-delay:{i * sec:g}s">{fn(i, n)}</g>' for i, fn in enumerate(SLIDES)]
|
||||
css = f"""
|
||||
.bg{{fill:#fff}} .tx{{fill:#0f1419}} .tx2{{fill:#3d444f}} .tx3{{fill:#8a919c}} .com{{fill:#646d7a}}
|
||||
.bl{{fill:#1747a6}} .blk{{fill:#1747a6}} .kw{{fill:#1747a6}} .ty{{fill:#0d6e72}} .fn{{fill:#6b3fb8}} .str{{fill:#23793a}} .num{{fill:#b04a07}} .pp{{fill:#a3247a}}
|
||||
.panel{{fill:#f5f7fa;stroke:#e3e6eb;stroke-width:1}} .rule{{stroke:#e3e6eb;stroke-width:1}}
|
||||
.wm-a{{fill:#0f1419}} .wm-s{{fill:#1747a6}}
|
||||
.on-a,.on-tx{{fill:#fff}} .red{{fill:#d93a3f}} .on-panel{{fill:#fff;fill-opacity:.08;stroke:#fff;stroke-opacity:.16}} .on-s{{fill:#bcd3fb}} .w70{{fill:#fff;fill-opacity:.7}} .on-tx2{{fill:#dbe6fb}} .chip{{fill:#fff;fill-opacity:.14}}
|
||||
.pat{{fill:#fff;fill-opacity:.09}}
|
||||
.h1{{stop-color:#1d55c4}} .h2{{stop-color:#123a8a}}
|
||||
.seg{{fill:#e3e6eb}} .seg-on{{fill:#1747a6}}
|
||||
.seg.on-band{{fill:#fff;fill-opacity:.25}} .seg-on.on-band{{fill:#fff;fill-opacity:1}}
|
||||
@media (prefers-color-scheme:dark){{
|
||||
.bg{{fill:#0f1218}} .tx,.wm-a{{fill:#f2f4f7}} .tx2{{fill:#c3c8d1}} .tx3{{fill:#7b828e}} .com{{fill:#8b929e}}
|
||||
.bl,.blk,.wm-s,.seg-on,.kw{{fill:#7aa7f5}} .ty{{fill:#4fd1c5}} .fn{{fill:#c4a3ff}} .str{{fill:#7ed491}} .num{{fill:#f5a65b}} .pp{{fill:#f48fc8}}
|
||||
.panel{{fill:#171b23;stroke:#272c36}} .rule{{stroke:#272c36}} .seg{{fill:#272c36}}
|
||||
.red{{fill:#c8383d}} .h1{{stop-color:#163f93}} .h2{{stop-color:#0c2556}}
|
||||
}}
|
||||
.s{{opacity:0;animation:show {T:g}s linear infinite backwards}}
|
||||
.fill{{transform-box:fill-box;transform-origin:left;animation:fill {T:g}s linear infinite backwards;animation-delay:inherit}}
|
||||
.s0{{animation-name:show0}} .sl{{animation-name:showl}}
|
||||
@keyframes show{{0%{{opacity:0;animation-timing-function:cubic-bezier(.2,.6,.2,1)}} {a:.4f}%{{opacity:1}} {c:.4f}%{{opacity:1}} {c + .01:.4f}%{{opacity:0}} 100%{{opacity:0}}}}
|
||||
@keyframes show0{{0%{{opacity:1}} {c:.4f}%{{opacity:1}} {c + .01:.4f}%{{opacity:0}} 100%{{opacity:0}}}}
|
||||
@keyframes showl{{0%{{opacity:0;animation-timing-function:cubic-bezier(.2,.6,.2,1)}} {a:.4f}%{{opacity:1}} {b:.4f}%{{opacity:1}} {c:.4f}%{{opacity:0}} 100%{{opacity:0}}}}
|
||||
@keyframes fill{{0%{{transform:scaleX(0)}} {b:.4f}%{{transform:scaleX(1)}} 100%{{transform:scaleX(1)}}}}
|
||||
@media (prefers-reduced-motion:reduce){{.s,.fill{{animation:none}} .s:first-of-type{{opacity:1}}}}
|
||||
"""
|
||||
css = re.sub(r"\n\s*", "", css)
|
||||
pat = pattern_def()
|
||||
return (f'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 {W} {H}" width="{W}" height="{H}" role="img">'
|
||||
f'<title>JSON for Modern C++: feature slideshow (animated, {n} slides, {sec:g} s each)</title>'
|
||||
f'<style>{css}</style>'
|
||||
f'<defs>{"".join(DEFS)}{pat}'
|
||||
f'<linearGradient id="hero" gradientTransform="rotate(70 .5 .5)"><stop class="h1" offset="0"/><stop class="h2" offset="1"/></linearGradient>'
|
||||
f'<linearGradient id="fadeg" x1="0" x2="1" y1="0" y2="0"><stop offset=".25" stop-color="#fff" stop-opacity="0"/><stop offset=".85" stop-color="#fff" stop-opacity="1"/></linearGradient>'
|
||||
f'<mask id="patfade" maskUnits="userSpaceOnUse" x="0" y="0" width="{W}" height="{H}"><rect width="{W}" height="{H}" fill="url(#fadeg)"/></mask></defs>'
|
||||
f'<rect class="bg" width="{W}" height="{H}"/>'
|
||||
f'{"".join(bodies)}</svg>')
|
||||
|
||||
|
||||
def fix_scheme(svg, scheme):
|
||||
"""Drop the dark-mode block (light) or apply it unconditionally (dark)."""
|
||||
if scheme == "auto":
|
||||
return svg
|
||||
start = svg.index("@media (prefers-color-scheme:dark){")
|
||||
i, depth = svg.index("{", start), 0
|
||||
for j in range(i, len(svg)):
|
||||
depth += {"{": 1, "}": -1}.get(svg[j], 0)
|
||||
if depth == 0:
|
||||
break
|
||||
inner = svg[i + 1:j] if scheme == "dark" else ""
|
||||
return svg[:start] + inner + svg[j + 1:]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
out = sys.argv[1]
|
||||
sec = float(sys.argv[2]) if len(sys.argv) > 2 else 15
|
||||
scheme = sys.argv[3] if len(sys.argv) > 3 else "auto"
|
||||
if scheme not in ("auto", "light", "dark"):
|
||||
sys.exit(f"unknown colour scheme {scheme!r}: use auto, light or dark")
|
||||
svg = fix_scheme(build(sec), scheme)
|
||||
# defs are appended while slides render, so build the slides first (done inside build via f-string order)
|
||||
open(out, "w").write(svg)
|
||||
print(out, len(svg.encode()) // 1024, "KB,", len(GLYPHS), "glyphs; code px:", " ".join(f"{c:g}" for _, c in CODE_SIZES))
|
||||
@@ -0,0 +1,412 @@
|
||||
<!--
|
||||
Feature slides for the animated slideshow. build.py adds the title slide before them and the closing slide after them.
|
||||
|
||||
# Title one slide per heading; `{badge: TEXT}` at the end adds a red chip
|
||||
- bullet bullets; a long bullet may continue on indented lines
|
||||
```cpp ... ``` the code shown on the right (a line starting with `$ ` is a shell command)
|
||||
-->
|
||||
|
||||
# Trivial integration
|
||||
|
||||
- single header file
|
||||
- no build system needed
|
||||
- vanilla C++11
|
||||
- works with GCC 4.8+, Clang 3.4+ and MSVC 2015+
|
||||
|
||||
```cpp
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
// for convenience
|
||||
using json = nlohmann::json;
|
||||
|
||||
int main()
|
||||
{
|
||||
// a JSON pretty-printer in 3 lines
|
||||
json j;
|
||||
std::cin >> j;
|
||||
std::cout << std::setw(4) << j << std::endl;
|
||||
}
|
||||
|
||||
$ g++ pretty.cpp -std=c++11 -o pretty
|
||||
```
|
||||
|
||||
# First-class datatype
|
||||
|
||||
- write JSON like any other C++ literal
|
||||
- initializer lists become arrays and objects
|
||||
|
||||
```cpp
|
||||
json j = {
|
||||
{"pi", 3.141},
|
||||
{"happy", true},
|
||||
{"name", "Niels"},
|
||||
{"nothing", nullptr},
|
||||
{"answer", {
|
||||
{"everything", 42}
|
||||
}},
|
||||
{"list", {1, 0, 2}},
|
||||
{"object", {
|
||||
{"currency", "USD"},
|
||||
{"value", 42.99}
|
||||
}}
|
||||
};
|
||||
```
|
||||
|
||||
# Array construction
|
||||
|
||||
- arrays work the way you expect
|
||||
- adding to a null value makes it an array
|
||||
|
||||
```cpp
|
||||
// create null value
|
||||
json j;
|
||||
|
||||
// add via member function
|
||||
j.push_back("Hello");
|
||||
j.emplace_back("World");
|
||||
|
||||
// add via += operator
|
||||
j += false;
|
||||
|
||||
// add via index
|
||||
j[2] = 42.3;
|
||||
|
||||
// gaps are filled with null
|
||||
j[4] = "World";
|
||||
```
|
||||
|
||||
# Object construction
|
||||
|
||||
- objects work the way you expect
|
||||
- adding to a null value makes it an object
|
||||
|
||||
```cpp
|
||||
// create null value
|
||||
json j;
|
||||
|
||||
// add via member function
|
||||
j.emplace("key2", "value");
|
||||
j.push_back({"key", "value"});
|
||||
|
||||
// add via += operator
|
||||
j += {"another key", false};
|
||||
|
||||
// add via key
|
||||
j["pi"] = 3.1415926;
|
||||
j["answer"]["everything"] = 42;
|
||||
```
|
||||
|
||||
# Container operations
|
||||
|
||||
- use JSON like an STL container
|
||||
- satisfies the ReversibleContainer requirements
|
||||
|
||||
```cpp
|
||||
// container empty?
|
||||
j.empty();
|
||||
|
||||
// current number of elements
|
||||
j.size();
|
||||
|
||||
// maximal number of elements
|
||||
j.max_size();
|
||||
|
||||
// swap elements
|
||||
j.swap(j2);
|
||||
std::swap(j, j2);
|
||||
|
||||
// clear a container
|
||||
j.clear();
|
||||
```
|
||||
|
||||
# STL containers
|
||||
|
||||
- switch between STL and JSON containers
|
||||
- conversion works in both directions
|
||||
|
||||
```cpp
|
||||
// some STL containers
|
||||
std::vector<int> v;
|
||||
std::unordered_map<std::string, bool> m;
|
||||
|
||||
// automatic transformation
|
||||
json j_array(v);
|
||||
json j_object(m);
|
||||
|
||||
// works both ways
|
||||
auto l = j_array.get<std::list<int>>();
|
||||
auto u = j_object
|
||||
.get<std::map<std::string, bool>>();
|
||||
```
|
||||
|
||||
# Type conversion
|
||||
|
||||
- convert your own types to and from JSON
|
||||
- one macro writes to_json and from_json
|
||||
- or write both functions for full control
|
||||
|
||||
```cpp
|
||||
// some user-defined type
|
||||
struct person {
|
||||
std::string name;
|
||||
int age;
|
||||
};
|
||||
|
||||
// defines to_json and from_json
|
||||
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(person,
|
||||
name, age)
|
||||
|
||||
// conversion in action
|
||||
person p {"Ned Flanders", 60};
|
||||
json j = p;
|
||||
// {"age": 60, "name": "Ned Flanders"}
|
||||
|
||||
auto p2 = j.get<person>();
|
||||
```
|
||||
|
||||
# Type deduction
|
||||
|
||||
- types are deduced automatically
|
||||
- member functions tell you the type
|
||||
|
||||
```cpp
|
||||
// type deduction
|
||||
json j1 = nullptr;
|
||||
json j2 = false;
|
||||
json j3 = 12.23;
|
||||
json j4 = {1, 2, 3};
|
||||
json j5 = {{"key", "value"}};
|
||||
json j6 = "Hello";
|
||||
|
||||
// quick checks
|
||||
j1.is_null();
|
||||
j2.is_boolean();
|
||||
j3.is_number();
|
||||
j4.is_array();
|
||||
j5.is_object();
|
||||
j6.is_string();
|
||||
```
|
||||
|
||||
# Serialization
|
||||
|
||||
- to a string or a stream
|
||||
- pretty-printing included
|
||||
|
||||
```cpp
|
||||
// default serialization
|
||||
auto s1 = j.dump();
|
||||
|
||||
// pretty-printed
|
||||
int indent = 4;
|
||||
std::string s2 = j.dump(indent);
|
||||
|
||||
// to stream
|
||||
std::cout << j << std::endl;
|
||||
|
||||
// pretty-printed
|
||||
std::cout << std::setw(indent) << j << std::endl;
|
||||
```
|
||||
|
||||
# Deserialization
|
||||
|
||||
- parse strings, streams and files
|
||||
- the _json literal puts plain JSON in your code
|
||||
- comments and trailing commas on request
|
||||
|
||||
```cpp
|
||||
// from string
|
||||
json j1 = json::parse("[1,null,false]");
|
||||
|
||||
// from file
|
||||
std::ifstream f("example.json");
|
||||
json j2 = json::parse(f);
|
||||
|
||||
// from string literal
|
||||
json j3 = "[3.14159]"_json;
|
||||
|
||||
// from raw string literal
|
||||
json j4 = R"(
|
||||
{"one": 1, "two": 2, "three": [1, 2, 3]}
|
||||
)"_json;
|
||||
```
|
||||
|
||||
# SAX parsing
|
||||
|
||||
- the parser can emit SAX events
|
||||
- process input without building a JSON value
|
||||
|
||||
```cpp
|
||||
// define a SAX event processor
|
||||
struct sax_processor : json::json_sax_t
|
||||
{
|
||||
bool boolean(bool val) override
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// implement other functions...
|
||||
};
|
||||
|
||||
// create object
|
||||
sax_processor my_processor;
|
||||
|
||||
// use it to process input
|
||||
json::sax_parse(input, &my_processor);
|
||||
```
|
||||
|
||||
# Zero-copy views {badge: NEW IN 3.13}
|
||||
|
||||
- parse without building a tree: 16 bytes per value
|
||||
- strings and numbers stay in the source text
|
||||
- materialize a json value only where you need one
|
||||
|
||||
```cpp
|
||||
#include <nlohmann/json_view.hpp>
|
||||
|
||||
// parse into a flat index, not a tree
|
||||
std::ifstream f("big.json");
|
||||
auto doc = nlohmann::json_document::parse(f);
|
||||
|
||||
// read-only views with familiar access
|
||||
auto view = doc.root();
|
||||
double pi = view["pi"].get<double>();
|
||||
for (auto v : view["list"])
|
||||
std::cout << v.get<int>() << '\n';
|
||||
|
||||
// build a json value only where needed
|
||||
json part = view["list"].materialize();
|
||||
```
|
||||
|
||||
# JSON Pointer
|
||||
|
||||
- address any element with a JSON Pointer
|
||||
- flatten a value to pointer/value pairs, and back
|
||||
|
||||
```cpp
|
||||
json j = {"good", "bad", "ugly"};
|
||||
|
||||
// JSON pointer to the 3rd list element
|
||||
auto ptr = json::json_pointer("/2");
|
||||
|
||||
// use JSON pointer to access element
|
||||
j[ptr] = {{"it", "cattivo"}};
|
||||
|
||||
// user-defined string literal
|
||||
j["/2/en"_json_pointer] = "ugly";
|
||||
// ["good","bad",{"en":"ugly","it":"cattivo"}]
|
||||
|
||||
json flat = j.flatten();
|
||||
// {"/0":"good","/1":"bad","/2/en":"ugly",
|
||||
// "/2/it":"cattivo"}
|
||||
|
||||
json j2 = flat.unflatten(); // j == j2
|
||||
```
|
||||
|
||||
# JSON Patches
|
||||
|
||||
- use JSON Patch to describe edit operations
|
||||
- create diffs from two JSON values
|
||||
- or use a JSON Merge Patch that looks like the document
|
||||
|
||||
```cpp
|
||||
json source = R"({"F": "Paris", "D": "Berlin"})"_json;
|
||||
|
||||
// a JSON Patch (RFC 6902)
|
||||
json p1 = R"([
|
||||
{"op": "add", "path": "/GB", "value": "London"}
|
||||
])"_json;
|
||||
json target = source.patch(p1);
|
||||
|
||||
// create a diff from two JSON values
|
||||
json p2 = json::diff(target, source);
|
||||
// [{"op": "remove", "path": "/GB"}]
|
||||
|
||||
// a JSON Merge Patch (RFC 7396)
|
||||
target.merge_patch(R"({"F": null})"_json);
|
||||
// {"D": "Berlin", "GB": "London"}
|
||||
```
|
||||
|
||||
# Binary formats
|
||||
|
||||
- compact binary representations
|
||||
- BJData, BON8, BSON, CBOR, MessagePack and UBJSON
|
||||
|
||||
```cpp
|
||||
// a JSON value
|
||||
json j = R"({"compact": true, "schema": 0})"_json;
|
||||
|
||||
// serialize to CBOR
|
||||
std::vector<std::uint8_t> v = json::to_cbor(j);
|
||||
// 0xA2, 0x67, 0x63, 0x6F, 0x6D, 0x70, 0x61,
|
||||
// 0x63, 0x74, 0xF5, 0x66, 0x73, 0x63, 0x68,
|
||||
// 0x65, 0x6D, 0x61, 0x00
|
||||
|
||||
// roundtrip
|
||||
json j_from_cbor = json::from_cbor(v);
|
||||
|
||||
// the same for the other formats
|
||||
auto b = json::to_bon8(j);
|
||||
auto m = json::to_msgpack(j);
|
||||
```
|
||||
|
||||
# Algorithms
|
||||
|
||||
- the standard algorithms just work
|
||||
- sort, find, count, merge and more
|
||||
|
||||
```cpp
|
||||
// sort JSON containers
|
||||
std::sort(j.begin(), j.end());
|
||||
|
||||
// find element
|
||||
auto pos = std::find(j.begin(), j.end(),
|
||||
json("needle"));
|
||||
|
||||
// count all numbers
|
||||
auto n = std::count_if(j.begin(), j.end(),
|
||||
[](const json& v) { return v.is_number(); });
|
||||
|
||||
// merge two sorted containers
|
||||
std::merge(j1.begin(), j1.end(),
|
||||
j2.begin(), j2.end(),
|
||||
std::back_inserter(j3));
|
||||
```
|
||||
|
||||
# Iterators
|
||||
|
||||
- iterators, const iterators, and reverse iterators
|
||||
- random access for arrays
|
||||
- items() gives you keys and values
|
||||
|
||||
```cpp
|
||||
json j = {1, 2, 3, 4, 5};
|
||||
|
||||
// range for
|
||||
for (auto& el : j)
|
||||
{
|
||||
// print all values
|
||||
std::cerr << el << '\n';
|
||||
}
|
||||
|
||||
// classical iterators
|
||||
for (auto it = j.begin(); it != j.end(); ++it)
|
||||
{
|
||||
// double all values
|
||||
*it = it->get<int>() * 2;
|
||||
}
|
||||
```
|
||||
|
||||
# Structured bindings
|
||||
|
||||
- since C++17, unpack keys and values with structured bindings
|
||||
|
||||
```cpp
|
||||
// JSON object
|
||||
auto j = R"({"foo": 23, "bar": 42})"_json;
|
||||
|
||||
// structured bindings
|
||||
for (auto& [key, val] : j.items())
|
||||
{
|
||||
std::cerr << key << ':' << val << '\n';
|
||||
}
|
||||
```
|
||||
Reference in new issue
Block a user