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>
This commit is contained in:
43 files changed
+2311
-1793
No files matched your search
@@ -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,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 @@
|
||||
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