diff --git a/docs/mkdocs/docs/api/basic_json/basic_json.md b/docs/mkdocs/docs/api/basic_json/basic_json.md index 087b1a56f..be55b4df7 100644 --- a/docs/mkdocs/docs/api/basic_json/basic_json.md +++ b/docs/mkdocs/docs/api/basic_json/basic_json.md @@ -126,6 +126,9 @@ basic_json(basic_json&& other) noexcept; F -->|"object"| ERR["throw type_error.301"] ``` +

Decision flow for an initializer list: hexagons are checks, edge labels are their + outcomes, and rectangles are the resulting value or exception.

+ The rules aim to create the best fit between a C++ initializer list and JSON values. The rationale is as follows: 1. The empty initializer list is written as `#!cpp {}` which is exactly an empty JSON object. diff --git a/docs/mkdocs/docs/api/basic_json/begin.md b/docs/mkdocs/docs/api/basic_json/begin.md index 850cf118a..bf16e23e5 100644 --- a/docs/mkdocs/docs/api/basic_json/begin.md +++ b/docs/mkdocs/docs/api/basic_json/begin.md @@ -11,6 +11,9 @@ Returns an iterator to the first element. --8<-- "images/range-begin-end.svg" +

`begin()` points to the first element and `end()` to the position after the last element; +the dashed cell is the past-the-last position, which must not be dereferenced.

+ ## Return value iterator to the first element diff --git a/docs/mkdocs/docs/api/basic_json/cbegin.md b/docs/mkdocs/docs/api/basic_json/cbegin.md index 2c200829a..57da3cb9a 100644 --- a/docs/mkdocs/docs/api/basic_json/cbegin.md +++ b/docs/mkdocs/docs/api/basic_json/cbegin.md @@ -10,6 +10,9 @@ Returns an iterator to the first element. --8<-- "images/range-begin-end.svg" +

`cbegin()` points to the first element, like `begin()`, and `end()` to the position after +the last element; the dashed cell is the past-the-last position, which must not be dereferenced.

+ ## Return value iterator to the first element diff --git a/docs/mkdocs/docs/api/basic_json/cend.md b/docs/mkdocs/docs/api/basic_json/cend.md index 7bc8adcfc..cec356dd2 100644 --- a/docs/mkdocs/docs/api/basic_json/cend.md +++ b/docs/mkdocs/docs/api/basic_json/cend.md @@ -10,6 +10,9 @@ Returns an iterator to one past the last element. --8<-- "images/range-begin-end.svg" +

`cend()` points to the position after the last element, like `end()`, and `begin()` to +the first element; the dashed cell is the past-the-last position, which must not be dereferenced.

+ ## Return value iterator one past the last element diff --git a/docs/mkdocs/docs/api/basic_json/crbegin.md b/docs/mkdocs/docs/api/basic_json/crbegin.md index ab7562cfa..c16631948 100644 --- a/docs/mkdocs/docs/api/basic_json/crbegin.md +++ b/docs/mkdocs/docs/api/basic_json/crbegin.md @@ -10,6 +10,10 @@ Returns an iterator to the reverse-beginning; that is, the last element. --8<-- "images/range-rbegin-rend.svg" +

`crbegin()` points to the last element, like `rbegin()`, and `rend()` to the position +before the first element. The upper row is the reversed sequence and the lower row the forward sequence; dashed arrows +link each reverse iterator to the forward iterator it stores, and dashed cells are past-the-last positions.

+ ## Return value reverse iterator to the last element diff --git a/docs/mkdocs/docs/api/basic_json/crend.md b/docs/mkdocs/docs/api/basic_json/crend.md index d23e41b31..2bfcdf5c0 100644 --- a/docs/mkdocs/docs/api/basic_json/crend.md +++ b/docs/mkdocs/docs/api/basic_json/crend.md @@ -11,6 +11,10 @@ attempting to access it results in undefined behavior. --8<-- "images/range-rbegin-rend.svg" +

`crend()` points to the position before the first element, like `rend()`, and `rbegin()` +to the last element. The upper row is the reversed sequence and the lower row the forward sequence; dashed arrows link +each reverse iterator to the forward iterator it stores, and dashed cells are past-the-last positions.

+ ## Return value reverse iterator to the element following the last element diff --git a/docs/mkdocs/docs/api/basic_json/end.md b/docs/mkdocs/docs/api/basic_json/end.md index 19b1441e2..c0431b406 100644 --- a/docs/mkdocs/docs/api/basic_json/end.md +++ b/docs/mkdocs/docs/api/basic_json/end.md @@ -11,6 +11,9 @@ Returns an iterator to one past the last element. --8<-- "images/range-begin-end.svg" +

`end()` points to the position after the last element and `begin()` to the first element; +the dashed cell is the past-the-last position, which must not be dereferenced.

+ ## Return value iterator one past the last element diff --git a/docs/mkdocs/docs/api/basic_json/exception.md b/docs/mkdocs/docs/api/basic_json/exception.md index 88cf51b1f..9320cdba4 100644 --- a/docs/mkdocs/docs/api/basic_json/exception.md +++ b/docs/mkdocs/docs/api/basic_json/exception.md @@ -40,6 +40,9 @@ classDiagram style json_exception fill:#CCCCFF ``` +

Class hierarchy of the library's exceptions; arrows point from a derived class to its +base class, and the highlighted box is `basic_json::exception`.

+ Subclasses: - [`parse_error`](parse_error.md) for exceptions indicating a parse error diff --git a/docs/mkdocs/docs/api/basic_json/invalid_iterator.md b/docs/mkdocs/docs/api/basic_json/invalid_iterator.md index d757825d8..3367fd2b4 100644 --- a/docs/mkdocs/docs/api/basic_json/invalid_iterator.md +++ b/docs/mkdocs/docs/api/basic_json/invalid_iterator.md @@ -40,6 +40,9 @@ classDiagram style json_invalid_iterator fill:#CCCCFF ``` +

Class hierarchy of the library's exceptions; arrows point from a derived class to its +base class, and the highlighted box is `basic_json::invalid_iterator`.

+ ## Member functions - **what** - returns explanatory string diff --git a/docs/mkdocs/docs/api/basic_json/other_error.md b/docs/mkdocs/docs/api/basic_json/other_error.md index 6149cc3fb..08b2f6669 100644 --- a/docs/mkdocs/docs/api/basic_json/other_error.md +++ b/docs/mkdocs/docs/api/basic_json/other_error.md @@ -40,6 +40,9 @@ classDiagram style json_other_error fill:#CCCCFF ``` +

Class hierarchy of the library's exceptions; arrows point from a derived class to its +base class, and the highlighted box is `basic_json::other_error`.

+ ## Member functions - **what** - returns explanatory string diff --git a/docs/mkdocs/docs/api/basic_json/out_of_range.md b/docs/mkdocs/docs/api/basic_json/out_of_range.md index be579b35d..113bcce2a 100644 --- a/docs/mkdocs/docs/api/basic_json/out_of_range.md +++ b/docs/mkdocs/docs/api/basic_json/out_of_range.md @@ -41,6 +41,9 @@ classDiagram style json_out_of_range fill:#CCCCFF ``` +

Class hierarchy of the library's exceptions; arrows point from a derived class to its +base class, and the highlighted box is `basic_json::out_of_range`.

+ ## Member functions - **what** - returns explanatory string diff --git a/docs/mkdocs/docs/api/basic_json/parse_error.md b/docs/mkdocs/docs/api/basic_json/parse_error.md index 0daae8de2..234bf6fc4 100644 --- a/docs/mkdocs/docs/api/basic_json/parse_error.md +++ b/docs/mkdocs/docs/api/basic_json/parse_error.md @@ -43,6 +43,9 @@ classDiagram style json_parse_error fill:#CCCCFF ``` +

Class hierarchy of the library's exceptions; arrows point from a derived class to its +base class, and the highlighted box is `basic_json::parse_error`.

+ ## Member functions - **what** - returns explanatory string diff --git a/docs/mkdocs/docs/api/basic_json/parse_event_t.md b/docs/mkdocs/docs/api/basic_json/parse_event_t.md index 14f5840a6..a880cd571 100644 --- a/docs/mkdocs/docs/api/basic_json/parse_event_t.md +++ b/docs/mkdocs/docs/api/basic_json/parse_event_t.md @@ -26,6 +26,9 @@ The parser callback distinguishes the following events: --8<-- "images/callback_events.svg" +

Each coloured dot marks where the parser reports a `parse_event_t` event while reading +the JSON text on the left; the legend on the right names the event per colour.

+ ??? example The following code parses a small JSON text with a parser callback that reports every event together with its diff --git a/docs/mkdocs/docs/api/basic_json/parser_callback_t.md b/docs/mkdocs/docs/api/basic_json/parser_callback_t.md index 52bc2a595..8014f6f0c 100644 --- a/docs/mkdocs/docs/api/basic_json/parser_callback_t.md +++ b/docs/mkdocs/docs/api/basic_json/parser_callback_t.md @@ -27,6 +27,9 @@ table describes the values of the parameters `depth`, `event`, and `parsed`. --8<-- "images/callback_events.svg" +

Parse events for a small JSON text: each coloured dot marks the position at which the +parser calls the callback, and the legend on the right names the event per colour.

+ Discarding a value (i.e., returning `#!cpp false`) has different effects depending on the context in which function was called: diff --git a/docs/mkdocs/docs/api/basic_json/rbegin.md b/docs/mkdocs/docs/api/basic_json/rbegin.md index c625211f1..ec95cfe68 100644 --- a/docs/mkdocs/docs/api/basic_json/rbegin.md +++ b/docs/mkdocs/docs/api/basic_json/rbegin.md @@ -11,6 +11,10 @@ Returns an iterator to the reverse-beginning; that is, the last element. --8<-- "images/range-rbegin-rend.svg" +

`rbegin()` points to the last element and `rend()` to the position before the first +element. The upper row is the reversed sequence and the lower row the forward sequence; dashed arrows link each reverse +iterator to the forward iterator it stores, and dashed cells are past-the-last positions.

+ ## Return value reverse iterator to the last element diff --git a/docs/mkdocs/docs/api/basic_json/rend.md b/docs/mkdocs/docs/api/basic_json/rend.md index 668247fea..357c7f4b7 100644 --- a/docs/mkdocs/docs/api/basic_json/rend.md +++ b/docs/mkdocs/docs/api/basic_json/rend.md @@ -12,6 +12,10 @@ attempting to access it results in undefined behavior. --8<-- "images/range-rbegin-rend.svg" +

`rend()` points to the position before the first element and `rbegin()` to the last +element. The upper row is the reversed sequence and the lower row the forward sequence; dashed arrows link each reverse +iterator to the forward iterator it stores, and dashed cells are past-the-last positions.

+ ## Return value reverse iterator to the element following the last element diff --git a/docs/mkdocs/docs/api/basic_json/type_error.md b/docs/mkdocs/docs/api/basic_json/type_error.md index dcc30b29a..b19d5c65f 100644 --- a/docs/mkdocs/docs/api/basic_json/type_error.md +++ b/docs/mkdocs/docs/api/basic_json/type_error.md @@ -41,6 +41,9 @@ classDiagram style json_type_error fill:#CCCCFF ``` +

Class hierarchy of the library's exceptions; arrows point from a derived class to its +base class, and the highlighted box is `basic_json::type_error`.

+ ## Member functions - **what** - returns explanatory string diff --git a/docs/mkdocs/docs/api/basic_json/value_t.md b/docs/mkdocs/docs/api/basic_json/value_t.md index 0d63bfdac..0aa82d0d1 100644 --- a/docs/mkdocs/docs/api/basic_json/value_t.md +++ b/docs/mkdocs/docs/api/basic_json/value_t.md @@ -48,6 +48,9 @@ functions [`is_null`](is_null.md), [`is_object`](is_object.md), [`is_array`](is_ F --> G[binary] ``` +

Arrows point from a type to the next greater type in the ordering used by + `operator<`.

+ !!! note "Types of numbers" There are three enumerators for numbers (`number_integer`, `number_unsigned`, and `number_float`) to distinguish diff --git a/docs/mkdocs/docs/api/json_pointer/index.md b/docs/mkdocs/docs/api/json_pointer/index.md index c64543c94..cdcf35bf9 100644 --- a/docs/mkdocs/docs/api/json_pointer/index.md +++ b/docs/mkdocs/docs/api/json_pointer/index.md @@ -36,6 +36,9 @@ flowchart LR parent["parent_pointer() returns /a/b"] -.-> B ``` +

Boxes are the reference tokens in order; dotted arrows show which token each member +function reads, removes, or adds to.

+ The diagram shows the reference tokens of the JSON pointer `/a/b/c`. ## Member types diff --git a/docs/mkdocs/docs/api/json_sax/index.md b/docs/mkdocs/docs/api/json_sax/index.md index fec1e0489..d005170cb 100644 --- a/docs/mkdocs/docs/api/json_sax/index.md +++ b/docs/mkdocs/docs/api/json_sax/index.md @@ -25,6 +25,9 @@ sequenceDiagram P->>H: end_object() ``` +

Each arrow is one call from the parser to the SAX handler, from top to bottom in the +order the calls happen.

+ Note `elements` is passed as `#!cpp std::numeric_limits::max()` (i.e., "unknown") for JSON text input; only binary formats such as CBOR or MessagePack may report the actual number of elements in `start_object`/ `start_array`. Also note that `1` is reported via `number_unsigned` rather than `number_integer` because it has no diff --git a/docs/mkdocs/docs/community/assurance_case.md b/docs/mkdocs/docs/community/assurance_case.md index 54da5c04c..02dc6d7d7 100644 --- a/docs/mkdocs/docs/community/assurance_case.md +++ b/docs/mkdocs/docs/community/assurance_case.md @@ -41,6 +41,8 @@ flowchart LR E --> F[Trusted caller] ``` +

The trust boundary: untrusted input enters on the left through the parser, the SAX interface, or the binary readers, and only a value tree reaches the trusted caller on the right.

+ ## Secure design - **Strict parsing.** The parser accepts exactly the JSON grammar of [RFC 8259](https://datatracker.ietf.org/doc/html/rfc8259). diff --git a/docs/mkdocs/docs/community/quality_assurance.md b/docs/mkdocs/docs/community/quality_assurance.md index 6c8046964..61ffdff85 100644 --- a/docs/mkdocs/docs/community/quality_assurance.md +++ b/docs/mkdocs/docs/community/quality_assurance.md @@ -4,6 +4,15 @@ Ensuring quality is paramount for this project, particularly because [numerous o depend on it. Each commit to the library undergoes rigorous checks against the following requirements, and any violations will result in a failed build. +The overview shows the checks a change passes on its way from a push or pull request to a release; the sections below +describe each of them. + +
+--8<-- "images/qa_pipeline.svg" +
+ +

The checks grouped by the stage at which they run, from push or pull request through the merge into develop to a release. The badge on each card says whether the check runs on every push or PR, on a schedule, or continuously.

+ ## C++ language compliance and compiler compatibility !!! success "Requirement: Compiler support" diff --git a/docs/mkdocs/docs/community/roadmap.md b/docs/mkdocs/docs/community/roadmap.md index d294bd5f7..343846466 100644 --- a/docs/mkdocs/docs/community/roadmap.md +++ b/docs/mkdocs/docs/community/roadmap.md @@ -91,6 +91,14 @@ Version 4.0 is therefore the sum of these macros: you can try it on the 3.x rele to its 4.0 value and fixing what no longer compiles or behaves differently. Once your code works with all of them, it is ready for version 4.0. +Each macro has a default value in 3.x and a different value in version 4.0. Define the macro to the 4.0 value to try it on the 3.x release train today; version 4.0 makes that value the default: + +
+--8<-- "images/roadmap_lanes.svg" +
+ +

Each row is one macro: the 3.x release that added it, its default in 3.x, and its value in version 4.0 (highlighted) or "removed". Versions on the 3.x lane are not to scale.

+ The following macros guard changes that are planned to become the default in version 4.0: | Macro | 3.x default | 4.0 behavior | CMake option | Added | @@ -105,6 +113,8 @@ The following macros guard changes that are planned to become the default in ver | [`JSON_DISABLE_TUPLE_REFERENCE_CONVERSION`](../api/macros/json_disable_tuple_reference_conversion.md) | `0` | `1`: a `basic_json` value can no longer be created from a one-element tuple of a reference to it, such as `std::forward_as_tuple(j)`| [`JSON_DisableTupleReferenceConversion`](../integration/cmake.md#json_disabletuplereferenceconversion) | 3.13.0 | | [`JSON_DELETE_DEPRECATED_FUNCTIONS`](../api/macros/json_delete_deprecated_functions.md) | `0` | removed: the deprecated functions are removed (see below); the `from_*(ptr, len)` overloads stay deleted | [`JSON_DeleteDeprecatedFunctions`](../integration/cmake.md#json_deletedeprecatedfunctions) | 3.13.0 | +

Opt-in macros for the version 4.0 behavior. "3.x default" is the value the library uses when the macro is not defined, "Added" is the release that introduced the macro, and – means there is no CMake option.

+ For example, the following makes a 3.x release behave like version 4.0 with respect to these changes: ```cpp @@ -146,6 +156,8 @@ to the `strict` parameter. | Comparing a [`json_pointer`](../api/json_pointer/index.md) with a string via [`operator==`](../api/json_pointer/operator_eq.md) or [`operator!=`](../api/json_pointer/operator_ne.md) | 3.11.2 | [JSON Pointers](../integration/migration_guide.md#json-pointers) | | [`from_bjdata`](../api/basic_json/from_bjdata.md) and [`from_bon8`](../api/basic_json/from_bon8.md) with `(ptr, len)` | 3.13.0 | [Parsing](../integration/migration_guide.md#parsing) | +

Deprecated functions, with the release that deprecated each one and the section of the migration guide that shows its replacement.

+ The deprecated legacy comparison of discarded values is controlled by a macro and therefore listed in the table above. New breaking changes will follow the same path: they are added to these tables when they land in a 3.x release. diff --git a/docs/mkdocs/docs/css/brand.css b/docs/mkdocs/docs/css/brand.css index 5aefef421..b3d61bcfe 100644 --- a/docs/mkdocs/docs/css/brand.css +++ b/docs/mkdocs/docs/css/brand.css @@ -737,3 +737,46 @@ body:has(.nj-hero) .md-content { font-size: .68rem; } } + +/* figure captions: "Figure N." in bold, close to the figure, set in from the text column; figures and tables are + numbered per page, each with its own counter */ +.md-content__inner { counter-reset: nj-figure nj-table; } +.md-typeset :is(.nj-caption, .nj-charts-caption) { + max-width: none; + margin: .3rem 2rem 1.5rem; + font-size: .75rem; + line-height: 1.5; + text-align: left; + color: var(--md-default-fg-color--light); +} +.md-typeset :is(.nj-fig-wrap, .railroad-wrap, .nj-charts-grid-wrap) + :is(.nj-caption, .nj-charts-caption) { margin-top: -1.1rem; } +.md-typeset .nj-charts-grid-wrap + :is(.nj-caption, .nj-charts-caption) { margin-top: .3rem; } +.md-typeset :is(.nj-caption, .nj-charts-caption)::before { + counter-increment: nj-figure; + content: "Figure " counter(nj-figure) ". "; + font-weight: 700; + color: var(--md-default-fg-color); +} +@media screen and (max-width: 44.9375em) { + .md-typeset :is(.nj-caption, .nj-charts-caption) { margin-inline: 1rem; } +} + +/* table captions: "Table N." in bold, directly below the table, in the figure caption style */ +.md-typeset .nj-table-caption { + max-width: none; + margin: .3rem 2rem 1.5rem; + font-size: .75rem; + line-height: 1.5; + text-align: left; + color: var(--md-default-fg-color--light); +} +.md-typeset :is(.md-typeset__scrollwrap, .md-typeset__table) + .nj-table-caption { margin-top: -.4rem; } +.md-typeset .nj-table-caption::before { + counter-increment: nj-table; + content: "Table " counter(nj-table) ". "; + font-weight: 700; + color: var(--md-default-fg-color); +} +@media screen and (max-width: 44.9375em) { + .md-typeset .nj-table-caption { margin-inline: 1rem; } +} diff --git a/docs/mkdocs/docs/css/fig-charts.css b/docs/mkdocs/docs/css/fig-charts.css new file mode 100644 index 000000000..742a70bb7 --- /dev/null +++ b/docs/mkdocs/docs/css/fig-charts.css @@ -0,0 +1,142 @@ +/* figures: charts (docs/images/compile_times.svg, binary_sizes_*.svg, ordered_json_scaling.svg from + scripts/figures/charts.py) and the number literal explorer on features/types/number_handling.md */ + +/* series colours: categorical slots validated for CVD separation on the light and the slate surface */ +.nj-charts, .nj-charts-key { + --nj-charts-c1: #2a78d6; + --nj-charts-c2: #eb6834; + --nj-charts-c3: #1baf7a; + --nj-charts-c4: #eda100; + --nj-charts-c5: #e87ba4; + --nj-charts-c6: #008300; + --nj-charts-ink2: var(--md-default-fg-color--light); + --nj-charts-grid: color-mix(in srgb, currentColor 12%, var(--md-default-bg-color)); + --nj-charts-base: color-mix(in srgb, currentColor 35%, var(--md-default-bg-color)); +} +[data-md-color-scheme="slate"] :is(.nj-charts, .nj-charts-key) { + --nj-charts-c1: #3987e5; + --nj-charts-c2: #d95926; + --nj-charts-c3: #199e70; + --nj-charts-c4: #c98500; + --nj-charts-c5: #d55181; + --nj-charts-c6: #008300; +} +/* concepts: two compilers, two object types */ +.nj-charts { --nj-charts-clang-c: var(--nj-charts-c1); --nj-charts-gcc-c: var(--nj-charts-c2); + --nj-charts-json-c: var(--nj-charts-c1); --nj-charts-ojson-c: var(--nj-charts-c2); } + +.md-typeset svg.nj-charts--compile, .md-typeset svg.nj-charts--scaling { max-width: 26rem; } +.md-typeset svg.nj-charts--sizes { max-width: 26rem; } + +.nj-charts text { fill: currentColor; font-size: 13px; } +.nj-charts .nj-charts-leg { font-size: 13.5px; } +.nj-charts .nj-charts-rowlabel { font-size: 13.5px; font-weight: 500; } +.nj-charts .nj-charts-code { font-family: var(--md-code-font-family), monospace; font-size: 12.5px; } +.nj-charts .nj-charts-sub { fill: var(--nj-charts-ink2); font-weight: 400; } +.nj-charts .nj-charts-fmt { text-anchor: end; font-weight: 400; } +.nj-charts .nj-charts-best { font-weight: 700; } +.nj-charts .nj-charts-panel { font-family: var(--md-code-font-family), monospace; font-size: 14px; font-weight: 600; } +.nj-charts .nj-charts-tick { fill: var(--nj-charts-ink2); font-size: 12px; text-anchor: middle; font-variant-numeric: tabular-nums; } +.nj-charts .nj-charts-tick--l { text-anchor: start; } +.nj-charts .nj-charts-tick--r { text-anchor: end; } +.nj-charts .nj-charts-val { font-size: 12px; font-variant-numeric: tabular-nums; + paint-order: stroke; stroke: var(--md-default-bg-color); stroke-width: 4px; stroke-linejoin: round; } +.nj-charts .nj-charts-factor { fill: var(--nj-charts-ink2); font-size: 12px; font-weight: 600; text-anchor: middle; } +.nj-charts .nj-charts-note { fill: var(--nj-charts-ink2); font-size: 12px; } + +.nj-charts .nj-charts-grid { stroke: var(--nj-charts-grid); stroke-width: 1; } +.nj-charts .nj-charts-zero, .nj-charts .nj-charts-axis { stroke: var(--nj-charts-base); stroke-width: 1.5; } +.nj-charts .nj-charts-ref { stroke: currentColor; stroke-width: 1.5; } + +.nj-charts .nj-charts-c1 { fill: var(--nj-charts-c1); } +.nj-charts .nj-charts-c2 { fill: var(--nj-charts-c2); } +.nj-charts .nj-charts-c3 { fill: var(--nj-charts-c3); } +.nj-charts .nj-charts-c4 { fill: var(--nj-charts-c4); } +.nj-charts .nj-charts-c5 { fill: var(--nj-charts-c5); } +.nj-charts .nj-charts-c6 { fill: var(--nj-charts-c6); } +.nj-charts .nj-charts-dim { fill-opacity: .45; } +.nj-charts .nj-charts-clang { fill: var(--nj-charts-clang-c); } +.nj-charts .nj-charts-gcc { fill: var(--nj-charts-gcc-c); } +.nj-charts polyline.nj-charts-json, .nj-charts line.nj-charts-json { stroke: var(--nj-charts-json-c); stroke-width: 2; fill: none; } +.nj-charts polyline.nj-charts-ojson, .nj-charts line.nj-charts-ojson { stroke: var(--nj-charts-ojson-c); stroke-width: 2; fill: none; } +.nj-charts .nj-charts-json-dot { fill: var(--nj-charts-json-c); stroke: var(--md-default-bg-color); stroke-width: 2; } +.nj-charts .nj-charts-ojson-dot { fill: var(--nj-charts-ojson-c); stroke: var(--md-default-bg-color); stroke-width: 2; } + +/* single wide charts: larger text on phones, where the SVG is scaled down */ +@media screen and (max-width: 44.9375em) { + svg.nj-charts.nj-charts--compile text, svg.nj-charts.nj-charts--scaling text { font-size: 15px; } + svg.nj-charts.nj-charts--compile .nj-charts-code { font-size: 14px; } + svg.nj-charts:is(.nj-charts--compile, .nj-charts--scaling) :is(.nj-charts-tick, .nj-charts-val, .nj-charts-factor, .nj-charts-note) { font-size: 14px; } +} + +/* small multiples: two panels per row, one on narrow screens */ +.md-typeset .nj-charts-grid-wrap { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); + gap: 1rem 2rem; + margin: 1.5rem 0 .5rem; +} +.md-typeset .nj-charts-grid-wrap > .nj-fig-wrap { margin: 0; } + +/* number literal explorer */ +.md-typeset .nj-charts-num-ui { + margin: 1rem 0 .75rem; + padding: .8rem; + border: 1px solid var(--md-default-fg-color--lightest); + border-radius: 8px; +} +.md-typeset .nj-charts-num-ui label { display: block; font-weight: 600; font-size: .75rem; margin-bottom: .3rem; } +.md-typeset .nj-charts-num-input { + width: 100%; + box-sizing: border-box; + min-height: 44px; + padding: .4rem .6rem; + font-family: var(--md-code-font-family), monospace; + font-size: .8rem; + color: var(--md-default-fg-color); + background: var(--md-code-bg-color); + border: 1px solid var(--md-default-fg-color--lighter); + border-radius: 6px; +} +.md-typeset .nj-charts-num-input:focus-visible, .md-typeset .nj-charts-num-chip:focus-visible { + outline: 2px solid var(--md-typeset-a-color); + outline-offset: 2px; +} +.md-typeset .nj-charts-num-ui ul.nj-charts-num-chips:not([hidden]) { display: flex; flex-wrap: wrap; gap: .35rem; margin: .6rem 0; padding: 0; list-style: none; } +.md-typeset .nj-charts-num-ui ul.nj-charts-num-chips > li { margin: 0; padding: 0; list-style: none; } +.md-typeset .nj-charts-num-ui ul.nj-charts-num-chips > li::before, .md-typeset .nj-charts-num-ui ul.nj-charts-num-chips > li::marker { content: none; } +.md-typeset .nj-charts-num-chip { + min-height: 44px; + max-width: 100%; + padding: .25rem .6rem; + overflow-wrap: anywhere; + font-family: var(--md-code-font-family), monospace; + font-size: .7rem; + color: var(--md-typeset-a-color); + background: color-mix(in srgb, var(--md-typeset-a-color) 8%, var(--md-default-bg-color)); + border: 1px solid color-mix(in srgb, var(--md-typeset-a-color) 35%, var(--md-default-bg-color)); + border-radius: 6px; + cursor: pointer; + text-align: left; +} +.md-typeset .nj-charts-num-chip[aria-pressed="true"] { + color: var(--md-default-bg-color); + background: var(--md-typeset-a-color); + border-color: var(--md-typeset-a-color); +} +.md-typeset .nj-charts-num-out { + display: grid; + grid-template-columns: max-content 1fr; + gap: .2rem .8rem; + margin: 0; + font-size: .75rem; + overflow-wrap: anywhere; +} +.md-typeset .nj-charts-num-out dt { color: var(--md-default-fg-color--light); margin: 0; } +.md-typeset .nj-charts-num-out dd { margin: 0; } +.md-typeset .nj-charts-num-miss { font-size: .75rem; color: var(--md-default-fg-color--light); margin: 0; } +.md-typeset .nj-charts-num tr.nj-charts-num-hit td { background: var(--md-accent-fg-color--transparent); } + +/* direct line labels replace the legend */ +.nj-charts .nj-charts-name-json { fill: var(--nj-charts-json-c); font-weight: 600; } +.nj-charts .nj-charts-name-ojson { fill: var(--nj-charts-ojson-c); font-weight: 600; } diff --git a/docs/mkdocs/docs/css/fig-exceptions.css b/docs/mkdocs/docs/css/fig-exceptions.css new file mode 100644 index 000000000..a3562f732 --- /dev/null +++ b/docs/mkdocs/docs/css/fig-exceptions.css @@ -0,0 +1,75 @@ +/* figures: exceptions (home/exceptions.md exception map, community/quality_assurance.md pipeline) */ + +/* the generated SVGs hold a wide and a narrow drawing; show one of them */ +.md-typeset svg.nj-exceptions-map--wide, .md-typeset svg.nj-exceptions-qa--wide { max-width: 47.5rem; } +.md-typeset svg.nj-exceptions-map--narrow, .md-typeset svg.nj-exceptions-qa--narrow { display: none; max-width: 24rem; } +@media screen and (max-width: 37.5em) { + .md-typeset svg.nj-exceptions-map--wide, .md-typeset svg.nj-exceptions-qa--wide { display: none; } + .md-typeset svg.nj-exceptions-map--narrow, .md-typeset svg.nj-exceptions-qa--narrow { display: block; } +} + +/* shared */ +.nj-fig { --nj-exceptions-line: color-mix(in srgb, currentColor 45%, var(--md-default-bg-color)); } +.nj-fig .nj-exceptions-edge { stroke: var(--nj-exceptions-line); stroke-width: 1.5; fill: none; } +.nj-fig .nj-exceptions-tri { fill: var(--md-default-bg-color); stroke: var(--nj-exceptions-line); stroke-width: 1.3; } +.nj-fig .nj-exceptions-box { + fill: color-mix(in srgb, currentColor 4%, var(--md-default-bg-color)); + stroke: color-mix(in srgb, currentColor 30%, var(--md-default-bg-color)); + stroke-width: 1.5; +} +.nj-fig .nj-exceptions-box--fam { + fill: color-mix(in srgb, var(--md-typeset-a-color) 9%, var(--md-default-bg-color)); + stroke: color-mix(in srgb, var(--md-typeset-a-color) 55%, var(--md-default-bg-color)); +} +.nj-fig .nj-exceptions-name { fill: currentColor; font-family: var(--md-code-font-family), monospace; font-size: 14px; font-weight: 600; } +.nj-fig .nj-exceptions-sub { fill: currentColor; fill-opacity: .68; font-size: 12.5px; } + +/* exception map: id chips are links */ +.nj-fig .nj-exceptions-chip rect { + fill: color-mix(in srgb, var(--md-typeset-a-color) 7%, var(--md-default-bg-color)); + stroke: color-mix(in srgb, var(--md-typeset-a-color) 40%, var(--md-default-bg-color)); + stroke-width: 1.2; + transition: fill .15s; +} +.nj-fig .nj-exceptions-chip--named rect { + fill: color-mix(in srgb, var(--md-typeset-a-color) 16%, var(--md-default-bg-color)); + stroke: var(--md-typeset-a-color); +} +.nj-fig .nj-exceptions-id { fill: var(--md-typeset-a-color); font-family: var(--md-code-font-family), monospace; font-size: 13px; font-weight: 600; } +.nj-fig .nj-exceptions-label { fill: currentColor; font-size: 12.5px; font-weight: 500; } +.nj-fig a:hover rect, .nj-fig a:focus-visible rect { + fill: color-mix(in srgb, var(--md-typeset-a-color) 28%, var(--md-default-bg-color)); +} +.nj-fig a:focus { outline: none; } +.nj-fig a:focus-visible rect { stroke: var(--md-typeset-a-color); stroke-width: 2.5; } +.nj-fig a:hover .nj-exceptions-name, .nj-fig a:focus-visible .nj-exceptions-name { text-decoration: underline; } +@media (prefers-reduced-motion: reduce) { + .nj-fig .nj-exceptions-chip rect { transition: none; } +} + +/* quality assurance pipeline */ +.nj-fig { --nj-exceptions-push: var(--md-typeset-a-color); --nj-exceptions-sched: #a86400; --nj-exceptions-cont: #1a7f37; } +[data-md-color-scheme="slate"] .nj-fig { --nj-exceptions-sched: #e3a33b; --nj-exceptions-cont: #56c46e; } +.nj-fig .nj-exceptions-groupbox { + fill: color-mix(in srgb, currentColor 2.5%, var(--md-default-bg-color)); + stroke: color-mix(in srgb, currentColor 22%, var(--md-default-bg-color)); + stroke-width: 1.2; + stroke-dasharray: 5 4; +} +.nj-fig .nj-exceptions-group { fill: currentColor; fill-opacity: .68; font-size: 13px; font-weight: 600; letter-spacing: .02em; } +.nj-fig .nj-exceptions-card .nj-exceptions-box { fill: var(--md-default-bg-color); } +.nj-fig .nj-exceptions-ctitle { fill: currentColor; font-size: 15px; font-weight: 700; } +.nj-fig .nj-exceptions-line { fill: currentColor; fill-opacity: .8; font-size: 12.5px; } +.nj-fig .nj-exceptions-badge { stroke-width: 1.2; } +.nj-fig .nj-exceptions-badgetext { font-size: 11px; font-weight: 600; text-anchor: middle; } +.nj-fig .nj-exceptions-badge--push { fill: color-mix(in srgb, var(--nj-exceptions-push) 12%, var(--md-default-bg-color)); stroke: var(--nj-exceptions-push); } +.nj-fig .nj-exceptions-badge--sched { fill: color-mix(in srgb, var(--nj-exceptions-sched) 12%, var(--md-default-bg-color)); stroke: var(--nj-exceptions-sched); } +.nj-fig .nj-exceptions-badge--cont { fill: color-mix(in srgb, var(--nj-exceptions-cont) 12%, var(--md-default-bg-color)); stroke: var(--nj-exceptions-cont); } +.nj-fig .nj-exceptions-badgetext--push { fill: var(--nj-exceptions-push); } +.nj-fig .nj-exceptions-badgetext--sched { fill: var(--nj-exceptions-sched); } +.nj-fig .nj-exceptions-badgetext--cont { fill: var(--nj-exceptions-cont); } +.nj-fig .nj-exceptions-stage { fill: var(--md-typeset-a-color); } +.nj-fig .nj-exceptions-stagetext { fill: #fff; font-size: 14px; font-weight: 700; text-anchor: middle; } +.nj-fig .nj-exceptions-stagesub { fill: #fff; fill-opacity: .85; font-size: 12px; text-anchor: middle; } +[data-md-color-scheme="slate"] .nj-fig .nj-exceptions-stage { fill: #1747a6; } +.nj-fig .nj-exceptions-qa .fg-head, .nj-exceptions-qa .fg-head { fill: var(--nj-exceptions-line); } diff --git a/docs/mkdocs/docs/css/fig-integration.css b/docs/mkdocs/docs/css/fig-integration.css new file mode 100644 index 000000000..da588e93c --- /dev/null +++ b/docs/mkdocs/docs/css/fig-integration.css @@ -0,0 +1,176 @@ +/* figures: integration */ + +/* namespace name builder (features/namespace.md, js/fig-integration.js) */ +.md-typeset .nj-integration-ns { + --nj-int-line: color-mix(in srgb, currentColor 30%, var(--md-default-bg-color)); + display: block; + width: auto; + max-width: none; + margin: 1.5rem 0; + padding: .9rem 1rem; + text-align: left; + border: 1px solid var(--nj-int-line); + border-radius: 8px; +} +.md-typeset .nj-integration-ns__strip { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + gap: .5rem .15rem; +} +.md-typeset .nj-integration-ns__seg { + display: inline-flex; + flex-direction: column; + align-items: center; + min-width: 0; +} +.md-typeset .nj-integration-ns__seg code { + padding: .25em .5em; + border: 1.5px solid var(--nj-int-line); + border-radius: 6px; + background: var(--md-default-bg-color); + color: var(--md-default-fg-color); + font-size: .8rem; + font-weight: 600; + word-break: normal; +} +.md-typeset .nj-integration-ns__seg small { + max-width: 10rem; + margin-top: .25rem; + color: var(--md-default-fg-color--light); + font-size: .58rem; + line-height: 1.25; + text-align: center; + overflow-wrap: anywhere; +} +.md-typeset .nj-integration-ns__seg--sep code { + border-color: transparent; + padding-inline: .1em; +} +.md-typeset .nj-integration-ns__seg--abi code { + border-color: var(--md-typeset-a-color); + background: color-mix(in srgb, var(--md-typeset-a-color) 12%, var(--md-default-bg-color)); + color: var(--md-typeset-a-color); +} +.md-typeset .nj-integration-ns__seg--tag code { + border-style: dashed; + border-color: var(--md-typeset-a-color); + color: var(--md-typeset-a-color); +} +.md-typeset .nj-integration-ns__seg--ver code { + background: color-mix(in srgb, currentColor 8%, var(--md-default-bg-color)); +} +.md-typeset .nj-integration-ns figcaption { + max-width: none; + margin: .8rem 0 0; + font-size: .75rem; + text-align: left; + color: var(--md-default-fg-color--light); +} +.md-typeset .nj-integration-ns__controls { + margin-top: 1rem; + padding-top: .8rem; + border-top: 1px solid var(--nj-int-line); + font-size: .7rem; +} +.md-typeset .nj-integration-ns__version select { + min-height: 2rem; + margin-left: .3rem; + padding: 0 .4rem; + border: 1px solid var(--nj-int-line); + border-radius: 6px; + background: var(--md-default-bg-color); + color: var(--md-default-fg-color); + font: inherit; +} +.md-typeset .nj-integration-ns__list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(min(100%, 21rem), 1fr)); + gap: 0 1rem; + margin: .7rem 0 0; + padding: 0; + border: 0; +} +.md-typeset .nj-integration-ns__list legend { + padding: 0; + margin-bottom: .3rem; + font-weight: 600; +} +.md-typeset .nj-integration-ns__opt { + display: grid; + grid-template-columns: 1.1rem 1fr; + align-items: center; + gap: 0 .55rem; + min-height: 2.2rem; + padding: .15rem 0; + cursor: pointer; +} +.md-typeset .nj-integration-ns__meta { + grid-column: 2; + display: flex; + flex-wrap: wrap; + gap: 0 .45rem; +} +.md-typeset .nj-integration-ns__opt input { + grid-row: 1 / span 2; + width: 1.1rem; + height: 1.1rem; + margin: 0; + accent-color: var(--md-typeset-a-color); +} +.md-typeset .nj-integration-ns__opt input:focus-visible { + outline: 2px solid var(--md-accent-fg-color); + outline-offset: 2px; +} +.md-typeset .nj-integration-ns__opt code { + font-size: .64rem; + overflow-wrap: anywhere; +} +.md-typeset .nj-integration-ns__hint { + color: var(--md-typeset-a-color); + font-family: var(--md-code-font-family); + font-size: .64rem; +} +.md-typeset .nj-integration-ns__since { + color: var(--md-default-fg-color--light); + font-size: .6rem; +} +.md-typeset .nj-integration-ns__opt.is-unavailable { + cursor: not-allowed; + opacity: .5; +} +@media screen and (max-width: 44.984375em) { + .md-typeset .nj-integration-ns__opt { + min-height: 2.75rem; + } +} + +/* roadmap lanes and release timeline (scripts/figures/integration.py, release_timeline.py) */ +.md-typeset svg.nj-fig--lanes { min-width: 44rem; max-width: 46rem; } +.md-typeset svg.nj-fig--timeline { min-width: 44rem; max-width: 52rem; } +.nj-fig .nj-int-lane { fill: color-mix(in srgb, var(--md-typeset-a-color) 10%, var(--md-default-bg-color)); stroke: var(--md-typeset-a-color); stroke-width: 1.5; } +.nj-fig .nj-int-lane--future { fill: none; stroke-dasharray: 6 4; } +.nj-fig .nj-int-lanetext { fill: var(--md-typeset-a-color); font-size: 15px; font-weight: 700; } +.nj-fig .nj-int-text { fill: currentColor; font-size: 13px; } +.nj-fig .nj-int-mute { fill: currentColor; fill-opacity: .7; font-size: 12.5px; } +.nj-fig .nj-int-macro { fill: currentColor; font-family: var(--md-code-font-family), monospace; font-size: 12.5px; } +.nj-fig .nj-int-val { fill: currentColor; font-family: var(--md-code-font-family), monospace; font-size: 13px; font-weight: 600; } +.nj-fig .nj-int-row { fill: color-mix(in srgb, currentColor 4%, var(--md-default-bg-color)); } +.nj-fig .nj-int-track { fill: color-mix(in srgb, currentColor 14%, var(--md-default-bg-color)); stroke: color-mix(in srgb, currentColor 45%, var(--md-default-bg-color)); stroke-width: 1.2; } +.nj-fig .nj-int-track--on { fill: var(--md-typeset-a-color); stroke: var(--md-typeset-a-color); } +.nj-fig .nj-int-knob { fill: var(--md-default-bg-color); stroke: color-mix(in srgb, currentColor 45%, var(--md-default-bg-color)); stroke-width: 1.2; } +.nj-fig .nj-int-knob--on { stroke: var(--md-typeset-a-color); } +.nj-fig .nj-int-axis { stroke: color-mix(in srgb, currentColor 45%, var(--md-default-bg-color)); stroke-width: 1.5; } +.nj-fig .nj-int-grid { stroke: color-mix(in srgb, currentColor 14%, var(--md-default-bg-color)); stroke-width: 1; } +.nj-fig .nj-int-minor { fill: var(--md-typeset-a-color); stroke: var(--md-default-bg-color); stroke-width: 1.5; } +.nj-fig .nj-int-patch { fill: var(--md-default-bg-color); stroke: var(--md-typeset-a-color); stroke-width: 1.5; } +.nj-fig .nj-int-major { fill: currentColor; fill-opacity: .7; font-size: 13px; font-weight: 600; } +.nj-fig .nj-int-label { fill: currentColor; font-size: 12px; font-weight: 600; text-anchor: middle; } +.nj-fig .nj-int-year { fill: currentColor; fill-opacity: .7; font-size: 12px; text-anchor: middle; } +.nj-fig .nj-int-leader { stroke: color-mix(in srgb, currentColor 30%, var(--md-default-bg-color)); stroke-width: 1; } +.nj-fig .nj-int-hit { fill: transparent; } + +/* roadmap: value pills (3.x default -> 4.0 value) */ +.nj-fig .nj-int-pill { fill: color-mix(in srgb, currentColor 8%, var(--md-default-bg-color)); stroke: color-mix(in srgb, currentColor 35%, var(--md-default-bg-color)); stroke-width: 1.2; } +.nj-fig .nj-int-pill--v4 { fill: var(--md-typeset-a-color); stroke: var(--md-typeset-a-color); } +.nj-fig .nj-int-val--v4 { fill: var(--md-default-bg-color); } diff --git a/docs/mkdocs/docs/css/fig-jsonview.css b/docs/mkdocs/docs/css/fig-jsonview.css new file mode 100644 index 000000000..89cd3bcc9 --- /dev/null +++ b/docs/mkdocs/docs/css/fig-jsonview.css @@ -0,0 +1,107 @@ +/* figures: jsonview (features/json_view.md; SVGs from scripts/figures/json_view_layout.py, stepper in js/fig-jsonview.js) */ + +/* one colour per concept, the same in every figure: structure (object/array, views) blue, keys navy, strings green, + numbers orange, decoded strings purple, owned edit storage teal, dangling/invalid red */ +.nj-jsonview, .nj-jsonview-cmp { + --jv-struct: var(--nj-blue); + --jv-key: var(--nj-hl-key); + --jv-str: var(--nj-hl-string); + --jv-num: var(--nj-hl-number); + --jv-dec: var(--nj-hl-function); + --jv-own: var(--nj-hl-type); + --jv-bad: var(--nj-red); + --jv-line: color-mix(in srgb, currentColor 55%, var(--md-default-bg-color)); +} +.md-typeset svg.nj-jsonview { min-width: 34rem; max-width: 42rem; } +.nj-jsonview .nj-jsonview-c-struct { --c: var(--jv-struct); } +.nj-jsonview .nj-jsonview-c-key { --c: var(--jv-key); } +.nj-jsonview .nj-jsonview-c-str { --c: var(--jv-str); } +.nj-jsonview .nj-jsonview-c-num { --c: var(--jv-num); } +.nj-jsonview .nj-jsonview-c-dec { --c: var(--jv-dec); } +.nj-jsonview .nj-jsonview-c-own { --c: var(--jv-own); } +.nj-jsonview .nj-jsonview-c-bad { --c: var(--jv-bad); } +.nj-jsonview .nj-jsonview-c-line { --c: var(--jv-line); } + +.nj-jsonview text { fill: currentColor; font-size: 13px; } +.nj-jsonview text[class*="nj-jsonview-c-"] { fill: var(--c); } +.nj-jsonview .nj-jsonview-h { font-size: 15px; font-weight: 600; fill-opacity: .85; } +.nj-jsonview .nj-jsonview-sm2 { font-size: 14px; } +.nj-jsonview .nj-jsonview-note { font-size: 13px; fill-opacity: .75; } +.nj-jsonview .nj-jsonview-strong { font-weight: 600; fill-opacity: 1; } +.nj-jsonview .nj-jsonview-tiny { font-size: 10.5px; fill-opacity: .7; } +.nj-jsonview .nj-jsonview-mono { font-family: var(--md-code-font-family), monospace; font-size: 13px; } +.nj-jsonview .nj-jsonview-sm { font-size: 12px; } +.nj-jsonview .nj-jsonview-xs { font-size: 11px; } +.nj-jsonview .nj-jsonview-muted { fill-opacity: .75; } +.nj-jsonview .nj-jsonview-byte { font-size: 12px; } +.nj-jsonview .nj-jsonview-kind { font-size: 12.5px; font-weight: 700; } + +.nj-jsonview .nj-jsonview-box { fill: var(--md-default-bg-color); stroke: var(--jv-line); stroke-width: 1.5; } +.nj-jsonview .nj-jsonview-heap { fill: color-mix(in srgb, currentColor 7%, var(--md-default-bg-color)); stroke: var(--jv-line); stroke-width: 1.5; stroke-dasharray: 5 3; } +.nj-jsonview .nj-jsonview-ribbon { fill: color-mix(in srgb, currentColor 4%, var(--md-default-bg-color)); stroke: var(--jv-line); stroke-width: 1.2; } +.nj-jsonview .nj-jsonview-span { fill: color-mix(in srgb, var(--c) 18%, var(--md-default-bg-color)); } +.nj-jsonview .nj-jsonview-frame { fill: none; stroke: var(--jv-line); stroke-width: 1.2; stroke-dasharray: 6 4; } +.nj-jsonview .nj-jsonview-store { fill: color-mix(in srgb, var(--jv-own) 6%, var(--md-default-bg-color)); stroke: color-mix(in srgb, var(--jv-own) 55%, var(--md-default-bg-color)); stroke-width: 1.2; stroke-dasharray: 6 4; } +.nj-jsonview .nj-jsonview-node { fill: color-mix(in srgb, var(--c) 12%, var(--md-default-bg-color)); stroke: var(--c); stroke-width: 1.5; } +.nj-jsonview .nj-jsonview-node.nj-jsonview-hot { stroke-width: 3; } +.nj-jsonview .nj-jsonview-node.nj-jsonview-gap { fill: var(--md-default-bg-color); stroke: var(--jv-bad); stroke-dasharray: 4 3; } +.nj-jsonview .nj-jsonview-node.nj-jsonview-free { fill: none; stroke-dasharray: 3 3; } +.nj-jsonview .nj-jsonview-sep { stroke: var(--jv-line); stroke-width: 1; fill: none; } +.nj-jsonview .nj-jsonview-arr { stroke: var(--c); stroke-width: 1.5; fill: none; } +.nj-jsonview .nj-jsonview-dash { stroke-dasharray: 4 3; } +.nj-jsonview .nj-jsonview-head { fill: var(--c); } +.nj-jsonview .nj-jsonview-bar { fill: color-mix(in srgb, var(--jv-struct) 30%, var(--md-default-bg-color)); stroke: var(--jv-struct); stroke-width: 1.2; } +.nj-jsonview .nj-jsonview-bad { + fill: color-mix(in srgb, var(--jv-bad) 20%, var(--md-default-bg-color)); + stroke: var(--jv-bad); stroke-width: 1.2; stroke-dasharray: 4 2; +} +.nj-jsonview .nj-jsonview-evt { stroke: var(--jv-bad); stroke-width: 2; fill: none; } +.nj-jsonview .nj-jsonview-evt--ok { stroke: var(--jv-struct); stroke-dasharray: 4 3; } +.nj-jsonview .nj-jsonview-onbad, .nj-jsonview .nj-jsonview-onok { font-size: 11.5px; fill-opacity: 1; } + +/* the edit stepper: without JS all frames are shown one below the other */ +.nj-jsonview-steps { margin: 1.5rem 0; } +.md-typeset .nj-jsonview-step { display: block; width: 100%; max-width: none; margin: 0 0 1.5rem; } +.nj-jsonview-step > figcaption { + margin: .25rem 0 0; font-size: .75rem; color: var(--md-default-fg-color--light); text-align: left; + max-width: none; +} +.nj-jsonview-step > figcaption code { font-size: .95em; } +.nj-jsonview-step .nj-jsonview-scroll { overflow-x: auto; } +.md-typeset .nj-jsonview-steps.is-enhanced .nj-jsonview-step { display: none; margin: 0; } +.md-typeset .nj-jsonview-steps.is-enhanced .nj-jsonview-step.is-current { display: block; } +.nj-jsonview-controls { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin: 0 0 .6rem; } +.nj-jsonview-controls button { + min-height: 2.2rem; min-width: 2.2rem; padding: 0 .8rem; + font: inherit; font-size: .7rem; font-weight: 600; cursor: pointer; + color: var(--md-typeset-a-color); background: var(--md-default-bg-color); + border: 1.5px solid var(--md-typeset-a-color); border-radius: .4rem; +} +.nj-jsonview-controls button:hover:not(:disabled) { background: color-mix(in srgb, var(--md-typeset-a-color) 10%, var(--md-default-bg-color)); } +.nj-jsonview-controls button:focus-visible { outline: 2px solid var(--md-accent-fg-color); outline-offset: 2px; } +.nj-jsonview-controls button:disabled { opacity: .45; cursor: default; } +.nj-jsonview-controls button[aria-current="step"], .nj-jsonview-controls button[aria-current="step"]:hover { color: var(--md-default-bg-color); background: var(--md-typeset-a-color); } +.nj-jsonview-controls .nj-jsonview-status { font-size: .7rem; color: var(--md-default-fg-color--light); margin-left: .25rem; } +@media (max-width: 44.9375em) { + .nj-jsonview-controls button { min-height: 44px; min-width: 44px; } +} +@media (prefers-reduced-motion: no-preference) { + .nj-jsonview-steps.is-enhanced .nj-jsonview-step.is-current { animation: nj-jsonview-fade .25s ease-out; } +} +@keyframes nj-jsonview-fade { from { opacity: .3; } to { opacity: 1; } } + +/* json vs ordered_json vs editable document */ +.nj-jsonview-cmp { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .6rem; margin: 1.2rem 0; } +.nj-jsonview-cmp > div { + border: 1px solid var(--md-default-fg-color--lightest); border-radius: .4rem; padding: .5rem .6rem .6rem; + background: var(--md-code-bg-color); min-width: 0; +} +.nj-jsonview-cmp > div.nj-jsonview-cmp--best { border-color: var(--jv-struct); } +.nj-jsonview-cmp .nj-jsonview-cmp-h { font-size: .7rem; font-weight: 600; margin: 0 0 .1rem; } +.nj-jsonview-cmp .nj-jsonview-cmp-v { font-size: .64rem; color: var(--md-default-fg-color--light); margin: 0 0 .4rem; } +.md-typeset .nj-jsonview-cmp pre { margin: 0; font-family: var(--md-code-font-family), monospace; font-size: .68rem; line-height: 1.5; white-space: pre; overflow-x: auto; background: none; color: var(--md-code-fg-color); } +.nj-jsonview-cmp .nj-jsonview-bad { color: var(--jv-bad); font-weight: 600; text-decoration: underline wavy; text-underline-offset: 3px; } +.nj-jsonview-cmp .nj-jsonview-ok { color: var(--jv-own); font-weight: 600; } +@media (max-width: 44.9375em) { + .nj-jsonview-cmp { grid-template-columns: minmax(0, 1fr); } +} diff --git a/docs/mkdocs/docs/css/fig-macros.css b/docs/mkdocs/docs/css/fig-macros.css new file mode 100644 index 000000000..07fdccd41 --- /dev/null +++ b/docs/mkdocs/docs/css/fig-macros.css @@ -0,0 +1,98 @@ +/* figures: macro chooser (js/fig-macros.js) on features/arbitrary_types.md; colours come from the brand + variables in brand.css, which switch with the light/slate scheme */ +.nj-macro-chooser { + --nj-mc-line: color-mix(in srgb, currentColor 18%, var(--md-default-bg-color)); + --nj-mc-panel: color-mix(in srgb, currentColor 3%, var(--md-default-bg-color)); + --nj-mc-ink2: var(--md-default-fg-color--light); + margin: 1.2em 0 1.6em; + font-size: .74rem; +} +.md-typeset .nj-macro-chooser .nj-mc-fallback > :first-child { margin-top: 0; } + +.nj-mc-app { border: 1px solid var(--nj-mc-line); border-radius: .3rem; background: var(--nj-mc-panel); + padding: .9rem; display: grid; grid-template-columns: minmax(0, 1fr); gap: 1rem; } +.nj-mc-form, .nj-mc-out, .nj-mc-editor, .nj-mc-members, .nj-mc-mbody { grid-template-columns: minmax(0, 1fr); } +.nj-mc-form { display: grid; gap: 1rem; margin: 0; } + +/* questions as segmented radio groups */ +.nj-mc-questions { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(14.5rem, 100%), 1fr)); gap: .7rem .9rem; } +.nj-mc-q { border: 0; margin: 0; padding: 0; min-width: 0; } +.nj-mc-q legend { font-weight: 600; padding: 0; margin-bottom: .3rem; font-size: .7rem; } +.nj-mc-seg { display: flex; border: 1px solid var(--nj-mc-line); border-radius: .25rem; overflow: hidden; + background: var(--md-default-bg-color); } +.nj-mc-seg input { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; pointer-events: none; } +.nj-mc-seg label { flex: 1 1 0; display: flex; align-items: center; justify-content: center; text-align: center; + min-height: 2.1rem; padding: .25rem .5rem; cursor: pointer; line-height: 1.25; font-size: .7rem; + color: var(--md-default-fg-color); transition: background-color .15s, color .15s; } +.nj-mc-seg label + input + label { border-left: 1px solid var(--nj-mc-line); } +.nj-mc-seg label:hover { background: var(--nj-blue-soft); } +.nj-mc-seg input:checked + label { background: var(--nj-blue); color: var(--md-default-bg-color); font-weight: 600; } +.nj-mc-seg input:focus-visible + label { outline: 2px solid var(--md-accent-fg-color); outline-offset: -3px; } +.nj-mc-q:disabled .nj-mc-seg { opacity: .45; } +.nj-mc-q:disabled label { cursor: not-allowed; } +.nj-mc-q:disabled label:hover { background: transparent; } +.md-typeset .nj-mc-qnote { margin: .3rem 0 0; font-size: .64rem; line-height: 1.4; color: var(--nj-mc-ink2); } + +/* type editor */ +.nj-mc-editor { display: grid; gap: .6rem; border-top: 1px solid var(--nj-mc-line); padding-top: .9rem; } +.nj-mc-names { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(11rem, 100%), 1fr)); gap: .6rem .9rem; } +.nj-mc-field { display: grid; gap: .2rem; } +.nj-mc-field label { font-weight: 600; font-size: .7rem; } +.nj-mc-field[hidden] { display: none; } +.nj-macro-chooser input[type="text"] { font-family: var(--md-code-font-family), monospace; font-size: .7rem; + min-height: 2.1rem; padding: .2rem .45rem; min-width: 0; width: 100%; box-sizing: border-box; + border: 1px solid var(--nj-mc-line); border-radius: .2rem; background: var(--md-default-bg-color); + color: var(--md-default-fg-color); } +.nj-macro-chooser input[type="text"]:focus { outline: 2px solid var(--md-accent-fg-color); outline-offset: 0; border-color: transparent; } + +.nj-mc-members { display: grid; gap: .35rem; } +.nj-mc-mbody { display: grid; gap: .35rem; } +.nj-mc-mrow { display: grid; grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr) 2.2rem; gap: .4rem; align-items: center; } +.nj-mc-custom .nj-mc-mrow { grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr) minmax(0, 1fr) 2.2rem; } +.nj-mc-mrow .nj-mc-keycol { display: none; } +.nj-mc-custom .nj-mc-mrow .nj-mc-keycol { display: block; } +.nj-mc-mhead { font-weight: 600; font-size: .66rem; color: var(--nj-mc-ink2); } +.nj-mc-mhead .nj-mc-keycol[hidden] { display: none; } + +.nj-macro-chooser button { font: inherit; cursor: pointer; border-radius: .2rem; } +.nj-macro-chooser button:focus-visible { outline: 2px solid var(--md-accent-fg-color); outline-offset: 2px; } +.nj-mc-rm { width: 2.2rem; height: 2.1rem; border: 1px solid var(--nj-mc-line); background: var(--md-default-bg-color); + color: var(--nj-mc-ink2); font-size: 1rem; line-height: 1; padding: 0; } +.nj-mc-rm:hover { color: var(--nj-red); border-color: var(--nj-red); } +.nj-mc-add, .nj-mc-copy { justify-self: start; min-height: 2.1rem; padding: .2rem .8rem; font-size: .7rem; font-weight: 600; + border: 1px solid var(--nj-blue); background: var(--md-default-bg-color); color: var(--nj-blue); } +.nj-mc-add:hover, .nj-mc-copy:hover { background: var(--nj-blue-soft); } + +/* output */ +.nj-mc-out { display: grid; gap: .7rem; } +.nj-mc-result { border-left: .2rem solid var(--nj-blue); background: var(--nj-blue-soft); border-radius: 0 .2rem .2rem 0; + padding: .55rem .8rem; } +.nj-mc-result.nj-mc-bad { border-left-color: var(--nj-red); background: var(--nj-red-soft); } +.md-typeset .nj-mc-result p { margin: 0; } +.md-typeset .nj-mc-result p + p { margin-top: .3rem; } +.md-typeset .nj-mc-label { font-size: .64rem; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; color: var(--nj-mc-ink2); } +.md-typeset .nj-mc-macro code { font-size: .8rem; font-weight: 600; background: transparent; padding: 0; + overflow-wrap: anywhere; color: var(--md-default-fg-color); } +.md-typeset .nj-mc-why { line-height: 1.5; } +.md-typeset .nj-mc-hints { margin: 0; padding-left: 1.1rem; font-size: .68rem; color: var(--nj-mc-ink2); } +.md-typeset .nj-mc-hints li { margin: 0; } +.nj-mc-hints[hidden], .nj-mc-code[hidden] { display: none; } + +.nj-mc-codebar { display: flex; align-items: center; gap: .6rem; margin-bottom: .3rem; } +.nj-mc-codetitle { font-weight: 600; font-size: .7rem; margin-right: auto; } +.nj-mc-copystatus { font-size: .66rem; color: var(--nj-mc-ink2); } +.md-typeset .nj-mc-code .highlight { margin: 0; } +.md-typeset .nj-mc-code pre { margin: 0; } +.md-typeset .nj-mc-code pre > code { max-height: 34rem; } + +@media (max-width: 44.9375em) { + .nj-mc-app { padding: .7rem; } + .nj-mc-seg label, .nj-macro-chooser input[type="text"], .nj-mc-add, .nj-mc-copy { min-height: 2.75rem; } + .nj-mc-rm { width: 2.75rem; height: 2.75rem; } + .nj-mc-mrow, .nj-mc-custom .nj-mc-mrow { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 2.75rem; } + .nj-mc-custom .nj-mc-mrow .nj-mc-keycol { grid-column: 1 / 3; } + .nj-mc-custom .nj-mc-mhead { display: none; } +} +@media (prefers-reduced-motion: reduce) { + .nj-mc-seg label { transition: none; } +} diff --git a/docs/mkdocs/docs/css/fig-pointer.css b/docs/mkdocs/docs/css/fig-pointer.css new file mode 100644 index 000000000..4f03eabcc --- /dev/null +++ b/docs/mkdocs/docs/css/fig-pointer.css @@ -0,0 +1,459 @@ +/* figures: pointer (features/json_pointer.md, json_patch.md, merge_patch.md; widgets in js/fig-pointer.js) */ + +/* value types: the colours of the code highlighting (brand.css), so a string looks the same everywhere */ +.nj-pointer-walk, .nj-pointer-fl, .nj-patch { + --nj-pointer-s: var(--nj-hl-string); + --nj-pointer-n: var(--nj-hl-number); + --nj-pointer-b: var(--nj-hl-function); + --nj-pointer-line: color-mix(in srgb, var(--md-default-fg-color) 28%, var(--md-default-bg-color)); + --nj-pointer-ok: #23793a; + --nj-pointer-ok-soft: #e6f4ea; +} +[data-md-color-scheme="slate"] :is(.nj-pointer-walk, .nj-pointer-fl, .nj-patch) { + --nj-pointer-ok: #7ed491; + --nj-pointer-ok-soft: #14301d; +} + +/* ---------- pointer walker ---------- */ +.md-typeset .nj-pointer-walk { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: 1rem 1.6rem; + margin: 1.5rem 0; + padding: 1rem 1.1rem; + border: 1px solid var(--md-default-fg-color--lightest); + border-radius: 6px; + font-size: .75rem; +} +@media screen and (min-width: 45em) { + .md-typeset .nj-pointer-walk--js { + grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr); + } + .md-typeset .nj-pointer-walk--js .nj-pointer-panel { order: 2; } +} +.md-typeset .nj-pointer-static { + margin: 0; + color: var(--md-default-fg-color--light); +} +.md-typeset .nj-pointer-walk code { + font-size: .95em; +} + +/* tree: elbow lines drawn by each item (::before up to its row, ::after down to the next sibling) */ +.md-typeset .nj-pointer-walk ul.nj-pointer-tree, +.md-typeset .nj-pointer-walk .nj-pointer-tree ul { + margin: 0; + padding: 0; + list-style: none; +} +.md-typeset .nj-pointer-walk .nj-pointer-tree ul { + padding-left: 1.5rem; +} +.md-typeset .nj-pointer-walk .nj-pointer-tree li { + position: relative; + margin: 0; + padding: 0; + list-style: none; + --nj-pointer-mid: .95rem; +} +.md-typeset .nj-pointer-walk .nj-pointer-tree li::before, +.md-typeset .nj-pointer-walk .nj-pointer-tree li::after { + content: none; +} +.md-typeset .nj-pointer-walk .nj-pointer-tree ul > li::before { + content: ""; + position: absolute; + top: 0; + left: -1rem; + width: .8rem; + height: var(--nj-pointer-mid); + border: 0 solid var(--nj-pointer-line); + border-width: 0 0 2px 2px; + border-bottom-left-radius: 4px; + background: none; +} +.md-typeset .nj-pointer-walk .nj-pointer-tree ul > li:not(:last-child)::after { + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: -1rem; + border-left: 2px solid var(--nj-pointer-line); +} +.md-typeset .nj-pointer-walk .nj-pointer-tree ul > li.is-on::before { + z-index: 1; + border-color: var(--md-typeset-a-color); +} +.md-typeset .nj-pointer-walk .nj-pointer-tree ul > li.is-pass::after { + z-index: 1; + border-left-color: var(--md-typeset-a-color); +} + +.md-typeset .nj-pointer-walk .nj-pointer-n { + display: inline-block; + margin: .15rem 0; + padding: .2rem .5rem; + border: 1px solid transparent; + border-radius: 6px; + background: none; + color: var(--md-default-fg-color); + font: inherit; + font-family: var(--md-code-font-family), monospace; + font-size: .95em; + line-height: 1.5; + text-align: left; +} +.md-typeset .nj-pointer-walk button.nj-pointer-n { + cursor: pointer; +} +.md-typeset .nj-pointer-walk button.nj-pointer-n:hover { + border-color: var(--md-default-fg-color--lightest); +} +.md-typeset .nj-pointer-walk :is(button):focus-visible { + outline: 2px solid var(--md-typeset-a-color); + outline-offset: 2px; +} +.nj-pointer-k { color: var(--nj-hl-key); } +.nj-pointer-t { + color: var(--md-default-fg-color--light); + font-family: var(--md-text-font-family), sans-serif; + font-size: .88em; + font-style: italic; +} +.nj-pointer-v--s { color: var(--nj-pointer-s); } +.nj-pointer-v--n { color: var(--nj-pointer-n); } +.nj-pointer-v--b { color: var(--nj-pointer-b); } + +.md-typeset .nj-pointer-walk li.is-on > .nj-pointer-n { + border-color: color-mix(in srgb, var(--md-typeset-a-color) 45%, transparent); +} +.md-typeset .nj-pointer-walk li.is-target > .nj-pointer-n { + border-color: var(--md-typeset-a-color); + background-color: var(--nj-blue-soft); + box-shadow: 0 0 0 1px var(--md-typeset-a-color); +} +/* the value a container pointer identifies includes its subtree */ +.md-typeset .nj-pointer-walk li.is-target li > .nj-pointer-n { + background-color: var(--nj-blue-soft); +} +.md-typeset .nj-pointer-walk li.is-miss > .nj-pointer-n { + border-color: var(--nj-red); + border-style: dashed; +} + +/* panel: pointer bar, result, example pointers */ +.nj-pointer-lbl { + margin: 0 0 .35rem; + color: var(--md-default-fg-color--light); + font-size: .62rem; + font-weight: 600; + letter-spacing: .04em; + text-transform: uppercase; +} +.nj-pointer-lbl span { + font-weight: 500; + letter-spacing: 0; + text-transform: none; +} +.nj-pointer-bar { + display: flex; + flex-wrap: wrap; + margin-bottom: .6rem; +} +.md-typeset .nj-pointer-seg { + margin: 0 -1px 0 0; + padding: .3rem .55rem; + border: 1px solid var(--md-typeset-a-color); + background-color: var(--md-default-bg-color); + color: var(--md-typeset-a-color); + cursor: pointer; + font: inherit; +} +.md-typeset .nj-pointer-seg:first-child { border-radius: 6px 0 0 6px; } +.md-typeset .nj-pointer-seg:last-child { border-radius: 0 6px 6px 0; } +.md-typeset .nj-pointer-seg:only-child { border-radius: 6px; } +.md-typeset .nj-pointer-seg code { color: inherit; font-size: 1em; } +.md-typeset .nj-pointer-seg--root code { color: var(--md-default-fg-color--light); } +.md-typeset .nj-pointer-seg.is-after { + border-color: var(--md-default-fg-color--lighter); + color: var(--md-default-fg-color--light); + border-style: dashed; +} +.md-typeset .nj-pointer-seg[aria-pressed="true"] { + position: relative; + background-color: var(--md-typeset-a-color); + color: var(--md-default-bg-color); +} +.md-typeset .nj-pointer-seg--root[aria-pressed="true"] code { color: inherit; } +.nj-pointer-out { + min-height: 3.2em; + margin-bottom: .9rem; + padding: .5rem .7rem; + border-left: 3px solid var(--md-typeset-a-color); + border-radius: 0 6px 6px 0; + background-color: var(--md-code-bg-color); + line-height: 1.6; + overflow-wrap: anywhere; +} +.md-typeset .nj-pointer-out .nj-pointer-val { color: var(--md-typeset-a-color); font-weight: 600; } +.nj-pointer-out .nj-pointer-note { color: var(--md-default-fg-color--light); } +.nj-pointer-out .nj-pointer-err { color: var(--nj-red); font-weight: 600; } +.nj-pointer-presets { + display: flex; + flex-wrap: wrap; + gap: .35rem; +} +.md-typeset .nj-pointer-chip { + padding: .2rem .5rem; + border: 1px solid var(--md-default-fg-color--lightest); + border-radius: 6px; + background-color: var(--md-default-bg-color); + color: var(--md-default-fg-color); + cursor: pointer; + font: inherit; +} +.md-typeset .nj-pointer-chip:hover { border-color: var(--md-typeset-a-color); } +.md-typeset .nj-pointer-chip[aria-pressed="true"] { + border-color: var(--md-typeset-a-color); + background-color: var(--nj-blue-soft); + color: var(--md-typeset-a-color); +} +.md-typeset .nj-pointer-chip code { color: inherit; font-size: 1em; } +@media (pointer: coarse), screen and (max-width: 44.984375em) { + .md-typeset :is(.nj-pointer-seg, .nj-pointer-chip, .nj-patch-btn) { min-height: 44px; min-width: 44px; } + .md-typeset .nj-pointer-walk button.nj-pointer-n { min-height: 40px; } +} + +/* ---------- flatten / unflatten figure (scripts/figures/json_pointer_flatten.py) ---------- */ +.md-typeset svg.nj-pointer-fl { min-width: 28rem; max-width: 44rem; } +.nj-pointer-fl .nj-pointer-fl-edge { fill: none; stroke: var(--fg-line); stroke-width: 1.5; } +.nj-pointer-fl .nj-pointer-fl-link { fill: none; stroke-width: 1.5; stroke-opacity: .75; } +.nj-pointer-fl .nj-pointer-fl-link.nj-pointer-fl--s, .nj-pointer-fl .nj-pointer-fl-dot.nj-pointer-fl--s { stroke: var(--nj-pointer-s); } +.nj-pointer-fl .nj-pointer-fl-link.nj-pointer-fl--n, .nj-pointer-fl .nj-pointer-fl-dot.nj-pointer-fl--n { stroke: var(--nj-pointer-n); } +.nj-pointer-fl .nj-pointer-fl-link.nj-pointer-fl--b, .nj-pointer-fl .nj-pointer-fl-dot.nj-pointer-fl--b { stroke: var(--nj-pointer-b); } +.nj-pointer-fl .nj-pointer-fl-dot { fill: var(--md-default-bg-color); stroke-width: 2; } +.nj-pointer-fl tspan.nj-pointer-fl--s { fill: var(--nj-pointer-s); } +.nj-pointer-fl tspan.nj-pointer-fl--n { fill: var(--nj-pointer-n); } +.nj-pointer-fl tspan.nj-pointer-fl--b { fill: var(--nj-pointer-b); } +.nj-pointer-fl .nj-pointer-fl-k, .nj-pointer-fl .nj-pointer-fl-inner, .nj-pointer-fl .nj-pointer-fl-key { fill: var(--nj-hl-key); } +.nj-pointer-fl .nj-pointer-fl-op { + fill: var(--md-typeset-a-color); + font-family: var(--md-code-font-family), monospace; + font-size: 15px; + font-weight: 600; + text-anchor: middle; +} + +/* ---------- JSON Patch stepper ---------- */ +.md-typeset .nj-patch--js .md-typeset__scrollwrap, +.md-typeset .nj-patch--js > .md-typeset__table, +.md-typeset .nj-patch--js > table { + display: none; +} +.md-typeset .nj-patch-stage { + margin: 1.5rem 0; + padding: 1rem 1.1rem; + border: 1px solid var(--md-default-fg-color--lightest); + border-radius: 6px; + font-size: .75rem; +} +.nj-patch-cols { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: 1rem 1.4rem; +} +@media screen and (min-width: 50em) { + .nj-patch-cols { grid-template-columns: minmax(0, .8fr) minmax(0, 1.2fr); } +} +.nj-patch-h { + margin: 0 0 .35rem; + color: var(--md-default-fg-color--light); + font-size: .62rem; + font-weight: 600; + letter-spacing: .04em; + text-transform: uppercase; +} +.nj-patch-doc { + --nj-patch-lh: 1.9rem; + --nj-patch-pad: .5rem; + position: relative; + padding: var(--nj-patch-pad) .6rem var(--nj-patch-pad) 2rem; + border: 1px solid var(--md-default-fg-color--lightest); + border-radius: 6px; + background-color: var(--md-code-bg-color); + font-family: var(--md-code-font-family), monospace; + font-size: .95em; +} +.nj-patch-line { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0 .6rem; + min-height: var(--nj-patch-lh); + margin: 0 -.3rem; + padding: 0 .3rem; + border-radius: 4px; + white-space: pre; +} +.nj-patch-code { white-space: pre; } +.nj-patch-k { color: var(--nj-hl-key); } +.nj-patch-s { color: var(--nj-pointer-s); } +.nj-patch-note { + padding: 0 .4rem; + border-radius: 4px; + font-family: var(--md-text-font-family), sans-serif; + font-size: .82em; + font-weight: 600; + white-space: nowrap; +} +.nj-patch-line--add { background-color: var(--nj-pointer-ok-soft); box-shadow: inset 3px 0 0 var(--nj-pointer-ok); } +.nj-patch-line--add .nj-patch-note { color: var(--nj-pointer-ok); } +.nj-patch-line--del { background-color: var(--nj-red-soft); box-shadow: inset 3px 0 0 var(--nj-red); } +.nj-patch-line--del .nj-patch-code { text-decoration: line-through; text-decoration-color: var(--nj-red); text-decoration-thickness: 2px; opacity: .75; } +.nj-patch-line--del .nj-patch-note { color: var(--nj-red); } +.nj-patch-line--mod .nj-patch-note { color: var(--nj-pointer-ok); } +.nj-patch-line--test { background-color: var(--nj-blue-soft); box-shadow: inset 3px 0 0 var(--md-typeset-a-color); } +.nj-patch-line--test .nj-patch-note { color: var(--md-typeset-a-color); } +.nj-patch-ins { padding: .05rem .15rem; border-radius: 3px; background-color: var(--nj-pointer-ok-soft); box-shadow: 0 0 0 1.5px var(--nj-pointer-ok); } +.nj-patch-src { padding: .05rem .15rem; border-radius: 3px; box-shadow: 0 0 0 1.5px var(--md-typeset-a-color); } + +/* move/copy connector in the left gutter: a bracket from the source line to the target line */ +.nj-patch-arc { + position: absolute; + left: .55rem; + width: .8rem; + border: 2px solid var(--md-typeset-a-color); + border-right: 0; + border-radius: 6px 0 0 6px; +} +.nj-patch-arc::after { + content: ""; + position: absolute; + right: -2px; + border: 5px solid transparent; + border-left: 7px solid var(--md-typeset-a-color); +} +.nj-patch-arc--down::after { bottom: -6px; } +.nj-patch-arc--up::after { top: -6px; } + +.md-typeset .nj-patch-ops { + margin: 0; + padding: 0; + list-style: none; + counter-reset: nj-patch; +} +.md-typeset .nj-patch-ops > li { + position: relative; + margin: 0 0 .3rem; + padding: .35rem .5rem .35rem 2rem; + border: 1px solid transparent; + border-radius: 6px; + counter-increment: nj-patch; + line-height: 1.5; +} +.md-typeset .nj-patch-ops > li::before { + content: counter(nj-patch); + position: absolute; + top: .35rem; + left: .6rem; + color: var(--md-default-fg-color--lighter); + font-weight: 600; + font-variant-numeric: tabular-nums; +} +.md-typeset .nj-patch-ops code { font-size: .9em; overflow-wrap: anywhere; } +.md-typeset .nj-patch-ops > li.is-done { opacity: .6; } +.md-typeset .nj-patch-ops > li.is-cur { + border-color: var(--md-typeset-a-color); + background-color: var(--nj-blue-soft); +} +.md-typeset .nj-patch-ops > li.is-cur::before { color: var(--md-typeset-a-color); } +.nj-patch-op { color: var(--nj-hl-keyword); font-weight: 600; } +.nj-patch-path { color: var(--nj-pointer-s); } +.md-typeset .nj-patch-ops > li.is-cur .nj-patch-path { + padding: 0 .15rem; + border-radius: 3px; + background-color: var(--md-typeset-a-color); + color: var(--md-default-bg-color); +} +.nj-patch-ctrl { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: .4rem; + margin-top: .9rem; +} +.md-typeset .nj-patch-btn { + padding: .3rem .9rem; + border: 1px solid var(--md-default-fg-color--lighter); + border-radius: 6px; + background-color: var(--md-default-bg-color); + color: var(--md-default-fg-color); + cursor: pointer; + font: inherit; + font-weight: 600; +} +.md-typeset .nj-patch-btn--primary { + border-color: var(--md-typeset-a-color); + background-color: var(--md-typeset-a-color); + color: var(--md-default-bg-color); +} +.md-typeset .nj-patch-btn:disabled { opacity: .45; cursor: default; } +.md-typeset .nj-patch-btn:focus-visible { outline: 2px solid var(--md-typeset-a-color); outline-offset: 2px; } +.nj-patch-count { + margin-left: auto; + color: var(--md-default-fg-color--light); + font-variant-numeric: tabular-nums; +} +.md-typeset .nj-patch-status { + min-height: 3.2em; + margin: .6rem 0 0; + color: var(--md-default-fg-color--light); +} +@media (prefers-reduced-motion: no-preference) { + .nj-patch-doc--anim :is(.nj-patch-line--add, .nj-patch-line--del, .nj-patch-line--test, .nj-patch-ins, .nj-patch-arc) { + animation: nj-patch-in 450ms ease-out; + } +} +@keyframes nj-patch-in { + from { opacity: 0; transform: translateX(-.4rem); } + to { opacity: 1; transform: none; } +} + +/* ---------- merge patch table ---------- */ +.nj-pointer-mpt { + --nj-pointer-mp-set: var(--nj-hl-keyword); + --nj-pointer-mp-add: #23793a; + --nj-pointer-mp-del: var(--nj-red); + --nj-pointer-mp-rec: var(--nj-hl-function); + --nj-pointer-mp-arr: var(--nj-hl-number); + --nj-pointer-mp-keep: var(--md-default-fg-color--light); +} +[data-md-color-scheme="slate"] .nj-pointer-mpt { --nj-pointer-mp-add: #7ed491; } +.md-typeset .nj-pointer-mpt td { vertical-align: top; } +.md-typeset .nj-pointer-mpt td:first-child { white-space: nowrap; } +.nj-pointer-mp { + display: inline-block; + padding: .05rem .5rem; + border: 1px solid currentColor; + border-radius: 999px; + background-color: color-mix(in srgb, currentColor 10%, var(--md-default-bg-color)); + font-size: .92em; + font-weight: 600; + line-height: 1.5; + white-space: nowrap; +} +.nj-pointer-mp--set { color: var(--nj-pointer-mp-set); } +.nj-pointer-mp--add { color: var(--nj-pointer-mp-add); } +.nj-pointer-mp--del { color: var(--nj-pointer-mp-del); } +.nj-pointer-mp--rec { color: var(--nj-pointer-mp-rec); } +.nj-pointer-mp--arr { color: var(--nj-pointer-mp-arr); } +.nj-pointer-mp--keep { color: var(--nj-pointer-mp-keep); } +.nj-pointer-mp-none { color: var(--md-default-fg-color--light); font-style: italic; } +.md-typeset .nj-pointer-mpt table:not([class]) td code { white-space: normal; } +.md-typeset .nj-pointer-mpt table:not([class]) td:nth-child(-n+2) code { white-space: nowrap; } +/* the result cell takes a tint of the rule's colour */ +.md-typeset .nj-pointer-mpt tr:has(.nj-pointer-mp--set) td:last-child { background-color: color-mix(in srgb, var(--nj-pointer-mp-set) 8%, transparent); } +.md-typeset .nj-pointer-mpt tr:has(.nj-pointer-mp--add) td:last-child { background-color: color-mix(in srgb, var(--nj-pointer-mp-add) 8%, transparent); } +.md-typeset .nj-pointer-mpt tr:has(.nj-pointer-mp--del) td:last-child { background-color: color-mix(in srgb, var(--nj-pointer-mp-del) 8%, transparent); } +.md-typeset .nj-pointer-mpt tr:has(.nj-pointer-mp--rec) td:last-child { background-color: color-mix(in srgb, var(--nj-pointer-mp-rec) 8%, transparent); } +.md-typeset .nj-pointer-mpt tr:has(.nj-pointer-mp--arr) td:last-child { background-color: color-mix(in srgb, var(--nj-pointer-mp-arr) 8%, transparent); } diff --git a/docs/mkdocs/docs/css/fig-sax.css b/docs/mkdocs/docs/css/fig-sax.css new file mode 100644 index 000000000..2b8bd645c --- /dev/null +++ b/docs/mkdocs/docs/css/fig-sax.css @@ -0,0 +1,207 @@ +/* figures: sax (SAX event stepper on features/parsing/sax_interface.md, js/fig-sax.js; memory_over_time.svg and + chunk_buffering.svg on features/parsing/large_and_partial_input.md). Event colours are the --cb-* variables of the + parser-callback figure in brand.css, which .nj-fig provides. */ + +/* ---------- stepper ---------- */ +.md-typeset .nj-sax { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: .9rem 1.4rem; + margin: 1.5rem 0; + padding: 1rem 1.1rem; + border: 1px solid var(--md-default-fg-color--lightest); + border-radius: 8px; + background: var(--md-default-bg-color); + font-size: .74rem; +} +@media screen and (min-width: 45em) { + .md-typeset .nj-sax { + grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr); + grid-template-areas: "ctl ctl" "text text" "ev state" "status status"; + } + .md-typeset .nj-sax > .nj-sax__controls { grid-area: ctl; } + .md-typeset .nj-sax > .nj-sax__lane--text { grid-area: text; } + .md-typeset .nj-sax > .nj-sax__lane--events { grid-area: ev; } + .md-typeset .nj-sax > .nj-sax__lane--state { grid-area: state; } + .md-typeset .nj-sax > .nj-sax__status { grid-area: status; } +} +.md-typeset .nj-sax p { margin: 0; } +.md-typeset .nj-sax .nj-sax__h { + margin: 0 0 .45rem; + color: var(--md-default-fg-color--light); + font-size: .68rem; + font-weight: 600; + letter-spacing: .02em; +} +.md-typeset .nj-sax .nj-sax__h code { font-size: .95em; } +.md-typeset .nj-sax .nj-sax__text code { + display: inline-block; + max-width: 100%; + overflow-x: auto; + padding: .35rem .6rem; + font-size: .95rem; + white-space: pre; + word-break: normal; +} +.nj-sax .nj-sax-unread { color: var(--md-default-fg-color--light); } +.nj-sax .nj-sax-unread--never { color: var(--md-default-fg-color--lighter); text-decoration: line-through; text-decoration-color: var(--md-default-fg-color--lighter); } +.nj-sax .nj-sax-read { color: var(--md-code-fg-color); } +.nj-sax .nj-sax-tok { border-radius: 3px; box-shadow: inset 0 -3px 0 var(--nj-sax-ev, var(--cb-v)); background: color-mix(in srgb, var(--nj-sax-ev, var(--cb-v)) 22%, transparent); } +.nj-sax .nj-sax-tok--os, .nj-sax .nj-sax-ev--os { --nj-sax-ev: var(--cb-os); } +.nj-sax .nj-sax-tok--oe, .nj-sax .nj-sax-ev--oe { --nj-sax-ev: var(--cb-oe); } +.nj-sax .nj-sax-tok--k, .nj-sax .nj-sax-ev--k { --nj-sax-ev: var(--cb-k); } +.nj-sax .nj-sax-tok--as, .nj-sax .nj-sax-ev--as { --nj-sax-ev: var(--cb-as); } +.nj-sax .nj-sax-tok--ae, .nj-sax .nj-sax-ev--ae { --nj-sax-ev: var(--cb-ae); } +.nj-sax .nj-sax-tok--v, .nj-sax .nj-sax-ev--v { --nj-sax-ev: var(--cb-v); } + +/* event ticker */ +.md-typeset .nj-sax ol.nj-sax__events { + margin: 0; + padding: 0; + list-style: none; + counter-reset: nj-sax; +} +.md-typeset .nj-sax ol.nj-sax__events > li { + position: relative; + margin: 0 0 .2rem; + padding: .12rem .4rem .12rem 2.6rem; + border-radius: 5px; + counter-increment: nj-sax; + line-height: 1.5; +} +.md-typeset .nj-sax ol.nj-sax__events > li::before { + content: counter(nj-sax); + position: absolute; + left: .35rem; + top: .2rem; + width: 1.1rem; + color: var(--md-default-fg-color--lighter); + font-size: .64rem; + font-variant-numeric: tabular-nums; + text-align: right; +} +.md-typeset .nj-sax ol.nj-sax__events > li::after { + content: ""; + position: absolute; + left: 1.75rem; + top: 50%; + width: .55rem; + height: .55rem; + margin-top: -.275rem; + border-radius: 50%; + background: var(--nj-sax-ev); +} +.md-typeset .nj-sax ol.nj-sax__events code { background: none; padding: 0; font-size: .8rem; } +.nj-sax .nj-sax-ev--todo { opacity: .42; } +.nj-sax .nj-sax-ev--skip { opacity: .42; text-decoration: line-through; } +.nj-sax .nj-sax-ev--skip::after { background: var(--md-default-fg-color--lighter) !important; } +.nj-sax .nj-sax-ev--cur { background: color-mix(in srgb, var(--nj-sax-ev) 16%, transparent); } +.md-typeset .nj-sax .nj-sax-ev--false > code::after { + content: " \2192 false"; + color: var(--nj-red); + font-weight: 600; +} + +/* handler state */ +.md-typeset .nj-sax pre.nj-sax__tree { margin: 0 0 .6rem; } +.md-typeset .nj-sax pre.nj-sax__tree > code { padding: .5rem .7rem; font-size: .76rem; line-height: 1.45; } +.nj-sax .nj-sax-hit { border-radius: 3px; background: color-mix(in srgb, var(--nj-blue) 22%, transparent); box-shadow: 0 0 0 1px color-mix(in srgb, var(--nj-blue) 55%, transparent); } +.nj-sax .nj-sax-dim { color: var(--md-default-fg-color--lighter); } +.md-typeset .nj-sax .nj-sax__stackh { margin: 0 0 .3rem; color: var(--md-default-fg-color--light); font-size: .66rem; font-weight: 600; } +.md-typeset .nj-sax ol.nj-sax__stack { + display: flex; + flex-direction: column-reverse; + gap: .25rem; + margin: 0 0 .6rem; + padding: 0; + list-style: none; +} +.md-typeset .nj-sax ol.nj-sax__stack > li { + margin: 0; + padding: .2rem .5rem; + border: 1px solid var(--md-default-fg-color--lightest); + border-left: 3px solid var(--nj-blue); + border-radius: 4px; +} +.md-typeset .nj-sax ol.nj-sax__stack > li.nj-sax-dim { border-left-color: var(--md-default-fg-color--lightest); } +.md-typeset .nj-sax ol.nj-sax__stack code { background: none; padding: 0; } +.md-typeset .nj-sax .nj-sax__note { color: var(--md-default-fg-color--light); font-size: .75rem; line-height: 1.5; } +.md-typeset .nj-sax dl.nj-sax__kv { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: .35rem .8rem; + margin: 0 0 .6rem; +} +.md-typeset .nj-sax dl.nj-sax__kv dt { color: var(--md-default-fg-color--light); font-weight: 600; } +.md-typeset .nj-sax dl.nj-sax__kv dd { margin: 0; } + +/* controls */ +.nj-sax .nj-sax__controls { display: flex; flex-wrap: wrap; gap: .5rem 1rem; align-items: center; } +.nj-sax .nj-sax__btns, .nj-sax .nj-sax__modes { display: flex; flex-wrap: wrap; gap: .35rem; } +.nj-sax .nj-sax__modes { margin-left: auto; } +.md-typeset .nj-sax .nj-sax-btn { + min-height: 2.2rem; + padding: .3rem .8rem; + border: 1px solid color-mix(in srgb, var(--nj-blue) 55%, var(--md-default-bg-color)); + border-radius: 6px; + background: var(--md-default-bg-color); + color: var(--nj-blue); + font: inherit; + font-weight: 600; + cursor: pointer; +} +.md-typeset .nj-sax .nj-sax-btn:hover:not(:disabled) { background: color-mix(in srgb, var(--nj-blue) 10%, var(--md-default-bg-color)); } +.md-typeset .nj-sax .nj-sax-btn--main, .md-typeset .nj-sax .nj-sax-btn[aria-pressed="true"] { + border-color: var(--nj-band); + background: var(--nj-band); + color: #fff; +} +.md-typeset .nj-sax .nj-sax-btn--main:hover:not(:disabled), .md-typeset .nj-sax .nj-sax-btn[aria-pressed="true"]:hover { + background: var(--md-primary-fg-color--light); +} +.md-typeset .nj-sax .nj-sax-btn:disabled { opacity: .4; cursor: default; } +.md-typeset .nj-sax .nj-sax-btn:focus-visible, .nj-sax .nj-sax__stop input:focus-visible { + outline: 2px solid var(--nj-blue); + outline-offset: 2px; +} +.nj-sax .nj-sax__stop { display: inline-flex; gap: .45rem; align-items: center; min-height: 2.2rem; cursor: pointer; } +.nj-sax .nj-sax__stop input { width: 1.05rem; height: 1.05rem; margin: 0; accent-color: var(--nj-red); } +.nj-sax .nj-sax__stop:has(input:disabled) { opacity: .45; cursor: default; } +.md-typeset .nj-sax .nj-sax__status { + padding-top: .6rem; + border-top: 1px solid var(--md-default-fg-color--lightest); + color: var(--md-default-fg-color--light); + font-size: .72rem; + min-height: 2.6em; +} +@media screen and (max-width: 44.984375em) { + .md-typeset .nj-sax { padding: .8rem; } + .md-typeset .nj-sax .nj-sax-btn { min-height: 44px; min-width: 44px; } + .nj-sax .nj-sax__stop { min-height: 44px; } + .nj-sax .nj-sax__modes { margin-left: 0; } +} +@media (prefers-reduced-motion: no-preference) { + .nj-sax .nj-sax__events > li { transition: opacity .2s, background-color .2s; } +} + +/* ---------- static SVG figures ---------- */ +.md-typeset svg.nj-fig--sax-mem { min-width: 34rem; max-width: 40rem; } +.md-typeset svg.nj-fig--sax-buf { min-width: 36rem; max-width: 42rem; } +.nj-fig .nj-sax-axis { fill: none; stroke: var(--fg-line); stroke-width: 1.5; } +.nj-fig .nj-sax-illus { font-style: italic; } +.nj-fig .nj-sax-curve { fill: none; stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; } +.nj-fig .nj-sax-c-dom { --nj-sax-c: var(--nj-red); } +.nj-fig .nj-sax-c-doc { --nj-sax-c: var(--nj-hl-type); } +.nj-fig .nj-sax-c-cb { --nj-sax-c: var(--nj-hl-number); } +.nj-fig .nj-sax-c-sax { --nj-sax-c: var(--nj-blue); } +.nj-fig .nj-sax-curve.nj-sax-c-doc { stroke-dasharray: 9 5; } +.nj-fig .nj-sax-curve { stroke: var(--nj-sax-c); } +.nj-fig .nj-sax-lab { fill: var(--nj-sax-c); font-size: 14.5px; font-weight: 700; } +.nj-fig .nj-sax-old { fill: color-mix(in srgb, currentColor 10%, var(--md-default-bg-color)); } +.nj-fig .nj-sax-new { fill: color-mix(in srgb, var(--md-typeset-a-color) 18%, var(--md-default-bg-color)); stroke: var(--md-typeset-a-color); stroke-width: 1; } +.nj-fig .nj-sax-buf { fill: currentColor; font-family: var(--md-code-font-family), monospace; font-size: 14px; white-space: pre; } +.nj-fig .nj-sax-sep { stroke: var(--fg-line); stroke-width: 1; stroke-dasharray: 3 3; } +.nj-fig .nj-sax-status { font-size: 14px; font-weight: 600; } +.nj-fig .nj-sax-s-wait { fill: var(--nj-blue); } +.nj-fig .nj-sax-s-ok { fill: var(--nj-hl-string); } +.nj-fig .nj-sax-s-err { fill: var(--nj-red); } diff --git a/docs/mkdocs/docs/features/arbitrary_types.md b/docs/mkdocs/docs/features/arbitrary_types.md index 4ead2aa4a..057a1a700 100644 --- a/docs/mkdocs/docs/features/arbitrary_types.md +++ b/docs/mkdocs/docs/features/arbitrary_types.md @@ -144,6 +144,8 @@ flowchart TD M -->|"no"| O["done"] ``` +

Decision tree for choosing a `NLOHMANN_DEFINE_*` macro: answer the questions from top to bottom; each answer picks one part of the macro name, and the parts are combined in this order. The table below lists the resulting macros.

+ | Need access to private members | Need only serialization | Allow missing values when de-serializing | macro | |------------------------------------------------------------------|------------------------------------------------------------------|------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------| |
:octicons-check-circle-fill-24:
|
:octicons-x-circle-fill-24:
|
:octicons-x-circle-fill-24:
| [**NLOHMANN_DEFINE_TYPE_INTRUSIVE**](../api/macros/nlohmann_define_type_intrusive.md) | @@ -153,6 +155,8 @@ flowchart TD |
:octicons-x-circle-fill-24:
|
:octicons-x-circle-fill-24:
|
:octicons-check-circle-fill-24:
| [**NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT**](../api/macros/nlohmann_define_type_non_intrusive.md) | |
:octicons-x-circle-fill-24:
|
:octicons-check-circle-fill-24:
|
:octicons-skip-fill-24:
| [**NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_ONLY_SERIALIZE**](../api/macros/nlohmann_define_type_non_intrusive.md) | +

Macros for classes and structs without a base class. A green check means yes, a red cross means no, and a grey skip sign means the column does not apply, as a macro that only serializes never reads values.

+ For _derived_ classes and structs, use the following macros | Need access to private members | Need only serialization | Allow missing values when de-serializing | macro | @@ -164,12 +168,33 @@ For _derived_ classes and structs, use the following macros |
:octicons-x-circle-fill-24:
|
:octicons-x-circle-fill-24:
|
:octicons-check-circle-fill-24:
| [**NLOHMANN_DEFINE_DERIVED_TYPE_NON_INTRUSIVE_WITH_DEFAULT**](../api/macros/nlohmann_define_derived_type.md) | |
:octicons-x-circle-fill-24:
|
:octicons-check-circle-fill-24:
|
:octicons-skip-fill-24:
| [**NLOHMANN_DEFINE_DERIVED_TYPE_NON_INTRUSIVE_ONLY_SERIALIZE**](../api/macros/nlohmann_define_derived_type.md) | +

Macros for derived classes and structs, read as Table 1.

+ !!! info "Implementation limits" - The current macro implementations are limited to at most 63 member variables. If you want to serialize/deserialize types with more than 63 member variables, you need to define the `to_json`/`from_json` functions manually. - For the `WITH_NAMES` variants the limit is halved to 31 member variables. +To pick the macro for your own type and get code to start from, answer the questions below. + +
+
+ +- Members are private: use an `INTRUSIVE` macro inside the class ([reference](../api/macros/nlohmann_define_type_intrusive.md){ data-nj-mc="intrusive" }). +- All members are public, or you cannot edit the class: use a `NON_INTRUSIVE` macro in the class's namespace ([reference](../api/macros/nlohmann_define_type_non_intrusive.md){ data-nj-mc="non_intrusive" }). +- The class adds members to a serializable base class: use a `DERIVED_TYPE` macro with the base class as second argument ([reference](../api/macros/nlohmann_define_derived_type.md){ data-nj-mc="derived" }). +- Missing keys should keep their default values: append `_WITH_DEFAULT`; otherwise a missing key throws. +- You only convert to JSON: append `_ONLY_SERIALIZE` instead. +- JSON keys differ from the member names: append `_WITH_NAMES` and write `"key", member` pairs ([reference](../api/macros/nlohmann_define_type_with_names.md){ data-nj-mc="with_names" }). +- Private members of a class you cannot edit: no macro works; write the conversion by hand ([third-party types](#how-do-i-convert-third-party-types){ data-nj-mc="third_party" }). +- The type is not default constructible: only `_ONLY_SERIALIZE` works ([non-default-constructible types](#how-can-i-use-get-for-non-default-constructiblenon-copyable-types){ data-nj-mc="non_default" }). + +
+
+ +

Macro chooser: each answer selects one part of the macro name; the generated program defines the type, calls the macro and converts a value to JSON and back. Combinations that no macro supports show the alternative instead of code.

+ ??? example "Example: using the `NLOHMANN_DEFINE_TYPE_*` macros" The `to_json`/`from_json` functions for the `person` struct above can be created with: @@ -299,6 +324,8 @@ flowchart LR B -->|"user specialization replaces the default"| E["user's adl_serializer specialization for T"] ``` +

How a conversion between `json` and a type `T` is dispatched: the default `adl_serializer` calls the free `to_json`/`from_json` functions found by argument-dependent lookup, while a user specialization of `adl_serializer` replaces this path.

+ The library uses **JSON Serializers** to convert types to JSON. The default serializer for `nlohmann::json` is `nlohmann::adl_serializer` (ADL means [Argument-Dependent Lookup](https://en.cppreference.com/w/cpp/language/adl)). diff --git a/docs/mkdocs/docs/features/binary_formats/index.md b/docs/mkdocs/docs/features/binary_formats/index.md index 8024b99cf..cb7368a65 100644 --- a/docs/mkdocs/docs/features/binary_formats/index.md +++ b/docs/mkdocs/docs/features/binary_formats/index.md @@ -53,7 +53,25 @@ See [binary values](../binary_values.md) for more information. | UBJSON (size) | 58.6 % | 92.3 % | 86.8 % | 97.4 % | | UBJSON (size+type) | 55.9 % | 92.3 % | 85.0 % | 95.0 % | -Sizes compared to minified JSON value; the smallest size per file is printed in bold. +

Size of each test file in each format, in percent of the minified JSON value; the smallest size per file is printed in bold. The (size) and (size+type) rows are written with the `use_size` and `use_type` options of [`to_bjdata`](../../api/basic_json/to_bjdata.md) and [`to_ubjson`](../../api/basic_json/to_ubjson.md).

+ +
+
+--8<-- "images/binary_sizes_canada.svg" +
+
+--8<-- "images/binary_sizes_twitter.svg" +
+
+--8<-- "images/binary_sizes_citm_catalog.svg" +
+
+--8<-- "images/binary_sizes_jeopardy.svg" +
+
+

The table above as charts: size per base format relative to minified JSON (vertical line at +100 %); the smallest format per file is drawn in a stronger colour than the others. The size and size+type variants of BJData and UBJSON are +omitted here; see the table.

## See also diff --git a/docs/mkdocs/docs/features/binary_values.md b/docs/mkdocs/docs/features/binary_values.md index b4c3bab6d..f3c240367 100644 --- a/docs/mkdocs/docs/features/binary_values.md +++ b/docs/mkdocs/docs/features/binary_values.md @@ -25,6 +25,8 @@ class vector ["std::vector"] vector <|-- binary_t ``` +

Class diagram of `json::binary_t`: it derives from `std::vector` and adds functions to set, clear, query, and test an optional subtype.

+ By default, binary values are stored as `std::vector`. This type can be changed by providing a template parameter to the `basic_json` type. To store binary subtypes, the storage type is extended and exposed as [`json::binary_t`](../api/basic_json/binary_t.md): diff --git a/docs/mkdocs/docs/features/element_access/checked_access.md b/docs/mkdocs/docs/features/element_access/checked_access.md index d8dab0104..592d5438a 100644 --- a/docs/mkdocs/docs/features/element_access/checked_access.md +++ b/docs/mkdocs/docs/features/element_access/checked_access.md @@ -89,3 +89,5 @@ non-existing, an exception is thrown. | access to valid array index | reference to existing value is returned | const reference to existing value is returned | | access to non-existing object key | `basic_json::out_of_range` exception is thrown | `basic_json::out_of_range` exception is thrown | | access to invalid array index | `basic_json::out_of_range` exception is thrown | `basic_json::out_of_range` exception is thrown | + +

Result of [`at`](../../api/basic_json/at.md) in each scenario, for a non-const and a const value.

diff --git a/docs/mkdocs/docs/features/element_access/index.md b/docs/mkdocs/docs/features/element_access/index.md index bb1029715..ce887ae30 100644 --- a/docs/mkdocs/docs/features/element_access/index.md +++ b/docs/mkdocs/docs/features/element_access/index.md @@ -12,16 +12,23 @@ Testing whether a key or index exists before accessing it is also possible, with [`contains`](../../api/basic_json/contains.md) or [`find`](../../api/basic_json/find.md) (which returns an iterator to the value, or `end()` if it is not found). +How do I get a value out? Start at the top and follow the first question that applies: + ```mermaid flowchart TD - A["accessing a value"] --> B{{"must it exist?"}} - B -->|"yes, missing is an error"| C["at() -- throws"] - B -->|"yes, but checking is my job"| D["operator[] -- unchecked"] - B -->|"no, a fallback is fine"| E["value() -- default value"] - A --> F{{"just testing first?"}} - F -->|"yes"| G["contains() / find()"] + A["getting a value out"] --> P{{"nested path such as /server/port?"}} + P -->|"yes"| JP["JSON Pointer: at(ptr), value(ptr, default), contains(ptr)"] + P -->|"no, one key or index"| T{{"only testing whether it exists?"}} + T -->|"yes"| CF["contains() / find()"] + T -->|"no"| M{{"may it be missing?"}} + M -->|"yes, a fallback is fine"| V["value(key, default)"] + M -->|"yes, and missing is an error"| AT["at(): throws out_of_range"] + M -->|"no, it exists and its type is known"| OP["operator[] and get#lt;T#gt;()"] + OP -.- W["non-const operator[] inserts null for a missing key"] ``` +

Decision tree for reading a value: the labels on the arrows are the answers to the questions, and the boxes name the functions to use. The dashed line marks a side effect of the non-const `operator[]`.

+ ## Does this key exist? Use [`contains`](../../api/basic_json/contains.md) to test for a key; it does not modify the value and does not throw: diff --git a/docs/mkdocs/docs/features/element_access/unchecked_access.md b/docs/mkdocs/docs/features/element_access/unchecked_access.md index 7e3f92ba1..39c1469c9 100644 --- a/docs/mkdocs/docs/features/element_access/unchecked_access.md +++ b/docs/mkdocs/docs/features/element_access/unchecked_access.md @@ -125,3 +125,5 @@ for (int i = 0; i < 1000; ++i) { | access to valid array index | reference to existing value is returned | const reference to existing value is returned | | access to non-existing object key | reference to newly inserted `#!json null` value is returned | **undefined behavior**; [runtime assertion](../assertions.md) in debug mode | | access to invalid array index | reference to newly inserted `#!json null` value is returned; any index between previous maximal index and passed index are filled with `#!json null` | **undefined behavior**; [runtime assertion](../assertions.md) in debug mode | + +

Result of [`operator[]`](../../api/basic_json/operator%5B%5D.md) in each scenario, for a non-const and a const value.

diff --git a/docs/mkdocs/docs/features/iterators.md b/docs/mkdocs/docs/features/iterators.md index 4523d2775..d05d2a3f9 100644 --- a/docs/mkdocs/docs/features/iterators.md +++ b/docs/mkdocs/docs/features/iterators.md @@ -13,6 +13,8 @@ placeholder and cannot be dereferenced. In case of null values, empty arrays, or --8<-- "images/range-begin-end.svg" +

The range of `begin()` and `end()`: `begin()` refers to the first element, `end()` to the position after the last element, which must not be dereferenced.

+ ### Iteration order for objects When iterating over objects, values are ordered with respect to the `object_comparator_t` type which defaults to `std::less`. See the [types documentation](types/index.md#key-order) for more information. @@ -118,6 +120,8 @@ sequence. --8<-- "images/range-rbegin-rend.svg" +

The range of `rbegin()` and `rend()` over the same elements: `rbegin()` refers to the last element and `rend()` to the position before the first one. Each reverse iterator stores an iterator to the element after the one it refers to.

+ ??? example "Example: reverse iteration with `rbegin()` and `rend()`" ```cpp diff --git a/docs/mkdocs/docs/features/json_patch.md b/docs/mkdocs/docs/features/json_patch.md index 878f0084c..ee854178f 100644 --- a/docs/mkdocs/docs/features/json_patch.md +++ b/docs/mkdocs/docs/features/json_patch.md @@ -7,6 +7,24 @@ of operations to apply to a JSON document. Operations address locations in the d [JSON Pointer](json_pointer.md) paths. With the [`patch`](../api/basic_json/patch.md) function, a JSON Patch is applied to the current JSON value by executing all operations from the patch, yielding the patched document as a new value. +The following steps through one operation of each kind. The first three operations are those of the example below; the +other three continue on its result. + +
+ +| Operation | Before | After | +|-------------------------------------------------------------|------------------------------------------------|-------------------------------------------------| +| `{"op": "replace", "path": "/baz", "value": "boo"}` | `{"baz": "qux", "foo": "bar"}` | `{"baz": "boo", "foo": "bar"}` | +| `{"op": "add", "path": "/hello", "value": ["world"]}` | `{"baz": "boo", "foo": "bar"}` | `{"baz": "boo", "foo": "bar", "hello": ["world"]}` | +| `{"op": "remove", "path": "/foo"}` | `{"baz": "boo", "foo": "bar", "hello": ["world"]}` | `{"baz": "boo", "hello": ["world"]}` | +| `{"op": "move", "from": "/baz", "path": "/hello/1"}` | `{"baz": "boo", "hello": ["world"]}` | `{"hello": ["world", "boo"]}` | +| `{"op": "copy", "from": "/hello/0", "path": "/greeting"}` | `{"hello": ["world", "boo"]}` | `{"greeting": "world", "hello": ["world", "boo"]}` | +| `{"op": "test", "path": "/greeting", "value": "world"}` | `{"greeting": "world", "hello": ["world", "boo"]}` | unchanged (a failed test throws) | + +
+ +

Stepper through the six operations: *Next* applies the next operation, *Back* undoes it, and *Reset* returns to the original document. The current operation is highlighted in the patch list, and the lines it changes are marked in the document.

+ !!! tip "Applying a patch without copying" [`patch`](../api/basic_json/patch.md) leaves the original value unchanged and returns the patched result as a copy. @@ -39,6 +57,8 @@ flowchart LR P -.->|"applied to source, yields"| T ``` +

How `diff` and `patch` relate: `diff(source, target)` yields a patch, and applying that patch to the source yields the target.

+ !!! success "Invariant" For two JSON values *source* and *target*, the following code yields always true: diff --git a/docs/mkdocs/docs/features/json_pointer.md b/docs/mkdocs/docs/features/json_pointer.md index 1bc5eabdb..c0c160e9b 100644 --- a/docs/mkdocs/docs/features/json_pointer.md +++ b/docs/mkdocs/docs/features/json_pointer.md @@ -18,6 +18,40 @@ Consider the following JSON document } ``` +Select a pointer (or a node of the tree) to walk it through this document; each segment of the pointer goes one level +deeper. The table below lists the same pointers. + +
+

Highlighted: the path of the pointer /nested/three/1, which identifies false.

+
    +
  • root object +
      +
    • "array" array +
        +
      • 0: "A"
      • +
      • 1: "B"
      • +
      • 2: "C"
      • +
      +
    • +
    • "nested" object +
        +
      • "one": 1
      • +
      • "two": 2
      • +
      • "three" array +
          +
        • 0: true
        • +
        • 1: false
        • +
        +
      • +
      +
    • +
    +
  • +
+
+ +

The highlighted nodes are the path from the root to the selected level, and the line below the tree shows the value the pointer resolves to. Select a segment in the pointer bar to stop at a shallower level, or a node of the tree to select its pointer.

+ Then every value inside the JSON document can be identified as follows: | JSON Pointer | JSON value | @@ -127,6 +161,14 @@ The reverse function, [`unflatten`](../api/basic_json/unflatten.md) recreates th auto j_original = j_flat.unflatten(); ``` +Each leaf of the document becomes one key of the flat object, and `unflatten()` reverses the mapping: + +
+--8<-- "images/json_pointer_flatten.svg" +
+ +

Left: the example document as a tree; right: the flat object it becomes. `flatten()` converts from left to right, `unflatten()` from right to left.

+ ## See also - Class [`json_pointer`](../api/json_pointer/index.md) diff --git a/docs/mkdocs/docs/features/json_view.md b/docs/mkdocs/docs/features/json_view.md index 4f0938273..f5d494249 100644 --- a/docs/mkdocs/docs/features/json_view.md +++ b/docs/mkdocs/docs/features/json_view.md @@ -33,6 +33,14 @@ index. It gives you the read-only, type-inspection part of the `basic_json` inte actual `basic_json` value for a subtree, [`materialize()`](../api/basic_json_view/materialize.md) builds exactly the one [`parse()`](../api/basic_json/parse.md) would have produced for it. +The same small text, parsed both ways: + +
+--8<-- "images/json_view_layout.svg" +
+ +

Top: the tree of separate heap allocations that `basic_json::parse()` builds. Bottom: `json_document::parse()` keeps the source text and builds one flat index of 16-byte nodes that point back into it, plus a buffer of decoded strings.

+ ## How to use it Include `` in addition to (or instead of) ``. Parse into a @@ -84,6 +92,12 @@ Moving the `basic_json_document<>` itself is fine and does **not** invalidate it allocation that keeps its address across the move. Take a fresh view from [`root()`](../api/basic_json_document/root.md) whenever any of the other conditions above was not met. +
+--8<-- "images/json_view_lifetime.svg" +
+ +

Four timelines of a document and its views, each bar showing when an object is alive or valid and when it dangles. Destroying the borrowed text, re-parsing, or shrinking the document invalidates the views taken before; moving the document does not.

+ A view can dangle, exactly like a `#!cpp std::string_view`: nothing stops you from using it after its document is gone. This is deliberate. A view is two pointers and trivially copyable; making it safe (a `#!cpp std::shared_ptr` in the document and a `#!cpp std::weak_ptr` in each view) would make it larger, add reference counting to every copy, @@ -274,6 +288,26 @@ or `ordered_json` value in place lossy. Say you parse a configuration file, patc - **`ordered_json`** keeps the key order, but still rewrites every number the same way: parsing has already reduced it to a `#!cpp double`/`#!cpp int64_t`, and there is no way back to how it was spelled in the source text. +The patch of the example below (set `"host"`, add `"retries"`) written back by each of them: + +
+

json

keys sorted, numbers rewritten

{"host":"db2",
+ "name":"cache",
+ "port":6379,
+ "price":19.9,
+ "retries":3}
+

ordered_json

key order kept, numbers rewritten

{"name":"cache",
+ "host":"db2",
+ "port":6379,
+ "price":19.9,
+ "retries":3}
+

json_editable_document

order and spelling kept

{"name":"cache",
+ "host":"db2",
+ "port":6379,
+ "price":19.90,
+ "retries":3}
+
+ An editable document keeps both. [`dump()`](../api/basic_json_view/dump.md) of an edited document writes members in document order -- a member [`set`](../api/basic_json_document/set.md) added goes at the end, exactly where it was inserted, and an [`erase`](../api/basic_json_document/erase.md)d member simply leaves a gap: everything around it @@ -301,8 +335,8 @@ parsed into for as long as it is not itself replaced. So every [view](../api/bas an edit, including a previously obtained [`root()`](../api/basic_json_document/root.md), stays valid and, if it still refers to the edited value, sees the edit; a view of a value a later edit drops or replaces just keeps showing what it last held. New values go to storage the document allocates and owns on demand. The one thing an edit does -invalidate is the **iterators** taken over an edited array or object: the first time one of its elements is set, -appended to, inserted into, or erased, its elements move from the parsed, fixed layout to a growable block of links +invalidate is the **iterators** taken over an edited array or object: the first time an element is added to it, +inserted into it, erased from it, or replaced by a value that spans a different number of nodes, its elements move from the parsed, fixed layout to a growable block of links so that [`push_back`](../api/basic_json_document/push_back.md) can later grow it in amortized constant time -- existing elements are not touched, but an iterator that was walking the old layout no longer matches. A string obtained with @@ -312,6 +346,43 @@ edits. See [`basic_json_document`'s Edits](../api/basic_json_document/index.md#e throws (the *basic* guarantee, not the strong one `dump()` and the read-only functions provide). How edits are kept in the index is described in the [architecture overview](../home/architecture.md#node-index-of-json-views). +Step through the edits of a small document (the node fields follow the [architecture overview](../home/architecture.md#node-index-of-json-views)): + +
+
+
+--8<-- "images/json_view_edit_0.svg" +
+
Step 1 of 5. After parsing: nine nodes in the parsed index, no owned storage yet.
+
+
+
+--8<-- "images/json_view_edit_1.svg" +
+
Step 2 of 5. doc.set(doc.root(), "port", 6380) overwrites the node of the integer in place; no member is added or removed, so the object keeps its parsed layout.
+
+
+
+--8<-- "images/json_view_edit_2.svg" +
+
Step 3 of 5. doc.erase(doc.root()["tags"], 0): the array is edited for the first time, so its elements move to a block of links in owned storage. The node of "eu" stays in the index, but nothing refers to it any more; iterators over the array taken before are invalid.
+
+
+
+--8<-- "images/json_view_edit_3.svg" +
+
Step 4 of 5. doc.push_back(doc.root()["tags"], "new"): the new string gets its own node, its text goes to the buffer of edited text, and a link to it is appended to the block of the array.
+
+
+
+--8<-- "images/json_view_edit_4.svg" +
+
Step 5 of 5. doc.set(doc.root(), "retries", 3): a new member, so the object moves to a block too: copies of its keys, links to the values it already had, then the new key and a link to the new integer.
+
+
+ +

The node index and the owned storage of the document after each of five edits. Use *Back*, *Next*, or the numbered buttons to switch between the steps; without JavaScript all steps are shown one below the other.

+ ## Images [`save()`](../api/basic_json_document/save.md) writes a document as an *image*: a byte buffer that @@ -373,6 +444,8 @@ either. | **Typical use** | general-purpose JSON handling: config, request/response bodies you build or modify, anything you hold onto | validating or projecting a text into your own data structure without ever holding the whole thing as JSON | large or high-volume input where you only need part of it, or need it repeatedly, and can keep the source text (or a copy) alive for as long as the document lives | a document you read, patch a few fields of, and write back -- a configuration file, for instance -- where the rest of it should come back exactly as it was | | **Caching/reload** | not applicable -- re-parse, or roll your own serialization | not applicable | [`save()`](../api/basic_json_document/save.md)/[`load()`](../api/basic_json_document/load.md): cache the parsed index as an image and reload it without parsing | same, saving the document's current -- possibly edited -- state | +

Each column is one way to work with JSON in this library, and each row compares one property of them.

+ ## Version history - Added in version 3.13.0. diff --git a/docs/mkdocs/docs/features/merge_patch.md b/docs/mkdocs/docs/features/merge_patch.md index ac732c7bf..cd3f5d658 100644 --- a/docs/mkdocs/docs/features/merge_patch.md +++ b/docs/mkdocs/docs/features/merge_patch.md @@ -12,6 +12,35 @@ easier to read and write for object-shaped documents. The function [`merge_diff`](../api/basic_json/merge_diff.md) creates a merge patch that changes one JSON value into another. +The example below shows every rule in one patch, member by member: + +
+ +| Rule | Key | Target | Patch | Result | +|------|-----|--------|-------|--------| +| change value | `title` | `#!json "Goodbye!"` | `#!json "Hello!"` | `#!json "Hello!"` | +| add key | `phoneNumber` | (no key) | `#!json "+01-123-456-7890"` | `#!json "+01-123-456-7890"` | +| null removes key | `author.familyName` | `#!json "Doe"` | `#!json null` | (removed) | +| object merges recursively | `author` | `#!json {"givenName": "John", "familyName": "Doe"}` | `#!json {"familyName": null}` | `#!json {"givenName": "John"}` | +| array replaced whole | `tags` | `#!json ["example", "sample"]` | `#!json ["example"]` | `#!json ["example"]` | +| unchanged key | `content` | `#!json "This will be unchanged"` | (no key) | `#!json "This will be unchanged"` | + +
+ +An array in the patch replaces the array in the target, so a merge patch cannot change a single element. Each member of +the patch is applied like this; a patch that is not an object replaces the whole target. + +```mermaid +flowchart LR + M["member of the patch"] --> N{{"value null?"}} + N -->|yes| D["delete the key"] + N -->|no| O{{"value an object?"}} + O -->|yes| R["merge recursively into the target's value"] + O -->|no| S["set the key to the value"] +``` + +

How one member of a merge patch is applied: a `null` value deletes the key, an object is merged recursively, and any other value replaces the value of the key.

+ !!! tip "Not a general deep merge" A merge patch is not a general deep merge: a `#!json null` value in the patch deletes the key from the target. diff --git a/docs/mkdocs/docs/features/modules.md b/docs/mkdocs/docs/features/modules.md index b87cb8950..84c01b216 100644 --- a/docs/mkdocs/docs/features/modules.md +++ b/docs/mkdocs/docs/features/modules.md @@ -4,6 +4,8 @@ This library has experimental support for C++ modules, introduced in C++20. The Please be aware that the module is experimental and a full test is outstanding, and the exported symbols are subject to change. +Importing the module instead of including the header reduces the compile time of every translation unit that uses the library, at the price of building the module once. The [Compile times](../integration/compile_times.md#use-c20-modules) page has measurements and the one-off cost. + ## Requirements The `nlohmann.json` module requires that the build system is configured to build and resolve modules when imported. Obviously, as modules were introduced in C++20, this feature can only be used in C++20 and subsequent versions. diff --git a/docs/mkdocs/docs/features/namespace.md b/docs/mkdocs/docs/features/namespace.md index 39cd58762..c76e5867a 100644 --- a/docs/mkdocs/docs/features/namespace.md +++ b/docs/mkdocs/docs/features/namespace.md @@ -40,6 +40,22 @@ For example, the namespace name for version 3.11.2 with `JSON_DIAGNOSTICS` defin nlohmann::json_abi_diag_v3_11_2 ``` +The name is assembled from these segments; pick a version and macros to see the resulting name: + +
+
+nlohmannroot namespace +::  +json_abialways +_diagJSON_DIAGNOSTICS=1 +_v3_11_2version 3.11.2 +
+
Version 3.11.2 with JSON_DIAGNOSTICS defined to 1. ABI tags are appended in the order of the +list above, the version suffix comes last.
+
+ +

The name of the inline namespace, assembled from its segments. Select a version and macros to see which ABI tags appear; a tag the chosen version does not have is disabled.

+ ## Purpose Several incompatibilities have been observed. Amongst the most common ones is linking code compiled with different @@ -57,6 +73,8 @@ graph app --> library ``` +

The application uses the library with `JSON_DIAGNOSTICS=1` and links `some library`, which uses the same library version with `JSON_DIAGNOSTICS=0`. Arrows point from a component to what it uses.

+ In releases prior to 3.11.0, mixing any version of the JSON library with different `JSON_DIAGNOSTICS` settings would result in a crashing application. If `some_library` never passes instances of JSON library types to the application, this scenario became safe in version 3.11.0 and above due to the inline namespace yielding distinct symbol names. diff --git a/docs/mkdocs/docs/features/parsing/index.md b/docs/mkdocs/docs/features/parsing/index.md index 447b65ffc..558272e3f 100644 --- a/docs/mkdocs/docs/features/parsing/index.md +++ b/docs/mkdocs/docs/features/parsing/index.md @@ -13,6 +13,8 @@ flowchart LR A --> V["bool: is the input valid JSON?"] ``` +

The three entry points for parsing: `parse()` builds a `basic_json` value, optionally filtered by a parser callback, `sax_parse()` delivers events to a SAX handler, and `accept()` only checks whether the input is valid.

+ ## Input The [`parse`](../../api/basic_json/parse.md) function reads a JSON value from an input. The input can be diff --git a/docs/mkdocs/docs/features/parsing/large_and_partial_input.md b/docs/mkdocs/docs/features/parsing/large_and_partial_input.md index d24b5fab1..1e2e1743a 100644 --- a/docs/mkdocs/docs/features/parsing/large_and_partial_input.md +++ b/docs/mkdocs/docs/features/parsing/large_and_partial_input.md @@ -17,6 +17,8 @@ flowchart TD H -->|"yes"| I["stop early with SAX, or read through a view"] ``` +

Questions about the input and the approach this page describes for each; an input can match more than one question.

+ ## Several values in one stream For newline-delimited input, read line by line and parse each line, as described in [JSON Lines](json_lines.md). For @@ -80,6 +82,14 @@ exception is one past the buffer size in the first case: The example feeds a document in five chunks. The first four are reported as incomplete, and a syntax error is reported as such, not as missing data. +The chunks of the example, and the bytes each attempt parses again: + +
+--8<-- "images/chunk_buffering.svg" +
+ +

Each row is one parse attempt on the growing buffer: an error at the byte after the end of the buffer means the value is incomplete, an error before the end is a real syntax error. The last line adds up the bytes parsed over all attempts.

+ !!! warning "Limits of buffering" - The buffer must contain exactly one value. Data that belongs to the next value is a syntax error. Use framing if @@ -93,6 +103,14 @@ such, not as missing data. The input does not have to become one huge `json` value. The two tools below process it piece by piece. +How the memory use develops while a large array of records is parsed (illustrative, not a measurement): + +
+--8<-- "images/memory_over_time.svg" +
+ +

Memory use over the number of records parsed: `parse()` keeps the whole tree, `json_document` the text plus its index, a discarding callback one record at a time, and SAX only what the handler keeps.

+ ### Handle and discard elements with a callback A [parser callback](parser_callbacks.md) is called for every finished value. If it returns `#!cpp false`, the value is diff --git a/docs/mkdocs/docs/features/parsing/sax_interface.md b/docs/mkdocs/docs/features/parsing/sax_interface.md index cfea680a4..70064c5bc 100644 --- a/docs/mkdocs/docs/features/parsing/sax_interface.md +++ b/docs/mkdocs/docs/features/parsing/sax_interface.md @@ -29,6 +29,8 @@ class sax_t ["json::sax_t"] { } ``` +

The interface `json::sax_t`: one function per event, each returning `bool`; the functions marked with `*` are pure virtual.

+ ```cpp // called when null is parsed bool null(); @@ -62,6 +64,34 @@ bool parse_error(std::size_t position, const std::string& last_token, const json The return value of each function determines whether parsing should proceed. +## Stepping through the events + +The events for a small input, step by step, and what two different handlers do with them. Press *Play* or step with *Next*: + +
+

input

{"a":[1,true],"b":"x"}

+

events, in the order the parser calls them

    +
  1. start_object(-1)
  2. +
  3. key("a")
  4. +
  5. start_array(-1)
  6. +
  7. number_unsigned(1)
  8. +
  9. boolean(true)
  10. +
  11. end_array()
  12. +
  13. key("b")
  14. +
  15. string("x")
  16. +
  17. end_object()
  18. +
+

handler: build a value tree, as parse does

{
+  "a": [
+    1,
+    true
+  ],
+  "b": "x"
+}

After end_object() the tree is complete and no container is open. The number of elements is passed as -1: the parser does not know it in advance.

+
+ +

The three lanes show the input consumed so far, the events called so far, and the state of the selected handler (*build a tree* or *keep only /b*). Check *return false at this event* to stop the parse at the current event and see which events are no longer called.

+ To implement your own SAX handler, proceed as follows: 1. Implement the SAX interface in a class. You can use class `nlohmann::json_sax` as base class, but you can also use any class where the functions described above are implemented and public. diff --git a/docs/mkdocs/docs/features/performance.md b/docs/mkdocs/docs/features/performance.md index 7ad7b187e..373c07112 100644 --- a/docs/mkdocs/docs/features/performance.md +++ b/docs/mkdocs/docs/features/performance.md @@ -96,6 +96,17 @@ comparison for text-heavy documents (63.5%-87.5%), at the cost of an whether it is worth the loss of human readability at all -- depends on the actual data; see the [comparison tables](binary_formats/index.md#comparison) before choosing one. +
+
+--8<-- "images/binary_sizes_canada.svg" +
+
+--8<-- "images/binary_sizes_jeopardy.svg" +
+
+

Two of the four panels from the [size comparison](binary_formats/index.md#sizes); +the smallest format per file is drawn in a stronger colour than the others.

+ ## Object type: `json` vs. `ordered_json` The default [`json`](../api/json.md) type stores object keys in a `#!cpp std::map`, giving logarithmic-time lookup, @@ -108,6 +119,13 @@ read. The [measurements on the `ordered_map` page](../api/ordered_map.md#complex negligible at typical object sizes (2000 keys: 0.7 ms for `json` vs. 3.6 ms for `ordered_json`, a 5x factor) but grows steeply for large, machine-generated objects (16 000 keys: 3.3 ms vs. 181.6 ms, a 54x factor). +
+--8<-- "images/ordered_json_scaling.svg" +
+

Parsing a flat object of `n` keys, measured with `-O2 -DNDEBUG`; all +four sizes from the [table on the `ordered_map` page](../api/ordered_map.md#complexity), both +axes logarithmic.

+ If insertion order matters *and* an object routinely has many thousands of keys, `ordered_json`'s quadratic build cost may not be acceptable. The library's [`ObjectType` template parameter](types/template_parameters.md#objecttype) can be set to a different container instead: `#!cpp nlohmann::fifo_map` keeps insertion order with a real lookup index diff --git a/docs/mkdocs/docs/features/types/index.md b/docs/mkdocs/docs/features/types/index.md index 03e8c0bae..22dc9da89 100644 --- a/docs/mkdocs/docs/features/types/index.md +++ b/docs/mkdocs/docs/features/types/index.md @@ -64,6 +64,8 @@ basic_json .. json_value basic_json .. value_t ``` +

Storage of a `basic_json` value: the enumeration `value_t` holds the type, and the union `json_value` holds the value, as a pointer for objects, arrays, strings, and binary values and directly for booleans and numbers.

+ ## Template arguments The data types to store a JSON value are derived from the template arguments passed to class `basic_json`: diff --git a/docs/mkdocs/docs/features/types/number_handling.md b/docs/mkdocs/docs/features/types/number_handling.md index d5e4ca6a5..d6b925b71 100644 --- a/docs/mkdocs/docs/features/types/number_handling.md +++ b/docs/mkdocs/docs/features/types/number_handling.md @@ -31,6 +31,8 @@ The following railroad diagram from [json.org](https://json.org) visualizes the --8<-- "images/json_syntax_number.svg" +

Read the diagram from left to right: every path along the lines spells a valid number, made of an optional minus sign, an integer part, an optional fraction, and an optional exponent.

+ ### Number interoperability On number interoperability, the following remarks are made: @@ -80,6 +82,8 @@ flowchart TD E -->|"overflow"| F ``` +

How the parser picks the storage type of a number literal: a fraction or exponent always gives `number_float_t`; otherwise the sign selects the signed or unsigned integer type, and a value that overflows it is stored as `number_float_t`.

+ !!! info "Notes" - Numbers with a decimal digit or scientific notation are always stored as `#!c double`. @@ -133,6 +137,32 @@ The JSON number grammar allows for different ways to express zero, and this libr That is, `-0` is stored as a signed integer, but the serialization does not reproduce the `-`. +The literals mentioned on this page, parsed with `json::parse` and serialized with `dump()` (checked against the +library; pick one or type it to see the row): + +
+ +| Literal | Stored as (`type()`) | `dump()` | +|---------|----------------------|----------| +| `0` | `number_unsigned` (`std::uint64_t`) | `0` | +| `-0` | `number_integer` (`std::int64_t`) | `0` | +| `-0.0` | `number_float` (`double`) | `-0.0` | +| `1E3` | `number_float` (`double`) | `1000.0` | +| `-12345678912345789123456789` | `number_float` (`double`) | `-1.2345678912345788e+25` | +| `18446744073709551615` | `number_unsigned` (`std::uint64_t`) | `18446744073709551615` | +| `18446744073709551616` | `number_float` (`double`) | `1.8446744073709552e+19` | +| `-9223372036854775808` | `number_integer` (`std::int64_t`) | `-9223372036854775808` | +| `1E400` | exception `out_of_range.406` | (nothing, parsing fails) | +| `1E-400` | `number_float` (`double`) | `0.0` | +| `3.141592653589793238462643383279` | `number_float` (`double`) | `3.141592653589793` | +| `2555.5599999999999` | `number_float` (`double`) | `2555.56` | +| `0.0000972439793401814` | `number_float` (`double`) | `9.72439793401814e-05` | +| `12345E-5` | `number_float` (`double`) | `0.12345` | + +
+ +

Each row shows how a literal is stored and how `dump()` writes it back. The explorer only knows the literals in this list; other input is reported as not in the verified list.

+ ### Number serialization - Integer numbers are serialized as is; that is, no scientific notation is used. @@ -331,6 +361,8 @@ the stored number: | [`type_name()`](../../api/basic_json/type_name.md) | `#!c "number"` | `#!c "number"` | `#!c "number"` | `#!c "string"` | | [`type()`](../../api/basic_json/type.md) | `number_unsigned` | `number_integer` | `number_float` | `string` | +

Return value of each function for a value that stores an unsigned integer, a signed integer, or a floating-point number, and for a string for comparison.

+ ### Template number types The number types can be changed with template parameters. diff --git a/docs/mkdocs/docs/home/architecture.md b/docs/mkdocs/docs/home/architecture.md index 295ea09ba..d88ac4e8e 100644 --- a/docs/mkdocs/docs/home/architecture.md +++ b/docs/mkdocs/docs/home/architecture.md @@ -32,6 +32,8 @@ flowchart LR oa --> output ``` +

Data flow through the library: input runs left to right through an input adapter and the lexer and parser (JSON text) or the `binary_reader` (binary formats) into SAX events, which build the value tree; the `serializer` or the `binary_writer` writes the tree to an output adapter.

+ - **JSON text** is read by an [input adapter](#input-adapters), tokenized by the lexer, and turned into SAX events by the parser. - **Binary formats** (BJData, BON8, BSON, CBOR, MessagePack, UBJSON) are read by an input adapter and turned into the same SAX @@ -192,6 +194,8 @@ packet-beta 96-127: "next" ``` +

Layout of one 16-byte node of a JSON view index. Each row is 32 bits wide, and the numbers are the bit offsets of the fields `kind`, `flags`, `extra`, `off`, `len`, and `next`.

+ | Bytes | Field | Type | Contents | |-------|---------|------------|-------------------------------------------------------------------------------------------------------------------------------| | 0 | `kind` | `uint8_t` | the type, numbered as [`value_t`](../api/basic_json/value_t.md): 0 null, 1 object, 2 array, 3 string, 4 boolean, 5 signed integer, 6 unsigned integer, 7 float; 10 for a link (see below). There is no node for a discarded value: a [discarded](../api/basic_json_view/is_discarded.md) view has none | @@ -201,6 +205,8 @@ packet-beta | 8-11 | `len` | `uint32_t` | strings: the length after decoding; floats and literals: the length of the token; arrays and objects: the number of elements | | 12-15 | `next` | `uint32_t` | arrays and objects: the number of nodes of the subtree, including the node itself | +

Fields of a node, by their byte offset within the 16-byte node. Integers use bytes 8-15 differently, see below.

+ - **Integers** keep their converted value in bytes 8-15 instead of `len` and `next`; the length of their token follows from the number of digits in `extra` (and the sign). Non-negative integers are unsigned integers, as with [`parse`](../api/basic_json/parse.md). @@ -234,6 +240,8 @@ flowchart LR n2 -. next .-> e ``` +

The five nodes of `#!json {"a": [1, 2.5]}` in document order. Solid arrows lead from each node to the one after it; dotted arrows show where `next` leads from the object and the array, past their subtrees.

+ | Node | `kind` | `extra` | `off` | `len` | `next` | |------|----------------------|---------|-------|-------|--------| | 0 | 1 (object) | 0 | 0 | 1 | 5 | @@ -242,6 +250,8 @@ flowchart LR | 3 | 6 (unsigned integer) | 0x0001 | 7 | - | - | | 4 | 7 (float) | 0x0101 | 10 | 3 | 0 | +

Fields of the five nodes of `#!json {"a": [1, 2.5]}`. `off` is the byte offset in the text, and - marks the bytes 8-15 that hold the integer's value instead of `len` and `next`.

+ All `flags` are 0. The integer's bytes 8-15 hold its value, 1; its `extra` says it has one digit. The float's `extra` says it has one integer and one fraction digit, and its `len` is that of the token `2.5`. @@ -287,8 +297,9 @@ template std::size_t get_elements(T* dest, std::size_t count = 1); ``` -The lexer detects two optional extensions at compile time. Only `iterator_input_adapter` provides them, and only for -random-access input of single-byte characters: +The lexer detects two optional extensions at compile time. `iterator_input_adapter` provides both, and only for +random-access input of single-byte characters. `input_stream_adapter` provides the second one (as a window onto the +streambuf's buffer) unless `JSON_DISABLE_STREAM_BULK_SCAN` is defined: - `supports_seek`, `get_consumed_count()`, and `copy_consumed_range()` let the lexer reconstruct already consumed input for error messages instead of copying every character it reads. With `get_read_count()` and @@ -306,6 +317,40 @@ The function `input_adapter` picks the right adapter for the argument passed to - `input_stream_adapter` reads from a `std::istream`. - `file_input_adapter` reads from a `std::FILE*`. +The adapters have no common base class: the lexer and the parser are templates on the adapter type, and +`wide_string_input_adapter` wraps an `iterator_input_adapter`: + +```mermaid +classDiagram + class input_adapter ["input adapter"] { + <> + +char_type + +get_character() int_type + +get_elements(T* dest, size_t count) size_t + } + class iterator_input_adapter { + -IteratorType current + -SentinelType end + } + class wide_string_input_adapter { + -BaseInputAdapter base_adapter + } + class input_stream_adapter { + -std::istream* is + -std::streambuf* sb + } + class file_input_adapter { + -std::FILE* m_file + } + input_adapter <|.. iterator_input_adapter + input_adapter <|.. wide_string_input_adapter + input_adapter <|.. input_stream_adapter + input_adapter <|.. file_input_adapter + wide_string_input_adapter o-- iterator_input_adapter : converts to UTF-8 +``` + +

The input adapters as a class diagram. The dotted arrows mean that each adapter implements the input adapter concept; the diamond shows that `wide_string_input_adapter` holds an `iterator_input_adapter`.

+ ## SAX interface The parser does not build values itself. It reports what it reads as events to a [SAX](../features/parsing/sax_interface.md) @@ -321,6 +366,7 @@ The library comes with two consumers in `detail/input/json_sax.hpp`: The `binary_reader` emits the same events for binary formats, so [`sax_parse`](../api/basic_json/sax_parse.md) works with a user-defined consumer for JSON and for all binary formats alike. +The [SAX event stepper](../features/parsing/sax_interface.md#sax-event-stepper) shows the events for a small input. ## Output adapters @@ -339,6 +385,36 @@ The `serializer` (used by [`dump`](../api/basic_json/dump.md) and [`operator<<`] - `output_stream_adapter` writes to a `std::ostream`. - `output_string_adapter` appends to a string. +They implement the abstract class `output_adapter_protocol`. The class `output_adapter` creates the one that matches +its argument and holds it in a `std::shared_ptr` (`output_adapter_t`): + +```mermaid +classDiagram + class output_adapter_protocol~CharType~ { + <> + +write_character(CharType c)* + +write_characters(const CharType* s, size_t length)* + } + class output_vector_adapter { + -std::vector& v + } + class output_stream_adapter { + -std::basic_ostream& stream + } + class output_string_adapter { + -StringType& str + } + class output_adapter { + -output_adapter_t oa + } + output_adapter_protocol <|-- output_vector_adapter + output_adapter_protocol <|-- output_stream_adapter + output_adapter_protocol <|-- output_string_adapter + output_adapter o-- output_adapter_protocol : shared_ptr +``` + +

The output adapters as a class diagram. The three adapters derive from `output_adapter_protocol` (solid arrows), and `output_adapter` holds one of them through a `shared_ptr` (diamond).

+ The `binary_writer` does not call an adapter for every byte. The `to_*` functions that return a `std::vector` write into the vector through a sink (`output_vector_sink`) that is not virtual. The functions that take an output adapter write through a sink (`output_adapter_sink`) that collects the output in a buffer of one kibibyte and passes it to the diff --git a/docs/mkdocs/docs/home/exceptions.md b/docs/mkdocs/docs/home/exceptions.md index 4b8f6edd9..3923ab0ce 100644 --- a/docs/mkdocs/docs/home/exceptions.md +++ b/docs/mkdocs/docs/home/exceptions.md @@ -6,35 +6,35 @@ All exceptions inherit from class `json::exception` (which in turn inherits from `std::exception`). It is used as the base class for all exceptions thrown by the `basic_json` class. This class can hence be used as "wildcard" to catch exceptions. -``` mermaid -classDiagram - direction LR - class `std::exception` { - <> - } +The map shows the class tree and every exception id documented on this page; click an id to jump to its section. - class `json::exception` { - +const int id - +const char* what() const - } +
+--8<-- "images/exception_map.svg" +
- class `json::parse_error` { - +const std::size_t byte - } +

The exception classes from `std::exception` down to the five families, each with the ids it can throw. Every id chip links to the section that describes it.

- class `json::invalid_iterator` - class `json::type_error` - class `json::out_of_range` - class `json::other_error` +Which exception an element access throws depends on the function and on the type of the value: - `std::exception` <|-- `json::exception` - `json::exception` <|-- `json::parse_error` - `json::exception` <|-- `json::invalid_iterator` - `json::exception` <|-- `json::type_error` - `json::exception` <|-- `json::out_of_range` - `json::exception` <|-- `json::other_error` +```mermaid +flowchart LR + S{{"which call?"}} -->|"at(key)"| A{{"j is"}} + A -->|"object, key missing"| A1["out_of_range.403"] + A -->|"array or null"| A2["type_error.304"] + S -->|"at(index)"| B{{"j is"}} + B -->|"array, index >= size()"| B1["out_of_range.401"] + B -->|"object or null"| B2["type_error.304"] + S -->|"non-const operator[](key)"| C{{"j is"}} + C -->|"object or null"| C1["no exception: a missing key is inserted as null"] + C -->|"array"| C2["type_error.305"] + S -->|"value(key, default)"| D{{"j is"}} + D -->|"object, key missing"| D1["no exception: returns the default"] + D -->|"object, incompatible value type"| D2["type_error.302"] + D -->|"array or null"| D3["type_error.306"] ``` +

Exceptions thrown by element access. Start at the call on the left, follow the edge that matches the type of `j`, and read the exception id (or "no exception") in the box at the end.

+ ### Switch off exceptions Exceptions are used widely within the library. They can, however, be switched off with either using the compiler flag `-fno-exceptions` or by defining the symbol [`JSON_NOEXCEPTION`](../api/macros/json_noexception.md). In this case, exceptions are replaced by `abort()` calls. You can further control this behavior by defining `JSON_THROW_USER` (overriding `#!cpp throw`), `JSON_TRY_USER` (overriding `#!cpp try`), and `JSON_CATCH_USER` (overriding `#!cpp catch`). diff --git a/docs/mkdocs/docs/home/faq.md b/docs/mkdocs/docs/home/faq.md index d52bdc006..6e4fbe8f6 100644 --- a/docs/mkdocs/docs/home/faq.md +++ b/docs/mkdocs/docs/home/faq.md @@ -192,6 +192,8 @@ The value is a `json`, and printing it produces JSON text. Convert it first: `#! Use `contains` or `find`. Do not test with the non-const `operator[]`: like `std::map::operator[]`, it inserts a `null` value for a missing key. See [Does this key exist?](../features/element_access/index.md#does-this-key-exist). +For choosing between `value`, `contains`/`find`, `at`, `operator[]`, and JSON Pointer, see the decision flow in +[Element access](../features/element_access/index.md). ### Reading a value with a fallback diff --git a/docs/mkdocs/docs/home/releases/index.md b/docs/mkdocs/docs/home/releases/index.md index e0dbca2d5..6fe1eafdf 100644 --- a/docs/mkdocs/docs/home/releases/index.md +++ b/docs/mkdocs/docs/home/releases/index.md @@ -14,6 +14,14 @@ changed in it. The API overview is generated from the "Version history" section [API pages](../../api/basic_json/index.md); it is not a change log, but helps to see whether a feature is available in the version you use. +All releases at a glance (hover a marker for its version and date): + +
+--8<-- "images/release_timeline.svg" +
+ +

Releases from 1.0.0 to the latest one, on a time axis by year. Filled dots with a version label are minor or major releases, rings are patch releases.

+ ## Unreleased | Release | Date | Summary | diff --git a/docs/mkdocs/docs/images/binary_sizes_canada.svg b/docs/mkdocs/docs/images/binary_sizes_canada.svg new file mode 100644 index 000000000..659c505ec --- /dev/null +++ b/docs/mkdocs/docs/images/binary_sizes_canada.svg @@ -0,0 +1,30 @@ + +Size of canada.json in each binary format relative to minified J​SON +B​JData 53.2 %; B​ON8 50.5 %; B​SON 85.8 %; C​BOR 50.5 %; MessagePack 50.5 %; U​BJSON 53.2 %. Smallest: B​ON8 and C​BOR and MessagePack. +canada.json + +0 % + +50 % +B​JData +canada.json, B​JData: 53.2 % of minified J​SON +B​ON8 +canada.json, B​ON8: 50.5 % of minified J​SON (smallest) +B​SON +canada.json, B​SON: 85.8 % of minified J​SON +C​BOR +canada.json, C​BOR: 50.5 % of minified J​SON (smallest) +MessagePack +canada.json, MessagePack: 50.5 % of minified J​SON (smallest) +U​BJSON +canada.json, U​BJSON: 53.2 % of minified J​SON + +minified J​SON = 100 % +100 % +53.2 % +50.5 % +85.8 % +50.5 % +50.5 % +53.2 % + diff --git a/docs/mkdocs/docs/images/binary_sizes_citm_catalog.svg b/docs/mkdocs/docs/images/binary_sizes_citm_catalog.svg new file mode 100644 index 000000000..814de63d2 --- /dev/null +++ b/docs/mkdocs/docs/images/binary_sizes_citm_catalog.svg @@ -0,0 +1,30 @@ + +Size of citm_catalog.json in each binary format relative to minified J​SON +B​JData 78.1 %; B​ON8 63.5 %; B​SON 95.8 %; C​BOR 68.4 %; MessagePack 68.5 %; U​BJSON 78.2 %. Smallest: B​ON8. +citm_catalog.json + +0 % + +50 % +B​JData +citm_catalog.json, B​JData: 78.1 % of minified J​SON +B​ON8 +citm_catalog.json, B​ON8: 63.5 % of minified J​SON (smallest) +B​SON +citm_catalog.json, B​SON: 95.8 % of minified J​SON +C​BOR +citm_catalog.json, C​BOR: 68.4 % of minified J​SON +MessagePack +citm_catalog.json, MessagePack: 68.5 % of minified J​SON +U​BJSON +citm_catalog.json, U​BJSON: 78.2 % of minified J​SON + +minified J​SON = 100 % +100 % +78.1 % +63.5 % +95.8 % +68.4 % +68.5 % +78.2 % + diff --git a/docs/mkdocs/docs/images/binary_sizes_jeopardy.svg b/docs/mkdocs/docs/images/binary_sizes_jeopardy.svg new file mode 100644 index 000000000..19ee8bb66 --- /dev/null +++ b/docs/mkdocs/docs/images/binary_sizes_jeopardy.svg @@ -0,0 +1,30 @@ + +Size of jeopardy.json in each binary format relative to minified J​SON +B​JData 96.6 %; B​ON8 87.5 %; B​SON 106.7 %; C​BOR 88 %; MessagePack 87.9 %; U​BJSON 96.6 %. Smallest: B​ON8. +jeopardy.json + +0 % + +50 % +B​JData +jeopardy.json, B​JData: 96.6 % of minified J​SON +B​ON8 +jeopardy.json, B​ON8: 87.5 % of minified J​SON (smallest) +B​SON +jeopardy.json, B​SON: 106.7 % of minified J​SON +C​BOR +jeopardy.json, C​BOR: 88 % of minified J​SON +MessagePack +jeopardy.json, MessagePack: 87.9 % of minified J​SON +U​BJSON +jeopardy.json, U​BJSON: 96.6 % of minified J​SON + +minified J​SON = 100 % +100 % +96.6 % +87.5 % +106.7 % +88.0 % +87.9 % +96.6 % + diff --git a/docs/mkdocs/docs/images/binary_sizes_twitter.svg b/docs/mkdocs/docs/images/binary_sizes_twitter.svg new file mode 100644 index 000000000..490f2cfe8 --- /dev/null +++ b/docs/mkdocs/docs/images/binary_sizes_twitter.svg @@ -0,0 +1,30 @@ + +Size of twitter.json in each binary format relative to minified J​SON +B​JData 91.1 %; B​ON8 83.8 %; B​SON 95.2 %; C​BOR 86.3 %; MessagePack 86 %; U​BJSON 91.3 %. Smallest: B​ON8. +twitter.json + +0 % + +50 % +B​JData +twitter.json, B​JData: 91.1 % of minified J​SON +B​ON8 +twitter.json, B​ON8: 83.8 % of minified J​SON (smallest) +B​SON +twitter.json, B​SON: 95.2 % of minified J​SON +C​BOR +twitter.json, C​BOR: 86.3 % of minified J​SON +MessagePack +twitter.json, MessagePack: 86 % of minified J​SON +U​BJSON +twitter.json, U​BJSON: 91.3 % of minified J​SON + +minified J​SON = 100 % +100 % +91.1 % +83.8 % +95.2 % +86.3 % +86.0 % +91.3 % + diff --git a/docs/mkdocs/docs/images/chunk_buffering.svg b/docs/mkdocs/docs/images/chunk_buffering.svg new file mode 100644 index 000000000..5b3c514d7 --- /dev/null +++ b/docs/mkdocs/docs/images/chunk_buffering.svg @@ -0,0 +1,50 @@ + +Buffering chunks until the value is complete: five chunks of {"name": "Mary", "list": [1, 2, 3]} appended to a buffer. +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. +chunk +buffer, parsed from byte 1 each time +result +chunk 1 + +{"na + +error at byte 5 = size + 1 +incomplete: wait for more +chunk 2 + + +{"name": "Ma + +error at byte 13 = size + 1 +incomplete: wait for more +chunk 3 + + +{"name": "Mary", "l + +error at byte 20 = size + 1 +incomplete: wait for more +chunk 4 + + +{"name": "Mary", "list": [1, 2 + +error at byte 31 = size + 1 +incomplete: wait for more +chunk 5 + + +{"name": "Mary", "list": [1, 2, 3]} +parsed, size 35 +complete: one value + +other input + +[1, ] + +error at byte 5, size 5 +before the end: a real error +Bytes parsed: 4 + 12 + 19 + 30 + 35 = 100 for a value of 35 bytes. +Every attempt starts again at byte 1, so the total work grows +quadratically with the size of the value. + diff --git a/docs/mkdocs/docs/images/compile_times.svg b/docs/mkdocs/docs/images/compile_times.svg new file mode 100644 index 000000000..e8e904a60 --- /dev/null +++ b/docs/mkdocs/docs/images/compile_times.svg @@ -0,0 +1,60 @@ + +Change in compile time against the default configuration at -O0, Apple clang and G​CC 16 +Grouped horizontal bars, negative is faster. json_fwd.hpp instead of json.hpp on a header-only translation unit: Apple clang -62 %, G​CC 16 -82 %; extern template on a parsing translation unit: Apple clang -7 %, G​CC 16 -21 %; JSON_NO_AUTOMATIC_UDLS on a model translation unit: Apple clang -11 %, G​CC 16 -11 %; JSON_NO_AUTOMATIC_UDLS on a parsing translation unit: Apple clang -1 %, G​CC 16 -2 %; JSON_HAS_FILESYSTEM=0 + JSON_NO_IO on a model translation unit: Apple clang -8 %, G​CC 16 -11 %; JSON_HAS_FILESYSTEM=0 on a model translation unit: Apple clang -5 %, G​CC 16 -9 %; JSON_NO_IO on a model translation unit: Apple clang -2 %, G​CC 16 -2 %; JSON_USE_GLOBAL_UDLS=0 on a model translation unit: Apple clang 0 %, G​CC 16 -1 %. + +Apple clang + +G​CC 16 + +−80 % + +−60 % + +−40 % + +−20 % +0 % +← faster +slower → +json_fwd.hpp instead of json.hpp · header-only TU +json_fwd.hpp instead of json.hpp, header-only TU (Apple clang): −62 % +−62 % +json_fwd.hpp instead of json.hpp, header-only TU (G​CC 16): −82 % +−82 % +extern template · parsing TU +extern template, parsing TU (Apple clang): −7 % +−7 % +extern template, parsing TU (G​CC 16): −21 % +−21 % +JSON_NO_AUTOMATIC_UDLS · model TU +JSON_NO_AUTOMATIC_UDLS, model TU (Apple clang): −11 % +−11 % +JSON_NO_AUTOMATIC_UDLS, model TU (G​CC 16): −11 % +−11 % +JSON_NO_AUTOMATIC_UDLS · parsing TU +JSON_NO_AUTOMATIC_UDLS, parsing TU (Apple clang): −1 % +−1 % +JSON_NO_AUTOMATIC_UDLS, parsing TU (G​CC 16): −2 % +−2 % +JSON_HAS_FILESYSTEM=0 + JSON_NO_IO · model TU +JSON_HAS_FILESYSTEM=0 + JSON_NO_IO, model TU (Apple clang): −8 % +−8 % +JSON_HAS_FILESYSTEM=0 + JSON_NO_IO, model TU (G​CC 16): −11 % +−11 % +JSON_HAS_FILESYSTEM=0 · model TU +JSON_HAS_FILESYSTEM=0, model TU (Apple clang): −5 % +−5 % +JSON_HAS_FILESYSTEM=0, model TU (G​CC 16): −9 % +−9 % +JSON_NO_IO · model TU* +JSON_NO_IO, model TU (Apple clang): −2 % +−2 % +JSON_NO_IO, model TU (G​CC 16): −2 % +−2 % +JSON_USE_GLOBAL_UDLS=0 · model TU* +JSON_USE_GLOBAL_UDLS=0, model TU (Apple clang): 0 % +0 % +JSON_USE_GLOBAL_UDLS=0, model TU (G​CC 16): −1 % +−1 % + + diff --git a/docs/mkdocs/docs/images/compile_times_modules.svg b/docs/mkdocs/docs/images/compile_times_modules.svg new file mode 100644 index 000000000..cb346e2b7 --- /dev/null +++ b/docs/mkdocs/docs/images/compile_times_modules.svg @@ -0,0 +1,30 @@ + +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. model TU, Apple clang: -48 % (1311 ms to 680 ms); model TU, G​CC 16: -47 % (2156 ms to 1153 ms); parsing TU, Apple clang: -42 % (1387 ms to 800 ms); parsing TU, G​CC 16: -36 % (2838 ms to 1810 ms); one-off build, Apple clang: 1630 ms; one-off build, G​CC 16: 2538 ms. + +Apple clang + +G​CC 16 + +−50 % + +−25 % +0 % +← faster per TU +model TU · import nlohmann.json +model TU, Apple clang: 1311 ms by default, 680 ms import nlohmann.json (−48 %) +−48 % +model TU, G​CC 16: 2156 ms by default, 1153 ms import nlohmann.json (−47 %) +−47 % +parsing TU · import nlohmann.json +parsing TU, Apple clang: 1387 ms by default, 800 ms import nlohmann.json (−42 %) +−42 % +parsing TU, G​CC 16: 2838 ms by default, 1810 ms import nlohmann.json (−36 %) +−36 % + +One-off cost · built once, reused by every TU + +Apple clang: 1630 ms once, saves 587 to 631 ms per TU + +G​CC 16: 2538 ms once, saves 1003 to 1028 ms per TU + diff --git a/docs/mkdocs/docs/images/compile_times_pch.svg b/docs/mkdocs/docs/images/compile_times_pch.svg new file mode 100644 index 000000000..95ddd970b --- /dev/null +++ b/docs/mkdocs/docs/images/compile_times_pch.svg @@ -0,0 +1,30 @@ + +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. model TU, Apple clang: -43 % (1045 ms to 594 ms); model TU, G​CC 16: -41 % (1789 ms to 1060 ms); parsing TU, Apple clang: -38 % (1148 ms to 714 ms); parsing TU, G​CC 16: -25 % (2498 ms to 1884 ms); one-off build, Apple clang: 771 ms; one-off build, G​CC 16: 2254 ms. + +Apple clang + +G​CC 16 + +−50 % + +−25 % +0 % +← faster per TU +model TU · with PCH +model TU, Apple clang: 1045 ms by default, 594 ms with PCH (−43 %) +−43 % +model TU, G​CC 16: 1789 ms by default, 1060 ms with PCH (−41 %) +−41 % +parsing TU · with PCH +parsing TU, Apple clang: 1148 ms by default, 714 ms with PCH (−38 %) +−38 % +parsing TU, G​CC 16: 2498 ms by default, 1884 ms with PCH (−25 %) +−25 % + +One-off cost · built once, reused by every TU + +Apple clang: 771 ms once, saves 434 to 451 ms per TU + +G​CC 16: 2254 ms once, saves 614 to 729 ms per TU + diff --git a/docs/mkdocs/docs/images/exception_map.svg b/docs/mkdocs/docs/images/exception_map.svg new file mode 100644 index 000000000..66931e2ff --- /dev/null +++ b/docs/mkdocs/docs/images/exception_map.svg @@ -0,0 +1,174 @@ + +Exception id map +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, 68 in total; every id links to its section. + + +std::exceptionC++ standard library base + +json::exceptionid, what() · catch-all + +json::parse_error1xx · 15 ids · adds member byte +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +112 +113 +114 +115 +116 + +json::invalid_iterator2xx · 14 ids +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 + +json::type_error3xx · 21 ids +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 + +json::out_of_range4xx · 16 ids +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 + +json::other_error5xx · 2 ids +501 +502 + + +Exception id map +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, 68 in total; every id links to its section. + + +std::exceptionC++ standard library base + +json::exceptionid, what() · catch-all + +json::parse_error1xx · 15 ids · adds member byte +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +112 +113 +114 +115 +116 + +json::invalid_iterator2xx · 14 ids +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 + +json::type_error3xx · 21 ids +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 + +json::out_of_range4xx · 16 ids +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 + +json::other_error5xx · 2 ids +501 +502 + diff --git a/docs/mkdocs/docs/images/json_pointer_flatten.svg b/docs/mkdocs/docs/images/json_pointer_flatten.svg new file mode 100644 index 000000000..93314187e --- /dev/null +++ b/docs/mkdocs/docs/images/json_pointer_flatten.svg @@ -0,0 +1,52 @@ + +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. + +nested value +flat object: key = J​SON Pointer + +flatten() + +unflatten() + + + + + + + + + + + + +"/array/0": "A" + + +"/array/1": "B" + + +"/array/2": "C" + + +"/nested/one": 1 + + +"/nested/two": 2 + + +"/nested/three/0": true + + +"/nested/three/1": false +root +"array" +0: "A" +1: "B" +2: "C" +"nested" +"one": 1 +"two": 2 +"three" +0: true +1: false + diff --git a/docs/mkdocs/docs/images/json_view_edit_0.svg b/docs/mkdocs/docs/images/json_view_edit_0.svg new file mode 100644 index 000000000..0768f9d23 --- /dev/null +++ b/docs/mkdocs/docs/images/json_view_edit_0.svg @@ -0,0 +1,57 @@ + +Step 1 of 5: parsed +After parse: nine nodes, nothing owned yet. The dump with number_format::source is {"port":6379,"price":19.90,"tags":["eu","a\"b"]}. + + + + + + +parsed index: fixed, never moves +the source text is never written + +0 +object +{ } 3 + +1 +key +port + +2 +uint +6379 + +3 +key +price + +4 +float +19.90 + +5 +key +tags + +6 +array +[ ] 2 + +7 +string +"eu" + +8 +string +a"b +owned storage: allocated on demand, never moves + +empty: a read-only json_document has none of this + +view v = root()["tags"] (taken after parse): +valid, sees every edit +iterator over tags (taken after parse): +valid +dump(): {"port":6379,"price":19.90,"tags":["eu","a\"b"]} + diff --git a/docs/mkdocs/docs/images/json_view_edit_1.svg b/docs/mkdocs/docs/images/json_view_edit_1.svg new file mode 100644 index 000000000..44883e7e1 --- /dev/null +++ b/docs/mkdocs/docs/images/json_view_edit_1.svg @@ -0,0 +1,57 @@ + +Step 2 of 5: doc.set(doc.root(), "port", 6380) +set: the node of the integer is overwritten in place. The dump with number_format::source is {"port":6380,"price":19.90,"tags":["eu","a\"b"]}. + + + + + + +parsed index: fixed, never moves +the source text is never written + +0 +object +{ } 3 + +1 +key +port + +2 +uint +6380 + +3 +key +price + +4 +float +19.90 + +5 +key +tags + +6 +array +[ ] 2 + +7 +string +"eu" + +8 +string +a"b +owned storage: allocated on demand, never moves + +empty: a read-only json_document has none of this + +view v = root()["tags"] (taken after parse): +valid, sees every edit +iterator over tags (taken after parse): +valid +dump(): {"port":6380,"price":19.90,"tags":["eu","a\"b"]} + diff --git a/docs/mkdocs/docs/images/json_view_edit_2.svg b/docs/mkdocs/docs/images/json_view_edit_2.svg new file mode 100644 index 000000000..159af3a4a --- /dev/null +++ b/docs/mkdocs/docs/images/json_view_edit_2.svg @@ -0,0 +1,70 @@ + +Step 3 of 5: doc.erase(doc.root()["tags"], 0) +erase: the array moves to a block of links in owned storage. The dump with number_format::source is {"port":6380,"price":19.90,"tags":["a\"b"]}. + + + + + + +parsed index: fixed, never moves +the source text is never written + +0 +object +{ } 3 + +1 +key +port + +2 +uint +6380 + +3 +key +price + +4 +float +19.90 + +5 +key +tags + +6 +array +moved +[ ] 2 + +7 +string +"eu" +unreferenced + +8 +string +a"b +owned storage: allocated on demand, never moves + + +0' +header +[ ] 1 + +1' +link +→ 8 + + +free + + +view v = root()["tags"] (taken after parse): +valid, sees every edit +iterator over tags (taken after parse): +invalid: tags no longer uses the parsed layout +dump(): {"port":6380,"price":19.90,"tags":["a\"b"]} + diff --git a/docs/mkdocs/docs/images/json_view_edit_3.svg b/docs/mkdocs/docs/images/json_view_edit_3.svg new file mode 100644 index 000000000..562977c49 --- /dev/null +++ b/docs/mkdocs/docs/images/json_view_edit_3.svg @@ -0,0 +1,80 @@ + +Step 4 of 5: doc.push_back(doc.root()["tags"], "new") +push_back: a new node, and a link appended to the block. The dump with number_format::source is {"port":6380,"price":19.90,"tags":["a\"b","new"]}. + + + + + + +parsed index: fixed, never moves +the source text is never written + +0 +object +{ } 3 + +1 +key +port + +2 +uint +6380 + +3 +key +price + +4 +float +19.90 + +5 +key +tags + +6 +array +moved +[ ] 2 + +7 +string +"eu" +unreferenced + +8 +string +a"b +owned storage: allocated on demand, never moves + + +0' +header +[ ] 2 + +1' +link +→ 8 + + +2' +link +→ 3' + + +3' +string +"new" + +edited text + +new + +view v = root()["tags"] (taken after parse): +valid, sees every edit +iterator over tags (taken after parse): +invalid: tags no longer uses the parsed layout +dump(): {"port":6380,"price":19.90,"tags":["a\"b","new"]} + diff --git a/docs/mkdocs/docs/images/json_view_edit_4.svg b/docs/mkdocs/docs/images/json_view_edit_4.svg new file mode 100644 index 000000000..29e6bc504 --- /dev/null +++ b/docs/mkdocs/docs/images/json_view_edit_4.svg @@ -0,0 +1,126 @@ + +Step 5 of 5: doc.set(doc.root(), "retries", 3) +set of a new member: the object moves to a block as well. The dump with number_format::source is {"port":6380,"price":19.90,"tags":["a\"b","new"],"retries":3}. + + + + + + +parsed index: fixed, never moves +the source text is never written + +0 +object +moved +{ } 3 + +1 +key +port + +2 +uint +6380 + +3 +key +price + +4 +float +19.90 + +5 +key +tags + +6 +array +moved +[ ] 2 + +7 +string +"eu" +unreferenced + +8 +string +a"b +owned storage: allocated on demand, never moves + + +0' +header +[ ] 2 + +1' +link +→ 8 + + +2' +link +→ 3' + + +3' +string +"new" + +4' +uint +3 + +5' +header +{ } 4 + +6' +key +port + +7' +link +→ 2 + + +8' +key +price + +9' +link +→ 4 + + +10' +key +tags + +11' +link +→ 6 + + +12' +key +retries + +13' +link +→ 4' + + + +edited text + +newretries + +view v = root()["tags"] (taken after parse): +valid, sees every edit +iterator over tags (taken after parse): +invalid: tags no longer uses the parsed layout +dump(): {"port":6380,"price":19.90,"tags":["a\"b","new"],"retries":3} + diff --git a/docs/mkdocs/docs/images/json_view_layout.svg b/docs/mkdocs/docs/images/json_view_layout.svg new file mode 100644 index 000000000..72277c1f1 --- /dev/null +++ b/docs/mkdocs/docs/images/json_view_layout.svg @@ -0,0 +1,182 @@ + +The same J​SON 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. + + + + + + + + + + +basic_json::parse(): a tree, every array, object, and string in its own heap allocation + +json + + +std::map + + +"port" +6379 + + +"price" +19.9 +double: 19.90 is gone + + +"tags" +• + + +std::vector + + + +json +json + + + +std::string eu + +std::string a"b + +a separate heap allocation + +json_document::parse(): the source text stays where it is; one flat index of 16-byte nodes +source text, 54 bytes (borrowed, or owned by the document) + + + + + + + + +{ +" +p +o +r +t +" +: +6 +3 +7 +9 +, +" +p +r +i +c +e +" +: +1 +9 +. +9 +0 +, +" +t +a +g +s +" +: +[ +" +e +u +" +, +" +a +\ +" +b +" +] +} +0 +10 +20 +30 +40 +50 + +json_document: owns the node index and the buffer of decoded strings +node index: 9 nodes × 16 bytes = 144 bytes, in document order + +0 +object +len 3 +next 9 + +1 +key +off 2 +len 4 + + +2 +uint +6379 +(converted) + +3 +key +off 16 +len 5 + + +4 +float +off 24 +len 5 + + +5 +key +off 32 +len 4 + + +6 +array +len 2 +next 3 + +7 +string +off 41 +len 2 + + +8 +string +escaped +len 3 +decoded strings + +a +" +b + +json_view v = doc.root()["tags"] + + +doc* +node* + + +integers are converted while parsing and kept in their node; +floats and strings point back into the text + diff --git a/docs/mkdocs/docs/images/json_view_lifetime.svg b/docs/mkdocs/docs/images/json_view_lifetime.svg new file mode 100644 index 000000000..8936b07c7 --- /dev/null +++ b/docs/mkdocs/docs/images/json_view_lifetime.svg @@ -0,0 +1,61 @@ + +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. + + + + +alive / valid + +dangling: using it is undefined behavior + +time → +The borrowed text is destroyed +std::string text + +json_document doc + + +json_view v + + + +text destroyed +every view, and the document itself +The document is re-parsed +json_document doc + +json_view v + + +v = doc.root() + + +doc.read(other) +views taken before +a fresh view is valid again +The document is shrunk +json_document doc + +json_view v + + +v = doc.root() + + +doc.shrink_to_fit() +views taken before +a fresh view is valid again +The document is moved, then destroyed +json_document doc + +moved = std::move(doc) + +json_view v + + + +std::move: views stay valid + +moved is destroyed + diff --git a/docs/mkdocs/docs/images/memory_over_time.svg b/docs/mkdocs/docs/images/memory_over_time.svg new file mode 100644 index 000000000..46b539ec8 --- /dev/null +++ b/docs/mkdocs/docs/images/memory_over_time.svg @@ -0,0 +1,20 @@ + +Illustrative memory use over time while a large array of records is parsed, not a measurement. +Four curves. D​OM, 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. S​AX: stays flat and lowest, because no value is built and only what the handler keeps is in memory. + +memory +time: records parsed +illustrative, not a measurement + + + + +D​OM: parse() +the whole tree +json_document +the text plus its index +callback, discard +one record at a time +S​AX +only what the handler keeps + diff --git a/docs/mkdocs/docs/images/ordered_json_scaling.svg b/docs/mkdocs/docs/images/ordered_json_scaling.svg new file mode 100644 index 000000000..fc6b55572 --- /dev/null +++ b/docs/mkdocs/docs/images/ordered_json_scaling.svg @@ -0,0 +1,31 @@ + +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). + +1 ms + +10 ms + +100 ms + +2 000 +4 000 +8 000 +16 000 + +json, 2000 keys: 0.7 ms +json, 4000 keys: 0.8 ms +json, 8000 keys: 1.6 ms +json, 16000 keys: 3.3 ms +json +3.3 ms +0.7 ms + +ordered_json, 2000 keys: 3.6 ms +ordered_json, 4000 keys: 14 ms +ordered_json, 8000 keys: 67.8 ms +ordered_json, 16000 keys: 181.6 ms +ordered_json +181.6 ms +3.6 ms + diff --git a/docs/mkdocs/docs/images/qa_pipeline.svg b/docs/mkdocs/docs/images/qa_pipeline.svg new file mode 100644 index 000000000..4bf54e1e2 --- /dev/null +++ b/docs/mkdocs/docs/images/qa_pipeline.svg @@ -0,0 +1,240 @@ + +Quality assurance pipeline +Every push or pull request starts these checks in parallel: Build matrix (every push / PR): 50+ compilers: G​CC, Clang, M​SVC, AppleClang, Intel, NVIDIA, ... C++11, 14, 17, 20, 23, and 26 libc++ and libstdc++ warnings: Clang -Weverything, G​CC 300+ warnings; Unit tests (doctest) (every push / PR): every public A​PI function every exception id and message all Unicode code points single header and split headers custom number, string, array, and object types; Coverage (every push / PR): unit tests cover all lines of the code base; Static analysis (every push / PR, scheduled): Clang-Tidy, Cppcheck, Infer Clang Static Analyzer (52 checkers), I​WYU, Codacy CodeQL, Semgrep, Flawfinder (also scheduled); Dynamic analysis (every push / PR): runtime assertions Valgrind (Memcheck) address and undefined behavior sanitizers, integer overflow, nullability violations; Fuzzing (every PR, continuous): CIFuzz on every pull request OSS-Fuzz: billions of inputs every crash fixed with a reproducing unit test; Style (every push / PR): Artistic Style (astyle), enforced in the C​I cpplint with 61 rules; Docs and examples (every push / PR, scheduled): every documentation example is tested; changed output fails site built with warnings as errors; links checked regularly (scheduled); Integration, supply chain (every push / PR, scheduled): CMake 3.5, 3.31.6, and 4.0.0 Meson, Swift Package Manager dependency review on every PR Dependabot daily, OpenSSF Scorecard, R​EUSE. When they pass, the change is merged into develop; a release is made only when no vulnerability alert is open. + + + +push or pull request + +checks, run in parallel on GitHub Actions and AppVeyor + + +Build matrix + +every push / PR +50+ compilers: G​CC, Clang, M​SVC, +AppleClang, Intel, NVIDIA, ... +C++11, 14, 17, 20, 23, and 26 +libc++ and libstdc++ +warnings: Clang -Weverything, +G​CC 300+ warnings + + + +Unit tests (doctest) + +every push / PR +every public A​PI function +every exception id and message +all Unicode code points +single header and split headers +custom number, string, array, +and object types + + + +Coverage + +every push / PR +unit tests cover all lines +of the code base + + + +Static analysis + +every push / PR + +scheduled +Clang-Tidy, Cppcheck, Infer +Clang Static Analyzer (52 +checkers), I​WYU, Codacy +CodeQL, Semgrep, Flawfinder +(also scheduled) + + + +Dynamic analysis + +every push / PR +runtime assertions +Valgrind (Memcheck) +address and undefined behavior +sanitizers, integer overflow, +nullability violations + + + +Fuzzing + +every PR + +continuous +CIFuzz on every pull request +OSS-Fuzz: billions of inputs +every crash fixed with a +reproducing unit test + + + +Style + +every push / PR +Artistic Style (astyle), +enforced in the C​I +cpplint with 61 rules + + + +Docs and examples + +every push / PR + +scheduled +every documentation example is +tested; changed output fails +site built with warnings +as errors; links checked +regularly (scheduled) + + + +Integration, supply chain + +every push / PR + +scheduled +CMake 3.5, 3.31.6, and 4.0.0 +Meson, Swift Package Manager +dependency review on every PR +Dependabot daily, OpenSSF +Scorecard, R​EUSE + + + +merge into develop + + +release +no open vulnerability alert + + +Quality assurance pipeline +Every push or pull request starts these checks in parallel: Build matrix (every push / PR): 50+ compilers: G​CC, Clang, M​SVC, AppleClang, Intel, NVIDIA, ... C++11, 14, 17, 20, 23, and 26 libc++ and libstdc++ warnings: Clang -Weverything, G​CC 300+ warnings; Unit tests (doctest) (every push / PR): every public A​PI function every exception id and message all Unicode code points single header and split headers custom number, string, array, and object types; Coverage (every push / PR): unit tests cover all lines of the code base; Static analysis (every push / PR, scheduled): Clang-Tidy, Cppcheck, Infer Clang Static Analyzer (52 checkers), I​WYU, Codacy CodeQL, Semgrep, Flawfinder (also scheduled); Dynamic analysis (every push / PR): runtime assertions Valgrind (Memcheck) address and undefined behavior sanitizers, integer overflow, nullability violations; Fuzzing (every PR, continuous): CIFuzz on every pull request OSS-Fuzz: billions of inputs every crash fixed with a reproducing unit test; Style (every push / PR): Artistic Style (astyle), enforced in the C​I cpplint with 61 rules; Docs and examples (every push / PR, scheduled): every documentation example is tested; changed output fails site built with warnings as errors; links checked regularly (scheduled); Integration, supply chain (every push / PR, scheduled): CMake 3.5, 3.31.6, and 4.0.0 Meson, Swift Package Manager dependency review on every PR Dependabot daily, OpenSSF Scorecard, R​EUSE. When they pass, the change is merged into develop; a release is made only when no vulnerability alert is open. + + + +push or pull request + +checks, run in parallel + + +Build matrix + +every push / PR +50+ compilers: G​CC, Clang, M​SVC, +AppleClang, Intel, NVIDIA, ... +C++11, 14, 17, 20, 23, and 26 +libc++ and libstdc++ +warnings: Clang -Weverything, +G​CC 300+ warnings + + + +Unit tests (doctest) + +every push / PR +every public A​PI function +every exception id and message +all Unicode code points +single header and split headers +custom number, string, array, +and object types + + + +Coverage + +every push / PR +unit tests cover all lines +of the code base + + + +Static analysis + +every push / PR + +scheduled +Clang-Tidy, Cppcheck, Infer +Clang Static Analyzer (52 +checkers), I​WYU, Codacy +CodeQL, Semgrep, Flawfinder +(also scheduled) + + + +Dynamic analysis + +every push / PR +runtime assertions +Valgrind (Memcheck) +address and undefined behavior +sanitizers, integer overflow, +nullability violations + + + +Fuzzing + +every PR + +continuous +CIFuzz on every pull request +OSS-Fuzz: billions of inputs +every crash fixed with a +reproducing unit test + + + +Style + +every push / PR +Artistic Style (astyle), +enforced in the C​I +cpplint with 61 rules + + + +Docs and examples + +every push / PR + +scheduled +every documentation example is +tested; changed output fails +site built with warnings +as errors; links checked +regularly (scheduled) + + + +Integration, supply chain + +every push / PR + +scheduled +CMake 3.5, 3.31.6, and 4.0.0 +Meson, Swift Package Manager +dependency review on every PR +Dependabot daily, OpenSSF +Scorecard, R​EUSE + + + +merge into develop + + +release +no open vulnerability alert + diff --git a/docs/mkdocs/docs/images/release_timeline.svg b/docs/mkdocs/docs/images/release_timeline.svg new file mode 100644 index 000000000..98a19f332 --- /dev/null +++ b/docs/mkdocs/docs/images/release_timeline.svg @@ -0,0 +1,109 @@ + +Timeline of the 44 releases from v1.0.0 (2015-12-28) to v3.12.0 (2025-04-11): 17 minor or major releases, drawn as filled dots with their version above the axis, and 27 patch releases, drawn as rings. + +2015 + +2016 + +2017 + +2018 + +2019 + +2020 + +2021 + +2022 + +2023 + +2024 + +2025 + + + +1.x + +2.x + +3.x + +1.0 + +1.1 + +2.0 + +2.1 + +3.0 + +3.1 + +3.2 + +3.3 + +3.4 + +3.5 + +3.6 + +3.7 + +3.8 + +3.9 + +3.10 + +3.11 + +3.12 +v2.0.1 released 2016-06-28 +v2.0.2 released 2016-07-31 +v2.0.3 released 2016-08-31 +v2.0.4 released 2016-09-11 +v2.0.5 released 2016-09-14 +v2.0.6 released 2016-10-15 +v2.0.7 released 2016-11-02 +v2.0.8 released 2016-12-02 +v2.0.9 released 2016-12-16 +v2.0.10 released 2017-01-02 +v2.1.1 released 2017-02-25 +v3.0.1 released 2017-12-29 +v3.1.1 released 2018-02-13 +v3.1.2 released 2018-03-14 +v3.7.1 released 2019-11-06 +v3.7.2 released 2019-11-10 +v3.7.3 released 2019-11-17 +v3.9.1 released 2020-08-06 +v3.10.1 released 2021-08-24 +v3.10.2 released 2021-08-26 +v3.10.3 released 2021-10-08 +v3.10.4 released 2021-10-16 +v3.10.5 released 2022-01-03 +v3.11.2 released 2022-08-12 +v3.11.3 released 2023-11-28 +v1.0.0 released 2015-12-28 +v1.1.0 released 2016-01-24 +v2.0.0 released 2016-06-24 +v2.1.0 released 2017-01-28 +v3.0.0 released 2017-12-17 +v3.1.0 released 2018-02-01 +v3.2.0 released 2018-08-20 +v3.3.0 released 2018-10-05 +v3.4.0 released 2018-10-30 +v3.5.0 released 2018-12-22 +v3.6.0, v3.6.1 released 2019-03-20 +v3.7.0 released 2019-07-28 +v3.8.0 released 2020-06-14 +v3.9.0 released 2020-07-27 +v3.10.0 released 2021-08-17 +v3.11.0, v3.11.1 released 2022-08-01 +v3.12.0 released 2025-04-11 + diff --git a/docs/mkdocs/docs/images/roadmap_lanes.svg b/docs/mkdocs/docs/images/roadmap_lanes.svg new file mode 100644 index 000000000..75b45c1b2 --- /dev/null +++ b/docs/mkdocs/docs/images/roadmap_lanes.svg @@ -0,0 +1,98 @@ + +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: JSON_USE_IMPLICIT_CONVERSIONS, added in 3.9.0, 3.x default 1, 4.0 value 0; JSON_USE_GLOBAL_UDLS, added in 3.11.0, 3.x default 1, 4.0 value 0; JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON, added in 3.11.0, 3.x default 0, 4.0 value 0, macro removed; JSON_BRACE_INIT_COPY_SEMANTICS, added in 3.13.0, 3.x default 0, 4.0 value 1; JSON_PRECISE_STREAM_POSITION, added in 3.13.0, 3.x default 0, 4.0 value 1; JSON_STRICT_NUL_HANDLING, added in 3.13.0, 3.x default 0, 4.0 value 1; JSON_STRICT_BINARY_UTF8, added in 3.13.0, 3.x default 0, 4.0 value 1; JSON_DISABLE_TUPLE_REFERENCE_CONVERSION, added in 3.13.0, 3.x default 0, 4.0 value 1; JSON_DELETE_DEPRECATED_FUNCTIONS, added in 3.13.0, 3.x default 0, 4.0 value 1, macro removed. + + +3.x release train +available today + +3.9.0 + +3.11.0 + +3.13.0 + + + +4.0 +no release date yet +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. +4.0 is not developed on a separate branch: it switches the defaults of these macros and removes the macros. +Macro +added +3.x default +define to try today +4.0 default + +JSON_USE_IMPLICIT_CONVERSIONS +3.9.0 + +1 + + +0 + +JSON_USE_GLOBAL_UDLS +3.11.0 + +1 + + +0 + +JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON +3.11.0 + +0 + + +removed + +JSON_BRACE_INIT_COPY_SEMANTICS +3.13.0 + +0 + + +1 + +JSON_PRECISE_STREAM_POSITION +3.13.0 + +0 + + +1 + +JSON_STRICT_NUL_HANDLING +3.13.0 + +0 + + +1 + +JSON_STRICT_BINARY_UTF8 +3.13.0 + +0 + + +1 + +JSON_DISABLE_TUPLE_REFERENCE_CONVERSION +3.13.0 + +0 + + +1 + +JSON_DELETE_DEPRECATED_FUNCTIONS +3.13.0 + +0 + + +removed +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. + diff --git a/docs/mkdocs/docs/integration/cmake.md b/docs/mkdocs/docs/integration/cmake.md index 65eed72a8..a277670ba 100644 --- a/docs/mkdocs/docs/integration/cmake.md +++ b/docs/mkdocs/docs/integration/cmake.md @@ -8,6 +8,19 @@ to point to the appropriate include directories and [`INTERFACE_COMPILE_FEATURES for the necessary C++11 flags. Most [package managers](package_managers.md) that provide a CMake package configuration for this library expose this same target. +All ways of adding the library described below end at this one target: + +```mermaid +flowchart LR + FP["find_package()
installed package"] --> T + AS["add_subdirectory()
embedded source tree"] --> T + FC["FetchContent
downloaded at configure time"] --> T + PM["Package manager
its CMake package"] --> T + T["nlohmann_json::nlohmann_json
one INTERFACE target"] --> L["target_link_libraries()
link it PRIVATE"] +``` + +

The four ways to add the library in CMake all lead to the same `nlohmann_json::nlohmann_json` interface target, which a project links with `target_link_libraries`. The CMake options below apply to `add_subdirectory()` and `FetchContent`; installed packages have their options fixed at install time.

+ ### External To use this library from a CMake project, you can locate it directly with [`find_package()`](https://cmake.org/cmake/help/latest/command/find_package.html) diff --git a/docs/mkdocs/docs/integration/compile_times.md b/docs/mkdocs/docs/integration/compile_times.md index 279a00ec7..221625cb9 100644 --- a/docs/mkdocs/docs/integration/compile_times.md +++ b/docs/mkdocs/docs/integration/compile_times.md @@ -6,9 +6,68 @@ that cost, ordered by how much they typically save. !!! info "Measurements" - The numbers below are medians of nine runs compiling a single translation unit with `-std=c++17 -c` against the - single-header version, with Apple clang and GCC 16 on macOS (Apple silicon). They show the order of magnitude to - expect; measure your own code before and after a change. + The numbers below are medians of the wall-clock time of nine runs compiling a single translation unit with + `-std=c++17 -c` against the single-header version, with Apple clang 21.0.0 (clang-2100.3.34.2) and Homebrew GCC + 16.2.0 on macOS (Apple M1 Max). All configurations were run interleaved, in alternating order. They show the order + of magnitude to expect; measure your own code before and after a change. + + The translation units are listed below. They replace the ones used for earlier versions of this page, whose + sources were not published, so the numbers differ from earlier versions. + +??? example "Measured translation units" + + The *model* and *parsing* translation units start with `#include `, `#include `, and + `#include `; the *header-only* translation unit includes only the library header. + + ```cpp title="model translation unit" + struct person + { + std::string name; + int age = 0; + std::vector tags; + }; + + void to_json(nlohmann::json& j, const person& p) + { + j = nlohmann::json{{"name", p.name}, {"age", p.age}, {"tags", p.tags}}; + } + + void from_json(const nlohmann::json& j, person& p) + { + j.at("name").get_to(p.name); + j.at("age").get_to(p.age); + j.at("tags").get_to(p.tags); + } + + nlohmann::json roundtrip(const person& p) + { + nlohmann::json j = p; + return nlohmann::json(j.get()); + } + ``` + + ```cpp title="parsing translation unit" + std::size_t parse_and_dump(const std::string& s) + { + const nlohmann::json j = nlohmann::json::parse(s); + return j.dump(2).size(); + } + ``` + + ```cpp title="header-only translation unit" + #include // or + + struct person; + void to_json(nlohmann::json& j, const person& p); + void from_json(const nlohmann::json& j, person& p); + ``` + +
+--8<-- "images/compile_times.svg" +
+

Change in compile time at `-O0` against the default configuration of the same +translation unit, taken from the tables below (medians of nine runs, see above). * computed from the milliseconds in +[Options without effect on compile times](#options-without-effect-on-compile-times).

## Include `json_fwd.hpp` in headers @@ -37,8 +96,10 @@ void from_json(const nlohmann::json& j, person& p) { /* ... */ } | Compiler | `json.hpp` (`-O0`) | `json_fwd.hpp` (`-O0`) | Change | |-------------|-------------------:|-----------------------:|-------:| -| Apple clang | 704 ms | 329 ms | −53% | -| GCC 16 | 779 ms | 242 ms | −69% | +| Apple clang | 868 ms | 332 ms | −62% | +| GCC 16 | 1164 ms | 210 ms | −82% | + +

Compile time of the header-only translation unit at `-O0` with each header, as the median wall-clock time of nine runs (see "Measurements" above). A negative change means a faster build.

This is the most effective option, because it avoids the full header in every translation unit that includes *your* headers. @@ -67,13 +128,15 @@ conversions or only pass `json` values around: | Compiler | Translation unit | Default (`-O0` / `-O2`) | `JSON_NO_AUTOMATIC_UDLS` (`-O0` / `-O2`) | Change | |-------------|------------------|------------------------:|-----------------------------------------:|------------:| -| Apple clang | model | 776 ms / 846 ms | 629 ms / 692 ms | −19% / −18% | -| GCC 16 | model | 1022 ms / 1120 ms | 882 ms / 965 ms | −14% / −14% | -| Apple clang | parsing | 992 ms / 1815 ms | 1006 ms / 1823 ms | +1% / 0% | -| GCC 16 | parsing | 2018 ms / 3420 ms | 1990 ms / 3454 ms | −1% / +1% | +| Apple clang | model | 1051 ms / 1300 ms | 935 ms / 1170 ms | −11% / −10% | +| GCC 16 | model | 1801 ms / 2106 ms | 1598 ms / 1983 ms | −11% / −6% | +| Apple clang | parsing | 1157 ms / 2016 ms | 1146 ms / 2006 ms | −1% / 0% | +| GCC 16 | parsing | 2529 ms / 4087 ms | 2484 ms / 4059 ms | −2% / −1% | -Translation units that include only the header save up to a third. Translation units that parse anyway instantiate -the parser regardless and see no difference. +

Compile time of the model and parsing translation units without and with `JSON_NO_AUTOMATIC_UDLS`, at `-O0` and `-O2`.

+ +The model translation unit saves about a tenth. Translation units that parse anyway instantiate the parser regardless +and see no difference. ## Instantiate `basic_json` once @@ -97,10 +160,12 @@ Include `json_instance.hpp` instead of `` and compile and lin | Compiler | Translation unit | Default (`-O0` / `-O2`) | `extern template` (`-O0` / `-O2`) | Change | |-------------|---------------------|------------------------:|----------------------------------:|------------:| -| Apple clang | parsing | 992 ms / 1815 ms | 953 ms / 1625 ms | −4% / −10% | -| GCC 16 | parsing | 2018 ms / 3420 ms | 1522 ms / 2728 ms | −25% / −20% | -| Apple clang | `json_instance.cpp` | — | 2166 ms / 4660 ms | — | -| GCC 16 | `json_instance.cpp` | — | 5085 ms / 10616 ms | — | +| Apple clang | parsing | 1157 ms / 2016 ms | 1071 ms / 1844 ms | −7% / −9% | +| GCC 16 | parsing | 2529 ms / 4087 ms | 1994 ms / 3190 ms | −21% / −22% | +| Apple clang | `json_instance.cpp` | — | 2573 ms / 6221 ms | — | +| GCC 16 | `json_instance.cpp` | — | 6489 ms / 13759 ms | — | + +

Compile time of the parsing translation unit without and with the `extern template` declaration, at `-O0` and `-O2`. The `json_instance.cpp` rows are the one-off cost of the translation unit that holds the instantiation.

Notes: @@ -111,12 +176,97 @@ Notes: - The declaration covers exactly `nlohmann::json`. Add the same lines for `nlohmann::ordered_json` (`nlohmann::basic_json`) or your own `basic_json` specializations if you use them. +## Switch off filesystem support + +With C++17, the library includes `` to provide conversions from and to `std::filesystem::path` +(see [`JSON_HAS_FILESYSTEM`](../api/macros/json_has_filesystem.md)). Parsing `` is a fixed cost in every +translation unit, and it also pulls in ``, ``, and `` (and, with GCC's libstdc++, +``). If you do not convert paths, switch the detection off for the whole project: + +```cmake +target_compile_definitions(my_target PRIVATE JSON_HAS_FILESYSTEM=0 JSON_HAS_EXPERIMENTAL_FILESYSTEM=0) +``` + +| Compiler | Translation unit | Default (`-O0` / `-O2`) | Filesystem off (`-O0` / `-O2`) | Change | +|-------------|------------------|------------------------:|-------------------------------:|-----------:| +| Apple clang | model | 1051 ms / 1300 ms | 998 ms / 1257 ms | −5% / −3% | +| GCC 16 | model | 1801 ms / 2106 ms | 1636 ms / 2008 ms | −9% / −5% | +| Apple clang | parsing | 1157 ms / 2016 ms | 1107 ms / 1971 ms | −4% / −2% | +| GCC 16 | parsing | 2529 ms / 4087 ms | 2405 ms / 4029 ms | −5% / −1% | + +

Compile time with `JSON_HAS_FILESYSTEM=0` and `JSON_HAS_EXPERIMENTAL_FILESYSTEM=0` against the default configuration, at `-O0` and `-O2`.

+ +Only with filesystem support switched off does [`JSON_NO_IO`](../api/macros/json_no_io.md) remove the stream headers +from the translation unit, so the two macros save most together: + +| Compiler | Translation unit | Default (`-O0` / `-O2`) | Filesystem off and `JSON_NO_IO` (`-O0` / `-O2`) | Change | +|-------------|------------------|------------------------:|------------------------------------------------:|-----------:| +| Apple clang | model | 1051 ms / 1300 ms | 969 ms / 1219 ms | −8% / −6% | +| GCC 16 | model | 1801 ms / 2106 ms | 1605 ms / 1982 ms | −11% / −6% | +| Apple clang | parsing | 1157 ms / 2016 ms | 1083 ms / 1947 ms | −6% / −3% | +| GCC 16 | parsing | 2529 ms / 4087 ms | 2384 ms / 3972 ms | −6% / −3% | + +

Compile time with filesystem support switched off and `JSON_NO_IO` defined, against the default configuration, at `-O0` and `-O2`.

+ +The trade-off: the library's own `to_json`/`from_json` overloads for `std::filesystem::path` (which store the path as +UTF-8) are removed. In a test with both compilers on macOS, paths still converted through their implicit conversion +from and to `std::string`; whether that fallback exists depends on the platform's native path string type +(`std::wstring` on Windows), so check code that converts paths. `JSON_NO_IO` additionally removes all stream +functions (see [Options without effect](#options-without-effect-on-compile-times)). + ## Use C++20 modules With a toolchain that supports named modules, `import nlohmann.json;` compiles the library once into a module and avoids parsing the header in every translation unit. See [Modules](../features/modules.md) for requirements and known issues. Module support is experimental and currently depends heavily on the compiler version. +
+--8<-- "images/compile_times_modules.svg" +
+

Change in compile time per translation unit at `-O0` when `#include ` is +replaced by `import nlohmann.json;`, both with `-std=c++20`, and the one-off cost of building the module, taken from +the table below.

+ +| Compiler | Translation unit | `#include` (`-O0` / `-O2`) | `import nlohmann.json;` (`-O0` / `-O2`) | Change | +|-------------|---------------------------|---------------------------:|----------------------------------------:|------------:| +| Apple clang | model | 1311 ms / 1597 ms | 680 ms / 914 ms | −48% / −43% | +| GCC 16 | model | 2156 ms / 2588 ms | 1153 ms / 1580 ms | −47% / −39% | +| Apple clang | parsing | 1387 ms / 2319 ms | 800 ms / 1676 ms | −42% / −28% | +| GCC 16 | parsing | 2838 ms / 4520 ms | 1810 ms / 3476 ms | −36% / −23% | +| Apple clang | `json.cppm` (module) | — | 1630 ms / 1642 ms | — | +| GCC 16 | `json.cppm` (module) | — | 2538 ms / 2547 ms | — | + +

Compile time with `#include ` and with `import nlohmann.json;`, both with `-std=c++20`, at `-O0` and `-O2`. The `json.cppm` rows are the one-off cost of building the module.

+ +Both columns use `-std=c++20`; with `-std=c++17`, the `#include` baseline is 17% to 20% faster (see the next +section). The module is built once per set of compiler flags and pays for itself from about the third translation +unit that imports it. Notes from the measurement: + +- Apple clang only accepts the module declarations with `-fcxx-modules`; without it, `module;` is a syntax error. +- GCC 16 warns with `-Wexpose-global-module-tu-local` about internal constants of the library when building the + module. With GCC 16.2, including `` and `` before `import nlohmann.json;` works, but including + them after it fails with redefinition errors ([#5103](https://github.com/nlohmann/json/issues/5103)). + +??? example "How these numbers were measured" + + The model and parsing translation units are the ones listed at the top of this page, with `import nlohmann.json;` + in place of `#include ` (after `#include ` and `#include `). Same method as + for the other tables, with `-std=c++20`. + + ```sh + # Apple clang: build the module once, then import it + clang++ -std=c++20 -fcxx-modules -O0 -I single_include -c -x c++-module src/modules/json.cppm \ + -fmodule-output=nlohmann.json.pcm -o json_mod.o + clang++ -std=c++20 -fcxx-modules -O0 -fmodule-file=nlohmann.json=nlohmann.json.pcm -c model.cpp + + # GCC 16 (Homebrew on macOS): writes gcm.cache/nlohmann.json.gcm, found by the importing translation unit + g++-16 --sysroot=$(xcrun --show-sdk-path) -std=c++20 -fmodules -O0 -I single_include -c src/modules/json.cppm \ + -o json_mod.o + g++-16 --sysroot=$(xcrun --show-sdk-path) -std=c++20 -fmodules -O0 -c model.cpp + ``` + + Link `json_mod.o` into the program. + ## Use precompiled headers Build systems can precompile `` together with other stable headers, for example with CMake's @@ -129,21 +279,70 @@ target_precompile_headers(my_target PRIVATE ) This removes the cost of parsing the header, but not of instantiating templates in each translation unit, so it combines well with the options above. +
+--8<-- "images/compile_times_pch.svg" +
+

Change in compile time per translation unit at `-O0` with a precompiled +`` (`-std=c++17`), and the one-off cost of building the precompiled header, taken from the table +below.

+ +| Compiler | Translation unit | Default (`-O0` / `-O2`) | With PCH (`-O0` / `-O2`) | Change | +|-------------|----------------------|------------------------:|-------------------------:|------------:| +| Apple clang | model | 1045 ms / 1295 ms | 594 ms / 842 ms | −43% / −35% | +| GCC 16 | model | 1789 ms / 2101 ms | 1060 ms / 1376 ms | −41% / −35% | +| Apple clang | parsing | 1148 ms / 2008 ms | 714 ms / 1584 ms | −38% / −21% | +| GCC 16 | parsing | 2498 ms / 4075 ms | 1884 ms / 3452 ms | −25% / −15% | +| Apple clang | building the PCH | — | 771 ms / 760 ms | — | +| GCC 16 | building the PCH | — | 2254 ms / 2218 ms | — | + +

Compile time without and with a precompiled ``, at `-O0` and `-O2`. The last two rows are the one-off cost of building the precompiled header.

+ +The saving is largest in translation units that instantiate little, and the PCH pays for itself from the second +(Apple clang) or fourth (GCC) translation unit. A precompiled header only works with the flags it was built with: +GCC silently ignores a mismatching `.gch` file (for example one built with another `-O` level) unless `-Winvalid-pch` +is given. + +??? example "How these numbers were measured" + + `pch.hpp` contains only `#include `; the translation units are the ones + listed at the top of this page. + + ```sh + # Apple clang + clang++ -std=c++17 -O0 -I single_include -x c++-header pch.hpp -o pch.hpp.pch + clang++ -std=c++17 -O0 -I single_include -include-pch pch.hpp.pch -include pch.hpp -c model.cpp + + # GCC 16 (Homebrew on macOS): pch.hpp.gch next to pch.hpp is picked up by -include pch.hpp + g++-16 --sysroot=$(xcrun --show-sdk-path) -std=c++17 -O0 -I single_include -x c++-header pch.hpp -o pch.hpp.gch + g++-16 --sysroot=$(xcrun --show-sdk-path) -std=c++17 -O0 -I single_include -include pch.hpp -Winvalid-pch -c model.cpp + ``` + ## Options without effect on compile times Some configuration macros change what the library declares, but do not measurably change compile times: | Macro | Apple clang, model (`-O0` / `-O2`) | GCC 16, model (`-O0` / `-O2`) | |------------------------------------------------------------------------|-----------------------------------:|------------------------------:| -| default | 776 ms / 846 ms | 1022 ms / 1120 ms | -| [`JSON_NO_IO`](../api/macros/json_no_io.md) | 764 ms / 836 ms | 1022 ms / 1117 ms | -| [`JSON_USE_GLOBAL_UDLS`](../api/macros/json_use_global_udls.md)`=0` | 763 ms / 852 ms | 1019 ms / 1106 ms | +| default | 1051 ms / 1300 ms | 1801 ms / 2106 ms | +| [`JSON_NO_IO`](../api/macros/json_no_io.md) | 1034 ms / 1288 ms | 1756 ms / 2087 ms | +| [`JSON_USE_GLOBAL_UDLS`](../api/macros/json_use_global_udls.md)`=0` | 1055 ms / 1298 ms | 1783 ms / 2109 ms | + +

Compile time of the model translation unit with each macro defined; the first row is the default configuration for comparison.

+ +`JSON_NO_IO` removes the library's stream functions and its `#include`s of `` and ``, but with the +default C++17 settings `` includes these headers anyway (and with libc++, `` already includes +`` via ``), so only a few hundred lines of the library's own stream code go away. It also does not +change what the translation unit instantiates. On the parsing translation unit, the change is 0% / 0% (Apple clang) +and −2% / 0% (GCC). To get a saving, combine it with +[switching off filesystem support](#switch-off-filesystem-support). `JSON_USE_GLOBAL_UDLS` only controls *where* the literals are declared; to avoid their cost, use `JSON_NO_AUTOMATIC_UDLS` instead. ## See also +- [`JSON_HAS_FILESYSTEM`](../api/macros/json_has_filesystem.md) - control the conversions from and to + `std::filesystem::path` - [`JSON_NO_AUTOMATIC_UDLS`](../api/macros/json_no_automatic_udls.md) - do not include the user-defined string literals automatically - [Modules](../features/modules.md) - C++20 module support diff --git a/docs/mkdocs/docs/integration/index.md b/docs/mkdocs/docs/integration/index.md index 5993b0e23..12a26abe4 100644 --- a/docs/mkdocs/docs/integration/index.md +++ b/docs/mkdocs/docs/integration/index.md @@ -5,24 +5,31 @@ pick one: ```mermaid flowchart TD - A[Add the library to a C++ project] --> B{{Already using CMake?}} - B -- no --> C{{Using pkg-config or plain Makefiles?}} - C -- yes --> D[pkg-config] - C -- no --> E[Copy the single header] - B -- yes --> F{{Library installed system-wide?}} - F -- yes --> G["find_package()"] - F -- no --> H{{Use a package manager?}} - H -- yes --> I[Package manager] - H -- no --> J["add_subdirectory() or FetchContent"] + A[Add the library to a C++ project] --> B{{Using CMake?}} + B -- no --> C{{Package manager available?}} + C -- yes --> D["Use a package manager
see Package Managers"] + C -- no --> E["Copy the single header
see Header only"] + B -- yes --> F{{Already installed?}} + F -- yes --> G["find_package
see CMake, External"] + F -- no --> H{{CMake 3.14 or later?}} + H -- yes --> I["FetchContent
see CMake, FetchContent"] + H -- no --> J["add_subdirectory
see CMake, Embedded"] + G --> T["nlohmann_json::nlohmann_json"] + I --> T + J --> T ``` -- **Copy the single header**, as described [below](#header-only) — no build-system integration required. -- **CMake**: use [`find_package()`](cmake.md#external) if the library is already installed, - [`add_subdirectory()`](cmake.md#embedded) to embed the source tree, or [`FetchContent`](cmake.md#fetchcontent) to - download it at configure time; see [CMake](cmake.md). -- **Package managers**: install the library with a package manager such as Homebrew, Conan, or vcpkg; see - [Package Managers](package_managers.md). -- **pkg-config**: if you use bare Makefiles instead of CMake, [pkg-config](pkg-config.md) can supply the include flags +

Choosing an integration method: answer the questions in the hexagons from top to bottom to reach a method. All CMake methods end at the same target, `nlohmann_json::nlohmann_json`; the list below links to each method.

+ +- **Single header**: copy [`json.hpp`](#header-only) into your project; no build-system integration required. +- **CMake**, see [CMake](cmake.md): + - [`find_package()`](cmake.md#external) if the library is already installed; + - [`FetchContent`](cmake.md#fetchcontent) to download it at configure time (CMake 3.14 or later); + - [`add_subdirectory()`](cmake.md#embedded) to embed a vendored copy of the source tree; + - or [support both](cmake.md#supporting-both) with an option that selects installed or vendored. +- **Package managers**: install the library with Homebrew, Conan, vcpkg, or one of the many others; see + [Package Managers](package_managers.md) for the full list. +- **pkg-config**: if you use bare Makefiles instead of CMake, [pkg-config](pkg-config.md) supplies the include flags for an already-installed library. Once the library is integrated, see the [Migration Guide](migration_guide.md) for how to keep your code future-proof diff --git a/docs/mkdocs/docs/js/fig-charts.js b/docs/mkdocs/docs/js/fig-charts.js new file mode 100644 index 000000000..310987811 --- /dev/null +++ b/docs/mkdocs/docs/js/fig-charts.js @@ -0,0 +1,106 @@ +/* figures: charts. Number literal explorer on features/types/number_handling.md. + The table in .nj-charts-num is the source and the no-JS fallback; its rows were verified by compiling a program + against include/ (json::parse + type() + dump()). The explorer only looks literals up in that table: it does not + parse numbers itself, so anything else is reported as "not in the verified list". */ +(function () { + "use strict"; + + var WHY = { + "0": "No fraction, exponent, or minus sign: stored as unsigned integer.", + "-0": "Leading minus sign: stored as signed integer; the serialization does not reproduce the -.", + "-0.0": "Has a fraction: stored as double, which keeps the sign of zero.", + "1E3": "Has an exponent: always stored as double.", + "-12345678912345789123456789": "Smaller than INT64_MIN: does not fit std::int64_t, so it is stored as double.", + "18446744073709551615": "2^64-1 is the largest value std::uint64_t can store without loss.", + "18446744073709551616": "2^64 does not fit std::uint64_t, so it is stored as double.", + "-9223372036854775808": "-2^63 is the smallest value std::int64_t can store.", + "1E400": "Its rounded value is not finite as double: parsing throws out_of_range.406.", + "1E-400": "Too small for double: becomes zero, with the sign of the number.", + "3.141592653589793238462643383279": "Rounded to the nearest double, then printed with the fewest digits that read back.", + "2555.5599999999999": "The fewest digits that read back as the same double are 2555.56.", + "0.0000972439793401814": "The %g layout switches to scientific notation.", + "12345E-5": "The %g layout prints this double without exponent." + }; + + function init() { + var roots = document.querySelectorAll(".nj-charts-num"); + Array.prototype.forEach.call(roots, setup); + } + + function setup(root, idx) { + if (root.dataset.njChartsReady) return; + var table = root.querySelector("table"); + if (!table) return; + root.dataset.njChartsReady = "1"; + + var entries = []; + Array.prototype.forEach.call(table.querySelectorAll("tbody tr"), function (tr) { + var c = tr.querySelectorAll("td"); + if (c.length < 3) return; + entries.push({ lit: c[0].textContent.trim(), type: c[1].textContent.trim(), dump: c[2].textContent.trim(), row: tr }); + }); + if (!entries.length) return; + + var id = "nj-charts-num-" + idx; + var ui = document.createElement("div"); + ui.className = "nj-charts-num-ui"; + ui.innerHTML = + '
    ' + + '
    '; + root.insertBefore(ui, root.firstChild); + + var list = ui.querySelector("ul"); + var out = ui.querySelector("#" + id + "-out"); + var buttons = []; + + entries.forEach(function (e) { + var li = document.createElement("li"); + var b = document.createElement("button"); + b.type = "button"; + b.className = "nj-charts-num-chip"; + b.textContent = e.lit; + b.setAttribute("aria-pressed", "false"); + b.setAttribute("aria-label", "Show literal " + e.lit); + b.addEventListener("click", function () { show(e.lit); }); + li.appendChild(b); + list.appendChild(li); + buttons.push(b); + }); + + function show(text) { + var lit = text.trim(); + var hit = null; + entries.forEach(function (e, i) { + var on = e.lit === lit; + if (on) hit = e; + buttons[i].setAttribute("aria-pressed", on ? "true" : "false"); + e.row.classList.toggle("nj-charts-num-hit", on); + }); + out.textContent = ""; + if (hit) { + var dl = document.createElement("dl"); + dl.className = "nj-charts-num-out"; + [["literal", hit.lit, true], ["stored as", hit.type, false], ["dump()", hit.dump, true], + ["why", WHY[hit.lit] || "", false]].forEach(function (p) { + if (!p[1]) return; + var dt = document.createElement("dt"); dt.textContent = p[0]; + var dd = document.createElement("dd"); + if (p[2] && p[1].indexOf("(") !== 0) { var c = document.createElement("code"); c.textContent = p[1]; dd.appendChild(c); } + else dd.textContent = p[1]; + dl.appendChild(dt); dl.appendChild(dd); + }); + out.appendChild(dl); + } + } + + show(entries[1] ? entries[1].lit : entries[0].lit); + } + + if (typeof document$ !== "undefined" && document$ && typeof document$.subscribe === "function") { + document$.subscribe(init); + } else if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + } else { + init(); + } +})(); diff --git a/docs/mkdocs/docs/js/fig-exceptions.js b/docs/mkdocs/docs/js/fig-exceptions.js new file mode 100644 index 000000000..f09e5a5fe --- /dev/null +++ b/docs/mkdocs/docs/js/fig-exceptions.js @@ -0,0 +1 @@ +/* figures: exceptions */ diff --git a/docs/mkdocs/docs/js/fig-integration.js b/docs/mkdocs/docs/js/fig-integration.js new file mode 100644 index 000000000..9f9e78787 --- /dev/null +++ b/docs/mkdocs/docs/js/fig-integration.js @@ -0,0 +1,130 @@ +/* figures: integration + * + * Namespace name builder on features/namespace.md. The page ships a static strip for version 3.11.2 with + * JSON_DIAGNOSTICS=1; this script adds a version selector and one checkbox per ABI-affecting macro and rebuilds the + * strip. Tags, their order, and the versions that know them follow include/nlohmann/detail/abi_macros.hpp and the + * "Version history" section of the page; a tag that a version does not have cannot be ticked. + */ +(function () { + "use strict"; + + var VERSIONS = ["3.11.2", "3.11.3", "3.12.0", "3.13.0"]; + // macro, tag, first version with the tag, true if any definition counts (#ifdef) instead of a non-zero value + var TAGS = [ + ["JSON_DIAGNOSTICS", "_diag", "3.11.2"], + ["JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON", "_ldvcmp", "3.11.2"], + ["JSON_DIAGNOSTIC_POSITIONS", "_dp", "3.12.0"], + ["JSON_BRACE_INIT_COPY_SEMANTICS", "_bics", "3.13.0"], + ["JSON_PRECISE_STREAM_POSITION", "_psp", "3.13.0"], + ["JSON_STRICT_NUL_HANDLING", "_snul", "3.13.0"], + ["JSON_STRICT_BINARY_UTF8", "_sbu8", "3.13.0"], + ["JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS", "_ekmo", "3.13.0"], + ["JSON_DISABLE_STREAM_BULK_SCAN", "_dsbs", "3.13.0"], + ["JSON_DISABLE_UNLOCKED_FILE_IO", "_dufio", "3.13.0"], + ["JSON_NO_SIMD", "_ns", "3.13.0", true], + ["JSON_VIEW_NO_SIMD", "_vns", "3.13.0", true], + ["JSON_VIEW_USE_SSSE3", "_vssse3", "3.13.0", true] + ]; + var NO_VERSION = "NLOHMANN_JSON_NAMESPACE_NO_VERSION"; + + function el(tag, cls, text) { + var node = document.createElement(tag); + if (cls) { node.className = cls; } + if (text !== undefined) { node.textContent = text; } + return node; + } + + function seg(kind, code, note) { + var span = el("span", "nj-integration-ns__seg nj-integration-ns__seg--" + kind); + span.appendChild(el("code", "", code)); + var small = el("small"); + // allow line breaks after underscores so that long macro names stay narrow + (note || "\u00a0").split("_").forEach(function (part, i, all) { + small.appendChild(document.createTextNode(part + (i < all.length - 1 ? "_" : ""))); + if (i < all.length - 1) { small.appendChild(el("wbr")); } + }); + span.appendChild(small); + return span; + } + + function setup(fig) { + if (fig.getAttribute("data-nj-ready")) { return; } + fig.setAttribute("data-nj-ready", "1"); + var strip = fig.querySelector(".nj-integration-ns__strip"); + var caption = fig.querySelector("figcaption"); + var uid = "nj-integration-ns-" + Math.random().toString(36).slice(2, 8); + var form = el("form", "nj-integration-ns__controls"); + form.setAttribute("aria-label", "Namespace name builder"); + form.addEventListener("submit", function (e) { e.preventDefault(); }); + + var vlabel = el("label", "nj-integration-ns__version", "Library version "); + var select = el("select"); + VERSIONS.forEach(function (v) { + var option = el("option", "", v); + option.value = v; + option.selected = v === "3.11.2"; + select.appendChild(option); + }); + vlabel.appendChild(select); + form.appendChild(vlabel); + + var list = el("fieldset", "nj-integration-ns__list"); + list.appendChild(el("legend", "", "ABI-affecting macros, in tag order")); + var boxes = TAGS.concat([[NO_VERSION, "", "3.11.2"]]).map(function (t, i) { + var label = el("label", "nj-integration-ns__opt"); + var box = el("input"); + box.type = "checkbox"; + box.id = uid + "-" + i; + box.checked = t[0] === "JSON_DIAGNOSTICS"; + label.htmlFor = box.id; + label.appendChild(box); + label.appendChild(el("code", "", t[0] + (t[3] ? "" : "=1"))); + var meta = el("span", "nj-integration-ns__meta"); + meta.appendChild(el("span", "nj-integration-ns__hint", + t[0] === NO_VERSION ? "drops the version" : (t[3] ? "defined: " : "") + t[1])); + meta.appendChild(el("span", "nj-integration-ns__since", "since " + t[2])); + label.appendChild(meta); + list.appendChild(label); + return { def: t, box: box, label: label }; + }); + form.appendChild(list); + fig.insertBefore(form, caption); + + function update() { + var version = select.value, index = VERSIONS.indexOf(version); + var parts = [seg("root", "nlohmann", "root namespace"), seg("sep", "::"), seg("abi", "json_abi", "always")]; + var noVersion = false, defs = []; + boxes.forEach(function (b) { + var available = VERSIONS.indexOf(b.def[2]) <= index; + b.box.disabled = !available; + if (!available) { b.box.checked = false; } + b.label.classList.toggle("is-unavailable", !available); + if (!b.box.checked) { return; } + defs.push(b.def[0] + (b.def[3] ? "" : "=1")); + if (b.def[0] === NO_VERSION) { noVersion = true; } else { parts.push(seg("tag", b.def[1], b.def[0] + (b.def[3] ? "" : "=1"))); } + }); + if (!noVersion) { parts.push(seg("ver", "_v" + version.replace(/\./g, "_"), "version " + version)); } + strip.textContent = ""; + parts.forEach(function (p) { strip.appendChild(p); }); + strip.setAttribute("aria-label", "Resulting namespace: nlohmann::" + parts.slice(2).map(function (p) { + return p.firstChild.textContent; + }).join("")); + caption.textContent = "Version " + version + (defs.length === 0 ? " with no ABI-affecting macro defined" : + defs.length > 3 ? " with " + defs.length + " of the macros defined" : " with " + defs.join(", ")) + "."; + } + form.addEventListener("change", update); + update(); + } + + function init() { + Array.prototype.forEach.call(document.querySelectorAll("[data-nj-integration-ns]"), setup); + } + + if (typeof document$ !== "undefined" && document$ && typeof document$.subscribe === "function") { + document$.subscribe(init); + } else if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + } else { + init(); + } +})(); diff --git a/docs/mkdocs/docs/js/fig-jsonview.js b/docs/mkdocs/docs/js/fig-jsonview.js new file mode 100644 index 000000000..f5f76f5f2 --- /dev/null +++ b/docs/mkdocs/docs/js/fig-jsonview.js @@ -0,0 +1,78 @@ +/* figures: jsonview -- stepper for the edit frames on features/json_view.md. + Without JS the frames (.nj-jsonview-step) show one below the other; with JS one frame at a time, with + previous/next and one button per step. Never advances on its own. */ +(function () { + "use strict"; + + function njJsonviewInit(root) { + if (root.classList.contains("is-enhanced")) return; + var steps = Array.prototype.slice.call(root.querySelectorAll(".nj-jsonview-step")); + if (steps.length < 2) return; + var current = 0; + + var bar = document.createElement("div"); + bar.className = "nj-jsonview-controls"; + bar.setAttribute("role", "group"); + bar.setAttribute("aria-label", "Edit steps"); + + function button(label, aria, onClick) { + var b = document.createElement("button"); + b.type = "button"; + b.textContent = label; + b.setAttribute("aria-label", aria); + b.addEventListener("click", onClick); + bar.appendChild(b); + return b; + } + + var prev = button("‹ Back", "Previous step", function () { show(current - 1, true); }); + var dots = steps.map(function (s, i) { + return button(String(i + 1), "Step " + (i + 1) + ": " + (s.getAttribute("data-label") || ""), function () { show(i, true); }); + }); + var next = button("Next ›", "Next step", function () { show(current + 1, true); }); + var status = document.createElement("span"); + status.className = "nj-jsonview-status"; + status.setAttribute("aria-live", "polite"); + bar.appendChild(status); + + function show(i, focusKeep) { + current = Math.max(0, Math.min(steps.length - 1, i)); + steps.forEach(function (s, k) { + s.classList.toggle("is-current", k === current); + s.setAttribute("aria-hidden", k === current ? "false" : "true"); + }); + dots.forEach(function (d, k) { + if (k === current) d.setAttribute("aria-current", "step"); else d.removeAttribute("aria-current"); + }); + prev.disabled = current === 0; + next.disabled = current === steps.length - 1; + status.textContent = "Step " + (current + 1) + " of " + steps.length; + if (focusKeep && document.activeElement && document.activeElement.disabled) { + (current === 0 ? next : prev).focus(); + } + } + + root.addEventListener("keydown", function (e) { + if (e.target.tagName !== "BUTTON") return; + if (e.key === "ArrowRight") { show(current + 1, true); e.preventDefault(); } + else if (e.key === "ArrowLeft") { show(current - 1, true); e.preventDefault(); } + }); + + root.insertBefore(bar, root.firstChild); + root.classList.add("is-enhanced"); + show(0, false); + } + + function njJsonviewAll() { + var roots = document.querySelectorAll("[data-nj-jsonview-steps]"); + for (var i = 0; i < roots.length; i++) njJsonviewInit(roots[i]); + } + + if (typeof document$ !== "undefined" && document$ && typeof document$.subscribe === "function") { + document$.subscribe(njJsonviewAll); + } else if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", njJsonviewAll); + } else { + njJsonviewAll(); + } +})(); diff --git a/docs/mkdocs/docs/js/fig-macros.js b/docs/mkdocs/docs/js/fig-macros.js new file mode 100644 index 000000000..cc067ff08 --- /dev/null +++ b/docs/mkdocs/docs/js/fig-macros.js @@ -0,0 +1,532 @@ +/* figures: macro chooser for the NLOHMANN_DEFINE_TYPE_* / NLOHMANN_DEFINE_DERIVED_TYPE_* macros. + Usage:
    + with a static fallback list inside (shown without JavaScript). Links marked with { data-nj-mc="KEY" } in the + fallback are reused for the reference links, so each embedding page resolves its own relative URLs. Keys: + intrusive, non_intrusive, derived, with_names, third_party, non_default. + The rules follow include/nlohmann/detail/macro_scope.hpp: WITH_NAMES takes "key", member pairs and no empty + member list; WITH_DEFAULT and the plain variants default-construct the type in from_json/get; ONLY_SERIALIZE + generates to_json only; DERIVED_TYPE calls the base type's to_json/from_json first; at most 63 members (31 with + WITH_NAMES). The generated programs were compiled and run with clang++ -std=c++11 against include/. */ +(function () { + "use strict"; + + var QUESTIONS = [ + { id: "edit", legend: "Can you edit the class?", options: [["yes", "Yes"], ["no", "No, it is third-party"]] }, + { id: "access", legend: "Member access", options: [["public", "All public"], ["private", "Some private"]] }, + { id: "base", legend: "Base class", options: [["no", "None"], ["yes", "Derived from a base"]] }, + { id: "dir", legend: "Direction", options: [["both", "To and from JSON"], ["to", "Only to JSON"]] }, + { id: "missing", legend: "Missing keys when parsing", options: [["throw", "Throw"], ["default", "Keep defaults"]] }, + { id: "keys", legend: "JSON keys", options: [["members", "Member names"], ["custom", "Custom names"]] }, + { id: "ctor", legend: "Default constructible?", options: [["yes", "Yes"], ["no", "No"]] } + ]; + + var KEYWORDS = ("alignas alignof and asm auto bool break case catch char class const constexpr continue decltype default " + + "delete do double else enum explicit export extern false float for friend goto if inline int long mutable namespace " + + "new noexcept not nullptr operator or private protected public register return short signed sizeof static " + + "struct switch template this throw true try typedef typeid typename union unsigned using virtual void volatile " + + "while").split(" "); + var HL_TYPES = ["int", "bool", "double", "float", "char", "void", "auto", "unsigned", "long", "short", "signed"]; + var HL_CONST = ["true", "false", "nullptr"]; + var IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/; + var MEMBER_TYPE = /^[A-Za-z_][A-Za-z0-9_:<>, ]*$/; + + var counter = 0; + + function el(tag, attrs, text) { + var e = document.createElement(tag); + if (attrs) { + Object.keys(attrs).forEach(function (k) { e.setAttribute(k, attrs[k]); }); + } + if (text !== undefined) e.textContent = text; + return e; + } + + function esc(s) { + return s.replace(/&/g, "&").replace(//g, ">"); + } + + /* small C++ tokenizer emitting the Pygments classes the site already colours */ + function highlight(code) { + var out = ""; + var re = /(\/\/[^\n]*)|(#include)|(<[a-z_/.]+\.hpp>|<[a-z]+>)|("(?:[^"\\\n]|\\.)*")|(\b\d+\b)|([A-Za-z_][A-Za-z0-9_]*)|(::|<<|==|[=:<>&])|([{}()\[\];,.!])|(\s+)|(.)/g; + var m, prev = ""; + while ((m = re.exec(code)) !== null) { + var t = m[0]; + if (m[1]) out += '' + esc(t) + ""; + else if (m[2]) out += '' + esc(t) + ""; + else if (m[3] && prev === "#include") out += '' + esc(t) + ""; + else if (m[4]) out += '' + esc(t) + ""; + else if (m[5]) out += '' + esc(t) + ""; + else if (m[6]) { + var cls = "n"; + if (HL_CONST.indexOf(t) >= 0) cls = "kc"; + else if (HL_TYPES.indexOf(t) >= 0) cls = "kt"; + else if (KEYWORDS.indexOf(t) >= 0) cls = "k"; + else if (prev === "namespace") cls = "nn"; + else if (prev === "struct" || prev === "class") cls = "nc"; + out += '' + esc(t) + ""; + } else if (m[7]) out += '' + esc(t) + ""; + else if (m[8]) out += '' + esc(t) + ""; + else out += esc(t); + if (!m[9]) prev = (m[3] && prev === "#include") ? "" : t; + } + return out; + } + + function parsePreset(s) { + var r = {}; + (s || "").split(",").forEach(function (kv) { + var p = kv.split("="); + if (p.length === 2) r[p[0].trim()] = p[1].trim(); + }); + return r; + } + + /* ---- the decision logic, independent of the DOM ---- */ + function decide(st) { + var res = { errors: [], warnings: [], macro: null, reason: "", place: "", ref: null }; + var n = st.members.length; + var names = st.keys === "custom"; + + if (st.edit === "no" && st.access === "private") { + res.errors.push({ text: "No macro can reach private members from outside the class. Write to_json/from_json by hand using the public interface, for example in a specialization of adl_serializer.", link: "third_party", linkText: "How do I convert third-party types?" }); + } + if (st.dir === "both" && st.ctor === "no") { + res.errors.push({ text: "Every macro that generates from_json default-constructs the type. Use Only to JSON, or write the conversion by hand.", link: "non_default", linkText: "Non-default-constructible types" }); + } + if (names && n === 0) { + res.errors.push({ text: "The WITH_NAMES macros need at least one key and member pair. Without members there is nothing to rename: choose Member names." }); + } + var limit = names ? 31 : 63; + if (n > limit) { + res.errors.push({ text: "The macros support at most " + limit + " members" + (names ? " with custom names" : "") + ". Write to_json/from_json by hand for larger types." }); + } + + /* identifier checks */ + if (!IDENT.test(st.type) || KEYWORDS.indexOf(st.type) >= 0) res.errors.push({ text: "The type name must be a C++ identifier." }); + if (st.base === "yes" && (!IDENT.test(st.baseName) || KEYWORDS.indexOf(st.baseName) >= 0)) res.errors.push({ text: "The base class name must be a C++ identifier." }); + if (st.base === "yes" && st.baseName === st.type) res.errors.push({ text: "The base class needs a different name than the type." }); + var seenName = {}, seenKey = {}; + st.members.forEach(function (mb, i) { + var label = "Member " + (i + 1); + if (!IDENT.test(mb.name) || KEYWORDS.indexOf(mb.name) >= 0) res.errors.push({ text: label + ": the name must be a C++ identifier." }); + else if (seenName[mb.name]) res.errors.push({ text: label + ": the name " + mb.name + " is used twice." }); + else if (mb.name === st.type) res.errors.push({ text: label + ": a member cannot have the name of its class." }); + seenName[mb.name] = true; + if (!MEMBER_TYPE.test(mb.mtype.trim())) res.errors.push({ text: label + ": enter a type such as int, std::string or std::vector." }); + var key = names ? mb.key : mb.name; + if (seenKey[key]) res.warnings.push(label + ": the JSON key \"" + key + "\" is used twice; the later member overwrites the earlier one in to_json."); + seenKey[key] = true; + if (st.base === "yes" && key === "id") res.warnings.push(label + ": the key \"id\" is also written by the example base class; the derived member overwrites it."); + }); + + if (st.edit === "no" && st.access === "private") return res; + + var intrusive = st.access === "private"; + var parts = ["NLOHMANN_DEFINE", st.base === "yes" ? "DERIVED_TYPE" : "TYPE", intrusive ? "INTRUSIVE" : "NON_INTRUSIVE"]; + if (st.dir === "to") parts.push("ONLY_SERIALIZE"); + else if (st.missing === "default") parts.push("WITH_DEFAULT"); + if (names) parts.push("WITH_NAMES"); + res.macro = parts.join("_"); + res.intrusive = intrusive; + res.ref = names ? "with_names" : (st.base === "yes" ? "derived" : (intrusive ? "intrusive" : "non_intrusive")); + + var why = []; + if (intrusive) why.push("Private members need the macro inside the class, where its friend functions can access them"); + else if (st.edit === "no") why.push("You cannot edit the class, so the macro goes next to it in its namespace, which works because all members are public"); + else why.push("All members are public, so the macro can stay outside the class, in its namespace"); + if (st.base === "yes") why.push("the conversion of " + (st.baseName || "the base") + " runs first"); + if (st.dir === "to") why.push("only to_json is generated"); + else if (st.missing === "default") why.push("missing keys keep the value of a default-constructed " + st.type); + else why.push("a missing key throws out_of_range.403"); + if (names) why.push("each member is preceded by its JSON key"); + res.reason = why.join("; ") + "."; + res.place = intrusive ? "Write the macro inside the class body." : "Write the macro after the class, in the same namespace."; + if (st.dir === "to" && st.ctor === "no") res.warnings.push("The type is not default constructible: fine for ONLY_SERIALIZE, but j.get<" + st.type + ">() will not compile."); + return res; + } + + /* C++ string literal; control characters as three-digit octal escapes, which cannot run into the next character */ + function cppString(s) { + return '"' + s.replace(/[\\"\u0000-\u001f\u007f]/g, function (c) { + if (c === "\\" || c === '"') return "\\" + c; + var o = c.charCodeAt(0).toString(8); + return "\\" + ("00" + o).slice(-3); + }) + '"'; + } + + function macroCall(name, head, pairs, indent) { + var one = name + "(" + head.concat(pairs).join(", ") + ")"; + if (one.length + indent.length <= 88) return indent + one; + var lines = [indent + name + "(" + head.join(", ") + (pairs.length ? "," : ")")]; + pairs.forEach(function (p, i) { + lines.push(indent + " " + p + (i === pairs.length - 1 ? ")" : ",")); + }); + return lines.join("\n"); + } + + function generate(st, res) { + var ns = "ns"; + var T = st.type, B = st.baseName; + var priv = res.intrusive; + var names = st.keys === "custom"; + var suffix = st.dir === "to" ? "_ONLY_SERIALIZE" : (st.missing === "default" ? "_WITH_DEFAULT" : ""); + var noCtor = st.ctor === "no" && st.members.length > 0; + var L = []; + L.push("#include "); + L.push("#include "); + L.push(""); + L.push("using nlohmann::json;"); + L.push(""); + L.push("namespace " + ns); + L.push("{"); + if (st.base === "yes") { + L.push("// stand-in for your base class: it must be serializable itself"); + if (priv) { + L.push("class " + B); + L.push("{"); + L.push(" private:"); + L.push(" int id{};"); + L.push(""); + L.push(" public:"); + L.push(" NLOHMANN_DEFINE_TYPE_INTRUSIVE" + suffix + "(" + B + ", id)"); + L.push("};"); + } else { + L.push("struct " + B); + L.push("{"); + L.push(" int id{};"); + L.push("};"); + L.push("NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE" + suffix + "(" + B + ", id)"); + } + L.push(""); + } + var kw = priv ? "class " : "struct "; + var inherit = st.base === "yes" ? (priv ? " : public " + B : " : " + B) : ""; + L.push(kw + T + inherit); + L.push("{"); + if (priv) L.push(" private:"); + st.members.forEach(function (mb) { + L.push(" " + mb.mtype.trim() + " " + mb.name + (noCtor ? ";" : "{};")); + }); + var head = [T]; + if (st.base === "yes") head.push(B); + var pairs = st.members.map(function (mb) { + return names ? cppString(mb.key) + ", " + mb.name : mb.name; + }); + if (noCtor || priv) { + if (st.members.length) L.push(""); + if (priv) L.push(" public:"); + } + if (noCtor) { + var params = st.members.map(function (mb) { return mb.mtype.trim() + " " + mb.name + "_"; }).join(", "); + var inits = st.members.map(function (mb) { return mb.name + "(" + mb.name + "_)"; }).join(", "); + L.push(" " + T + "(" + params + ")"); + L.push(" : " + inits); + L.push(" {}"); + if (priv) L.push(""); + } + if (priv) { + L.push(macroCall(res.macro, head, pairs, " ")); + L.push("};"); + } else { + L.push("};"); + L.push(""); + L.push(macroCall(res.macro, head, pairs, "")); + } + L.push("} // namespace " + ns); + L.push(""); + L.push("int main()"); + L.push("{"); + var Q = ns + "::" + T; + if (noCtor) { + L.push(" " + Q + " p{" + st.members.map(function (mb) { return mb.mtype.trim() + "{}"; }).join(", ") + "};"); + } else { + L.push(" " + Q + " p{};"); + } + L.push(" json j = p; // to_json"); + L.push(" std::cout << j.dump() << '\\n';"); + if (st.dir === "both") { + L.push(""); + L.push(" auto p2 = j.get<" + Q + ">(); // from_json"); + L.push(" std::cout << (json(p2) == j) << '\\n'; // round trip: 1"); + if (st.missing === "default") { + L.push(""); + L.push(" // missing keys keep the defaults"); + L.push(" std::cout << json(json::object().get<" + Q + ">()).dump() << '\\n';"); + } else { + L.push(" // a missing key throws json::out_of_range (403)"); + } + } + L.push("}"); + return L.join("\n") + "\n"; + } + + function copyText(text, done) { + function fallback() { + var ta = el("textarea", { readonly: "", "aria-hidden": "true" }); + ta.value = text; + ta.style.position = "fixed"; + ta.style.opacity = "0"; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand("copy"); } catch (e) { ok = false; } + document.body.removeChild(ta); + done(ok); + } + if (navigator.clipboard && window.isSecureContext) { + navigator.clipboard.writeText(text).then(function () { done(true); }, fallback); + } else { + fallback(); + } + } + + /* ---- DOM ---- */ + function setup(root) { + if (root.dataset.njMcReady) return; + root.dataset.njMcReady = "1"; + var uid = "nj-mc-" + (++counter); + var variant = root.getAttribute("data-variant") || "full"; + var preset = parsePreset(root.getAttribute("data-preset")); + + var links = {}; + Array.prototype.forEach.call(root.querySelectorAll("a[data-nj-mc]"), function (a) { + links[a.getAttribute("data-nj-mc")] = a.getAttribute("href"); + }); + + var st = { edit: "yes", access: "public", base: "no", dir: "both", missing: "throw", keys: "members", ctor: "yes", + type: "person", baseName: "entity", + members: [{ mtype: "std::string", name: "name", key: "name" }, { mtype: "int", name: "age", key: "age" }] }; + QUESTIONS.forEach(function (q) { + if (preset[q.id] && q.options.some(function (o) { return o[0] === preset[q.id]; })) st[q.id] = preset[q.id]; + }); + + var fallback = root.querySelector(".nj-mc-fallback"); + if (fallback) fallback.hidden = true; + + var app = el("div", { "class": "nj-mc-app" }); + var form = el("form", { "class": "nj-mc-form", "aria-label": "Macro chooser options" }); + form.addEventListener("submit", function (e) { e.preventDefault(); }); + + var qgrid = el("div", { "class": "nj-mc-questions" }); + var notes = {}; + var groups = {}; + QUESTIONS.forEach(function (q) { + var fs = el("fieldset", { "class": "nj-mc-q" }); + fs.appendChild(el("legend", null, q.legend)); + var seg = el("div", { "class": "nj-mc-seg" }); + q.options.forEach(function (o) { + var id = uid + "-" + q.id + "-" + o[0]; + var inp = el("input", { type: "radio", id: id, name: uid + "-" + q.id, value: o[0] }); + if (st[q.id] === o[0]) inp.checked = true; + inp.addEventListener("change", function () { st[q.id] = o[0]; update(); }); + seg.appendChild(inp); + seg.appendChild(el("label", { "for": id }, o[1])); + }); + fs.appendChild(seg); + var note = el("p", { "class": "nj-mc-qnote", id: uid + "-" + q.id + "-note" }); + note.hidden = true; + fs.appendChild(note); + fs.setAttribute("aria-describedby", note.id); + notes[q.id] = note; + groups[q.id] = fs; + qgrid.appendChild(fs); + }); + form.appendChild(qgrid); + + /* type editor */ + var editor = el("div", { "class": "nj-mc-editor" }); + var names = el("div", { "class": "nj-mc-names" }); + function textField(label, value, onInput, cls) { + var id = uid + "-f" + (++counter); + var w = el("div", { "class": "nj-mc-field" + (cls ? " " + cls : "") }); + w.appendChild(el("label", { "for": id }, label)); + var inp = el("input", { type: "text", id: id, spellcheck: "false", autocomplete: "off", autocapitalize: "off" }); + inp.value = value; + inp.addEventListener("input", function () { onInput(inp.value); update(); }); + w.appendChild(inp); + return w; + } + names.appendChild(textField("Type name", st.type, function (v) { st.type = v.trim(); })); + var baseField = textField("Base class", st.baseName, function (v) { st.baseName = v.trim(); }, "nj-mc-basefield"); + names.appendChild(baseField); + editor.appendChild(names); + + var mtable = el("div", { "class": "nj-mc-members", role: "group", "aria-label": "Member variables" }); + var mhead = el("div", { "class": "nj-mc-mrow nj-mc-mhead", "aria-hidden": "true" }); + mhead.appendChild(el("span", null, "Member type")); + mhead.appendChild(el("span", null, "Member name")); + var keyHead = el("span", { "class": "nj-mc-keycol" }, "JSON key"); + mhead.appendChild(keyHead); + mhead.appendChild(el("span")); + mtable.appendChild(mhead); + var mbody = el("div", { "class": "nj-mc-mbody" }); + mtable.appendChild(mbody); + var addBtn = el("button", { type: "button", "class": "nj-mc-add" }, "Add member"); + addBtn.addEventListener("click", function () { + var k = st.members.length + 1; + var nm = "member" + k; + while (st.members.some(function (mb) { return mb.name === nm; })) nm = "member" + (++k); + st.members.push({ mtype: "int", name: nm, key: nm }); + renderMembers(); + update(); + var rows = mbody.querySelectorAll(".nj-mc-mrow"); + var last = rows[rows.length - 1]; + if (last) last.querySelector("input").focus(); + }); + editor.appendChild(mtable); + editor.appendChild(addBtn); + if (variant === "full") form.appendChild(editor); + + function renderMembers() { + mbody.textContent = ""; + st.members.forEach(function (mb, i) { + var row = el("div", { "class": "nj-mc-mrow" }); + function cell(prop, label, cls) { + var inp = el("input", { type: "text", spellcheck: "false", autocomplete: "off", autocapitalize: "off", + "aria-label": label + " of member " + (i + 1), placeholder: label, "class": cls || "" }); + inp.value = mb[prop]; + inp.addEventListener("input", function () { + var keyFollows = prop === "name" && mb.key === mb.name; + mb[prop] = prop === "key" ? inp.value : inp.value.trim(); + if (keyFollows) { + mb.key = mb.name; + var k = row.querySelector(".nj-mc-keycol"); + if (k) k.value = mb.key; + } + update(); + }); + row.appendChild(inp); + } + cell("mtype", "Type"); + cell("name", "Name"); + cell("key", "JSON key", "nj-mc-keycol"); + var rm = el("button", { type: "button", "class": "nj-mc-rm", "aria-label": "Remove member " + (i + 1), title: "Remove member" }); + rm.appendChild(el("span", { "aria-hidden": "true" }, "\u00d7")); + rm.addEventListener("click", function () { + st.members.splice(i, 1); + renderMembers(); + update(); + var btns = mbody.querySelectorAll(".nj-mc-rm"); + (btns[Math.min(i, btns.length - 1)] || addBtn).focus(); + }); + row.appendChild(rm); + mbody.appendChild(row); + }); + } + renderMembers(); + app.appendChild(form); + + /* output */ + var out = el("div", { "class": "nj-mc-out" }); + var result = el("div", { "class": "nj-mc-result", role: "status", "aria-live": "polite" }); + out.appendChild(result); + var hints = el("ul", { "class": "nj-mc-hints" }); + out.appendChild(hints); + var codeWrap = el("div", { "class": "nj-mc-code" }); + var bar = el("div", { "class": "nj-mc-codebar" }); + bar.appendChild(el("span", { "class": "nj-mc-codetitle" }, "Generated code (C++11)")); + var copyBtn = el("button", { type: "button", "class": "nj-mc-copy", "aria-label": "Copy generated code" }, "Copy"); + var copyStatus = el("span", { "class": "nj-mc-copystatus", role: "status", "aria-live": "polite" }); + bar.appendChild(copyStatus); + bar.appendChild(copyBtn); + codeWrap.appendChild(bar); + var hl = el("div", { "class": "language-cpp highlight" }); + var pre = el("pre"); + var code = el("code", { tabindex: "0", "aria-label": "Generated C++ code" }); + pre.appendChild(code); + hl.appendChild(pre); + codeWrap.appendChild(hl); + if (variant === "full") out.appendChild(codeWrap); + app.appendChild(out); + root.appendChild(app); + + var currentCode = ""; + var statusTimer = null; + copyBtn.addEventListener("click", function () { + copyText(currentCode, function (ok) { + copyStatus.textContent = ok ? "Copied" : "Press Ctrl+C / Cmd+C to copy"; + if (!ok) { + var r = document.createRange(); + r.selectNodeContents(code); + var s = window.getSelection(); + s.removeAllRanges(); + s.addRange(r); + } + clearTimeout(statusTimer); + statusTimer = setTimeout(function () { copyStatus.textContent = ""; }, 2500); + }); + }); + + function setNote(id, text) { + notes[id].hidden = !text; + notes[id].textContent = text || ""; + } + + function addLink(parent, key, text) { + if (!key || !links[key]) return; + parent.appendChild(document.createTextNode(" ")); + parent.appendChild(el("a", { href: links[key] }, text)); + } + + function update() { + var dirTo = st.dir === "to"; + groups.missing.disabled = dirTo; + groups.missing.classList.toggle("nj-mc-off", dirTo); + setNote("missing", dirTo ? "Not applicable: ONLY_SERIALIZE generates no from_json." : ""); + setNote("access", st.edit === "no" && st.access === "private" ? "Private members of a class you cannot edit are out of reach for every macro." : ""); + setNote("ctor", st.ctor === "no" && !dirTo ? "Parsing needs a default constructor; only Only to JSON works." : ""); + baseField.hidden = st.base !== "yes"; + mtable.classList.toggle("nj-mc-custom", st.keys === "custom"); + keyHead.hidden = st.keys !== "custom"; + + var res = decide(st); + result.textContent = ""; + hints.textContent = ""; + if (res.macro && !res.errors.length) { + result.className = "nj-mc-result"; + result.appendChild(el("p", { "class": "nj-mc-label" }, "Recommended macro")); + var name = el("p", { "class": "nj-mc-macro" }); + name.appendChild(el("code", null, res.macro)); + result.appendChild(name); + var why = el("p", { "class": "nj-mc-why" }, res.reason + " " + res.place); + addLink(why, res.ref, "Reference"); + result.appendChild(why); + } else { + result.className = "nj-mc-result nj-mc-bad"; + result.appendChild(el("p", { "class": "nj-mc-label" }, res.macro ? "Cannot generate this macro yet" : "No macro fits")); + res.errors.forEach(function (e) { + var p = el("p", { "class": "nj-mc-why" }, e.text); + addLink(p, e.link, e.linkText); + result.appendChild(p); + }); + } + res.warnings.forEach(function (w) { hints.appendChild(el("li", null, w)); }); + hints.hidden = !res.warnings.length; + + var ok = res.macro && !res.errors.length; + codeWrap.hidden = !ok; + if (ok) { + currentCode = generate(st, res); + code.innerHTML = highlight(currentCode); + } + root.setAttribute("data-nj-mc-macro", ok ? res.macro : ""); + } + update(); + } + + function init() { + Array.prototype.forEach.call(document.querySelectorAll(".nj-macro-chooser"), setup); + } + + if (typeof document$ !== "undefined" && document$ && typeof document$.subscribe === "function") { + document$.subscribe(init); + } else if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + } else { + init(); + } + + /* exported for the verification scripts */ + window.njMacroChooser = { decide: decide, generate: generate }; +})(); diff --git a/docs/mkdocs/docs/js/fig-pointer.js b/docs/mkdocs/docs/js/fig-pointer.js new file mode 100644 index 000000000..1096c7991 --- /dev/null +++ b/docs/mkdocs/docs/js/fig-pointer.js @@ -0,0 +1,306 @@ +/* figures: pointer + * + * Two widgets that enhance static markup (the markup alone is a complete, readable figure): + * - [data-nj-pointer-walk] in features/json_pointer.md: a tree of the example document plus a pointer bar whose + * segments select a prefix of the pointer; the path down to that level is highlighted and its value is shown. + * - [data-nj-patch] in features/json_patch.md: a stepper through six JSON Patch operations; the static table of the + * operations is hidden once the stepper is built. + * Initialised through Material's document$ (navigation.instant), idempotent per element. + */ +(function () { + "use strict"; + + function el(tag, cls, html) { + var e = document.createElement(tag); + if (cls) e.className = cls; + if (html !== undefined) e.innerHTML = html; + return e; + } + function esc(s) { + return String(s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); + } + + /* ---------- JSON Pointer walker ---------- */ + + var PRESETS = ["", "/array", "/array/0", "/array/1", "/array/2", "/nested", "/nested/one", "/nested/two", + "/nested/three", "/nested/three/0", "/nested/three/1", "/"]; + + function tokens(ptr) { + if (ptr === "") return []; + return ptr.slice(1).split("/").map(function (t) { return t.replace(/~1/g, "/").replace(/~0/g, "~"); }); + } + function joinTokens(toks) { + return toks.map(function (t) { return "/" + t.replace(/~/g, "~0").replace(/\//g, "~1"); }).join(""); + } + // RFC 6901 resolution as json::at() does it; returns {ok, value, depth, error} + function resolve(doc, toks) { + var v = doc; + for (var i = 0; i < toks.length; ++i) { + var t = toks[i]; + if (Array.isArray(v)) { + if (!/^(0|[1-9][0-9]*)$/.test(t) || Number(t) >= v.length) { + return { ok: false, depth: i, error: "at() throws out_of_range: array index '" + t + "' is out of range" }; + } + v = v[Number(t)]; + } else if (v !== null && typeof v === "object") { + if (!Object.prototype.hasOwnProperty.call(v, t)) { + return { ok: false, depth: i, error: "at() throws out_of_range.403: key '" + t + "' not found" }; + } + v = v[t]; + } else { + return { ok: false, depth: i, error: "a primitive value has no children" }; + } + } + return { ok: true, value: v, depth: toks.length }; + } + function ptrLabel(p) { return p === "" ? '""' : p; } + + function initWalk(root) { + if (root.dataset.njReady) return; + var doc; + try { doc = JSON.parse(root.getAttribute("data-doc")); } catch (e) { return; } + root.dataset.njReady = "1"; + root.classList.add("nj-pointer-walk--js"); + + var tree = root.querySelector(".nj-pointer-tree"); + var items = Array.prototype.slice.call(tree.querySelectorAll("li[data-p]")); + var state = { ptr: "/nested/three/1", level: 3 }; + + // tree labels become buttons that select their own pointer + items.forEach(function (li) { + var span = li.querySelector(":scope > .nj-pointer-n"); + var b = el("button", "nj-pointer-n", span.innerHTML); + b.type = "button"; + var p = li.getAttribute("data-p"); + b.setAttribute("aria-label", "Select the pointer " + ptrLabel(p)); + b.addEventListener("click", function () { set(p, tokens(p).length); }); + span.replaceWith(b); + }); + + var panel = el("div", "nj-pointer-panel"); + panel.appendChild(el("div", "nj-pointer-lbl", "Pointer (select a segment)")); + var bar = el("div", "nj-pointer-bar"); + bar.setAttribute("role", "group"); + bar.setAttribute("aria-label", "Pointer segments"); + panel.appendChild(bar); + var out = el("div", "nj-pointer-out"); + out.setAttribute("aria-live", "polite"); + panel.appendChild(out); + panel.appendChild(el("div", "nj-pointer-lbl", "Examples")); + var presets = el("div", "nj-pointer-presets"); + presets.setAttribute("role", "group"); + presets.setAttribute("aria-label", "Example pointers"); + PRESETS.forEach(function (p) { + var b = el("button", "nj-pointer-chip", "" + esc(ptrLabel(p)) + ""); + b.type = "button"; + b.setAttribute("data-p", p); + b.setAttribute("aria-label", p === "" ? "Pointer \"\" (the whole document)" : "Pointer " + p); + b.addEventListener("click", function () { set(p, tokens(p).length); }); + presets.appendChild(b); + }); + panel.appendChild(presets); + var stat = root.querySelector(".nj-pointer-static"); + if (stat) stat.hidden = true; + root.insertBefore(panel, root.firstChild); + + function liFor(p) { + for (var i = 0; i < items.length; ++i) if (items[i].getAttribute("data-p") === p) return items[i]; + return null; + } + + function render() { + var toks = tokens(state.ptr); + var sel = toks.slice(0, state.level); + var r = resolve(doc, sel); + + // bar: root chip plus one button per reference token + bar.innerHTML = ""; + var parts = [{ label: '""', text: "root", n: 0 }]; + toks.forEach(function (t, i) { parts.push({ label: joinTokens([t]), n: i + 1 }); }); + parts.forEach(function (part) { + var b = el("button", "nj-pointer-seg" + (part.n === 0 ? " nj-pointer-seg--root" : ""), + part.n === 0 ? "\"\"" : "" + esc(part.label) + ""); + b.type = "button"; + b.setAttribute("aria-pressed", part.n === state.level ? "true" : "false"); + if (part.n > state.level) b.classList.add("is-after"); + var prefix = joinTokens(toks.slice(0, part.n)); + b.setAttribute("aria-label", "Go to level " + part.n + ": " + ptrLabel(prefix) + (part.n === 0 ? " (the whole document)" : "")); + b.addEventListener("click", function () { state.level = part.n; render(); focusSeg(part.n); }); + bar.appendChild(b); + }); + + // tree highlight + items.forEach(function (li) { li.classList.remove("is-on", "is-target", "is-pass", "is-miss"); }); + var depth = r.ok ? sel.length : r.depth; + for (var d = 0; d <= depth; ++d) { + var li = liFor(joinTokens(sel.slice(0, d))); + if (!li) break; + li.classList.add("is-on"); + if (d > 0) { + for (var s = li.previousElementSibling; s; s = s.previousElementSibling) s.classList.add("is-pass"); + } + if (r.ok && d === depth) li.classList.add("is-target"); + } + if (!r.ok) { + var miss = liFor(joinTokens(sel.slice(0, r.depth))); + if (miss) miss.classList.add("is-miss"); + } + + var shown = joinTokens(sel); + out.innerHTML = r.ok + ? "" + esc(ptrLabel(shown)) + " resolves to " + esc(JSON.stringify(r.value)) + "" + + (shown === "" ? " (the whole document)" : "") + : "" + esc(ptrLabel(shown)) + " identifies no value: " + esc(r.error) + + ". The token after / is the empty key, and the document has none."; + + Array.prototype.forEach.call(presets.children, function (b) { + b.setAttribute("aria-pressed", b.getAttribute("data-p") === shown && state.level === tokens(state.ptr).length ? "true" : "false"); + }); + } + function focusSeg(n) { + var b = bar.children[n]; + if (b) b.focus(); + } + function set(p, level) { + state.ptr = p; + state.level = level; + render(); + } + render(); + } + + /* ---------- JSON Patch stepper ---------- */ + + function k(s) { return '"' + s + '"'; } + function s(v) { return '"' + v + '"'; } + function ins(x) { return '' + x + ""; } + function src(x) { return '' + x + ""; } + var I = " "; + + var OPS = [ + { op: "replace", path: "/baz", extra: ', "value": "boo"' }, + { op: "add", path: "/hello", extra: ', "value": ["world"]' }, + { op: "remove", path: "/foo", extra: "" }, + { op: "move", from: "/baz", path: "/hello/1", extra: "" }, + { op: "copy", from: "/hello/0", path: "/greeting", extra: "" }, + { op: "test", path: "/greeting", extra: ', "value": "world"' } + ]; + // each frame: document lines [html, line class, note], optional connector {from, to}, status text + var FRAMES = [ + { lines: [["{"], [I + k("baz") + ": " + s("qux") + ","], [I + k("foo") + ": " + s("bar")], ["}"]], + status: "Start: the original document of the example below. Press Next to apply the first operation." }, + { lines: [["{"], [I + k("baz") + ": " + s("qux") + ",", "del"], [I + k("baz") + ": " + s("boo") + ",", "add", "replaced"], + [I + k("foo") + ": " + s("bar")], ["}"]], + status: "replace /baz: the value \"qux\" is replaced by \"boo\"." }, + { lines: [["{"], [I + k("baz") + ": " + s("boo") + ","], [I + k("foo") + ": " + s("bar") + ","], + [I + k("hello") + ": [" + s("world") + "]", "add", "added"], ["}"]], + status: "add /hello: the new member \"hello\" with the value [\"world\"] is added." }, + { lines: [["{"], [I + k("baz") + ": " + s("boo") + ","], [I + k("foo") + ": " + s("bar") + ",", "del", "removed"], + [I + k("hello") + ": [" + s("world") + "]"], ["}"]], + status: "remove /foo: the member \"foo\" is removed. This is the result of the example below." }, + { lines: [["{"], [I + k("baz") + ": " + s("boo") + ",", "del", "moved"], + [I + k("hello") + ": [" + s("world") + ", " + ins(s("boo")) + "]", "mod", "from /baz"], ["}"]], + arc: { from: 1, to: 2 }, + status: "move from /baz to /hello/1: \"boo\" is removed from /baz and inserted into the array at index 1." }, + { lines: [["{"], [I + k("greeting") + ": " + s("world") + ",", "add", "copy of /hello/0"], + [I + k("hello") + ": [" + src(s("world")) + ", " + s("boo") + "]"], ["}"]], + arc: { from: 2, to: 1 }, + status: "copy from /hello/0 to /greeting: the value \"world\" is copied to the new member \"greeting\"." }, + { lines: [["{"], [I + k("greeting") + ": " + s("world") + ",", "test", "test passed"], + [I + k("hello") + ": [" + s("world") + ", " + s("boo") + "]"], ["}"]], + status: "test /greeting: the value equals \"world\", so the document is unchanged. A failed test would make patch() throw other_error 501." } + ]; + + function opHtml(o) { + var h = '{"op": "' + o.op + '"'; + if (o.from) h += ', "from": "' + o.from + '"'; + h += ', "path": "' + o.path + '"' + esc(o.extra) + "}"; + return h; + } + + function initPatch(root) { + if (root.dataset.njReady) return; + root.dataset.njReady = "1"; + root.classList.add("nj-patch--js"); + var step = 0; + + var stage = el("div", "nj-patch-stage"); + var cols = el("div", "nj-patch-cols"); + var left = el("div", "nj-patch-col"); + left.appendChild(el("div", "nj-patch-h", "Document")); + var docBox = el("div", "nj-patch-doc"); + left.appendChild(docBox); + var right = el("div", "nj-patch-col"); + right.appendChild(el("div", "nj-patch-h", "Patch")); + var list = el("ol", "nj-patch-ops"); + OPS.forEach(function (o) { list.appendChild(el("li", "", "" + opHtml(o) + "")); }); + right.appendChild(list); + cols.appendChild(left); + cols.appendChild(right); + stage.appendChild(cols); + // controls and status sit under the document, so on a phone both stay in view together + + var ctrl = el("div", "nj-patch-ctrl"); + function btn(label, aria, fn) { + var b = el("button", "nj-patch-btn", label); + b.type = "button"; + b.setAttribute("aria-label", aria); + b.addEventListener("click", fn); + ctrl.appendChild(b); + return b; + } + var bReset = btn("Reset", "Back to the original document", function () { step = 0; render(true); }); + var bPrev = btn("Back", "Previous operation", function () { if (step > 0) { step--; render(true); } }); + var bNext = btn("Next", "Apply the next operation", function () { if (step < OPS.length) { step++; render(true); } }); + bNext.classList.add("nj-patch-btn--primary"); + var count = el("span", "nj-patch-count"); + ctrl.appendChild(count); + left.appendChild(ctrl); + var status = el("p", "nj-patch-status"); + status.setAttribute("aria-live", "polite"); + left.appendChild(status); + root.insertBefore(stage, root.firstChild); + + function render(animate) { + var f = FRAMES[step]; + docBox.innerHTML = ""; + f.lines.forEach(function (ln) { + var row = el("div", "nj-patch-line" + (ln[1] ? " nj-patch-line--" + ln[1] : "")); + row.appendChild(el("span", "nj-patch-code", ln[0])); + if (ln[2]) row.appendChild(el("span", "nj-patch-note", esc(ln[2]))); + docBox.appendChild(row); + }); + if (f.arc) { + var a = el("div", "nj-patch-arc " + (f.arc.to > f.arc.from ? "nj-patch-arc--down" : "nj-patch-arc--up")); + var top = Math.min(f.arc.from, f.arc.to), n = Math.abs(f.arc.to - f.arc.from); + a.style.top = "calc(var(--nj-patch-pad) + " + (top + 0.5) + " * var(--nj-patch-lh))"; + a.style.height = "calc(" + n + " * var(--nj-patch-lh))"; + a.setAttribute("aria-hidden", "true"); + docBox.appendChild(a); + } + docBox.classList.toggle("nj-patch-doc--anim", !!animate); + Array.prototype.forEach.call(list.children, function (li, i) { + li.className = i + 1 === step ? "is-cur" : (i + 1 < step ? "is-done" : ""); + if (i + 1 === step) li.setAttribute("aria-current", "step"); else li.removeAttribute("aria-current"); + }); + bPrev.disabled = step === 0; + bReset.disabled = step === 0; + bNext.disabled = step === OPS.length; + count.textContent = "Step " + step + " of " + OPS.length; + status.textContent = f.status; + } + render(false); + } + + function init() { + Array.prototype.forEach.call(document.querySelectorAll("[data-nj-pointer-walk]"), initWalk); + Array.prototype.forEach.call(document.querySelectorAll("[data-nj-patch]"), initPatch); + } + if (typeof window.document$ !== "undefined" && window.document$.subscribe) { + window.document$.subscribe(init); + } else if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + } else { + init(); + } +})(); diff --git a/docs/mkdocs/docs/js/fig-sax.js b/docs/mkdocs/docs/js/fig-sax.js new file mode 100644 index 000000000..55e271c4e --- /dev/null +++ b/docs/mkdocs/docs/js/fig-sax.js @@ -0,0 +1,248 @@ +/* + * figures: sax + * + * SAX event stepper on features/parsing/sax_interface.md. The page contains the final state as static HTML (input, + * all nine events, the finished tree); this script adds the controls and redraws the three lanes for each step: + * the consumed part of the input, the events called so far, and the state of one of two handlers (a DOM builder + * that keeps a stack of open containers, or a handler that keeps only the value at /b). "return false at this + * event" stops the parse at the current event: the later events are not called and the rest of the input is not read. + */ +(function () { + "use strict"; + + var SRC = '{"a":[1,true],"b":"x"}'; + // tok: [start, end) of the token that makes the parser call the event + var EVENTS = [ + { call: "start_object(-1)", cls: "os", tok: [0, 1], op: "so" }, + { call: 'key("a")', cls: "k", tok: [1, 4], op: "key", v: "a" }, + { call: "start_array(-1)", cls: "as", tok: [5, 6], op: "sa" }, + { call: "number_unsigned(1)", cls: "v", tok: [6, 7], op: "val", v: { t: "n", v: "1" } }, + { call: "boolean(true)", cls: "v", tok: [8, 12], op: "val", v: { t: "b", v: "true" } }, + { call: "end_array()", cls: "ae", tok: [12, 13], op: "end" }, + { call: 'key("b")', cls: "k", tok: [14, 17], op: "key", v: "b" }, + { call: 'string("x")', cls: "v", tok: [18, 21], op: "val", v: { t: "s", v: '"x"' } }, + { call: "end_object()", cls: "oe", tok: [21, 22], op: "end" } + ]; + var N = EVENTS.length; + + function esc(s) { + return String(s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); + } + + // ---- handler A: DOM builder (like json_sax_dom_parser) ---- + function domState(steps) { + var root = null, stack = [], last = null; + function place(v) { + if (!root) { root = v; return; } + var top = stack[stack.length - 1]; + if (top.t === "a") { top.e.push(v); } else { top.m[top.m.length - 1][1] = v; } + } + for (var i = 0; i < steps; ++i) { + var ev = EVENTS[i], v; + if (ev.op === "so" || ev.op === "sa") { + v = ev.op === "so" ? { t: "o", m: [] } : { t: "a", e: [] }; + var top = stack[stack.length - 1]; + v.ptr = !top ? "" : top.ptr + "/" + (top.t === "a" ? top.e.length : top.m[top.m.length - 1][0]); + place(v); stack.push(v); last = v; + } else if (ev.op === "key") { + v = { t: "z", v: "null" }; // the DOM parser adds null at the key and fills it in later + stack[stack.length - 1].m.push([ev.v, v]); last = v; + } else if (ev.op === "val") { + v = { t: ev.v.t, v: ev.v.v }; place(v); last = v; + } else { + last = stack.pop(); + } + } + return { root: root, stack: stack, last: last }; + } + + function pretty(v, ind, last, out, prefix, suffix) { + var pad = new Array(ind + 1).join(" "); + var mark = function (s) { return v === last ? '' + s + "" : s; }; + if (v.t === "o" || v.t === "a") { + var items = v.t === "o" ? v.m : v.e, open = v.t === "o" ? "{" : "[", close = v.t === "o" ? "}" : "]"; + if (!items.length) { out.push(pad + prefix + mark(open + close) + suffix); return; } + out.push(pad + prefix + mark(open)); + for (var i = 0; i < items.length; ++i) { + var comma = i < items.length - 1 ? "," : ""; + if (v.t === "o") { + pretty(items[i][1], ind + 1, last, out, esc('"' + items[i][0] + '": '), comma); + } else { + pretty(items[i], ind + 1, last, out, "", comma); + } + } + out.push(pad + mark(close) + suffix); + } else { + out.push(pad + prefix + mark(esc(v.v)) + suffix); + } + } + + function renderDom(steps, stopped) { + var st = domState(steps), lines = []; + var tree = st.root ? (pretty(st.root, 0, st.last, lines, "", ""), lines.join("\n")) : '(nothing built yet)'; + var chips = st.stack.length ? st.stack.map(function (c) { + return '
  • ' + (c.t === "o" ? "{}" : "[]") + " " + esc(c.ptr === "" ? "root" : c.ptr) + "
  • "; + }).join("") : '
  • empty
  • '; + var note; + if (steps === 0) { note = "The handler starts with an empty stack."; } + else if (stopped) { note = "The handler keeps what it built so far; the containers on the stack stay unfinished."; } + else if (steps === N) { note = "The tree is complete and no container is open. The number of elements is passed as -1: the parser does not know it in advance."; } + else { note = EVENTS[steps - 1].op === "key" ? "A key adds the member with a null value, which the next value replaces." : "Values go into the container on top of the stack."; } + return '
    ' + tree + "
    " + + '

    stack of open containers

      ' + chips + "
    " + + '

    ' + note + "

    "; + } + + // ---- handler B: keep only the value at /b ---- + function renderKeep(steps, stopped) { + var frames = [], ptr = "", kept = null, keptAt = 0, action = ""; + var where = function () { + return frames.map(function (f) { return "/" + (f.t === "a" ? f.i : f.k); }).join(""); + }; + for (var i = 0; i < steps; ++i) { + var ev = EVENTS[i], top = frames[frames.length - 1]; + if (ev.op === "key") { top.k = ev.v; ptr = where(); action = "remembers the key"; continue; } + if (ev.op === "end") { ptr = frames.pop().loc; action = "leaves the container"; continue; } + ptr = where(); + action = ptr === "/b" ? "keeps it" : "ignores it"; + if (top && top.t === "a") { top.i++; } + if (ev.op === "so") { frames.push({ t: "o", k: "", loc: ptr }); } + else if (ev.op === "sa") { frames.push({ t: "a", i: 0, loc: ptr }); } + else if (ptr === "/b") { kept = ev.v.v; keptAt = i + 1; } + } + var note; + if (steps === 0) { note = "The handler keeps only the current position and, once it is found, one value."; } + else if (stopped) { note = kept !== null ? "The value is found, so stopping here saves reading the rest of the input." : "Stopped before /b was read: nothing is kept."; } + else { note = "At " + (ptr === "" ? "the root" : ptr) + " the handler " + action + "."; } + if (!stopped && steps === keptAt && kept !== null) { + note += " A handler that needs nothing else could return false here."; + } + return '
    ' + + "
    position
    " + (steps ? esc(ptr === "" ? "(root)" : ptr) : "-") + "
    " + + "
    kept value
    " + (kept !== null ? '' + esc(kept) + "" : 'none') + "
    " + + "
    " + + '

    ' + note + " No tree is built.

    "; + } + + function init(root) { + if (root.getAttribute("data-nj-sax-ready")) { return; } + root.setAttribute("data-nj-sax-ready", "1"); + var src = root.querySelector(".nj-sax__src"); + var items = root.querySelectorAll(".nj-sax__events li"); + var head = root.querySelector(".nj-sax__lane--state .nj-sax__h"); + var state = root.querySelector(".nj-sax__state"); + var reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; + var step = 0, mode = "dom", stopAt = 0, timer = null; + + // controls + var bar = document.createElement("div"); + bar.className = "nj-sax__controls"; + bar.innerHTML = + '
    ' + + '' + + '' + + '' + + '' + + "
    " + + '' + + '
    ' + + '' + + '' + + "
    "; + root.insertBefore(bar, root.firstChild); + var status = document.createElement("p"); + status.className = "nj-sax__status"; + status.setAttribute("aria-live", "polite"); + root.appendChild(status); + var btn = function (a) { return bar.querySelector('[data-a="' + a + '"]'); }; + var stopBox = btn("stop"); + + function stopped() { return stopAt > 0 && step === stopAt; } + + function render() { + var end = step ? EVENTS[step - 1].tok[1] : 0, cur = step ? EVENTS[step - 1] : null; + var html = ""; + if (cur) { + html += '' + esc(SRC.slice(0, cur.tok[0])) + ""; + html += '' + esc(SRC.slice(cur.tok[0], end)) + ""; + } + html += '' + esc(SRC.slice(end)) + ""; + src.innerHTML = html; + for (var i = 0; i < N; ++i) { + var li = items[i]; + li.classList.toggle("nj-sax-ev--done", i < step); + li.classList.toggle("nj-sax-ev--cur", i === step - 1); + li.classList.toggle("nj-sax-ev--todo", i >= step && !stopped()); + li.classList.toggle("nj-sax-ev--skip", i >= step && stopped()); + li.classList.toggle("nj-sax-ev--false", stopped() && i === step - 1); + if (i === step - 1) { li.setAttribute("aria-current", "step"); } else { li.removeAttribute("aria-current"); } + } + head.innerHTML = mode === "dom" ? "handler: build a value tree, as parse does" : "handler: keep only the value at /b"; + state.innerHTML = mode === "dom" ? renderDom(step, stopped()) : renderKeep(step, stopped()); + bar.querySelectorAll("[data-m]").forEach(function (b) { b.setAttribute("aria-pressed", String(b.getAttribute("data-m") === mode)); }); + btn("prev").disabled = step === 0; + btn("reset").disabled = step === 0; + btn("next").disabled = step === N || stopped(); + stopBox.disabled = step === 0; + stopBox.checked = stopped(); + var playing = timer !== null; + btn("play").textContent = playing ? "Pause" : "Play"; + btn("play").setAttribute("aria-label", playing ? "Pause" : "Play all events"); + var msg; + if (step === 0) { msg = "Before the first event: nothing is read yet."; } + else if (stopped()) { msg = "Event " + step + " of " + N + ": " + cur.call + " returned false. Parsing stops, sax_parse returns false, and the rest of the input is not read."; } + else if (step === N) { msg = "Event " + N + " of " + N + ": " + cur.call + ". The input is read completely and sax_parse returns true."; } + else { msg = "Event " + step + " of " + N + ": the parser read " + SRC.slice(cur.tok[0], cur.tok[1]) + " and called " + cur.call + "."; } + status.textContent = msg; + } + + function stop() { if (timer !== null) { clearInterval(timer); timer = null; } } + function go(s) { + step = Math.max(0, Math.min(N, s)); + if (stopAt && step < stopAt) { stopAt = 0; } + render(); + } + + bar.addEventListener("click", function (e) { + var b = e.target.closest("button"); + if (!b) { return; } + var m = b.getAttribute("data-m"); + if (m) { mode = m; render(); return; } + var a = b.getAttribute("data-a"); + if (a === "play") { + if (timer !== null) { stop(); render(); return; } + if (step === N || stopped()) { stopAt = 0; step = 0; } + timer = setInterval(function () { + if (step >= N || stopped()) { stop(); render(); return; } + go(step + 1); + if (step >= N) { stop(); render(); } + }, reduce ? 1400 : 900); + go(step + 1); + return; + } + stop(); + if (a === "reset") { stopAt = 0; go(0); } + else if (a === "prev") { go(step - 1); } + else if (a === "next") { go(step + 1); } + }); + stopBox.addEventListener("change", function () { + stop(); + stopAt = stopBox.checked ? step : 0; + render(); + }); + root.classList.add("nj-sax--live"); + render(); + } + + function setup() { + document.querySelectorAll("[data-nj-sax]").forEach(init); + } + + if (typeof window.document$ !== "undefined" && window.document$ && typeof window.document$.subscribe === "function") { + window.document$.subscribe(setup); + } else if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", setup); + } else { + setup(); + } +})(); diff --git a/docs/mkdocs/mkdocs.yml b/docs/mkdocs/mkdocs.yml index cb65b5a66..a0779b0a7 100644 --- a/docs/mkdocs/mkdocs.yml +++ b/docs/mkdocs/mkdocs.yml @@ -572,12 +572,18 @@ plugins: -1: # connection errors - 'https://repology.org/*' # repology.org is suspended since 2026-09 - 'http://2ak5ape.257.cz/' # customers.md: kept although unreachable - 401: ['https://fossies.org/*'] # answers the plugin's user agent with 401, browsers and curl with 200 + 400: ['https://www.meta.com/*'] # customers.md: answers the plugin's user agent with 400, browsers with 200 + 401: + - 'https://fossies.org/*' # answers the plugin's user agent with 401, browsers and curl with 200 + - 'https://mysupport.razer.com/*' # customers.md: same, browsers answer 200 + 419: ['https://news.ycombinator.com/*'] # answers the plugin's user agent with 419, browsers with 200 404: - 'https://gitlab.b-data.ch/*' # answers the plugin's user agent with 404, browsers with 200 # customers.md: kept although dead - 'https://www.cisco.com/c/dam/en_us/about/doing_business/open_source/docs/CiscoWebexDeskCamera-23-1622100417.pdf' - 'https://docs.cyberark.com/Downloads/Legal/Privileged%20Session%20Manager%20for%20SSH%20Third-Party%20Notices.pdf' + - 'https://docs.citrix.com/en-us/provisioning/2203-ltsr/downloads/pvs-third-party-notices-2203.pdf' + - 'https://docs.citrix.com/en-us/citrix-virtual-apps-desktops/2305/downloads/third-party-notices-apps-and-desktops.pdf' 500: ['https://marne.io/licenses'] # customers.md: kept although dead ignore_urls: - mailto:* @@ -626,6 +632,20 @@ plugins: extra_css: - css/custom.css - css/brand.css + - css/fig-sax.css + - css/fig-jsonview.css + - css/fig-exceptions.css + - css/fig-pointer.css + - css/fig-charts.css + - css/fig-integration.css + - css/fig-macros.css extra_javascript: - js/mermaid-brand.js + - js/fig-sax.js + - js/fig-jsonview.js + - js/fig-exceptions.js + - js/fig-pointer.js + - js/fig-charts.js + - js/fig-integration.js + - js/fig-macros.js diff --git a/docs/mkdocs/scripts/figures/charts.py b/docs/mkdocs/scripts/figures/charts.py new file mode 100644 index 000000000..6f1c4dc3e --- /dev/null +++ b/docs/mkdocs/scripts/figures/charts.py @@ -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_.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/.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 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 inside the SVG text +ABBR = re.compile(r"(?' % (cls, W, H, tid, tid), + '%s' % (tid, esc(title)), + '%s' % (tid, esc(desc))] + s += body + s.append('') + (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('' % L) + b.append('Apple clang' % (L + 18)) + b.append('' % (L + 118)) + b.append('GCC 16' % (L + 136)) + # grid and ticks + axis_y = top + pitch * len(COMPILE) - 8 + for t in (-80, -60, -40, -20): + b.append('' % (X(t), top - 6, X(t), axis_y)) + b.append('%s' % (X(t), axis_y + 16, pct(t))) + b.append('0 %%' % (X(0), axis_y + 16)) + b.append('← faster' % (L, axis_y + 34)) + b.append('slower →' % (W - 8, axis_y + 34)) + for i, (label, tu, clang, gcc, computed) in enumerate(COMPILE): + y = top + i * pitch + b.append('%s' + ' · %s%s' + % (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('%s, %s (%s): %s' + % (cls, hbar(X(0), X(v), by, bh), esc(label), tu, name, esc(pct(v)))) + b.append('%s' % (max(X(0), X(v)) + 5, by + bh - 1.5, pct(v))) + b.append('' % (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('' % L) + b.append('Apple clang' % (L + 18)) + b.append('' % (L + 118)) + b.append('GCC 16' % (L + 136)) + axis_y = top + pitch * len(rows) - 8 + for t in (-50, -25): + b.append('' % (X(t), top - 6, X(t), axis_y)) + b.append('%s' % (X(t), axis_y + 16, pct(t))) + b.append('0 %%' % (X(0), axis_y + 16)) + b.append('← faster per TU' % (L, axis_y + 34)) + for i, (tu, vals) in enumerate(rows.items()): + y = top + i * pitch + b.append('%s · %s' + % (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('%s: not available: %s' + % (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('%s, %s: %d ms by default, %d ms %s (%s)' + % (cls, hbar(X(0), X(v), by, bh), tu, comp, d, w, esc(what), esc(pct(v)))) + b.append('%s' % (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('' % (X(0), top - 6, X(0), axis_y)) + # one-off cost + y = axis_y + 62 + b.append('One-off cost · built once, ' + 'reused by every TU' % (L, y)) + for j, (comp, cls) in enumerate(comps): + yy = y + 20 + j * 20 + b.append('' % (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('%s' % (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('%s' % fname) + bottom = top + pitch * len(FORMATS) - 6 + for t in (0, 50): + b.append('' % (X(t), top - 6, X(t), bottom)) + b.append('%d %%' % (X(t), bottom + 15, t)) + for i, (fmt, v) in enumerate(zip(FORMATS, vals)): + y = top + i * pitch + best = v == lo + b.append('%s' + % (" nj-charts-best" if best else "", L - 8, y + bh - 3.5, fmt)) + b.append('%s, %s: %g %% of minified %s%s' + % (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('%.1f %%' + % (" nj-charts-best" if best else "", X(v) + 5, y + bh - 3.5, v)) + b.append('' % (X(100), top - 10, X(100), bottom)) + b.append('minified %s = 100 %%' + % (X(100) + 2, top - 14, JSON)) + b.append('100 %%' % (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('' % (L, Y(t), R, Y(t))) + b.append('%g ms' % (L - 6, Y(t) + 4, t)) + b.append('' % (L, B, R, B)) + for n, a, o, f in SCALING: + b.append('%s' % (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('' % (cls, " ".join("%g,%g" % p for p in pts))) + for (x, y), r in zip(pts, SCALING): + b.append('%s, %d keys: %g ms' + % (cls, x, y, name, r[0], r[idx])) + last = SCALING[-1] + b.append('%s' % (cls, pts[-1][0] + 10, pts[-1][1] - 2, name)) + b.append('%g ms' % (pts[-1][0] + 10, pts[-1][1] + 13, last[idx])) + first = SCALING[0] + dy = 16 if key == "json" else 20 + b.append('%g ms' % (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() diff --git a/docs/mkdocs/scripts/figures/chunk_buffering.py b/docs/mkdocs/scripts/figures/chunk_buffering.py new file mode 100644 index 000000000..f21ef26a0 --- /dev/null +++ b/docs/mkdocs/scripts/figures/chunk_buffering.py @@ -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 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('%s' % (y + 5, esc(label))) + if old: + body.append('' % (TX - 3, y - 12, CW * len(old) + 3)) + body.append('' % (TX + CW * len(old), y - 12, CW * len(new) + 3)) + body.append('%s' % (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('' % (status_cls, cx, y + 14)) + body.append('%s' % (status_cls, SX, y - 1, esc(status))) + body.append('%s' % (SX, y + 15, esc(note))) + + +def main(): + W = 700 + body = [] + body.append('chunk') + body.append('buffer, parsed from byte 1 each time' % TX) + body.append('result' % 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('' % (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('%s' % (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 = ['' % (W, H, tid, did), + '%s' % (tid, esc(title)), + '%s' % (did, esc(desc))] + s += body + s.append("") + (OUT / "chunk_buffering.svg").write_text("\n".join(s) + "\n") + + +main() diff --git a/docs/mkdocs/scripts/figures/exception_map.py b/docs/mkdocs/scripts/figures/exception_map.py new file mode 100644 index 000000000..c56fa2891 --- /dev/null +++ b/docs/mkdocs/scripts/figures/exception_map.py @@ -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 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"(?' % (P, " %schip--named" % P if label else "", anchor, esc(aria)), + '' % (x, y, w, CH), + '%d' % (P, x + 9, y + CH / 2 + 4.5, num)] + if label: + s.append('%s' % (P, x + 9 + 3 * MONO + 9, y + CH / 2 + 4.5, noabbr(esc(label)))) + s.append('') + 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('' % (P, href, esc(title + ", " + sub))) + s.append('' % (P, cls, x, y, w, h)) + s.append('%s' % (P, x + 12, y + 22, noabbr(esc(title)))) + s.append('%s' % (P, x + 12, y + 41, noabbr(esc(sub)))) + if href: + s.append('') + 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('' % (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('' % (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, '' % (P, trunk, last_branch, trunk, 158, mid)) + H = y - 14 + 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 = ['' + % (P, P, "narrow" if narrow else "wide", W, H, tid, did), + 'Exception id map' % tid, + '%s' % (did, noabbr(esc(desc))), + defs] + body + [''] + 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() diff --git a/docs/mkdocs/scripts/figures/integration.py b/docs/mkdocs/scripts/figures/integration.py new file mode 100644 index 000000000..73bcb6ef4 --- /dev/null +++ b/docs/mkdocs/scripts/figures/integration.py @@ -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 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 = ['' % (W, H)] + s.append('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.' + % "; ".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('') + # lanes + s.append('') + s.append('3.x release train') + s.append('available today') + 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('' % (x, x)) + s.append('%s' % (x, v)) + s.append('') + s.append('') + s.append('') + s.append('4.0') + s.append('no release date yet') + # guarantee + s.append('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.') + s.append('4.0 is not developed on a separate branch: it switches the ' + 'defaults of these macros and removes the macros.') + # header + hy = TOP - 18 + s.append('Macro' % hy) + s.append('added' % hy) + s.append('3.x default' % hy) + s.append('define to try today' % hy) + s.append('4.0 default' % hy) + for i, r in enumerate(rows): + y = TOP + i * ROW + ROW / 2 + s.append('' % (y - ROW / 2 + 2, ROW - 4)) + s.append('%s' % (y + 4.5, r["macro"])) + s.append('%s' % (y + 4.5, r["added"])) + s.append('' % (y - 11)) + s.append('%s' % (y + 4.5, r["default"])) + s.append('' % (y, y)) + s.append('' % (y - 11)) + if r["removed"]: + s.append('removed' % (y + 4.5)) + else: + s.append('%s' % (y + 4.5, r["v4"])) + s.append('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.' % (H - 8)) + s.append('') + 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") diff --git a/docs/mkdocs/scripts/figures/json_pointer_flatten.py b/docs/mkdocs/scripts/figures/json_pointer_flatten.py new file mode 100644 index 000000000..e9e1a744d --- /dev/null +++ b/docs/mkdocs/scripts/figures/json_pointer_flatten.py @@ -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 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 = ('' % mid) + + body.append('nested value' % X0) + body.append('flat object: key = J​SON Pointer' % FX) + + # flatten() arrow on top, unflatten() arrow at the bottom + ax1, ax2 = 250, FX - 20 + body.append('' % (ax1, ax2, mid)) + body.append('flatten()' % ((ax1 + ax2) // 2)) + yb = H - 22 + body.append('' % (ax2, yb, ax1, yb, mid)) + body.append('unflatten()' % ((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('' % (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('' + % (typ, lx, y - 5, lx + 120, y - 5, FX - 130, fy - 5, FX - 10, fy - 5)) + body.append('' % (typ, lx, y - 5)) + body.append('"%s": ' + '%s' % (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('%s' % (cls, x, y, esc(label))) + else: + body.append('%s: ' + '%s' % (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 = ['' % (W, H), + '%s' % esc(title), defs] + s += body + s.append('') + (OUT / "json_pointer_flatten.svg").write_text("\n".join(s) + "\n") + + +main() diff --git a/docs/mkdocs/scripts/figures/json_view_layout.py b/docs/mkdocs/scripts/figures/json_view_layout.py new file mode 100644 index 000000000..9d362ab9a --- /dev/null +++ b/docs/mkdocs/scripts/figures/json_view_layout.py @@ -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 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 = ['' % (extra_cls, W, H, tid, tid), + '%s' % (tid, esc(title).replace("\u200b", "​")), + '%s' % (tid, esc(desc))] + s += body + s.append('') + 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 = [''] + for k in kinds: + m.append('' % (prefix, k, k)) + m.append('') + return m + + +def rect(x, y, w, h, cls, c=None, rx=5): + cc = " nj-jsonview-c-%s" % c if c else "" + return '' % (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 '%s' % (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 '' % ( + 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 '' % ( + 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('' % (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('' % (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('' % (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('' % (top - 2, bottom)) + b.append(text(424, top - 4, "std::move: views stay valid", "nj-jsonview-tiny")) + b.append('' % (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('' % (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('' % (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) diff --git a/docs/mkdocs/scripts/figures/memory_over_time.py b/docs/mkdocs/scripts/figures/memory_over_time.py new file mode 100644 index 000000000..9cdd94515 --- /dev/null +++ b/docs/mkdocs/scripts/figures/memory_over_time.py @@ -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 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('' % (X0, Y0 - 12, Y1, X1 + 8)) + body.append('memory' % (X0 - 40)) + body.append('time: records parsed' % (X0, Y1 + 22)) + body.append('illustrative, not a measurement' % (W - 12)) + + # DOM: grows with every record until the whole tree is built + body.append('' % (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('' % (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('' % d) + # SAX: no value is built, only what the handler keeps + body.append('' % (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('%s' % (cls, X1 + 16, y - 9, esc(name))) + body.append('%s' % (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 = ['' % (W, H, tid, did), + '%s' % (tid, esc(title)), + '%s' % (did, esc(desc))] + s += body + s.append("") + (OUT / "memory_over_time.svg").write_text("\n".join(s) + "\n") + + +main() diff --git a/docs/mkdocs/scripts/figures/qa_pipeline.py b/docs/mkdocs/scripts/figures/qa_pipeline.py new file mode 100644 index 000000000..b706e76a4 --- /dev/null +++ b/docs/mkdocs/scripts/figures/qa_pipeline.py @@ -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 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"(?' % P, + '' % (P, x, y, w, h), + '%s' % (P, x + 14, y + 25, txt(title))] + bx = x + 14 + for t in triggers: + label, mod = TRIGGERS[t] + bw = badge_w(label) + s.append('' % (P, P, mod, bx, y + 36, bw)) + s.append('%s' % (P, P, mod, bx + bw / 2, y + 50, txt(label))) + bx += bw + 6 + for i, line in enumerate(lines): + s.append('%s' % (P, x + 14, y + 78 + i * 18, txt(line))) + s.append('') + return "\n".join(s) + + +def pill(cx, y, w, label, sub=None, cls=""): + h = 46 if sub else 34 + s = ['' % (P, cls, cx - w / 2, y, w, h, h / 2), + '%s' % (P, cx, y + 22, txt(label))] + if sub: + s.append('%s' % (P, cx, y + 38, txt(sub))) + return "\n".join(s), h + + +def arrow(x, y1, y2, mid): + return '' % (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('checks, run in parallel on GitHub Actions and AppVeyor' % (P, 12 + pad, top + 24) if cols > 1 else + 'checks, run in parallel' % (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, '' % (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 = ('' + '' % 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 = ['' + % (P, P, "narrow" if cols == 1 else "wide", W, H, tid, did), + 'Quality assurance pipeline' % tid, + '%s' % (did, txt(desc)), + defs] + body + [''] + return "\n".join(out) + + +def main(): + (OUT / "qa_pipeline.svg").write_text(drawing(760, 3, "w") + "\n" + drawing(360, 1, "n") + "\n") + + +main() diff --git a/docs/mkdocs/scripts/figures/release_timeline.py b/docs/mkdocs/scripts/figures/release_timeline.py new file mode 100644 index 000000000..52be04f13 --- /dev/null +++ b/docs/mkdocs/scripts/figures/release_timeline.py @@ -0,0 +1,121 @@ +"""Draws the release timeline of home/releases/index.md as an inline SVG into docs/images/. + +The releases and their dates come from the tables of that page (rows like `| [v3.11.2](v3.11.2.md) | 2022-08-12 | ...`), +so after adding a release to the page, run `python3 scripts/figures/release_timeline.py` from docs/mkdocs and commit +the regenerated docs/images/release_timeline.svg. The page includes the SVG text with +`--8<-- "images/release_timeline.svg"`, so it follows the light/dark toggle (see the `nj-fig` rules in +docs/css/brand.css and the `nj-int-*` rules in docs/css/fig-integration.css). Every marker has a tooltip. +Rows without a date (the "Unreleased" row) are skipped; any other release row that cannot be parsed stops the script. +""" +import datetime +import re +import sys +from pathlib import Path + +DOCS = Path(__file__).resolve().parents[2] / "docs" +OUT = Path(__file__).resolve().parents[2] / "docs" / "images" +PAGE = DOCS / "home" / "releases" / "index.md" + +ROW = re.compile(r"^\|\s*\[v(\d+)\.(\d+)\.(\d+)\]\([^)]*\)\s*\|\s*([^|]*?)\s*\|") +W, LEFT, RIGHT = 880, 24, 856 +LABEL_ROW, CHAR_W = 17, 7.0 + + +def parse(markdown): + releases = [] + for line in markdown.splitlines(): + if not line.startswith("| [v"): + continue + m = ROW.match(line) + if not m: + sys.exit("cannot parse release row: " + line) + try: + date = datetime.date.fromisoformat(m.group(4)) + except ValueError: + sys.exit("cannot parse date in row: " + line) + releases.append(((int(m.group(1)), int(m.group(2)), int(m.group(3))), date)) + if len(releases) < 2: + sys.exit("found fewer than two releases in " + str(PAGE)) + if len({v for v, _ in releases}) != len(releases): + sys.exit("duplicate release in " + str(PAGE)) + return sorted(releases, key=lambda r: (r[1], r[0])) + + +def name(version): + return "v%d.%d.%d" % version + + +def draw(releases): + first, last = releases[0][1], releases[-1][1] + start, end = datetime.date(first.year, 1, 1), datetime.date(last.year + 1, 1, 1) + span = (end - start).days + + def xof(date): + return LEFT + (RIGHT - LEFT) * (date - start).days / span + + # minor labels, placed greedily into the lowest free row above the axis + rows_end, labels = [], [] + for version, date in releases: + if version[2] != 0: + continue + text = "%d.%d" % version[:2] + x, half = xof(date), len(text) * CHAR_W / 2 + 2 + row = next((i for i, e in enumerate(rows_end) if e < x - half), None) + if row is None: + rows_end.append(0) + row = len(rows_end) - 1 + rows_end[row] = x + half + labels.append((x, row, text)) + top = 14 + LABEL_ROW * len(rows_end) + axis = top + 12 + height = axis + 70 + + s = ['<svg class="nj-fig nj-fig--timeline" viewBox="0 0 %d %d" role="img" aria-labelledby="nj-int-tl-title" ' + 'xmlns="http://www.w3.org/2000/svg">' % (W, height)] + minors = sum(1 for v, _ in releases if v[2] == 0) + s.append('<title id="nj-int-tl-title">Timeline of the %d releases from %s (%s) to %s (%s): %d minor or major ' + 'releases, drawn as filled dots with their version above the axis, and %d patch releases, drawn as ' + 'rings.' % (len(releases), name(releases[0][0]), first.isoformat(), name(releases[-1][0]), + last.isoformat(), minors, len(releases) - minors)) + # year grid + for year in range(start.year, end.year + 1): + x = xof(datetime.date(year, 1, 1)) + s.append('' % (x, top - 4, x, axis + 8)) + if year < end.year: + mid = xof(datetime.date(year, 7, 1)) + s.append('%d' % (mid, axis + 24, year)) + s.append('' % (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('' % (x0, y - 5, y, x1, y - 5)) + s.append('%d.x' + % ((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('' % (x, y + 3, x, axis - 7)) + s.append('%s' % (x, y, text)) + # markers: one per date, patch rings first so that minor dots stay on top + by_date = {} + for version, date in releases: + by_date.setdefault(date, []).append(version) + groups = sorted(by_date.items(), key=lambda g: any(v[2] == 0 for v in g[1])) + for date, versions in groups: + minor = any(v[2] == 0 for v in versions) + x = xof(date) + tip = ", ".join(name(v) for v in sorted(versions)) + " released " + date.isoformat() + s.append('%s' + '' + % (tip, x, axis, "nj-int-minor" if minor else "nj-int-patch", x, axis, 6 if minor else 3.5)) + s.append('') + 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")